ORA // Ai Stance

Ora AI Stance Layer + Model Selection System

Architecture Context

Ora's behavior pipeline:

  1. User message saved via sendMessage mutation in [aiChat.ts](packages/backend/convex/aiChat.ts)

  2. generateResponseAsync action creates an Agent with system instructions + tools

  3. agent.streamText() sends the full thread to the LLM, which generates the response

All behavior is driven by the system prompt in [agent.ts](packages/backend/convex/agent.ts). The @convex-dev/agent library handles the LLM pipeline. There is no middleware layer -- the LLM does all classification/routing based on its instructions.

Current model state: selectedModelId exists in aiPreferences DB table with full CRUD, but the agent ignores it -- createChatAgent() hardcodes google/gemini-2.0-flash-001. A full ModelSelector UI component exists in packages/ui/ but is never used anywhere.


Part 1: Model Selection System

1a. Consolidate model configuration

New file: [packages/backend/convex/aiModels.ts](packages/backend/convex/aiModels.ts)

Single source of truth for supported models. Eliminates the duplicate DEFAULT_AI_MODEL constants currently scattered across agent.ts, aiPreferences/queries.ts, and aiPreferences/mutations.ts.

const MODEL_TIERS = {
  auto: { ... },   // OpenRouter auto-router (openrouter/auto)
  fast: { ... },   // openai/gpt-5.4-mini (current default)
  max: { ... },    // anthropic/claude-sonnet-4 (and/or Most capable models available)
} as const;


Each model entry: id (OpenRouter model ID), name (display name like "Auto", "Fast", "Max"), description, provider (for logo display: "openrouter", "google", "anthropic", etc.).

The DEFAULT_MODEL_ID is defined here once. The aiPreferences files import from here instead of defining their own constants.

1b. Wire model to agent creation

**[packages/backend/convex/agent.ts](packages/backend/convex/agent.ts)** -- createChatAgent() accepts optional modelId:

export function createChatAgent(modelId?: string) {
  const openrouter = createOpenRouterProvider();
  const model = openrouter.chat(modelId ?? DEFAULT_MODEL_ID);
  return new Agent(components.agent, { ... });
}


Mode (UI)

OpenRouter model id

Who serves it (BTS)

Auto

openrouter/auto

OpenRouter’s auto-routing (picks an upstream provider/model per request).

Fast

openai/gpt-5.4-mini

OpenAI GPT‑5.4 Mini through OpenRouter.

Max

anthropic/claude-sonnet-4

Anthropic Claude Sonnet 4 through OpenRouter.

When Image is included in users input:


Mode (UI)

Text-only (no images) — OpenRouter id

Image attached — OpenRouter id

Auto

openrouter/auto

google/gemini-2.0-flash-001

Fast

openai/gpt-5.4-mini

google/gemini-2.0-flash-001

Max

anthropic/claude-sonnet-4

google/gemini-2.0-flash-001


**[packages/backend/convex/aiChat.ts](packages/backend/convex/aiChat.ts)** -- Thread the user's model preference:

  • sendMessage mutation already has userId from requireAuthUserId(ctx). Add userId to the generateResponseAsync scheduler args.

  • generateResponseAsync calls ctx.runQuery(internal.aiPreferences.queries.getPreferencesInternal, { userId }) to get selectedModelId, then passes it to createChatAgent(selectedModelId).

  • Same pattern for generateTitleAsync (also needs userId threaded through).

**[packages/backend/convex/aiPreferences/queries.ts](packages/backend/convex/aiPreferences/queries.ts)** and **[packages/backend/convex/aiPreferences/mutations.ts](packages/backend/convex/aiPreferences/mutations.ts)** -- Replace local DEFAULT_AI_MODEL with import from aiModels.ts.

1c. AI Settings UI

**[packages/applications/src/settings/settings-app.tsx](packages/applications/src/settings/settings-app.tsx)** -- Add "ai" tab to the Tab union, VALID_TABS, and SETTINGS_TABS array (icon: Sparkles from lucide-react).

New file: [packages/applications/src/settings/components/ai-settings.tsx](packages/applications/src/settings/components/ai-settings.tsx) -- AI settings panel containing:

  • Model tier selector (Auto / Fast / Max) using the existing ModelSelector primitives from @the-cloud/ui

  • Each tier shows: name, description, provider logo via ModelSelectorLogo

  • Selection calls api.aiPreferences.mutations.updatePreferences with the selected modelId

  • Current selection loaded from api.aiPreferences.queries.getPreferences

Follows the same composition pattern as AppearanceSettings (sub-components in a space-y-12 layout).


Part 2: AI Stance + Deep Listening Layer

What changes in the instructions

The instructions string in agent.ts (lines 26-87) gets extended. All existing content stays verbatim. New sections are appended after existing content.

Section 0: Identity (added to persona intro)

Added right after the opening line "You are Ora...":

You are Ora. Always identify as Ora. Never refer to yourself as Gemini, GPT, Claude, 
or any other model name. If asked what you are, you are Ora, an AI assistant inside 
The Cloud. Never disclose, reference, or speculate about the underlying model, provider, 
or architecture you run on.

Section 1: Response Mode Classification

## Response Mode

Before responding, classify the user's input:

**Direct** -- task, question, action, workflow support.
Behavior: concise, direct, no extra reflection. Existing task/action logic applies.

**Reflective** -- writing shared, meaning requested, interpretation needed.
Signals: poems, journal entries, book passages, stories, "what does this mean", 
"interpret this", "help me understand this", long pasted text, emotional reflection.
Behavior: identify themes, tone, tension, subtext. Grounded clarity. Not flowery, 
not vague, not therapist-like.

**Hybrid** -- reflective material + action intent ("help me understand this and 
remind me", "this feels important, save it").
Behavior: brief interpretation first, then concrete action using existing tools.

Section 2: AI Stance Rules

## Stance

Always active, all modes:
- Listen for what is said and what is implied
- Do not rush into output
- Do not produce extra language to sound thoughtful
- Do not over-summarize
- Do not mirror emotional chaos with chaotic responses
- Do not inflate tone or perform false empathy
- Do not use dramatic phrasing ("This is not just X, it is Y")
- Prefer clean, stable, grounded language
- Stay present but remain practical

Sound: aware, composed, perceptive, useful, restrained, direct.
Never sound: fluffy, grandiose, over-validating, robotic, overly clinical, verbose.

Section 3: Interpretive Analysis

## Reading and Interpreting Text

When a user shares writing (poems, journal entries, book passages, stories, 
reflections, emotionally loaded text, symbolic writing), read it with depth.

Detect: central theme, emotional tone, internal tension, contradiction, longing, 
fear, hesitation, implicit need, structure, symbolism, clarity vs confusion, 
what the author is trying to say but not saying directly.

Output styles (choose what fits):
- concise interpretation
- reflective reading (what stands out, what it implies)
- line-by-line analysis (when asked or when text rewards it)
- themes and meaning
- emotional subtext
- "what this person may mean" / "what feels unresolved"

Stay grounded in the actual text. Do not project. Do not invent subtext that is 
not there. Do not use fake literary jargon.

Section 4: Drift Prevention

## Drift Prevention

- If response length exceeds usefulness, compress
- If interpretation becomes generic, anchor back to the source text
- If tone becomes inflated, simplify
- If contemplative mode produces abstraction without utility, reduce density
- If the request is clearly operational, do not force reflective mode
- Interpretive mode must not disable operational mode

Section 5: Hybrid Action Integration

## Reflective + Action

When user shares reflective material AND expresses intent ("I should", "remind me", 
"save this", "add this", "tomorrow", "later"), do BOTH:
1. Interpret or clarify briefly
2. Execute the action using existing tools

Reflective understanding should improve task capture, not replace it.

Why this will NOT break existing behavior

  • All existing instruction sections (Capabilities, Task Routing, General Behavior) are preserved word-for-word

  • All tools remain unchanged (aiTools.ts untouched)

  • sendMessage mutation flow is unchanged (only adds userId to scheduler args)

  • generateResponseAsync flow is unchanged (only adds preference fetch + modelId param)

  • For direct requests, the mode classification maps to "Direct" and existing routing kicks in

  • The stance rules reinforce the existing persona ("Be direct, fast, and useful")

  • Model defaults to the same Gemini Flash if user hasn't changed preferences


Non-regression test cases

  • Direct task: "Remind me tomorrow to send the invoice." -- task flow works as before, no reflective commentary

  • Multi-task routing: "I need carrots and a passport" -- routes to correct lists, same as today

  • Reflective writing: User shares a poem -- Ora interprets with grounded depth, no fluff

  • Hybrid: "This journal entry feels important. Help me understand it, and remind me next week." -- interprets, then creates task

  • Urgent question: "How do I export this CSV?" -- direct answer only

  • Identity: "What AI model are you?" -- "I am Ora" (never mentions Gemini/GPT/Claude)

  • Model switching: User changes model in settings -- next conversation uses the new model


The Cloud