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.
- package/README.md +87 -0
- package/agents/001.yaml +32 -0
- package/agents/example.yaml +6 -0
- package/bin/anorion.js +8093 -0
- package/package.json +72 -0
- package/scripts/cli.ts +182 -0
- package/scripts/postinstall.js +6 -0
- package/scripts/setup.ts +255 -0
- package/src/agents/pipeline.ts +231 -0
- package/src/agents/registry.ts +153 -0
- package/src/agents/runtime.ts +593 -0
- package/src/agents/session.ts +338 -0
- package/src/agents/subagent.ts +185 -0
- package/src/bridge/client.ts +221 -0
- package/src/bridge/federator.ts +221 -0
- package/src/bridge/protocol.ts +88 -0
- package/src/bridge/server.ts +221 -0
- package/src/channels/base.ts +43 -0
- package/src/channels/router.ts +122 -0
- package/src/channels/telegram.ts +592 -0
- package/src/channels/webhook.ts +143 -0
- package/src/cli/index.ts +1036 -0
- package/src/cli/interactive.ts +26 -0
- package/src/gateway/routes-v2.ts +165 -0
- package/src/gateway/server.ts +512 -0
- package/src/gateway/ws.ts +75 -0
- package/src/index.ts +182 -0
- package/src/llm/provider.ts +243 -0
- package/src/llm/providers.ts +381 -0
- package/src/memory/context.ts +125 -0
- package/src/memory/store.ts +214 -0
- package/src/scheduler/cron.ts +239 -0
- package/src/shared/audit.ts +231 -0
- package/src/shared/config.ts +129 -0
- package/src/shared/db/index.ts +165 -0
- package/src/shared/db/prepared.ts +111 -0
- package/src/shared/db/schema.ts +84 -0
- package/src/shared/events.ts +79 -0
- package/src/shared/logger.ts +10 -0
- package/src/shared/metrics.ts +190 -0
- package/src/shared/rbac.ts +151 -0
- package/src/shared/token-budget.ts +157 -0
- package/src/shared/types.ts +166 -0
- package/src/tools/builtin/echo.ts +19 -0
- package/src/tools/builtin/file-read.ts +78 -0
- package/src/tools/builtin/file-write.ts +64 -0
- package/src/tools/builtin/http-request.ts +63 -0
- package/src/tools/builtin/memory.ts +71 -0
- package/src/tools/builtin/shell.ts +94 -0
- package/src/tools/builtin/web-search.ts +22 -0
- package/src/tools/executor.ts +126 -0
- package/src/tools/registry.ts +56 -0
- 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
|
+
}
|