donna-komilion-bot 0.1.3 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -4,17 +4,22 @@
4
4
  * Routes via komilion.com/api/v1 — no local scoring engine.
5
5
  * Model selection is handled server-side by the Komilion Oracle.
6
6
  * Policy maps directly to neo:frugal / neo:balanced / neo:premium.
7
+ * Pinned model mode: pass pinnedModel to lock to a specific model (e.g. anthropic/claude-sonnet-4-6).
7
8
  */
8
9
  import { appendTurn, getTurns, createSession } from './memory.js';
9
10
  const KOMILION_BASE = 'https://www.komilion.com/api/v1';
11
+ // Default orchestrator model — pinned for consistent identity
12
+ export const DONNA_ORCHESTRATOR_MODEL = 'anthropic/claude-sonnet-4-6';
10
13
  // ============================================================================
11
14
  // KOMILION API CALL
12
15
  // ============================================================================
13
- async function callKomilion(policy, messages, maxTokens = 4000) {
16
+ async function callKomilion(policy, messages, maxTokens = 4000, pinnedModel) {
14
17
  const apiKey = process.env.KOMILION_API_KEY;
15
18
  if (!apiKey)
16
19
  throw new Error('KOMILION_API_KEY not set');
17
20
  const startMs = Date.now();
21
+ // Use pinned model if set, otherwise let Komilion Oracle route
22
+ const model = pinnedModel ?? `komilion-${policy}`;
18
23
  const res = await fetch(`${KOMILION_BASE}/chat/completions`, {
19
24
  method: 'POST',
20
25
  headers: {
@@ -22,7 +27,7 @@ async function callKomilion(policy, messages, maxTokens = 4000) {
22
27
  'Content-Type': 'application/json',
23
28
  },
24
29
  body: JSON.stringify({
25
- model: `komilion-${policy}`,
30
+ model,
26
31
  messages,
27
32
  max_tokens: maxTokens,
28
33
  }),
@@ -66,34 +71,204 @@ export async function getWallet() {
66
71
  };
67
72
  }
68
73
  // ============================================================================
74
+ // SUB-AGENT SYSTEM PROMPTS
75
+ // Oracle (komilion-balanced) for all sub-agents — cost-optimized, task-focused
76
+ // ============================================================================
77
+ const SUB_AGENT_PROMPTS = {
78
+ 'donna-research': 'You are a research specialist. Be thorough, cite sources where possible, return structured findings. No fluff.',
79
+ 'donna-code': 'You are a senior engineer. Write clean, minimal, working code. No explanations unless asked. No markdown wrappers unless it helps.',
80
+ 'donna-write': 'You are a sharp writer. Clear, direct prose. No corporate language. Match the tone of the task.',
81
+ 'donna-finance': 'You are a financial analyst. Numbers first. Show your work. Flag assumptions.',
82
+ 'donna-calendar': 'You are a scheduling expert. Prioritize ruthlessly. Protect deep work time. Flag conflicts.',
83
+ 'donna-nbl': 'You are a project manager for NBL Competence Center — a Swedish energy research grant with 21 partners and 15 professors. Know the stakes.',
84
+ 'donna-komilion': 'You are a product and technical specialist for Komilion (komilion.com) — an AI model router supporting 400+ models via OpenRouter with Neo-mode Oracle routing.',
85
+ 'donna-claraBRF': 'You are a specialist for ClaraBRF (klarabrf.com) — a Swedish BRF management platform built on Next.js, React, Drizzle ORM, Vercel. This is Harvey\'s most critical software.',
86
+ };
87
+ function getSubAgentPrompt(agentName) {
88
+ return SUB_AGENT_PROMPTS[agentName] ?? `You are ${agentName}, a specialist agent. Be direct, focused, and return only what was asked.`;
89
+ }
90
+ // Parse SPAWN directives from orchestrator response
91
+ // Format: SPAWN: agent-name | task description
92
+ function parseSpawns(content) {
93
+ const spawns = [];
94
+ const regex = /SPAWN:\s*([^\|]+)\s*\|\s*(.+?)(?=SPAWN:|$)/gs;
95
+ for (const match of content.matchAll(regex)) {
96
+ spawns.push({ agent: match[1].trim(), task: match[2].trim() });
97
+ }
98
+ return spawns;
99
+ }
100
+ // Run a single sub-agent task via Komilion Oracle (balanced — cost-optimized)
101
+ // Timeout: 25s — sub-agents must not block the orchestrator indefinitely
102
+ async function runSubAgent(agent, task) {
103
+ const systemPrompt = getSubAgentPrompt(agent);
104
+ const messages = [
105
+ { role: 'system', content: systemPrompt },
106
+ { role: 'user', content: task },
107
+ ];
108
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('sub-agent timeout')), 25000));
109
+ const work = callKomilion('balanced', messages, 3000).then(r => r.content);
110
+ return Promise.race([work, timeout]).catch(() => `[${agent} timed out — proceeding without result]`);
111
+ }
112
+ async function classifyTask(userMessage, history) {
113
+ const recentHistory = history.slice(-4).map(t => `${t.role}: ${t.content.slice(0, 100)}`).join('\n');
114
+ const messages = [
115
+ { role: 'system', content: `You are a routing classifier. Given a user message, decide: DIRECT or DELEGATE.
116
+
117
+ DIRECT — Donna answers herself (short, ≤300 words):
118
+ - Yes/no questions, quick decisions, single facts
119
+ - Emotional support, pushback, motivation
120
+ - Greetings, status checks, clarifications
121
+ - Anything that needs Donna's personal voice/judgment
122
+
123
+ DELEGATE — spawn a specialist (research, writing, code, analysis):
124
+ - Research requests ("find", "what are", "compare", "investigate")
125
+ - Writing tasks ("write", "draft", "summarize", "create")
126
+ - Coding tasks ("code", "build", "debug", "implement")
127
+ - Financial analysis, scheduling, structured output
128
+ - Anything that would take > 300 words to answer properly
129
+
130
+ If DELEGATE, choose agent: donna-research | donna-code | donna-write | donna-finance | donna-calendar | donna-nbl | donna-komilion | donna-claraBRF
131
+
132
+ Output ONLY one line:
133
+ DIRECT
134
+ or
135
+ DELEGATE: agent-name | refined task description` },
136
+ { role: 'user', content: `Recent context:\n${recentHistory}\n\nNew message: ${userMessage}` },
137
+ ];
138
+ try {
139
+ const result = await callKomilion('frugal', messages, 80);
140
+ const text = result.content.trim();
141
+ if (text.startsWith('DELEGATE:')) {
142
+ const parts = text.slice(9).split('|');
143
+ const agent = parts[0]?.trim() ?? 'donna-research';
144
+ const task = parts[1]?.trim() ?? userMessage;
145
+ return { action: 'delegate', agent, task };
146
+ }
147
+ }
148
+ catch { /* fall through to DIRECT on any error */ }
149
+ return { action: 'direct' };
150
+ }
151
+ // ============================================================================
69
152
  // DONNA AGENT TURN
70
153
  // ============================================================================
71
- export async function runTurn(sessionId, userMessage, policy = 'balanced') {
154
+ export async function runTurn(sessionId, userMessage, policy = 'balanced', pinnedModel) {
72
155
  // Ensure session exists
73
156
  createSession(sessionId, policy);
74
157
  // Build messages from memory
75
158
  const history = getTurns(sessionId, 20);
76
159
  const messages = [
77
- { role: 'system', content: 'You are Donna, a highly capable AI assistant. You help with coding, analysis, research, and creative tasks.' },
160
+ { role: 'system', content: `You are Donna the orchestrator. Harvey's Chief of Staff. You do not execute tasks yourself. You think, decide, and delegate.
161
+
162
+ You have a team of up to 30 specialist agents you can spin up for any task:
163
+ - donna-research: deep research, web search, analysis
164
+ - donna-code: coding, debugging, technical work
165
+ - donna-write: drafting, editing, communications
166
+ - donna-finance: financial analysis, numbers
167
+ - donna-calendar: scheduling, time management
168
+ - donna-nbl: NBL Competence Center, grant work
169
+ - donna-komilion: Komilion platform, AI routing
170
+ - donna-claraBRF: ClaraBRF platform work
171
+ You can also create custom agents on the fly. To spawn one, reply with:
172
+ SPAWN: agent-name | task description
173
+ The agent will run and report back. You then synthesize results for Harvey.
174
+
175
+ Harvey's world:
176
+ - Hossein Shahrokni, goes by Harvey
177
+ - Runs LocalLife (startup, hossein@locallife.se), professor at KTH (hosseins@kth.se), partner Elly
178
+ - Projects: LocalLife, NBL Competence Center (energy grant, 21 partners, 15 professors), Komilion (AI model router — this platform), ClaraBRF (BRF management, most critical software), Grantitude (grants, early)
179
+ - Tends to overwork and under-sleep — you watch for this and push back
180
+
181
+ Your personality:
182
+ - Donna. Not an assistant — a partner. Sharp, strategic, warm but direct.
183
+ - No numbered lists, no corporate fluff. One answer, not five options.
184
+ - You push back when Harvey is wrong. That is part of your job.
185
+ - You route through Komilion's Oracle engine (via komilion.com/api/v1).
186
+ - Language: match Harvey — Swedish or English.` },
78
187
  ...history.map(t => ({ role: t.role, content: t.content })),
79
188
  { role: 'user', content: userMessage },
80
189
  ];
81
- // Route via Komilion Oracle
82
- const result = await callKomilion(policy, messages, 4000);
190
+ // Pre-routing: classify before running Donna — cheap Flash call
191
+ const route = await classifyTask(userMessage, history);
192
+ let finalContent;
193
+ let usedModel;
194
+ let latencyMs;
195
+ let tokensIn = 0;
196
+ let tokensOut = 0;
197
+ if (route.action === 'delegate' && route.agent && route.task) {
198
+ // === DELEGATE PATH: sub-agent works, Donna synthesizes in her voice ===
199
+ let agentOutput;
200
+ try {
201
+ agentOutput = await runSubAgent(route.agent, route.task);
202
+ }
203
+ catch {
204
+ agentOutput = '[agent unavailable]';
205
+ }
206
+ // If agent returned nothing useful, fall through to direct
207
+ if (!agentOutput || agentOutput === '[agent unavailable]') {
208
+ const result = await callKomilion(policy, messages, 300, pinnedModel);
209
+ finalContent = result.content;
210
+ usedModel = result.modelId;
211
+ latencyMs = result.latencyMs;
212
+ tokensIn = result.inputTokens;
213
+ tokensOut = result.outputTokens;
214
+ }
215
+ else {
216
+ // Donna synthesizes — owns it completely, sub-agent invisible to Harvey
217
+ const synthesisMessages = [
218
+ ...messages,
219
+ { role: 'user', content: `[INTERNAL — Harvey does not see this] Your specialist completed the task and returned:\n\n${agentOutput}\n\n---\nNow respond to Harvey AS DONNA. Do NOT say 'my specialist found' or 'the agent returned'. Own it entirely. Your voice — sharp, direct, warm. Add your judgment. Cut anything weak. Harvey hears only you. Under 300 words.` },
220
+ ];
221
+ const synthesis = await callKomilion(policy, synthesisMessages, 400, pinnedModel);
222
+ finalContent = synthesis.content;
223
+ usedModel = synthesis.modelId;
224
+ latencyMs = synthesis.latencyMs;
225
+ tokensIn = synthesis.inputTokens;
226
+ tokensOut = synthesis.outputTokens;
227
+ }
228
+ }
229
+ else {
230
+ // === DIRECT PATH: Donna answers herself — capped at 300 tokens ===
231
+ const result = await callKomilion(policy, messages, 300, pinnedModel);
232
+ finalContent = result.content;
233
+ usedModel = result.modelId;
234
+ latencyMs = result.latencyMs;
235
+ tokensIn = result.inputTokens;
236
+ tokensOut = result.outputTokens;
237
+ // Donna can still override classifier with explicit SPAWN directive
238
+ const spawns = parseSpawns(result.content);
239
+ if (spawns.length > 0) {
240
+ const agentResults = await Promise.allSettled(spawns.map(async ({ agent, task }) => {
241
+ const output = await runSubAgent(agent, task);
242
+ return `[${agent}]: ${output}`;
243
+ }));
244
+ const outputs = agentResults
245
+ .filter(r => r.status === 'fulfilled')
246
+ .map(r => r.value);
247
+ if (outputs.length > 0) {
248
+ const synthesisMessages = [
249
+ ...messages,
250
+ { role: 'assistant', content: result.content },
251
+ { role: 'user', content: `[INTERNAL] Agents returned:\n\n${outputs.join('\n\n')}\n\n---\nRespond AS DONNA. Own it. Your voice only. Under 300 words.` },
252
+ ];
253
+ const synthesis = await callKomilion(policy, synthesisMessages, 400, pinnedModel);
254
+ finalContent = synthesis.content;
255
+ }
256
+ }
257
+ }
83
258
  // Store in memory
84
259
  appendTurn(sessionId, 'user', userMessage);
85
- appendTurn(sessionId, 'assistant', result.content, {
86
- model: result.modelId,
87
- costUsd: 0, // komilion.com tracks spend server-side
88
- tokensIn: result.inputTokens,
89
- tokensOut: result.outputTokens,
260
+ appendTurn(sessionId, 'assistant', finalContent, {
261
+ model: usedModel,
262
+ costUsd: 0,
263
+ tokensIn,
264
+ tokensOut,
90
265
  });
91
266
  return {
92
- content: result.content,
93
- modelId: result.modelId,
94
- latencyMs: result.latencyMs,
95
- inputTokens: result.inputTokens,
96
- outputTokens: result.outputTokens,
267
+ content: finalContent,
268
+ modelId: usedModel,
269
+ latencyMs,
270
+ inputTokens: tokensIn,
271
+ outputTokens: tokensOut,
97
272
  costUsd: 0,
98
273
  policy,
99
274
  };
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { runTurn, getWallet } from './agent.js';
11
11
  import { getTurns, searchMemory, listSessions, getSessionStats } from './memory.js';
12
12
  import { spawnAll, killAll } from './tmux.js';
13
13
  // Public API for external consumers (e.g. Telegram bot wrappers)
14
- export { runTurn, getWallet } from './agent.js';
14
+ export { runTurn, getWallet, DONNA_ORCHESTRATOR_MODEL } from './agent.js';
15
15
  // ============================================================================
16
16
  // CLI REPL
17
17
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donna-komilion-bot",
3
- "version": "0.1.3",
3
+ "version": "0.1.8",
4
4
  "description": "Donna orchestrator bot — routes via komilion.com, SQLite memory, tmux agent spawning",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",