@ottocode/server 0.1.265 → 0.1.266

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 (72) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/provider/custom.ts +73 -0
  64. package/src/runtime/provider/index.ts +2 -85
  65. package/src/runtime/provider/reasoning-builders.ts +280 -0
  66. package/src/runtime/provider/reasoning.ts +67 -264
  67. package/src/tools/adapter/events.ts +116 -0
  68. package/src/tools/adapter/execution.ts +160 -0
  69. package/src/tools/adapter/pending.ts +37 -0
  70. package/src/tools/adapter/persistence.ts +166 -0
  71. package/src/tools/adapter/results.ts +97 -0
  72. package/src/tools/adapter.ts +124 -451
@@ -1,9 +1,5 @@
1
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 { logger, type DiscoveredTool } from '@ottocode/sdk';
6
- import { getCwd, setCwd, joinRelative } from '../runtime/utils/cwd.ts';
2
+ import type { DiscoveredTool } from '@ottocode/sdk';
7
3
  import type {
8
4
  ToolAdapterContext,
9
5
  StepExecutionState,
@@ -19,6 +15,37 @@ import {
19
15
  skipsGuardApproval,
20
16
  } from '../runtime/tools/approval.ts';
21
17
  import { guardToolCall } from '../runtime/tools/guards.ts';
18
+ import { consumeToolStream, executeBaseTool } from './adapter/execution.ts';
19
+ import {
20
+ logToolCall,
21
+ logToolResult,
22
+ publishPlanUpdated,
23
+ publishToolCall,
24
+ publishToolDelta,
25
+ publishToolResult,
26
+ } from './adapter/events.ts';
27
+ import {
28
+ computeToolTiming,
29
+ persistToolCall,
30
+ persistToolErrorResult,
31
+ persistToolResultWithIndex,
32
+ updateToolSessionStats,
33
+ } from './adapter/persistence.ts';
34
+ import {
35
+ extractToolCallId,
36
+ getPendingQueue,
37
+ shiftPendingCall,
38
+ type PendingCallMeta,
39
+ } from './adapter/pending.ts';
40
+ import {
41
+ buildToolResultContent,
42
+ createBlockedToolResult,
43
+ createRejectedToolResult,
44
+ createToolExceptionResult,
45
+ markToolFailed,
46
+ markToolSucceeded,
47
+ type ToolFailureState,
48
+ } from './adapter/results.ts';
22
49
 
23
50
  export type { ToolAdapterContext } from '../runtime/tools/context.ts';
24
51
 
@@ -34,57 +61,6 @@ type ToolExecuteOptions = ToolExecuteSignature['options'] extends never
34
61
  : ToolExecuteSignature['options'];
35
62
  type ToolExecuteReturn = ToolExecuteSignature['result'];
36
63
 
37
- type PendingCallMeta = {
38
- callId: string;
39
- startTs: number;
40
- stepIndex?: number;
41
- args?: unknown;
42
- approvalPromise?: Promise<boolean>;
43
- blocked?: boolean;
44
- blockReason?: string;
45
- };
46
-
47
- function getPendingQueue(
48
- map: Map<string, PendingCallMeta[]>,
49
- name: string,
50
- ): PendingCallMeta[] {
51
- let queue = map.get(name);
52
- if (!queue) {
53
- queue = [];
54
- map.set(name, queue);
55
- }
56
- return queue;
57
- }
58
-
59
- function extractToolCallId(options: unknown): string | undefined {
60
- return (options as { toolCallId?: string } | undefined)?.toolCallId;
61
- }
62
-
63
- const DEFAULT_TRACED_TOOL_INPUTS = new Set([
64
- 'write',
65
- 'edit',
66
- 'multiedit',
67
- 'copy_into',
68
- 'apply_patch',
69
- ]);
70
-
71
- function shouldTraceToolInput(name: string): boolean {
72
- void DEFAULT_TRACED_TOOL_INPUTS;
73
- void name;
74
- return false;
75
- }
76
-
77
- function summarizeTraceValue(value: unknown, max = 160): string {
78
- try {
79
- const json = JSON.stringify(value);
80
- if (typeof json === 'string') {
81
- return json.length > max ? `${json.slice(0, max)}…` : json;
82
- }
83
- } catch {}
84
- const fallback = String(value);
85
- return fallback.length > max ? `${fallback.slice(0, max)}…` : fallback;
86
- }
87
-
88
64
  function unwrapDoubleWrappedArgs(
89
65
  input: unknown,
90
66
  expectedName: string,
@@ -117,7 +93,7 @@ export function adaptTools(
117
93
  ) {
118
94
  const out: Record<string, Tool> = {};
119
95
  const pendingCalls = new Map<string, PendingCallMeta[]>();
120
- const failureState: { active: boolean; toolName?: string } = {
96
+ const failureState: ToolFailureState = {
121
97
  active: false,
122
98
  toolName: undefined,
123
99
  };
@@ -150,67 +126,6 @@ export function adaptTools(
150
126
 
151
127
  const processedToolErrors = new WeakSet<object>();
152
128
 
153
- const persistToolErrorResult = async (
154
- errorResult: unknown,
155
- {
156
- callId,
157
- startTs,
158
- stepIndexForEvent,
159
- args,
160
- }: {
161
- callId?: string;
162
- startTs?: number;
163
- stepIndexForEvent?: number;
164
- args?: unknown;
165
- },
166
- ) => {
167
- const resultPartId = crypto.randomUUID();
168
- const endTs = Date.now();
169
- const effectiveStepIndex = stepIndexForEvent ?? ctx.stepIndex;
170
- const dur =
171
- typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
172
-
173
- const contentObj: {
174
- name: string;
175
- result: unknown;
176
- callId?: string;
177
- args?: unknown;
178
- } = {
179
- name,
180
- result: errorResult,
181
- callId,
182
- };
183
-
184
- if (args !== undefined) {
185
- contentObj.args = args;
186
- }
187
-
188
- const index = await ctx.nextIndex();
189
-
190
- await ctx.db.insert(messageParts).values({
191
- id: resultPartId,
192
- messageId: ctx.messageId,
193
- index,
194
- stepIndex: effectiveStepIndex,
195
- type: 'tool_result',
196
- content: JSON.stringify(contentObj),
197
- agent: ctx.agent,
198
- provider: ctx.provider,
199
- model: ctx.model,
200
- startedAt: startTs,
201
- completedAt: endTs,
202
- toolName: name,
203
- toolCallId: callId,
204
- toolDurationMs: dur ?? undefined,
205
- });
206
-
207
- publish({
208
- type: 'tool.result',
209
- sessionId: ctx.sessionId,
210
- payload: { ...contentObj, stepIndex: effectiveStepIndex },
211
- });
212
- };
213
-
214
129
  // Add cache control for Anthropic to cache tool definitions (max 2 tools)
215
130
  const shouldCache =
216
131
  provider === 'anthropic' &&
@@ -236,9 +151,6 @@ export function adaptTools(
236
151
  startTs: Date.now(),
237
152
  stepIndex: ctx.stepIndex,
238
153
  });
239
- if (shouldTraceToolInput(name)) {
240
- void (sdkCallId ?? queue[queue.length - 1]?.callId ?? 'unknown');
241
- }
242
154
  if (typeof base.onInputStart === 'function')
243
155
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
244
156
  await base.onInputStart(options as any);
@@ -246,25 +158,15 @@ export function adaptTools(
246
158
  async onInputDelta(options: unknown) {
247
159
  const delta = (options as { inputTextDelta?: string } | undefined)
248
160
  ?.inputTextDelta;
249
- const sdkCallId = extractToolCallId(options);
250
161
  const queue = pendingCalls.get(name);
251
162
  const meta = queue?.length ? queue[queue.length - 1] : undefined;
252
- if (shouldTraceToolInput(name)) {
253
- void (sdkCallId ?? meta?.callId ?? 'unknown');
254
- void summarizeTraceValue(delta ?? '');
255
- }
256
163
  // Stream tool argument deltas as events if needed
257
- publish({
258
- type: 'tool.delta',
259
- sessionId: ctx.sessionId,
260
- payload: {
261
- name,
262
- channel: 'input',
263
- delta,
264
- stepIndex: meta?.stepIndex ?? ctx.stepIndex,
265
- callId: meta?.callId,
266
- messageId: ctx.messageId,
267
- },
164
+ publishToolDelta(ctx, {
165
+ name,
166
+ channel: 'input',
167
+ delta,
168
+ stepIndex: meta?.stepIndex ?? ctx.stepIndex,
169
+ callId: meta?.callId,
268
170
  });
269
171
  if (typeof base.onInputDelta === 'function')
270
172
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
@@ -291,10 +193,6 @@ export function adaptTools(
291
193
  const callId = meta.callId;
292
194
  const callPartId = crypto.randomUUID();
293
195
  const startTs = meta.startTs;
294
- if (shouldTraceToolInput(name)) {
295
- void callId;
296
- void summarizeTraceValue(args);
297
- }
298
196
 
299
197
  if (
300
198
  !firstToolCallReported &&
@@ -308,40 +206,22 @@ export function adaptTools(
308
206
 
309
207
  // Special-case: progress updates must render instantly. Publish before any DB work.
310
208
  if (name === 'progress_update') {
311
- publish({
312
- type: 'tool.call',
313
- sessionId: ctx.sessionId,
314
- payload: {
315
- name,
316
- args,
317
- callId,
318
- stepIndex: ctx.stepIndex,
319
- messageId: ctx.messageId,
320
- },
321
- });
322
- logger.debug(`[tools] call ${name}`, {
323
- sessionId: ctx.sessionId,
324
- messageId: ctx.messageId,
325
- toolName: name,
209
+ publishToolCall(ctx, {
210
+ name,
211
+ input: args,
326
212
  callId,
327
213
  stepIndex: ctx.stepIndex,
328
214
  });
215
+ logToolCall(ctx, { name, callId, stepIndex: ctx.stepIndex });
329
216
  // Persist synchronously to maintain correct ordering
330
217
  try {
331
- const index = await ctx.nextIndex();
332
- await ctx.db.insert(messageParts).values({
333
- id: callPartId,
334
- messageId: ctx.messageId,
335
- index,
218
+ await persistToolCall(ctx, {
219
+ partId: callPartId,
220
+ name,
221
+ input: args,
222
+ callId,
223
+ startTs,
336
224
  stepIndex: ctx.stepIndex,
337
- type: 'tool_call',
338
- content: JSON.stringify({ name, args, callId }),
339
- agent: ctx.agent,
340
- provider: ctx.provider,
341
- model: ctx.model,
342
- startedAt: startTs,
343
- toolName: name,
344
- toolCallId: callId,
345
225
  });
346
226
  } catch {}
347
227
  if (typeof base.onInputAvailable === 'function') {
@@ -352,33 +232,21 @@ export function adaptTools(
352
232
  }
353
233
 
354
234
  // Publish promptly so UI shows the call header before results
355
- publish({
356
- type: 'tool.call',
357
- sessionId: ctx.sessionId,
358
- payload: {
359
- name,
360
- args,
361
- callId,
362
- stepIndex: ctx.stepIndex,
363
- messageId: ctx.messageId,
364
- },
235
+ publishToolCall(ctx, {
236
+ name,
237
+ input: args,
238
+ callId,
239
+ stepIndex: ctx.stepIndex,
365
240
  });
366
241
  // Persist synchronously to maintain correct ordering
367
242
  try {
368
- const index = await ctx.nextIndex();
369
- await ctx.db.insert(messageParts).values({
370
- id: callPartId,
371
- messageId: ctx.messageId,
372
- index,
243
+ await persistToolCall(ctx, {
244
+ partId: callPartId,
245
+ name,
246
+ input: args,
247
+ callId,
248
+ startTs,
373
249
  stepIndex: ctx.stepIndex,
374
- type: 'tool_call',
375
- content: JSON.stringify({ name, args, callId }),
376
- agent: ctx.agent,
377
- provider: ctx.provider,
378
- model: ctx.model,
379
- startedAt: startTs,
380
- toolName: name,
381
- toolCallId: callId,
382
250
  });
383
251
  } catch {}
384
252
  // Start approval request with full args
@@ -421,9 +289,7 @@ export function adaptTools(
421
289
  async execute(input: ToolExecuteInput, options: ToolExecuteOptions) {
422
290
  input = unwrapDoubleWrappedArgs(input, name);
423
291
  const sdkCallId = extractToolCallId(options);
424
- const queue = pendingCalls.get(name);
425
- const meta = queue?.shift();
426
- if (queue && queue.length === 0) pendingCalls.delete(name);
292
+ const meta = shiftPendingCall(pendingCalls, name);
427
293
  const callIdFromQueue = sdkCallId || meta?.callId;
428
294
  const startTsFromQueue = meta?.startTs;
429
295
  const stepIndexForEvent = meta?.stepIndex ?? ctx.stepIndex;
@@ -446,16 +312,14 @@ export function adaptTools(
446
312
  const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
447
313
  try {
448
314
  if (meta?.blocked) {
449
- const blockedResult = {
450
- ok: false,
451
- error: `Blocked: ${meta.blockReason}`,
452
- details: { reason: 'safety_guard' },
453
- };
454
- await persistToolErrorResult(blockedResult, {
315
+ const blockedResult = createBlockedToolResult(meta.blockReason);
316
+ await persistToolErrorResult(ctx, {
317
+ name,
318
+ errorResult: blockedResult,
455
319
  callId: callIdFromQueue,
456
320
  startTs: startTsFromQueue,
457
321
  stepIndexForEvent,
458
- args: meta?.args,
322
+ input: meta?.args,
459
323
  });
460
324
  return blockedResult as ToolExecuteReturn;
461
325
  }
@@ -463,143 +327,49 @@ export function adaptTools(
463
327
  if (meta?.approvalPromise) {
464
328
  const approved = await meta.approvalPromise;
465
329
  if (!approved) {
466
- const rejectedResult = {
467
- ok: false,
468
- error: 'Tool execution rejected by user',
469
- details: { reason: 'user_rejected' },
470
- };
471
- await persistToolErrorResult(rejectedResult, {
330
+ const rejectedResult = createRejectedToolResult();
331
+ await persistToolErrorResult(ctx, {
332
+ name,
333
+ errorResult: rejectedResult,
472
334
  callId: callIdFromQueue,
473
335
  startTs: startTsFromQueue,
474
336
  stepIndexForEvent,
475
- args: meta?.args,
337
+ input: meta?.args,
476
338
  });
477
339
  return rejectedResult as ToolExecuteReturn;
478
340
  }
479
341
  }
480
342
  // Handle session-relative paths and cwd tools
481
- let res: ToolExecuteReturn | { cwd: string } | null | undefined;
482
- const cwd = getCwd(ctx.sessionId);
483
- if (name === 'pwd') {
484
- res = { cwd };
485
- } else if (name === 'cd') {
486
- const next = joinRelative(
487
- cwd,
488
- String((input as Record<string, unknown>)?.path ?? '.'),
489
- );
490
- setCwd(ctx.sessionId, next);
491
- res = { cwd: next };
492
- } else if (
493
- ['read', 'write', 'ls', 'tree'].includes(name) &&
494
- typeof (input as Record<string, unknown>)?.path === 'string'
495
- ) {
496
- const rel = joinRelative(
497
- cwd,
498
- String((input as Record<string, unknown>).path),
499
- );
500
- const nextInput = {
501
- ...(input as Record<string, unknown>),
502
- path: rel,
503
- } as ToolExecuteInput;
504
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
505
- res = base.execute?.(nextInput, options as any);
506
- } else if (name === 'shell' || name === 'bash') {
507
- const needsCwd =
508
- !input ||
509
- typeof (input as Record<string, unknown>).cwd !== 'string';
510
- const nextInput = needsCwd
511
- ? ({
512
- ...(input as Record<string, unknown>),
513
- cwd,
514
- } as ToolExecuteInput)
515
- : input;
516
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
517
- res = base.execute?.(nextInput, options as any);
518
- } else {
519
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
520
- res = base.execute?.(input, options as any);
521
- }
343
+ const res = executeBaseTool(ctx, {
344
+ base,
345
+ name,
346
+ input,
347
+ options,
348
+ });
522
349
  let result: unknown = res;
523
350
  // If tool returns an async iterable, stream deltas while accumulating
524
351
  if (res && typeof res === 'object' && Symbol.asyncIterator in res) {
525
- const chunks: unknown[] = [];
526
- let streamedResult: unknown = null;
527
- for await (const chunk of res as AsyncIterable<unknown>) {
528
- chunks.push(chunk);
529
- if (chunk && typeof chunk === 'object' && 'result' in chunk) {
530
- streamedResult = (chunk as { result: unknown }).result;
531
- continue;
532
- }
533
- if (
534
- chunk &&
535
- typeof chunk === 'object' &&
536
- 'terminalId' in chunk &&
537
- typeof (chunk as { terminalId?: unknown }).terminalId ===
538
- 'string'
539
- ) {
540
- publish({
541
- type: 'tool.delta',
542
- sessionId: ctx.sessionId,
543
- payload: {
544
- name,
545
- channel: 'terminal',
546
- delta: (chunk as { terminalId: string }).terminalId,
547
- stepIndex: stepIndexForEvent,
548
- callId: callIdFromQueue,
549
- messageId: ctx.messageId,
550
- },
551
- });
552
- continue;
553
- }
554
- const delta =
555
- typeof chunk === 'string'
556
- ? chunk
557
- : chunk &&
558
- typeof chunk === 'object' &&
559
- 'delta' in chunk &&
560
- typeof (chunk as { delta?: unknown }).delta === 'string'
561
- ? ((chunk as { delta: string }).delta ?? '')
562
- : null;
563
- if (!delta) continue;
564
- const channel =
565
- chunk &&
566
- typeof chunk === 'object' &&
567
- 'channel' in chunk &&
568
- typeof (chunk as { channel?: unknown }).channel === 'string'
569
- ? ((chunk as { channel: string }).channel ?? 'output')
570
- : 'output';
571
- publish({
572
- type: 'tool.delta',
573
- sessionId: ctx.sessionId,
574
- payload: {
575
- name,
576
- channel,
577
- delta,
578
- stepIndex: stepIndexForEvent,
579
- callId: callIdFromQueue,
580
- messageId: ctx.messageId,
581
- },
582
- });
583
- }
584
- result =
585
- streamedResult ??
586
- (chunks.length > 0 ? chunks[chunks.length - 1] : null);
352
+ result = await consumeToolStream(ctx, {
353
+ stream: res as AsyncIterable<unknown>,
354
+ name,
355
+ stepIndex: stepIndexForEvent,
356
+ callId: callIdFromQueue,
357
+ });
587
358
  } else {
588
359
  // Await promise or passthrough value
589
360
  result = await Promise.resolve(res as ToolExecuteReturn);
590
361
  }
591
362
 
592
363
  if (isToolError(result)) {
593
- stepState.failed = true;
594
- stepState.failedToolName = name;
595
- failureState.active = true;
596
- failureState.toolName = name;
364
+ markToolFailed(stepState, failureState, name);
597
365
 
598
- await persistToolErrorResult(result, {
366
+ await persistToolErrorResult(ctx, {
367
+ name,
368
+ errorResult: result,
599
369
  callId: callIdFromQueue,
600
370
  startTs: startTsFromQueue,
601
371
  stepIndexForEvent,
602
- args: meta?.args,
372
+ input: meta?.args,
603
373
  });
604
374
  processedToolErrors.add(result as object);
605
375
  return result as ToolExecuteReturn;
@@ -608,155 +378,66 @@ export function adaptTools(
608
378
  const resultPartId = crypto.randomUUID();
609
379
  const callId = callIdFromQueue;
610
380
  const startTs = startTsFromQueue;
611
- const contentObj: {
612
- name: string;
613
- result: unknown;
614
- callId?: string;
615
- artifact?: unknown;
616
- args?: unknown;
617
- } = {
381
+ const contentObj = buildToolResultContent({
618
382
  name,
619
383
  result,
620
384
  callId,
621
- };
622
- if (meta?.args !== undefined) {
623
- contentObj.args = meta.args;
624
- }
625
- if (result && typeof result === 'object' && 'artifact' in result) {
626
- try {
627
- const maybeArtifact = (result as { artifact?: unknown })
628
- .artifact;
629
- if (maybeArtifact !== undefined)
630
- contentObj.artifact = maybeArtifact;
631
- } catch {}
632
- }
385
+ input: meta?.args,
386
+ });
633
387
 
634
388
  const index = await ctx.nextIndex();
635
- const endTs = Date.now();
636
- const dur =
637
- typeof startTs === 'number' ? Math.max(0, endTs - startTs) : null;
389
+ const { endTs, durationMs } = computeToolTiming(startTs);
638
390
 
639
391
  // Special-case: keep progress_update result lightweight; publish first, persist best-effort
640
392
  if (name === 'progress_update') {
641
- stepState.failed = false;
642
- stepState.failedToolName = undefined;
643
- if (failureState.active && failureState.toolName === name) {
644
- failureState.active = false;
645
- failureState.toolName = undefined;
646
- }
647
- publish({
648
- type: 'tool.result',
649
- sessionId: ctx.sessionId,
650
- payload: { ...contentObj, stepIndex: stepIndexForEvent },
651
- });
393
+ markToolSucceeded(stepState, failureState, name);
394
+ publishToolResult(ctx, contentObj, stepIndexForEvent);
652
395
  // Persist without blocking the event loop
653
396
  (async () => {
654
397
  try {
655
- await ctx.db.insert(messageParts).values({
656
- id: resultPartId,
657
- messageId: ctx.messageId,
398
+ await persistToolResultWithIndex(ctx, {
399
+ partId: resultPartId,
658
400
  index,
401
+ name,
402
+ content: contentObj,
403
+ startTs,
404
+ callId,
659
405
  stepIndex: stepIndexForEvent,
660
- type: 'tool_result',
661
- content: JSON.stringify(contentObj),
662
- agent: ctx.agent,
663
- provider: ctx.provider,
664
- model: ctx.model,
665
- startedAt: startTs,
666
- completedAt: endTs,
667
- toolName: name,
668
- toolCallId: callId,
669
- toolDurationMs: dur ?? undefined,
406
+ endTs,
407
+ durationMs,
670
408
  });
671
409
  } catch {}
672
410
  })();
673
411
  return result as ToolExecuteReturn;
674
412
  }
675
413
 
676
- stepState.failed = false;
677
- stepState.failedToolName = undefined;
678
- if (failureState.active && failureState.toolName === name) {
679
- failureState.active = false;
680
- failureState.toolName = undefined;
681
- }
414
+ markToolSucceeded(stepState, failureState, name);
682
415
 
683
- await ctx.db.insert(messageParts).values({
684
- id: resultPartId,
685
- messageId: ctx.messageId,
416
+ await persistToolResultWithIndex(ctx, {
417
+ partId: resultPartId,
686
418
  index,
419
+ name,
420
+ content: contentObj,
421
+ startTs,
422
+ callId,
687
423
  stepIndex: stepIndexForEvent,
688
- type: 'tool_result',
689
- content: JSON.stringify(contentObj),
690
- agent: ctx.agent,
691
- provider: ctx.provider,
692
- model: ctx.model,
693
- startedAt: startTs,
694
- completedAt: endTs,
695
- toolName: name,
696
- toolCallId: callId,
697
- toolDurationMs: dur ?? undefined,
424
+ endTs,
425
+ durationMs,
698
426
  });
699
427
  // Update session aggregates: total tool time and counts per tool
700
- try {
701
- const sessRows = await ctx.db
702
- .select()
703
- .from(sessions)
704
- .where(eq(sessions.id, ctx.sessionId));
705
- if (sessRows.length) {
706
- const row = sessRows[0] as typeof sessions.$inferSelect;
707
- const totalToolTimeMs =
708
- Number(row.totalToolTimeMs || 0) + (dur ?? 0);
709
- let counts: Record<string, number> = {};
710
- try {
711
- counts = row.toolCountsJson
712
- ? JSON.parse(row.toolCountsJson)
713
- : {};
714
- } catch {}
715
- counts[name] = (counts[name] || 0) + 1;
716
- await ctx.db
717
- .update(sessions)
718
- .set({
719
- totalToolTimeMs,
720
- toolCountsJson: JSON.stringify(counts),
721
- lastActiveAt: endTs,
722
- })
723
- .where(eq(sessions.id, ctx.sessionId));
724
- }
725
- } catch {}
726
- publish({
727
- type: 'tool.result',
728
- sessionId: ctx.sessionId,
729
- payload: { ...contentObj, stepIndex: stepIndexForEvent },
730
- });
731
- logger.debug(`[tools] result ${name}`, {
732
- sessionId: ctx.sessionId,
733
- messageId: ctx.messageId,
734
- toolName: name,
735
- callId,
736
- stepIndex: stepIndexForEvent,
428
+ await updateToolSessionStats(ctx, {
429
+ name,
430
+ durationMs,
431
+ endTs,
737
432
  });
433
+ publishToolResult(ctx, contentObj, stepIndexForEvent);
434
+ logToolResult(ctx, { name, callId, stepIndex: stepIndexForEvent });
738
435
  if (name === 'update_todos') {
739
- try {
740
- const resultValue = (contentObj as { result?: unknown })
741
- .result as { items?: unknown; note?: unknown } | undefined;
742
- if (resultValue && Array.isArray(resultValue.items)) {
743
- publish({
744
- type: 'plan.updated',
745
- sessionId: ctx.sessionId,
746
- payload: {
747
- items: resultValue.items,
748
- note: resultValue.note,
749
- },
750
- });
751
- }
752
- } catch {}
436
+ publishPlanUpdated(ctx, contentObj.result);
753
437
  }
754
438
  return result as ToolExecuteReturn;
755
439
  } catch (error) {
756
- stepState.failed = true;
757
- stepState.failedToolName = name;
758
- failureState.active = true;
759
- failureState.toolName = name;
440
+ markToolFailed(stepState, failureState, name);
760
441
 
761
442
  // Tool execution failed
762
443
  if (
@@ -768,23 +449,15 @@ export function adaptTools(
768
449
 
769
450
  const errorResult = isToolError(error)
770
451
  ? error
771
- : (() => {
772
- const errorMessage =
773
- error instanceof Error ? error.message : String(error);
774
- const errorStack =
775
- error instanceof Error ? error.stack : undefined;
776
- return {
777
- ok: false,
778
- error: errorMessage,
779
- stack: errorStack,
780
- };
781
- })();
452
+ : createToolExceptionResult(error);
782
453
 
783
- await persistToolErrorResult(errorResult, {
454
+ await persistToolErrorResult(ctx, {
455
+ name,
456
+ errorResult,
784
457
  callId: callIdFromQueue,
785
458
  startTs: startTsFromQueue,
786
459
  stepIndexForEvent,
787
- args: meta?.args,
460
+ input: meta?.args,
788
461
  });
789
462
 
790
463
  if (isToolError(error)) {