@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.
@@ -39,15 +39,40 @@ function getPendingQueue(
39
39
  return queue;
40
40
  }
41
41
 
42
- export function adaptTools(tools: DiscoveredTool[], ctx: ToolAdapterContext) {
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
- // Handle session-relative paths and cwd tools
189
- let res: ToolExecuteReturn | { cwd: string } | null | undefined;
190
- const cwd = getCwd(ctx.sessionId);
191
- if (name === 'pwd') {
192
- res = { cwd };
193
- } else if (name === 'cd') {
194
- const next = joinRelative(
195
- cwd,
196
- String((input as Record<string, unknown>)?.path ?? '.'),
197
- );
198
- setCwd(ctx.sessionId, next);
199
- res = { cwd: next };
200
- } else if (
201
- ['read', 'write', 'ls', 'tree'].includes(name) &&
202
- typeof (input as Record<string, unknown>)?.path === 'string'
203
- ) {
204
- const rel = joinRelative(
205
- cwd,
206
- String((input as Record<string, unknown>).path),
207
- );
208
- const nextInput = {
209
- ...(input as Record<string, unknown>),
210
- path: rel,
211
- } as ToolExecuteInput;
212
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
213
- res = base.execute?.(nextInput, options as any);
214
- } else if (name === 'bash') {
215
- const needsCwd =
216
- !input ||
217
- typeof (input as Record<string, unknown>).cwd !== 'string';
218
- const nextInput = needsCwd
219
- ? ({
220
- ...(input as Record<string, unknown>),
221
- cwd,
222
- } as ToolExecuteInput)
223
- : input;
224
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
225
- res = base.execute?.(nextInput, options as any);
226
- } else {
227
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
228
- res = base.execute?.(input, options as any);
229
- }
230
- let result: unknown = res;
231
- // If tool returns an async iterable, stream deltas while accumulating
232
- if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
233
- const chunks: unknown[] = [];
234
- for await (const chunk of res as AsyncIterable<unknown>) {
235
- chunks.push(chunk);
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.delta',
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
- // Prefer the last chunk as the result if present, otherwise the entire array
249
- result = chunks.length > 0 ? chunks[chunks.length - 1] : null;
250
- } else {
251
- // Await promise or passthrough value
252
- result = await Promise.resolve(res as ToolExecuteReturn);
253
- }
254
- const resultPartId = crypto.randomUUID();
255
- const callId = callIdFromQueue;
256
- const startTs = startTsFromQueue;
257
- const contentObj: {
258
- name: string;
259
- result: unknown;
260
- callId?: string;
261
- artifact?: unknown;
262
- args?: unknown;
263
- } = {
264
- name,
265
- result,
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 maybeArtifact = (result as { artifact?: unknown }).artifact;
274
- if (maybeArtifact !== undefined)
275
- contentObj.artifact = maybeArtifact;
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
- // Persist without blocking the event loop
292
- (async () => {
390
+ if (name === 'update_plan') {
293
391
  try {
294
- await ctx.db.insert(messageParts).values({
295
- id: resultPartId,
296
- messageId: ctx.messageId,
297
- index,
298
- stepIndex: stepIndexForEvent,
299
- type: 'tool_result',
300
- content: JSON.stringify(contentObj),
301
- agent: ctx.agent,
302
- provider: ctx.provider,
303
- model: ctx.model,
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 as ToolExecuteReturn;
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
- await ctx.db.insert(messageParts).values({
316
- id: resultPartId,
317
- messageId: ctx.messageId,
318
- index,
319
- stepIndex: stepIndexForEvent,
320
- type: 'tool_result',
321
- content: JSON.stringify(contentObj),
322
- agent: ctx.agent,
323
- provider: ctx.provider,
324
- model: ctx.model,
325
- startedAt: startTs,
326
- completedAt: endTs,
327
- toolName: name,
328
- toolCallId: callId,
329
- toolDurationMs: dur ?? undefined,
330
- });
331
- // Update session aggregates: total tool time and counts per tool
332
- try {
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
- } catch {}
356
- publish({
357
- type: 'tool.result',
358
- sessionId: ctx.sessionId,
359
- payload: { ...contentObj, stepIndex: stepIndexForEvent },
360
- });
361
- if (name === 'update_plan') {
362
- try {
363
- const result = (contentObj as { result?: unknown }).result as
364
- | { items?: unknown; note?: unknown }
365
- | undefined;
366
- if (result && Array.isArray(result.items)) {
367
- publish({
368
- type: 'plan.updated',
369
- sessionId: ctx.sessionId,
370
- payload: { items: result.items, note: result.note },
371
- });
372
- }
373
- } catch {}
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
  }