anorion 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +87 -0
  2. package/agents/001.yaml +32 -0
  3. package/agents/example.yaml +6 -0
  4. package/bin/anorion.js +8093 -0
  5. package/package.json +72 -0
  6. package/scripts/cli.ts +182 -0
  7. package/scripts/postinstall.js +6 -0
  8. package/scripts/setup.ts +255 -0
  9. package/src/agents/pipeline.ts +231 -0
  10. package/src/agents/registry.ts +153 -0
  11. package/src/agents/runtime.ts +593 -0
  12. package/src/agents/session.ts +338 -0
  13. package/src/agents/subagent.ts +185 -0
  14. package/src/bridge/client.ts +221 -0
  15. package/src/bridge/federator.ts +221 -0
  16. package/src/bridge/protocol.ts +88 -0
  17. package/src/bridge/server.ts +221 -0
  18. package/src/channels/base.ts +43 -0
  19. package/src/channels/router.ts +122 -0
  20. package/src/channels/telegram.ts +592 -0
  21. package/src/channels/webhook.ts +143 -0
  22. package/src/cli/index.ts +1036 -0
  23. package/src/cli/interactive.ts +26 -0
  24. package/src/gateway/routes-v2.ts +165 -0
  25. package/src/gateway/server.ts +512 -0
  26. package/src/gateway/ws.ts +75 -0
  27. package/src/index.ts +182 -0
  28. package/src/llm/provider.ts +243 -0
  29. package/src/llm/providers.ts +381 -0
  30. package/src/memory/context.ts +125 -0
  31. package/src/memory/store.ts +214 -0
  32. package/src/scheduler/cron.ts +239 -0
  33. package/src/shared/audit.ts +231 -0
  34. package/src/shared/config.ts +129 -0
  35. package/src/shared/db/index.ts +165 -0
  36. package/src/shared/db/prepared.ts +111 -0
  37. package/src/shared/db/schema.ts +84 -0
  38. package/src/shared/events.ts +79 -0
  39. package/src/shared/logger.ts +10 -0
  40. package/src/shared/metrics.ts +190 -0
  41. package/src/shared/rbac.ts +151 -0
  42. package/src/shared/token-budget.ts +157 -0
  43. package/src/shared/types.ts +166 -0
  44. package/src/tools/builtin/echo.ts +19 -0
  45. package/src/tools/builtin/file-read.ts +78 -0
  46. package/src/tools/builtin/file-write.ts +64 -0
  47. package/src/tools/builtin/http-request.ts +63 -0
  48. package/src/tools/builtin/memory.ts +71 -0
  49. package/src/tools/builtin/shell.ts +94 -0
  50. package/src/tools/builtin/web-search.ts +22 -0
  51. package/src/tools/executor.ts +126 -0
  52. package/src/tools/registry.ts +56 -0
  53. package/src/tools/skill-manager.ts +252 -0
@@ -0,0 +1,593 @@
1
+ import {
2
+ generateText,
3
+ streamText,
4
+ type ModelMessage,
5
+ type Tool as AiTool,
6
+ } from 'ai';
7
+ import type {
8
+ Agent,
9
+ ToolCall,
10
+ ToolResultEntry,
11
+ StreamChunk,
12
+ OnChunkCallback,
13
+ CategorizedError,
14
+ AgentRunMetrics,
15
+ } from '../shared/types';
16
+ import { toolRegistry } from '../tools/registry';
17
+ import { executeTool, executeToolsParallel, type ParallelToolCall } from '../tools/executor';
18
+ import { sessionManager } from './session';
19
+ import { agentRegistry } from './registry';
20
+ import { shouldCompact, compactMessages } from '../memory/context';
21
+ import { logger } from '../shared/logger';
22
+ import { memoryManager } from '../memory/store';
23
+ import { eventBus } from '../shared/events';
24
+ import { resolveModel } from '../llm/providers';
25
+ import { jsonSchema } from '@ai-sdk/provider-utils';
26
+
27
+ // ── Interfaces ──
28
+
29
+ export interface SendMessageInput {
30
+ agentId: string;
31
+ sessionId?: string;
32
+ text: string;
33
+ channelId?: string;
34
+ stream?: boolean;
35
+ abortSignal?: AbortSignal;
36
+ onChunk?: OnChunkCallback;
37
+ maxIterations?: number;
38
+ }
39
+
40
+ export interface SendMessageResult {
41
+ sessionId: string;
42
+ content: string;
43
+ toolCalls: ToolCall[];
44
+ toolResults: ToolResultEntry[];
45
+ usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
46
+ durationMs: number;
47
+ iterations: number;
48
+ metrics?: AgentRunMetrics;
49
+ }
50
+
51
+ // ── Error categorization ──
52
+
53
+ function categorizeError(err: Error): CategorizedError {
54
+ const msg = err.message.toLowerCase();
55
+ const statusMatch = msg.match(/status[_ ]?(\d{3})/);
56
+ const status = statusMatch?.[1] ? parseInt(statusMatch[1]) : 0;
57
+
58
+ if (status === 429 || msg.includes('rate limit') || msg.includes('too many requests')) {
59
+ const retryAfter = msg.match(/retry[_-]?after[:\s]+(\d+)/);
60
+ return {
61
+ category: 'rate_limit',
62
+ message: err.message,
63
+ retryable: true,
64
+ retryAfterMs: retryAfter?.[1] ? parseInt(retryAfter[1]) * 1000 : 2000,
65
+ originalError: err,
66
+ };
67
+ }
68
+
69
+ if (status === 401 || status === 403 || msg.includes('auth') || msg.includes('api key') || msg.includes('forbidden')) {
70
+ return { category: 'authentication', message: err.message, retryable: false, originalError: err };
71
+ }
72
+
73
+ if (err.name === 'AbortError' || msg.includes('timeout') || msg.includes('timed out') || msg.includes('etimedout')) {
74
+ return { category: 'timeout', message: err.message, retryable: true, retryAfterMs: 1000, originalError: err };
75
+ }
76
+
77
+ if (status === 400 && (msg.includes('context') || msg.includes('token') || msg.includes('max'))) {
78
+ return { category: 'context_length', message: err.message, retryable: false, originalError: err };
79
+ }
80
+
81
+ if (status >= 500 || msg.includes('overloaded') || msg.includes('server error')) {
82
+ return { category: 'model_error', message: err.message, retryable: true, retryAfterMs: 3000, originalError: err };
83
+ }
84
+
85
+ return { category: 'unknown', message: err.message, retryable: false, originalError: err };
86
+ }
87
+
88
+ // ── Retry with error-aware backoff ──
89
+
90
+ async function retryWithBackoff<T>(
91
+ fn: () => Promise<T>,
92
+ maxRetries: number = 3,
93
+ ): Promise<T> {
94
+ let lastErr: CategorizedError | null = null;
95
+
96
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
97
+ try {
98
+ return await fn();
99
+ } catch (err) {
100
+ lastErr = categorizeError(err as Error);
101
+
102
+ if (!lastErr.retryable || attempt >= maxRetries) {
103
+ throw lastErr.originalError;
104
+ }
105
+
106
+ const delay = lastErr.retryAfterMs
107
+ ? lastErr.retryAfterMs * Math.pow(2, attempt)
108
+ : 1000 * Math.pow(2, attempt);
109
+
110
+ const jitter = Math.random() * 500;
111
+ const waitMs = Math.min(delay + jitter, 30_000);
112
+
113
+ logger.warn(
114
+ { category: lastErr.category, attempt: attempt + 1, waitMs: Math.round(waitMs), error: lastErr.message },
115
+ 'LLM call failed, retrying',
116
+ );
117
+
118
+ await new Promise((r) => setTimeout(r, waitMs));
119
+ }
120
+ }
121
+
122
+ throw lastErr!.originalError;
123
+ }
124
+
125
+ // ── Build AI SDK tools (v6 compatible) ──
126
+
127
+ function buildAiTools(
128
+ agentId: string,
129
+ sessionId: string,
130
+ signal?: AbortSignal,
131
+ ): Record<string, AiTool> {
132
+ const tools = toolRegistry.listForAgent(agentId);
133
+ const aiTools: Record<string, AiTool> = {};
134
+
135
+ for (const t of tools) {
136
+ aiTools[t.name] = {
137
+ description: t.description,
138
+ inputSchema: jsonSchema(t.parameters as any),
139
+ execute: async (args: Record<string, unknown>, { }: { toolCallId: string }) => {
140
+ if (signal?.aborted) return { error: 'Aborted' };
141
+ try {
142
+ const result = await executeTool(t, args, { agentId, sessionId, signal });
143
+ if (result.error) {
144
+ return { error: result.error, content: result.content };
145
+ }
146
+ return result.content;
147
+ } catch (err) {
148
+ return { error: (err as Error).message };
149
+ }
150
+ },
151
+ } as AiTool;
152
+ }
153
+
154
+ return aiTools;
155
+ }
156
+
157
+ // ── Helper to extract tool call info from v6 TypedToolCall ──
158
+
159
+ function extractToolCallInfo(tc: { toolCallId: string; toolName: string; input: unknown }): ToolCall {
160
+ return {
161
+ id: tc.toolCallId,
162
+ name: tc.toolName,
163
+ arguments: JSON.stringify(tc.input),
164
+ };
165
+ }
166
+
167
+ function extractUsage(usage: { inputTokens: number | undefined; outputTokens: number | undefined } | undefined): { promptTokens: number; completionTokens: number; totalTokens: number } {
168
+ const p = usage?.inputTokens ?? 0;
169
+ const c = usage?.outputTokens ?? 0;
170
+ return { promptTokens: p, completionTokens: c, totalTokens: p + c };
171
+ }
172
+
173
+ // ── Main sendMessage with proper agentic loop ──
174
+
175
+ export async function sendMessage(input: SendMessageInput): Promise<SendMessageResult> {
176
+ const agent = agentRegistry.get(input.agentId) || agentRegistry.getByName(input.agentId);
177
+ if (!agent) throw new Error(`Agent not found: ${input.agentId}`);
178
+ const agentId = agent.id;
179
+
180
+ agentRegistry.setState(agentId, 'processing');
181
+
182
+ const abortController = new AbortController();
183
+ const signal = input.abortSignal ?? abortController.signal;
184
+
185
+ // Link external abort signal
186
+ if (input.abortSignal) {
187
+ input.abortSignal.addEventListener('abort', () => abortController.abort(), { once: true });
188
+ }
189
+
190
+ try {
191
+ // Get or create session
192
+ let sessionId = input.sessionId ?? '';
193
+ if (!sessionId) {
194
+ const session = await sessionManager.create(agentId, input.channelId);
195
+ sessionId = session.id;
196
+ }
197
+
198
+ eventBus.emit('agent:processing', { agentId, sessionId, timestamp: Date.now() });
199
+
200
+ // Store user message
201
+ await sessionManager.addMessage({
202
+ sessionId,
203
+ agentId,
204
+ role: 'user',
205
+ content: input.text,
206
+ priority: 'high',
207
+ });
208
+
209
+ const startTime = Date.now();
210
+ const onChunk = input.onChunk;
211
+
212
+ // Build context from history
213
+ const history = await sessionManager.getMessagesAsCore(sessionId, 50);
214
+ let contextMessages: ModelMessage[] = history;
215
+ if (shouldCompact(history as any[])) {
216
+ const { messages: compacted } = compactMessages(history as any[]);
217
+ contextMessages = compacted as ModelMessage[];
218
+ logger.info({ sessionId }, 'Context compacted before inference');
219
+ }
220
+
221
+ // Ensure at least one user message
222
+ if (!contextMessages.some((m) => m.role === 'user')) {
223
+ contextMessages.push({ role: 'user', content: input.text });
224
+ }
225
+
226
+ const memoryContext = memoryManager.buildContext(agent.id);
227
+ const systemPrompt = memoryContext
228
+ ? `${agent.systemPrompt}\n\n${memoryContext}`
229
+ : agent.systemPrompt;
230
+
231
+ const maxIter = input.maxIterations || agent.maxIterations || 10;
232
+ const resolved = resolveModel(agent.model);
233
+
234
+ // Track metrics
235
+ const allToolCalls: ToolCall[] = [];
236
+ const allToolResults: ToolResultEntry[] = [];
237
+ let totalPromptTokens = 0;
238
+ let totalCompletionTokens = 0;
239
+ let iterations = 0;
240
+ let finalContent = '';
241
+
242
+ // ── Agentic loop ──
243
+ // We use a simple approach: call generateText with stopWhen: stepCountIs(maxIter)
244
+ // but with our own onStepFinish for parallel tool execution tracking.
245
+ // AI SDK v6 handles the multi-turn loop internally with stepCountIs.
246
+
247
+ const aiTools = buildAiTools(agent.id, sessionId, signal);
248
+ const hasTools = Object.keys(aiTools).length > 0;
249
+
250
+ try {
251
+ const agentId = agent.id;
252
+ const agentName = agent.name;
253
+ const result = await retryWithBackoff(() =>
254
+ generateText({
255
+ model: resolved.instance,
256
+ system: systemPrompt,
257
+ messages: contextMessages,
258
+ tools: hasTools ? aiTools : undefined,
259
+ stopWhen: hasTools ? stepCountIs(maxIter) : stepCountIs(1),
260
+ maxOutputTokens: 4096,
261
+ temperature: 0.7,
262
+ abortSignal: signal,
263
+ onStepFinish: (step) => {
264
+ // Track tool calls and results
265
+ for (const tc of step.toolCalls as any[]) {
266
+ const call = extractToolCallInfo(tc);
267
+ allToolCalls.push(call);
268
+ eventBus.emit('agent:tool-call', {
269
+ agentId: agent.id,
270
+ sessionId,
271
+ toolName: tc.toolName,
272
+ toolCallId: tc.toolCallId,
273
+ timestamp: Date.now(),
274
+ });
275
+ if (onChunk) {
276
+ onChunk({ type: 'tool_call', toolCall: call });
277
+ }
278
+ }
279
+ for (const tr of step.toolResults as any[]) {
280
+ const entry: ToolResultEntry = {
281
+ toolCallId: tr.toolCallId,
282
+ toolName: tr.toolName,
283
+ content: typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result),
284
+ };
285
+ allToolResults.push(entry);
286
+ if (onChunk) {
287
+ onChunk({ type: 'tool_result', toolResult: entry });
288
+ }
289
+ }
290
+ // Accumulate usage per step
291
+ if (step.usage) {
292
+ const u = extractUsage(step.usage);
293
+ totalPromptTokens += u.promptTokens;
294
+ totalCompletionTokens += u.completionTokens;
295
+ }
296
+ // Stream text from each step
297
+ if (step.text && onChunk) {
298
+ onChunk({ type: 'delta', content: step.text });
299
+ }
300
+ },
301
+ }),
302
+ );
303
+
304
+ // Final usage from result
305
+ if (result.usage) {
306
+ const u = extractUsage(result.usage as any);
307
+ // Use the final result usage as the most accurate
308
+ totalPromptTokens = u.promptTokens || totalPromptTokens;
309
+ totalCompletionTokens = u.completionTokens || totalCompletionTokens;
310
+ }
311
+
312
+ finalContent = result.text || '';
313
+ iterations = allToolCalls.length > 0 ? maxIter : 1; // approximate
314
+
315
+ // Stream final text
316
+ if (finalContent && onChunk) {
317
+ // Already streamed via onStepFinish, but ensure final is captured
318
+ }
319
+
320
+ } catch (err) {
321
+ const cat = categorizeError(err as Error);
322
+
323
+ // If context_length error, try compacting and retrying once
324
+ if (cat.category === 'context_length') {
325
+ logger.info({ sessionId }, 'Context length exceeded, forcing compaction');
326
+ const { messages: compacted } = compactMessages(history as any[], {
327
+ thresholdPercent: 0.5,
328
+ keepLastMessages: 10,
329
+ });
330
+ contextMessages = compacted as ModelMessage[];
331
+
332
+ const retryResult = await generateText({
333
+ model: resolved.instance,
334
+ system: systemPrompt,
335
+ messages: contextMessages,
336
+ tools: hasTools ? aiTools : undefined,
337
+ stopWhen: hasTools ? stepCountIs(maxIter) : stepCountIs(1),
338
+ maxOutputTokens: 4096,
339
+ temperature: 0.7,
340
+ abortSignal: signal,
341
+ });
342
+ finalContent = retryResult.text || '';
343
+ if (retryResult.usage) {
344
+ const u = extractUsage(retryResult.usage as any);
345
+ totalPromptTokens = u.promptTokens;
346
+ totalCompletionTokens = u.completionTokens;
347
+ }
348
+ } else if (agent.fallbackModel && (cat.category === 'model_error' || cat.category === 'rate_limit')) {
349
+ // Try fallback model
350
+ logger.warn({ fallback: agent.fallbackModel }, 'Trying fallback model');
351
+ const fallbackResolved = resolveModel(agent.fallbackModel);
352
+ const fbResult = await retryWithBackoff(() =>
353
+ generateText({
354
+ model: fallbackResolved.instance,
355
+ system: systemPrompt,
356
+ messages: contextMessages,
357
+ tools: hasTools ? aiTools : undefined,
358
+ stopWhen: hasTools ? stepCountIs(maxIter) : stepCountIs(1),
359
+ maxOutputTokens: 4096,
360
+ temperature: 0.7,
361
+ abortSignal: signal,
362
+ }),
363
+ );
364
+ finalContent = fbResult.text || '';
365
+ if (fbResult.usage) {
366
+ const u = extractUsage(fbResult.usage as any);
367
+ totalPromptTokens = u.promptTokens;
368
+ totalCompletionTokens = u.completionTokens;
369
+ }
370
+ } else {
371
+ throw err;
372
+ }
373
+ }
374
+
375
+ const durationMs = Date.now() - startTime;
376
+
377
+ // If empty after all iterations with tool calls, generate summary
378
+ if (!finalContent && allToolCalls.length > 0) {
379
+ logger.warn({ sessionId }, 'Empty response after tool usage, requesting summary');
380
+ try {
381
+ const summaryResult = await generateText({
382
+ model: resolved.instance,
383
+ system: systemPrompt,
384
+ messages: [
385
+ ...contextMessages,
386
+ { role: 'user' as const, content: 'Please summarize the results of the tool calls that were just executed.' },
387
+ ],
388
+ maxOutputTokens: 1024,
389
+ temperature: 0.5,
390
+ });
391
+ finalContent = summaryResult.text || 'I completed the requested actions but could not generate a summary.';
392
+ } catch {
393
+ finalContent = 'I completed the requested actions but could not generate a summary.';
394
+ }
395
+ } else if (!finalContent) {
396
+ finalContent = 'I processed your request but had no text response to share.';
397
+ }
398
+
399
+ const usage = {
400
+ promptTokens: totalPromptTokens,
401
+ completionTokens: totalCompletionTokens,
402
+ totalTokens: totalPromptTokens + totalCompletionTokens,
403
+ };
404
+
405
+ const metrics: AgentRunMetrics = {
406
+ agentId: agent.id,
407
+ sessionId,
408
+ model: agent.model,
409
+ durationMs,
410
+ iterations,
411
+ toolCallCount: allToolCalls.length,
412
+ ...usage,
413
+ };
414
+
415
+ // Emit token usage and metrics
416
+ eventBus.emit('token:usage', {
417
+ agentId: agent.id,
418
+ sessionId,
419
+ model: agent.model,
420
+ promptTokens: totalPromptTokens,
421
+ completionTokens: totalCompletionTokens,
422
+ timestamp: Date.now(),
423
+ });
424
+
425
+ // Store assistant response
426
+ await sessionManager.addMessage({
427
+ sessionId,
428
+ agentId: agent.id,
429
+ role: 'assistant',
430
+ content: finalContent,
431
+ toolCalls: allToolCalls.length > 0 ? allToolCalls : undefined,
432
+ toolResults: allToolResults.length > 0 ? allToolResults : undefined,
433
+ model: agent.model,
434
+ tokensIn: usage.promptTokens,
435
+ tokensOut: usage.completionTokens,
436
+ durationMs,
437
+ });
438
+
439
+ eventBus.emit('agent:response', {
440
+ agentId: agent.id,
441
+ sessionId,
442
+ content: finalContent,
443
+ durationMs,
444
+ tokensUsed: usage.totalTokens,
445
+ timestamp: Date.now(),
446
+ });
447
+
448
+ logger.info(
449
+ { agentId: agent.id, sessionId, iterations, toolCalls: allToolCalls.length, tokens: usage.totalTokens, durationMs },
450
+ 'Agent turn completed',
451
+ );
452
+
453
+ if (onChunk) {
454
+ onChunk({ type: 'done', usage });
455
+ }
456
+
457
+ return {
458
+ sessionId,
459
+ content: finalContent,
460
+ toolCalls: allToolCalls,
461
+ toolResults: allToolResults,
462
+ usage,
463
+ durationMs,
464
+ iterations,
465
+ metrics,
466
+ };
467
+ } catch (err) {
468
+ const cat = categorizeError(err as Error);
469
+ logger.error(
470
+ { agentId: agent.id, category: cat.category, error: (err as Error).message },
471
+ 'Agent runtime error',
472
+ );
473
+ agentRegistry.setState(agent.id, 'error');
474
+ eventBus.emit('agent:error', {
475
+ agentId: agent.id,
476
+ sessionId: input.sessionId || '',
477
+ error: (err as Error).message,
478
+ category: cat.category,
479
+ timestamp: Date.now(),
480
+ });
481
+ throw err;
482
+ } finally {
483
+ agentRegistry.setState(agent.id, 'idle');
484
+ eventBus.emit('agent:idle', { agentId: agent.id, timestamp: Date.now() });
485
+ }
486
+ }
487
+
488
+ // ── Import stepCountIs ──
489
+ import { stepCountIs } from 'ai';
490
+
491
+ // ── Streaming variant ──
492
+
493
+ export async function* streamMessage(input: SendMessageInput) {
494
+ const agent = agentRegistry.get(input.agentId) || agentRegistry.getByName(input.agentId);
495
+ if (!agent) throw new Error(`Agent not found: ${input.agentId}`);
496
+
497
+ let sessionId = input.sessionId;
498
+ if (!sessionId) {
499
+ const session = await sessionManager.create(agent.id, input.channelId);
500
+ sessionId = session.id;
501
+ }
502
+
503
+ await sessionManager.addMessage({
504
+ sessionId,
505
+ agentId: agent.id,
506
+ role: 'user',
507
+ content: input.text,
508
+ priority: 'high',
509
+ });
510
+
511
+ const history = await sessionManager.getMessagesAsCore(sessionId, 50);
512
+ let contextMessages = history;
513
+ if (shouldCompact(history as any[])) {
514
+ const { messages: compacted } = compactMessages(history as any[]);
515
+ contextMessages = compacted as ModelMessage[];
516
+ }
517
+
518
+ const resolved = resolveModel(agent.model);
519
+ const agentTools = toolRegistry.listForAgent(agent.id);
520
+ const memoryContext = memoryManager.buildContext(agent.id);
521
+ const systemPrompt = memoryContext
522
+ ? `${agent.systemPrompt}\n\n${memoryContext}`
523
+ : agent.systemPrompt;
524
+
525
+ const aiTools: Record<string, AiTool> = {};
526
+ for (const t of agentTools) {
527
+ aiTools[t.name] = {
528
+ description: t.description,
529
+ inputSchema: jsonSchema(t.parameters as any),
530
+ execute: async (args: Record<string, unknown>) => {
531
+ const result = await executeTool(t, args, { agentId: agent.id, sessionId, signal: input.abortSignal });
532
+ if (result.error) return { error: result.error, content: result.content };
533
+ return result.content;
534
+ },
535
+ } as AiTool;
536
+ }
537
+
538
+ const stream = streamText({
539
+ model: resolved.instance,
540
+ system: systemPrompt,
541
+ messages: contextMessages,
542
+ tools: Object.keys(aiTools).length > 0 ? aiTools : undefined,
543
+ maxOutputTokens: 4096,
544
+ abortSignal: input.abortSignal,
545
+ });
546
+
547
+ let fullContent = '';
548
+ for await (const chunk of stream.fullStream) {
549
+ if (chunk.type === 'text-delta') {
550
+ const text = (chunk as any).delta ?? (chunk as any).text ?? '';
551
+ fullContent += text;
552
+ yield { sessionId, chunk: { type: 'delta', content: text } };
553
+ if (input.onChunk) input.onChunk({ type: 'delta', content: text });
554
+ } else if (chunk.type === 'tool-call') {
555
+ yield {
556
+ sessionId,
557
+ chunk: {
558
+ type: 'tool_call',
559
+ toolCall: { id: chunk.toolCallId, name: chunk.toolName, arguments: JSON.stringify((chunk as any).input ?? (chunk as any).args) },
560
+ },
561
+ };
562
+ } else if (chunk.type === 'tool-result') {
563
+ yield {
564
+ sessionId,
565
+ chunk: {
566
+ type: 'tool_result',
567
+ toolResult: { toolCallId: (chunk as any).toolCallId, toolName: (chunk as any).toolName, content: String((chunk as any).result) },
568
+ },
569
+ };
570
+ } else if (chunk.type === 'finish') {
571
+ const totalUsage = (chunk as any).totalUsage;
572
+ if (totalUsage) {
573
+ eventBus.emit('token:usage', {
574
+ agentId: agent.id,
575
+ sessionId,
576
+ model: agent.model,
577
+ promptTokens: totalUsage.inputTokens ?? 0,
578
+ completionTokens: totalUsage.outputTokens ?? 0,
579
+ timestamp: Date.now(),
580
+ });
581
+ }
582
+ yield { sessionId, chunk: { type: 'done' } };
583
+ }
584
+ }
585
+
586
+ await sessionManager.addMessage({
587
+ sessionId,
588
+ agentId: agent.id,
589
+ role: 'assistant',
590
+ content: fullContent,
591
+ model: agent.model,
592
+ });
593
+ }