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 +191 -16
- package/dist/index.js +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
-
//
|
|
82
|
-
const
|
|
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',
|
|
86
|
-
model:
|
|
87
|
-
costUsd: 0,
|
|
88
|
-
tokensIn
|
|
89
|
-
tokensOut
|
|
260
|
+
appendTurn(sessionId, 'assistant', finalContent, {
|
|
261
|
+
model: usedModel,
|
|
262
|
+
costUsd: 0,
|
|
263
|
+
tokensIn,
|
|
264
|
+
tokensOut,
|
|
90
265
|
});
|
|
91
266
|
return {
|
|
92
|
-
content:
|
|
93
|
-
modelId:
|
|
94
|
-
latencyMs
|
|
95
|
-
inputTokens:
|
|
96
|
-
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
|
// ============================================================================
|