ORA // Ai Stance
Ora AI Stance Layer + Model Selection System
Architecture Context
Ora's behavior pipeline:
User message saved via
sendMessagemutation in[aiChat.ts](packages/backend/convex/aiChat.ts)generateResponseAsyncaction creates anAgentwith systeminstructions+ toolsagent.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’s auto-routing (picks an upstream provider/model per request). |
Fast |
| OpenAI GPT‑5.4 Mini through OpenRouter. |
Max |
| 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 |
|
|
Fast |
|
|
Max |
|
|
**[packages/backend/convex/aiChat.ts](packages/backend/convex/aiChat.ts)** -- Thread the user's model preference:
sendMessagemutation already hasuserIdfromrequireAuthUserId(ctx). AdduserIdto thegenerateResponseAsyncscheduler args.generateResponseAsynccallsctx.runQuery(internal.aiPreferences.queries.getPreferencesInternal, { userId })to getselectedModelId, then passes it tocreateChatAgent(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
ModelSelectorprimitives from@the-cloud/uiEach tier shows: name, description, provider logo via
ModelSelectorLogoSelection calls
api.aiPreferences.mutations.updatePreferenceswith the selectedmodelIdCurrent 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 modeSection 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.tsuntouched)sendMessagemutation flow is unchanged (only addsuserIdto scheduler args)generateResponseAsyncflow 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