@ottocode/server 0.1.173

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 (111) hide show
  1. package/package.json +42 -0
  2. package/src/events/bus.ts +43 -0
  3. package/src/events/types.ts +32 -0
  4. package/src/index.ts +281 -0
  5. package/src/openapi/helpers.ts +64 -0
  6. package/src/openapi/paths/ask.ts +70 -0
  7. package/src/openapi/paths/config.ts +218 -0
  8. package/src/openapi/paths/files.ts +72 -0
  9. package/src/openapi/paths/git.ts +457 -0
  10. package/src/openapi/paths/messages.ts +92 -0
  11. package/src/openapi/paths/sessions.ts +90 -0
  12. package/src/openapi/paths/setu.ts +154 -0
  13. package/src/openapi/paths/stream.ts +26 -0
  14. package/src/openapi/paths/terminals.ts +226 -0
  15. package/src/openapi/schemas.ts +345 -0
  16. package/src/openapi/spec.ts +49 -0
  17. package/src/presets.ts +85 -0
  18. package/src/routes/ask.ts +113 -0
  19. package/src/routes/auth.ts +592 -0
  20. package/src/routes/branch.ts +106 -0
  21. package/src/routes/config/agents.ts +44 -0
  22. package/src/routes/config/cwd.ts +21 -0
  23. package/src/routes/config/defaults.ts +45 -0
  24. package/src/routes/config/index.ts +16 -0
  25. package/src/routes/config/main.ts +73 -0
  26. package/src/routes/config/models.ts +139 -0
  27. package/src/routes/config/providers.ts +46 -0
  28. package/src/routes/config/utils.ts +120 -0
  29. package/src/routes/files.ts +218 -0
  30. package/src/routes/git/branch.ts +75 -0
  31. package/src/routes/git/commit.ts +209 -0
  32. package/src/routes/git/diff.ts +137 -0
  33. package/src/routes/git/index.ts +18 -0
  34. package/src/routes/git/push.ts +160 -0
  35. package/src/routes/git/schemas.ts +48 -0
  36. package/src/routes/git/staging.ts +208 -0
  37. package/src/routes/git/status.ts +83 -0
  38. package/src/routes/git/types.ts +31 -0
  39. package/src/routes/git/utils.ts +249 -0
  40. package/src/routes/openapi.ts +6 -0
  41. package/src/routes/research.ts +392 -0
  42. package/src/routes/root.ts +5 -0
  43. package/src/routes/session-approval.ts +63 -0
  44. package/src/routes/session-files.ts +387 -0
  45. package/src/routes/session-messages.ts +170 -0
  46. package/src/routes/session-stream.ts +61 -0
  47. package/src/routes/sessions.ts +814 -0
  48. package/src/routes/setu.ts +346 -0
  49. package/src/routes/terminals.ts +227 -0
  50. package/src/runtime/agent/registry.ts +351 -0
  51. package/src/runtime/agent/runner-reasoning.ts +108 -0
  52. package/src/runtime/agent/runner-setup.ts +257 -0
  53. package/src/runtime/agent/runner.ts +375 -0
  54. package/src/runtime/agent-registry.ts +6 -0
  55. package/src/runtime/ask/service.ts +369 -0
  56. package/src/runtime/context/environment.ts +202 -0
  57. package/src/runtime/debug/index.ts +117 -0
  58. package/src/runtime/debug/state.ts +140 -0
  59. package/src/runtime/errors/api-error.ts +192 -0
  60. package/src/runtime/errors/handling.ts +199 -0
  61. package/src/runtime/message/compaction-auto.ts +154 -0
  62. package/src/runtime/message/compaction-context.ts +101 -0
  63. package/src/runtime/message/compaction-detect.ts +26 -0
  64. package/src/runtime/message/compaction-limits.ts +37 -0
  65. package/src/runtime/message/compaction-mark.ts +111 -0
  66. package/src/runtime/message/compaction-prune.ts +75 -0
  67. package/src/runtime/message/compaction.ts +21 -0
  68. package/src/runtime/message/history-builder.ts +266 -0
  69. package/src/runtime/message/service.ts +468 -0
  70. package/src/runtime/message/tool-history-tracker.ts +204 -0
  71. package/src/runtime/prompt/builder.ts +167 -0
  72. package/src/runtime/provider/anthropic.ts +50 -0
  73. package/src/runtime/provider/copilot.ts +12 -0
  74. package/src/runtime/provider/google.ts +8 -0
  75. package/src/runtime/provider/index.ts +60 -0
  76. package/src/runtime/provider/moonshot.ts +8 -0
  77. package/src/runtime/provider/oauth-adapter.ts +237 -0
  78. package/src/runtime/provider/openai.ts +18 -0
  79. package/src/runtime/provider/opencode.ts +7 -0
  80. package/src/runtime/provider/openrouter.ts +7 -0
  81. package/src/runtime/provider/selection.ts +118 -0
  82. package/src/runtime/provider/setu.ts +126 -0
  83. package/src/runtime/provider/zai.ts +16 -0
  84. package/src/runtime/session/branch.ts +280 -0
  85. package/src/runtime/session/db-operations.ts +285 -0
  86. package/src/runtime/session/manager.ts +99 -0
  87. package/src/runtime/session/queue.ts +243 -0
  88. package/src/runtime/stream/abort-handler.ts +65 -0
  89. package/src/runtime/stream/error-handler.ts +371 -0
  90. package/src/runtime/stream/finish-handler.ts +101 -0
  91. package/src/runtime/stream/handlers.ts +5 -0
  92. package/src/runtime/stream/step-finish.ts +93 -0
  93. package/src/runtime/stream/types.ts +25 -0
  94. package/src/runtime/tools/approval.ts +180 -0
  95. package/src/runtime/tools/context.ts +83 -0
  96. package/src/runtime/tools/mapping.ts +154 -0
  97. package/src/runtime/tools/setup.ts +44 -0
  98. package/src/runtime/topup/manager.ts +110 -0
  99. package/src/runtime/utils/cwd.ts +69 -0
  100. package/src/runtime/utils/token.ts +35 -0
  101. package/src/tools/adapter.ts +634 -0
  102. package/src/tools/database/get-parent-session.ts +183 -0
  103. package/src/tools/database/get-session-context.ts +161 -0
  104. package/src/tools/database/index.ts +42 -0
  105. package/src/tools/database/present-session-links.ts +47 -0
  106. package/src/tools/database/query-messages.ts +160 -0
  107. package/src/tools/database/query-sessions.ts +126 -0
  108. package/src/tools/database/search-history.ts +135 -0
  109. package/src/types/sql-imports.d.ts +5 -0
  110. package/sst-env.d.ts +8 -0
  111. package/tsconfig.json +7 -0
@@ -0,0 +1,634 @@
1
+ import type { Tool } from 'ai';
2
+ import { messageParts, sessions } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { publish } from '../events/bus.ts';
5
+ import type { DiscoveredTool } from '@ottocode/sdk';
6
+ import { getCwd, setCwd, joinRelative } from '../runtime/utils/cwd.ts';
7
+ import type {
8
+ ToolAdapterContext,
9
+ StepExecutionState,
10
+ } from '../runtime/tools/context.ts';
11
+ import { isToolError } from '@ottocode/sdk/tools/error';
12
+ import {
13
+ toClaudeCodeName,
14
+ requiresClaudeCodeNaming,
15
+ } from '../runtime/tools/mapping.ts';
16
+ import {
17
+ requiresApproval,
18
+ requestApproval,
19
+ } from '../runtime/tools/approval.ts';
20
+
21
+ export type { ToolAdapterContext } from '../runtime/tools/context.ts';
22
+
23
+ type ToolExecuteSignature = Tool['execute'] extends (
24
+ input: infer Input,
25
+ options: infer Options,
26
+ ) => infer Result
27
+ ? { input: Input; options: Options; result: Result }
28
+ : { input: unknown; options: unknown; result: unknown };
29
+ type ToolExecuteInput = ToolExecuteSignature['input'];
30
+ type ToolExecuteOptions = ToolExecuteSignature['options'] extends never
31
+ ? undefined
32
+ : ToolExecuteSignature['options'];
33
+ type ToolExecuteReturn = ToolExecuteSignature['result'];
34
+
35
+ type PendingCallMeta = {
36
+ callId: string;
37
+ startTs: number;
38
+ stepIndex?: number;
39
+ args?: unknown;
40
+ approvalPromise?: Promise<boolean>;
41
+ };
42
+
43
+ function getPendingQueue(
44
+ map: Map<string, PendingCallMeta[]>,
45
+ name: string,
46
+ ): PendingCallMeta[] {
47
+ let queue = map.get(name);
48
+ if (!queue) {
49
+ queue = [];
50
+ map.set(name, queue);
51
+ }
52
+ return queue;
53
+ }
54
+
55
+ export function adaptTools(
56
+ tools: DiscoveredTool[],
57
+ ctx: ToolAdapterContext,
58
+ provider?: string,
59
+ authType?: string,
60
+ ) {
61
+ const out: Record<string, Tool> = {};
62
+ const pendingCalls = new Map<string, PendingCallMeta[]>();
63
+ const failureState: { active: boolean; toolName?: string } = {
64
+ active: false,
65
+ toolName: undefined,
66
+ };
67
+ let firstToolCallReported = false;
68
+
69
+ // Determine if we need Claude Code naming (PascalCase)
70
+ const useClaudeCodeNaming = requiresClaudeCodeNaming(
71
+ provider ?? '',
72
+ authType,
73
+ );
74
+
75
+ if (!ctx.stepExecution) {
76
+ ctx.stepExecution = { states: new Map<number, StepExecutionState>() };
77
+ }
78
+ const stepStates = ctx.stepExecution.states;
79
+
80
+ // Anthropic allows max 4 cache_control blocks
81
+ // Cache only the most frequently used tools: read, write, bash
82
+ const cacheableTools = new Set(['read', 'write', 'bash', 'edit']);
83
+ let cachedToolCount = 0;
84
+
85
+ for (const { name: canonicalName, tool } of tools) {
86
+ const base = tool;
87
+ // Use PascalCase for Claude Code OAuth, otherwise canonical (snake_case)
88
+ const registrationName = useClaudeCodeNaming
89
+ ? toClaudeCodeName(canonicalName)
90
+ : canonicalName;
91
+ // Always use canonical name for DB storage and events
92
+ const name = canonicalName;
93
+
94
+ const processedToolErrors = new WeakSet<object>();
95
+
96
+ const persistToolErrorResult = async (
97
+ errorResult: unknown,
98
+ {
99
+ callId,
100
+ startTs,
101
+ stepIndexForEvent,
102
+ args,
103
+ }: {
104
+ callId?: string;
105
+ startTs?: number;
106
+ stepIndexForEvent: number;
107
+ args?: unknown;
108
+ },
109
+ ) => {
110
+ const resultPartId = crypto.randomUUID();
111
+ const endTs = Date.now();
112
+ const dur =
113
+ typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
114
+
115
+ const contentObj: {
116
+ name: string;
117
+ result: unknown;
118
+ callId?: string;
119
+ args?: unknown;
120
+ } = {
121
+ name,
122
+ result: errorResult,
123
+ callId,
124
+ };
125
+
126
+ if (args !== undefined) {
127
+ contentObj.args = args;
128
+ }
129
+
130
+ const index = await ctx.nextIndex();
131
+
132
+ await ctx.db.insert(messageParts).values({
133
+ id: resultPartId,
134
+ messageId: ctx.messageId,
135
+ index,
136
+ stepIndex: stepIndexForEvent,
137
+ type: 'tool_result',
138
+ content: JSON.stringify(contentObj),
139
+ agent: ctx.agent,
140
+ provider: ctx.provider,
141
+ model: ctx.model,
142
+ startedAt: startTs,
143
+ completedAt: endTs,
144
+ toolName: name,
145
+ toolCallId: callId,
146
+ toolDurationMs: dur ?? undefined,
147
+ });
148
+
149
+ publish({
150
+ type: 'tool.result',
151
+ sessionId: ctx.sessionId,
152
+ payload: { ...contentObj, stepIndex: stepIndexForEvent },
153
+ });
154
+ };
155
+
156
+ // Add cache control for Anthropic to cache tool definitions (max 2 tools)
157
+ const shouldCache =
158
+ provider === 'anthropic' &&
159
+ cacheableTools.has(name) &&
160
+ cachedToolCount < 2;
161
+
162
+ if (shouldCache) {
163
+ cachedToolCount++;
164
+ }
165
+
166
+ const providerOptions = shouldCache
167
+ ? { anthropic: { cacheControl: { type: 'ephemeral' as const } } }
168
+ : undefined;
169
+
170
+ out[registrationName] = {
171
+ ...base,
172
+ ...(providerOptions ? { providerOptions } : {}),
173
+ async onInputStart(options: unknown) {
174
+ const queue = getPendingQueue(pendingCalls, name);
175
+ queue.push({
176
+ callId: crypto.randomUUID(),
177
+ startTs: Date.now(),
178
+ stepIndex: ctx.stepIndex,
179
+ });
180
+ if (typeof base.onInputStart === 'function')
181
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
182
+ await base.onInputStart(options as any);
183
+ },
184
+ async onInputDelta(options: unknown) {
185
+ const delta = (options as { inputTextDelta?: string } | undefined)
186
+ ?.inputTextDelta;
187
+ const queue = pendingCalls.get(name);
188
+ const meta = queue?.length ? queue[queue.length - 1] : undefined;
189
+ // Stream tool argument deltas as events if needed
190
+ publish({
191
+ type: 'tool.delta',
192
+ sessionId: ctx.sessionId,
193
+ payload: {
194
+ name,
195
+ channel: 'input',
196
+ delta,
197
+ stepIndex: meta?.stepIndex ?? ctx.stepIndex,
198
+ callId: meta?.callId,
199
+ messageId: ctx.messageId,
200
+ },
201
+ });
202
+ if (typeof base.onInputDelta === 'function')
203
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
204
+ await base.onInputDelta(options as any);
205
+ },
206
+ async onInputAvailable(options: unknown) {
207
+ const args = (options as { input?: unknown } | undefined)?.input;
208
+ const queue = getPendingQueue(pendingCalls, name);
209
+ let meta = queue.length ? queue[queue.length - 1] : undefined;
210
+ if (!meta) {
211
+ meta = {
212
+ callId: crypto.randomUUID(),
213
+ startTs: Date.now(),
214
+ stepIndex: ctx.stepIndex,
215
+ };
216
+ queue.push(meta);
217
+ }
218
+ meta.stepIndex = ctx.stepIndex;
219
+ meta.args = args;
220
+ const callId = meta.callId;
221
+ const callPartId = crypto.randomUUID();
222
+ const startTs = meta.startTs;
223
+
224
+ if (
225
+ !firstToolCallReported &&
226
+ typeof ctx.onFirstToolCall === 'function'
227
+ ) {
228
+ firstToolCallReported = true;
229
+ try {
230
+ ctx.onFirstToolCall();
231
+ } catch {}
232
+ }
233
+
234
+ // Special-case: progress updates must render instantly. Publish before any DB work.
235
+ if (name === 'progress_update') {
236
+ publish({
237
+ type: 'tool.call',
238
+ sessionId: ctx.sessionId,
239
+ payload: {
240
+ name,
241
+ args,
242
+ callId,
243
+ stepIndex: ctx.stepIndex,
244
+ messageId: ctx.messageId,
245
+ },
246
+ });
247
+ // Persist synchronously to maintain correct ordering
248
+ try {
249
+ const index = await ctx.nextIndex();
250
+ await ctx.db.insert(messageParts).values({
251
+ id: callPartId,
252
+ messageId: ctx.messageId,
253
+ index,
254
+ stepIndex: ctx.stepIndex,
255
+ type: 'tool_call',
256
+ content: JSON.stringify({ name, args, callId }),
257
+ agent: ctx.agent,
258
+ provider: ctx.provider,
259
+ model: ctx.model,
260
+ startedAt: startTs,
261
+ toolName: name,
262
+ toolCallId: callId,
263
+ });
264
+ } catch {}
265
+ if (typeof base.onInputAvailable === 'function') {
266
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
267
+ await base.onInputAvailable(options as any);
268
+ }
269
+ return;
270
+ }
271
+
272
+ // Publish promptly so UI shows the call header before results
273
+ publish({
274
+ type: 'tool.call',
275
+ sessionId: ctx.sessionId,
276
+ payload: {
277
+ name,
278
+ args,
279
+ callId,
280
+ stepIndex: ctx.stepIndex,
281
+ messageId: ctx.messageId,
282
+ },
283
+ });
284
+ // Persist synchronously to maintain correct ordering
285
+ try {
286
+ const index = await ctx.nextIndex();
287
+ await ctx.db.insert(messageParts).values({
288
+ id: callPartId,
289
+ messageId: ctx.messageId,
290
+ index,
291
+ stepIndex: ctx.stepIndex,
292
+ type: 'tool_call',
293
+ content: JSON.stringify({ name, args, callId }),
294
+ agent: ctx.agent,
295
+ provider: ctx.provider,
296
+ model: ctx.model,
297
+ startedAt: startTs,
298
+ toolName: name,
299
+ toolCallId: callId,
300
+ });
301
+ } catch {}
302
+ // Start approval request with full args
303
+ if (
304
+ ctx.toolApprovalMode &&
305
+ requiresApproval(name, ctx.toolApprovalMode)
306
+ ) {
307
+ meta.approvalPromise = requestApproval(
308
+ ctx.sessionId,
309
+ ctx.messageId,
310
+ callId,
311
+ name,
312
+ args,
313
+ );
314
+ }
315
+ if (typeof base.onInputAvailable === 'function') {
316
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
317
+ await base.onInputAvailable(options as any);
318
+ }
319
+ },
320
+ async execute(input: ToolExecuteInput, options: ToolExecuteOptions) {
321
+ const queue = pendingCalls.get(name);
322
+ const meta = queue?.shift();
323
+ if (queue && queue.length === 0) pendingCalls.delete(name);
324
+ const callIdFromQueue = meta?.callId;
325
+ const startTsFromQueue = meta?.startTs;
326
+ const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
327
+
328
+ const stepKey =
329
+ typeof stepIndexForEvent === 'number' &&
330
+ Number.isFinite(stepIndexForEvent)
331
+ ? stepIndexForEvent
332
+ : 0;
333
+ let stepState = stepStates.get(stepKey);
334
+ if (!stepState) {
335
+ stepState = {
336
+ chain: Promise.resolve(),
337
+ failed: false,
338
+ failedToolName: undefined,
339
+ };
340
+ stepStates.set(stepKey, stepState);
341
+ }
342
+
343
+ const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
344
+ try {
345
+ // Await approval if it was requested in onInputAvailable
346
+ if (meta?.approvalPromise) {
347
+ const approved = await meta.approvalPromise;
348
+ if (!approved) {
349
+ return {
350
+ ok: false,
351
+ error: 'Tool execution rejected by user',
352
+ } as ToolExecuteReturn;
353
+ }
354
+ }
355
+ // Handle session-relative paths and cwd tools
356
+ let res: ToolExecuteReturn | { cwd: string } | null | undefined;
357
+ const cwd = getCwd(ctx.sessionId);
358
+ if (name === 'pwd') {
359
+ res = { cwd };
360
+ } else if (name === 'cd') {
361
+ const next = joinRelative(
362
+ cwd,
363
+ String((input as Record<string, unknown>)?.path ?? '.'),
364
+ );
365
+ setCwd(ctx.sessionId, next);
366
+ res = { cwd: next };
367
+ } else if (
368
+ ['read', 'write', 'ls', 'tree'].includes(name) &&
369
+ typeof (input as Record<string, unknown>)?.path === 'string'
370
+ ) {
371
+ const rel = joinRelative(
372
+ cwd,
373
+ String((input as Record<string, unknown>).path),
374
+ );
375
+ const nextInput = {
376
+ ...(input as Record<string, unknown>),
377
+ path: rel,
378
+ } as ToolExecuteInput;
379
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
380
+ res = base.execute?.(nextInput, options as any);
381
+ } else if (name === 'bash') {
382
+ const needsCwd =
383
+ !input ||
384
+ typeof (input as Record<string, unknown>).cwd !== 'string';
385
+ const nextInput = needsCwd
386
+ ? ({
387
+ ...(input as Record<string, unknown>),
388
+ cwd,
389
+ } as ToolExecuteInput)
390
+ : input;
391
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
392
+ res = base.execute?.(nextInput, options as any);
393
+ } else {
394
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
395
+ res = base.execute?.(input, options as any);
396
+ }
397
+ let result: unknown = res;
398
+ // If tool returns an async iterable, stream deltas while accumulating
399
+ if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
400
+ const chunks: unknown[] = [];
401
+ for await (const chunk of res as AsyncIterable<unknown>) {
402
+ chunks.push(chunk);
403
+ publish({
404
+ type: 'tool.delta',
405
+ sessionId: ctx.sessionId,
406
+ payload: {
407
+ name,
408
+ channel: 'output',
409
+ delta: chunk,
410
+ stepIndex: stepIndexForEvent,
411
+ callId: callIdFromQueue,
412
+ messageId: ctx.messageId,
413
+ },
414
+ });
415
+ }
416
+ // Prefer the last chunk as the result if present, otherwise the entire array
417
+ result = chunks.length > 0 ? chunks[chunks.length - 1] : null;
418
+ } else {
419
+ // Await promise or passthrough value
420
+ result = await Promise.resolve(res as ToolExecuteReturn);
421
+ }
422
+
423
+ if (isToolError(result)) {
424
+ stepState.failed = true;
425
+ stepState.failedToolName = name;
426
+ failureState.active = true;
427
+ failureState.toolName = name;
428
+
429
+ await persistToolErrorResult(result, {
430
+ callId: callIdFromQueue,
431
+ startTs: startTsFromQueue,
432
+ stepIndexForEvent,
433
+ args: meta?.args,
434
+ });
435
+ processedToolErrors.add(result as object);
436
+ return result as ToolExecuteReturn;
437
+ }
438
+
439
+ const resultPartId = crypto.randomUUID();
440
+ const callId = callIdFromQueue;
441
+ const startTs = startTsFromQueue;
442
+ const contentObj: {
443
+ name: string;
444
+ result: unknown;
445
+ callId?: string;
446
+ artifact?: unknown;
447
+ args?: unknown;
448
+ } = {
449
+ name,
450
+ result,
451
+ callId,
452
+ };
453
+ if (meta?.args !== undefined) {
454
+ contentObj.args = meta.args;
455
+ }
456
+ if (result && typeof result === 'object' && 'artifact' in result) {
457
+ try {
458
+ const maybeArtifact = (result as { artifact?: unknown })
459
+ .artifact;
460
+ if (maybeArtifact !== undefined)
461
+ contentObj.artifact = maybeArtifact;
462
+ } catch {}
463
+ }
464
+
465
+ const index = await ctx.nextIndex();
466
+ const endTs = Date.now();
467
+ const dur =
468
+ typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
469
+
470
+ // Special-case: keep progress_update result lightweight; publish first, persist best-effort
471
+ if (name === 'progress_update') {
472
+ stepState.failed = false;
473
+ stepState.failedToolName = undefined;
474
+ if (failureState.active && failureState.toolName === name) {
475
+ failureState.active = false;
476
+ failureState.toolName = undefined;
477
+ }
478
+ publish({
479
+ type: 'tool.result',
480
+ sessionId: ctx.sessionId,
481
+ payload: { ...contentObj, stepIndex: stepIndexForEvent },
482
+ });
483
+ // Persist without blocking the event loop
484
+ (async () => {
485
+ try {
486
+ await ctx.db.insert(messageParts).values({
487
+ id: resultPartId,
488
+ messageId: ctx.messageId,
489
+ index,
490
+ stepIndex: stepIndexForEvent,
491
+ type: 'tool_result',
492
+ content: JSON.stringify(contentObj),
493
+ agent: ctx.agent,
494
+ provider: ctx.provider,
495
+ model: ctx.model,
496
+ startedAt: startTs,
497
+ completedAt: endTs,
498
+ toolName: name,
499
+ toolCallId: callId,
500
+ toolDurationMs: dur ?? undefined,
501
+ });
502
+ } catch {}
503
+ })();
504
+ return result as ToolExecuteReturn;
505
+ }
506
+
507
+ stepState.failed = false;
508
+ stepState.failedToolName = undefined;
509
+ if (failureState.active && failureState.toolName === name) {
510
+ failureState.active = false;
511
+ failureState.toolName = undefined;
512
+ }
513
+
514
+ await ctx.db.insert(messageParts).values({
515
+ id: resultPartId,
516
+ messageId: ctx.messageId,
517
+ index,
518
+ stepIndex: stepIndexForEvent,
519
+ type: 'tool_result',
520
+ content: JSON.stringify(contentObj),
521
+ agent: ctx.agent,
522
+ provider: ctx.provider,
523
+ model: ctx.model,
524
+ startedAt: startTs,
525
+ completedAt: endTs,
526
+ toolName: name,
527
+ toolCallId: callId,
528
+ toolDurationMs: dur ?? undefined,
529
+ });
530
+ // Update session aggregates: total tool time and counts per tool
531
+ try {
532
+ const sessRows = await ctx.db
533
+ .select()
534
+ .from(sessions)
535
+ .where(eq(sessions.id, ctx.sessionId));
536
+ if (sessRows.length) {
537
+ const row = sessRows[0] as typeof sessions.$inferSelect;
538
+ const totalToolTimeMs =
539
+ Number(row.totalToolTimeMs || 0) + (dur ?? 0);
540
+ let counts: Record<string, number> = {};
541
+ try {
542
+ counts = row.toolCountsJson
543
+ ? JSON.parse(row.toolCountsJson)
544
+ : {};
545
+ } catch {}
546
+ counts[name] = (counts[name] || 0) + 1;
547
+ await ctx.db
548
+ .update(sessions)
549
+ .set({
550
+ totalToolTimeMs,
551
+ toolCountsJson: JSON.stringify(counts),
552
+ lastActiveAt: endTs,
553
+ })
554
+ .where(eq(sessions.id, ctx.sessionId));
555
+ }
556
+ } catch {}
557
+ publish({
558
+ type: 'tool.result',
559
+ sessionId: ctx.sessionId,
560
+ payload: { ...contentObj, stepIndex: stepIndexForEvent },
561
+ });
562
+ if (name === 'update_todos') {
563
+ try {
564
+ const resultValue = (contentObj as { result?: unknown })
565
+ .result as { items?: unknown; note?: unknown } | undefined;
566
+ if (resultValue && Array.isArray(resultValue.items)) {
567
+ publish({
568
+ type: 'plan.updated',
569
+ sessionId: ctx.sessionId,
570
+ payload: {
571
+ items: resultValue.items,
572
+ note: resultValue.note,
573
+ },
574
+ });
575
+ }
576
+ } catch {}
577
+ }
578
+ return result as ToolExecuteReturn;
579
+ } catch (error) {
580
+ stepState.failed = true;
581
+ stepState.failedToolName = name;
582
+ failureState.active = true;
583
+ failureState.toolName = name;
584
+
585
+ // Tool execution failed
586
+ if (
587
+ isToolError(error) &&
588
+ processedToolErrors.has(error as object)
589
+ ) {
590
+ throw error;
591
+ }
592
+
593
+ const errorResult = isToolError(error)
594
+ ? error
595
+ : (() => {
596
+ const errorMessage =
597
+ error instanceof Error ? error.message : String(error);
598
+ const errorStack =
599
+ error instanceof Error ? error.stack : undefined;
600
+ return {
601
+ ok: false,
602
+ error: errorMessage,
603
+ stack: errorStack,
604
+ };
605
+ })();
606
+
607
+ await persistToolErrorResult(errorResult, {
608
+ callId: callIdFromQueue,
609
+ startTs: startTsFromQueue,
610
+ stepIndexForEvent,
611
+ args: meta?.args,
612
+ });
613
+
614
+ if (isToolError(error)) {
615
+ processedToolErrors.add(error as object);
616
+ }
617
+
618
+ return errorResult as ToolExecuteReturn;
619
+ }
620
+ };
621
+
622
+ const queued = stepState.chain
623
+ .catch(() => undefined)
624
+ .then(() => executeWithGuards());
625
+ stepState.chain = queued.then(
626
+ () => undefined,
627
+ () => undefined,
628
+ );
629
+ return queued;
630
+ },
631
+ } as Tool;
632
+ }
633
+ return out;
634
+ }