@agi-cli/server 0.1.58 → 0.1.61
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/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/openapi/spec.ts +641 -0
- package/src/runtime/agent-registry.ts +3 -2
- package/src/runtime/cache-optimizer.ts +93 -0
- package/src/runtime/context-optimizer.ts +206 -0
- package/src/runtime/db-operations.ts +173 -39
- package/src/runtime/history-truncator.ts +26 -0
- package/src/runtime/runner.ts +116 -240
- package/src/runtime/session-manager.ts +2 -0
- package/src/runtime/stream-handlers.ts +199 -184
- package/src/tools/adapter.ts +261 -173
package/src/tools/adapter.ts
CHANGED
|
@@ -39,15 +39,40 @@ function getPendingQueue(
|
|
|
39
39
|
return queue;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function adaptTools(
|
|
42
|
+
export function adaptTools(
|
|
43
|
+
tools: DiscoveredTool[],
|
|
44
|
+
ctx: ToolAdapterContext,
|
|
45
|
+
provider?: string,
|
|
46
|
+
) {
|
|
43
47
|
const out: Record<string, Tool> = {};
|
|
44
48
|
const pendingCalls = new Map<string, PendingCallMeta[]>();
|
|
45
49
|
let firstToolCallReported = false;
|
|
46
50
|
|
|
51
|
+
// Anthropic allows max 4 cache_control blocks
|
|
52
|
+
// Cache only the most frequently used tools: read, write, bash
|
|
53
|
+
const cacheableTools = new Set(['read', 'write', 'bash', 'edit']);
|
|
54
|
+
let cachedToolCount = 0;
|
|
55
|
+
|
|
47
56
|
for (const { name, tool } of tools) {
|
|
48
57
|
const base = tool;
|
|
58
|
+
|
|
59
|
+
// Add cache control for Anthropic to cache tool definitions (max 2 tools)
|
|
60
|
+
const shouldCache =
|
|
61
|
+
provider === 'anthropic' &&
|
|
62
|
+
cacheableTools.has(name) &&
|
|
63
|
+
cachedToolCount < 2;
|
|
64
|
+
|
|
65
|
+
if (shouldCache) {
|
|
66
|
+
cachedToolCount++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const providerOptions = shouldCache
|
|
70
|
+
? { anthropic: { cacheControl: { type: 'ephemeral' as const } } }
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
49
73
|
out[name] = {
|
|
50
74
|
...base,
|
|
75
|
+
...(providerOptions ? { providerOptions } : {}),
|
|
51
76
|
async onInputStart(options: unknown) {
|
|
52
77
|
const queue = getPendingQueue(pendingCalls, name);
|
|
53
78
|
queue.push({
|
|
@@ -185,194 +210,257 @@ export function adaptTools(tools: DiscoveredTool[], ctx: ToolAdapterContext) {
|
|
|
185
210
|
const callIdFromQueue = meta?.callId;
|
|
186
211
|
const startTsFromQueue = meta?.startTs;
|
|
187
212
|
const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
chunks
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Handle session-relative paths and cwd tools
|
|
216
|
+
let res: ToolExecuteReturn | { cwd: string } | null | undefined;
|
|
217
|
+
const cwd = getCwd(ctx.sessionId);
|
|
218
|
+
if (name === 'pwd') {
|
|
219
|
+
res = { cwd };
|
|
220
|
+
} else if (name === 'cd') {
|
|
221
|
+
const next = joinRelative(
|
|
222
|
+
cwd,
|
|
223
|
+
String((input as Record<string, unknown>)?.path ?? '.'),
|
|
224
|
+
);
|
|
225
|
+
setCwd(ctx.sessionId, next);
|
|
226
|
+
res = { cwd: next };
|
|
227
|
+
} else if (
|
|
228
|
+
['read', 'write', 'ls', 'tree'].includes(name) &&
|
|
229
|
+
typeof (input as Record<string, unknown>)?.path === 'string'
|
|
230
|
+
) {
|
|
231
|
+
const rel = joinRelative(
|
|
232
|
+
cwd,
|
|
233
|
+
String((input as Record<string, unknown>).path),
|
|
234
|
+
);
|
|
235
|
+
const nextInput = {
|
|
236
|
+
...(input as Record<string, unknown>),
|
|
237
|
+
path: rel,
|
|
238
|
+
} as ToolExecuteInput;
|
|
239
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
240
|
+
res = base.execute?.(nextInput, options as any);
|
|
241
|
+
} else if (name === 'bash') {
|
|
242
|
+
const needsCwd =
|
|
243
|
+
!input ||
|
|
244
|
+
typeof (input as Record<string, unknown>).cwd !== 'string';
|
|
245
|
+
const nextInput = needsCwd
|
|
246
|
+
? ({
|
|
247
|
+
...(input as Record<string, unknown>),
|
|
248
|
+
cwd,
|
|
249
|
+
} as ToolExecuteInput)
|
|
250
|
+
: input;
|
|
251
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
252
|
+
res = base.execute?.(nextInput, options as any);
|
|
253
|
+
} else {
|
|
254
|
+
// biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
|
|
255
|
+
res = base.execute?.(input, options as any);
|
|
256
|
+
}
|
|
257
|
+
let result: unknown = res;
|
|
258
|
+
// If tool returns an async iterable, stream deltas while accumulating
|
|
259
|
+
if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
|
|
260
|
+
const chunks: unknown[] = [];
|
|
261
|
+
for await (const chunk of res as AsyncIterable<unknown>) {
|
|
262
|
+
chunks.push(chunk);
|
|
263
|
+
publish({
|
|
264
|
+
type: 'tool.delta',
|
|
265
|
+
sessionId: ctx.sessionId,
|
|
266
|
+
payload: {
|
|
267
|
+
name,
|
|
268
|
+
channel: 'output',
|
|
269
|
+
delta: chunk,
|
|
270
|
+
stepIndex: stepIndexForEvent,
|
|
271
|
+
callId: callIdFromQueue,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Prefer the last chunk as the result if present, otherwise the entire array
|
|
276
|
+
result = chunks.length > 0 ? chunks[chunks.length - 1] : null;
|
|
277
|
+
} else {
|
|
278
|
+
// Await promise or passthrough value
|
|
279
|
+
result = await Promise.resolve(res as ToolExecuteReturn);
|
|
280
|
+
}
|
|
281
|
+
const resultPartId = crypto.randomUUID();
|
|
282
|
+
const callId = callIdFromQueue;
|
|
283
|
+
const startTs = startTsFromQueue;
|
|
284
|
+
const contentObj: {
|
|
285
|
+
name: string;
|
|
286
|
+
result: unknown;
|
|
287
|
+
callId?: string;
|
|
288
|
+
artifact?: unknown;
|
|
289
|
+
args?: unknown;
|
|
290
|
+
} = {
|
|
291
|
+
name,
|
|
292
|
+
result,
|
|
293
|
+
callId,
|
|
294
|
+
};
|
|
295
|
+
if (meta?.args !== undefined) {
|
|
296
|
+
contentObj.args = meta.args;
|
|
297
|
+
}
|
|
298
|
+
if (result && typeof result === 'object' && 'artifact' in result) {
|
|
299
|
+
try {
|
|
300
|
+
const maybeArtifact = (result as { artifact?: unknown }).artifact;
|
|
301
|
+
if (maybeArtifact !== undefined)
|
|
302
|
+
contentObj.artifact = maybeArtifact;
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const index = await ctx.nextIndex();
|
|
307
|
+
const endTs = Date.now();
|
|
308
|
+
const dur =
|
|
309
|
+
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
310
|
+
|
|
311
|
+
// Special-case: keep progress_update result lightweight; publish first, persist best-effort
|
|
312
|
+
if (name === 'progress_update') {
|
|
236
313
|
publish({
|
|
237
|
-
type: 'tool.
|
|
314
|
+
type: 'tool.result',
|
|
238
315
|
sessionId: ctx.sessionId,
|
|
239
|
-
payload: {
|
|
240
|
-
name,
|
|
241
|
-
channel: 'output',
|
|
242
|
-
delta: chunk,
|
|
243
|
-
stepIndex: stepIndexForEvent,
|
|
244
|
-
callId: callIdFromQueue,
|
|
245
|
-
},
|
|
316
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
246
317
|
});
|
|
318
|
+
// Persist without blocking the event loop
|
|
319
|
+
(async () => {
|
|
320
|
+
try {
|
|
321
|
+
await ctx.db.insert(messageParts).values({
|
|
322
|
+
id: resultPartId,
|
|
323
|
+
messageId: ctx.messageId,
|
|
324
|
+
index,
|
|
325
|
+
stepIndex: stepIndexForEvent,
|
|
326
|
+
type: 'tool_result',
|
|
327
|
+
content: JSON.stringify(contentObj),
|
|
328
|
+
agent: ctx.agent,
|
|
329
|
+
provider: ctx.provider,
|
|
330
|
+
model: ctx.model,
|
|
331
|
+
startedAt: startTs,
|
|
332
|
+
completedAt: endTs,
|
|
333
|
+
toolName: name,
|
|
334
|
+
toolCallId: callId,
|
|
335
|
+
toolDurationMs: dur ?? undefined,
|
|
336
|
+
});
|
|
337
|
+
} catch {}
|
|
338
|
+
})();
|
|
339
|
+
return result as ToolExecuteReturn;
|
|
247
340
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
callId,
|
|
267
|
-
};
|
|
268
|
-
if (meta?.args !== undefined) {
|
|
269
|
-
contentObj.args = meta.args;
|
|
270
|
-
}
|
|
271
|
-
if (result && typeof result === 'object' && 'artifact' in result) {
|
|
341
|
+
|
|
342
|
+
await ctx.db.insert(messageParts).values({
|
|
343
|
+
id: resultPartId,
|
|
344
|
+
messageId: ctx.messageId,
|
|
345
|
+
index,
|
|
346
|
+
stepIndex: stepIndexForEvent,
|
|
347
|
+
type: 'tool_result',
|
|
348
|
+
content: JSON.stringify(contentObj),
|
|
349
|
+
agent: ctx.agent,
|
|
350
|
+
provider: ctx.provider,
|
|
351
|
+
model: ctx.model,
|
|
352
|
+
startedAt: startTs,
|
|
353
|
+
completedAt: endTs,
|
|
354
|
+
toolName: name,
|
|
355
|
+
toolCallId: callId,
|
|
356
|
+
toolDurationMs: dur ?? undefined,
|
|
357
|
+
});
|
|
358
|
+
// Update session aggregates: total tool time and counts per tool
|
|
272
359
|
try {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
360
|
+
const sessRows = await ctx.db
|
|
361
|
+
.select()
|
|
362
|
+
.from(sessions)
|
|
363
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
364
|
+
if (sessRows.length) {
|
|
365
|
+
const row = sessRows[0] as typeof sessions.$inferSelect;
|
|
366
|
+
const totalToolTimeMs =
|
|
367
|
+
Number(row.totalToolTimeMs || 0) + (dur ?? 0);
|
|
368
|
+
let counts: Record<string, number> = {};
|
|
369
|
+
try {
|
|
370
|
+
counts = row.toolCountsJson
|
|
371
|
+
? JSON.parse(row.toolCountsJson)
|
|
372
|
+
: {};
|
|
373
|
+
} catch {}
|
|
374
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
375
|
+
await ctx.db
|
|
376
|
+
.update(sessions)
|
|
377
|
+
.set({
|
|
378
|
+
totalToolTimeMs,
|
|
379
|
+
toolCountsJson: JSON.stringify(counts),
|
|
380
|
+
lastActiveAt: endTs,
|
|
381
|
+
})
|
|
382
|
+
.where(eq(sessions.id, ctx.sessionId));
|
|
383
|
+
}
|
|
276
384
|
} catch {}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const index = await ctx.nextIndex();
|
|
280
|
-
const endTs = Date.now();
|
|
281
|
-
const dur =
|
|
282
|
-
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
283
|
-
|
|
284
|
-
// Special-case: keep progress_update result lightweight; publish first, persist best-effort
|
|
285
|
-
if (name === 'progress_update') {
|
|
286
385
|
publish({
|
|
287
386
|
type: 'tool.result',
|
|
288
387
|
sessionId: ctx.sessionId,
|
|
289
388
|
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
290
389
|
});
|
|
291
|
-
|
|
292
|
-
(async () => {
|
|
390
|
+
if (name === 'update_plan') {
|
|
293
391
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
startedAt: startTs,
|
|
305
|
-
completedAt: endTs,
|
|
306
|
-
toolName: name,
|
|
307
|
-
toolCallId: callId,
|
|
308
|
-
toolDurationMs: dur ?? undefined,
|
|
309
|
-
});
|
|
392
|
+
const result = (contentObj as { result?: unknown }).result as
|
|
393
|
+
| { items?: unknown; note?: unknown }
|
|
394
|
+
| undefined;
|
|
395
|
+
if (result && Array.isArray(result.items)) {
|
|
396
|
+
publish({
|
|
397
|
+
type: 'plan.updated',
|
|
398
|
+
sessionId: ctx.sessionId,
|
|
399
|
+
payload: { items: result.items, note: result.note },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
310
402
|
} catch {}
|
|
311
|
-
}
|
|
312
|
-
return result
|
|
313
|
-
}
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// Tool execution failed - save error to database as tool_result
|
|
407
|
+
const resultPartId = crypto.randomUUID();
|
|
408
|
+
const callId = callIdFromQueue;
|
|
409
|
+
const startTs = startTsFromQueue;
|
|
410
|
+
const endTs = Date.now();
|
|
411
|
+
const dur =
|
|
412
|
+
typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
|
|
314
413
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const sessRows = await ctx.db
|
|
334
|
-
.select()
|
|
335
|
-
.from(sessions)
|
|
336
|
-
.where(eq(sessions.id, ctx.sessionId));
|
|
337
|
-
if (sessRows.length) {
|
|
338
|
-
const row = sessRows[0] as typeof sessions.$inferSelect;
|
|
339
|
-
const totalToolTimeMs =
|
|
340
|
-
Number(row.totalToolTimeMs || 0) + (dur ?? 0);
|
|
341
|
-
let counts: Record<string, number> = {};
|
|
342
|
-
try {
|
|
343
|
-
counts = row.toolCountsJson ? JSON.parse(row.toolCountsJson) : {};
|
|
344
|
-
} catch {}
|
|
345
|
-
counts[name] = (counts[name] || 0) + 1;
|
|
346
|
-
await ctx.db
|
|
347
|
-
.update(sessions)
|
|
348
|
-
.set({
|
|
349
|
-
totalToolTimeMs,
|
|
350
|
-
toolCountsJson: JSON.stringify(counts),
|
|
351
|
-
lastActiveAt: endTs,
|
|
352
|
-
})
|
|
353
|
-
.where(eq(sessions.id, ctx.sessionId));
|
|
414
|
+
const errorMessage =
|
|
415
|
+
error instanceof Error ? error.message : String(error);
|
|
416
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
417
|
+
|
|
418
|
+
const errorResult = {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: errorMessage,
|
|
421
|
+
stack: errorStack,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const contentObj = {
|
|
425
|
+
name,
|
|
426
|
+
result: errorResult,
|
|
427
|
+
callId,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (meta?.args !== undefined) {
|
|
431
|
+
contentObj.args = meta.args;
|
|
354
432
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
433
|
+
|
|
434
|
+
const index = await ctx.nextIndex();
|
|
435
|
+
|
|
436
|
+
// Save error result to database
|
|
437
|
+
await ctx.db.insert(messageParts).values({
|
|
438
|
+
id: resultPartId,
|
|
439
|
+
messageId: ctx.messageId,
|
|
440
|
+
index,
|
|
441
|
+
stepIndex: stepIndexForEvent,
|
|
442
|
+
type: 'tool_result',
|
|
443
|
+
content: JSON.stringify(contentObj),
|
|
444
|
+
agent: ctx.agent,
|
|
445
|
+
provider: ctx.provider,
|
|
446
|
+
model: ctx.model,
|
|
447
|
+
startedAt: startTs,
|
|
448
|
+
completedAt: endTs,
|
|
449
|
+
toolName: name,
|
|
450
|
+
toolCallId: callId,
|
|
451
|
+
toolDurationMs: dur ?? undefined,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Publish error result
|
|
455
|
+
publish({
|
|
456
|
+
type: 'tool.result',
|
|
457
|
+
sessionId: ctx.sessionId,
|
|
458
|
+
payload: { ...contentObj, stepIndex: stepIndexForEvent },
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Re-throw so AI SDK can handle it
|
|
462
|
+
throw error;
|
|
374
463
|
}
|
|
375
|
-
return result;
|
|
376
464
|
},
|
|
377
465
|
} as Tool;
|
|
378
466
|
}
|