@ai-sdk/harness 1.0.100 → 1.0.102

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.
@@ -17,10 +17,13 @@ import { toHarnessStream } from './to-harness-stream';
17
17
  import {
18
18
  executeTool,
19
19
  generateId,
20
+ type InferToolSetContext,
20
21
  isExecutableTool,
21
22
  safeParseJSON,
22
23
  type Context,
23
24
  type Experimental_SandboxSession as SandboxSession,
25
+ type ToolApprovalResponse,
26
+ type ToolResultPart,
24
27
  type ToolSet,
25
28
  } from '@ai-sdk/provider-utils';
26
29
  import {
@@ -29,7 +32,7 @@ import {
29
32
  type LanguageModelV4ToolCall,
30
33
  type LanguageModelV4Usage,
31
34
  } from '@ai-sdk/provider';
32
- import { parseToolCall } from 'ai/internal';
35
+ import { asLanguageModelUsage, parseToolCall } from 'ai/internal';
33
36
  import type {
34
37
  ContentPart,
35
38
  OutputInterface as Output,
@@ -38,22 +41,46 @@ import type {
38
41
  StopCondition,
39
42
  TelemetryOptions,
40
43
  TextStreamPart,
44
+ TypedToolCall,
45
+ TypedToolError,
46
+ TypedToolResult,
41
47
  } from 'ai';
42
- import type { HarnessAgentToolApprovalContinuation } from '../harness-agent-tool-approval-continuation';
43
- import type { HarnessAgentToolResultContinuation } from '../harness-agent-tool-result-continuation';
44
48
  import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-settings';
45
49
  import { HarnessStreamTextResult } from './harness-stream-text-result';
46
50
  import { translateStreamPart } from './translate-stream-part';
47
51
  import { createToolInputWorkDirStripper, stripWorkDir } from './strip-work-dir';
48
52
  import {
49
- createTurnTelemetry,
50
- type TurnContentPart,
51
- type TurnTelemetry,
53
+ createTurnLifecycle,
54
+ type HarnessAgentLifecycleCallbacks,
55
+ type TurnLifecycle,
52
56
  } from './turn-telemetry';
53
57
  import { resolveCustomToolApproval } from './permission-mode';
54
58
  import { logBridgeError } from '../../utils/bridge-diagnostics';
55
59
  import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
56
60
 
61
+ function unwrapToolResultOutput(toolResult: ToolResultPart): {
62
+ output: unknown;
63
+ isError?: boolean;
64
+ } {
65
+ switch (toolResult.output.type) {
66
+ case 'text':
67
+ case 'json':
68
+ return { output: toolResult.output.value };
69
+ case 'error-text':
70
+ case 'error-json':
71
+ return { output: toolResult.output.value, isError: true };
72
+ case 'execution-denied':
73
+ return {
74
+ output: {
75
+ type: toolResult.output.type,
76
+ reason: toolResult.output.reason,
77
+ },
78
+ };
79
+ case 'content':
80
+ return { output: toolResult.output };
81
+ }
82
+ }
83
+
57
84
  /**
58
85
  * Drive one prompt turn end-to-end:
59
86
  * - call `session.doPromptTurn` via `toHarnessStream`
@@ -95,16 +122,13 @@ export function runPrompt<
95
122
  responseFormat?: HarnessV1ResponseFormat | undefined;
96
123
  output?: OUTPUT | undefined;
97
124
  telemetry?: TelemetryOptions | undefined;
125
+ callbacks?: HarnessAgentLifecycleCallbacks<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
98
126
  stopConditions?: ReadonlyArray<StopCondition<TOOLS, RUNTIME_CONTEXT>>;
99
127
  toolApproval?: HarnessAgentToolApprovalConfiguration | undefined;
100
128
  pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
101
129
  pendingToolResults?: readonly HarnessV1PendingToolResult[];
102
- toolApprovalContinuations?:
103
- | readonly HarnessAgentToolApprovalContinuation[]
104
- | undefined;
105
- toolResultContinuations?:
106
- | readonly HarnessAgentToolResultContinuation[]
107
- | undefined;
130
+ toolApprovalContinuations?: readonly ToolApprovalResponse[] | undefined;
131
+ toolResultContinuations?: readonly ToolResultPart[] | undefined;
108
132
  onPendingToolApproval?: (approval: HarnessV1PendingToolApproval) => void;
109
133
  onToolApprovalSettled?: (approvalId: string) => void;
110
134
  onPendingToolResult?: (pendingResult: HarnessV1PendingToolResult) => void;
@@ -122,13 +146,15 @@ export function runPrompt<
122
146
  result: HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>;
123
147
  done: Promise<void>;
124
148
  } {
149
+ const callId = generateId();
150
+ const toolsContext = {} as InferToolSetContext<TOOLS>;
125
151
  const result = new HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>({
126
152
  tools: input.tools,
127
153
  runtimeContext: input.runtimeContext,
128
- // toolsContext is not configurable for harnesses; pass undefined cast.
129
- toolsContext: undefined as never,
154
+ toolsContext,
130
155
  harnessId: input.harness.harnessId,
131
- sessionId: input.session.sessionId,
156
+ callId,
157
+ modelId: input.model ?? '',
132
158
  output: input.output,
133
159
  });
134
160
  const pendingToolApprovals = input.pendingToolApprovals ?? [];
@@ -151,16 +177,23 @@ export function runPrompt<
151
177
  })),
152
178
  );
153
179
 
154
- const telemetry = createTurnTelemetry({
180
+ const lifecycle = createTurnLifecycle({
181
+ callId,
155
182
  telemetry: input.telemetry,
183
+ callbacks: input.callbacks ?? {},
156
184
  harnessId: input.harness.harnessId,
157
185
  modelId: input.model,
158
186
  instructions: input.instructions,
159
187
  tools: input.tools,
160
188
  activeToolNames,
161
189
  toolSpecs: input.toolSpecs,
162
- promptText: input.prompt != null ? promptToText(input.prompt) : '',
190
+ messages:
191
+ input.prompt == null
192
+ ? []
193
+ : [{ role: 'user', content: promptToText(input.prompt) }],
163
194
  runtimeContext: input.runtimeContext,
195
+ toolsContext,
196
+ output: input.output,
164
197
  });
165
198
 
166
199
  /*
@@ -224,7 +257,7 @@ export function runPrompt<
224
257
  },
225
258
  });
226
259
  } catch (err) {
227
- await telemetry.error(err);
260
+ await lifecycle.error(err);
228
261
  logBridgeError({
229
262
  harnessId: input.harness.harnessId,
230
263
  sessionId: input.session.sessionId,
@@ -267,7 +300,7 @@ export function runPrompt<
267
300
  );
268
301
  const continuationsByApprovalId = new Map(
269
302
  (input.toolApprovalContinuations ?? []).map(continuation => [
270
- continuation.approvalResponse.approvalId,
303
+ continuation.approvalId,
271
304
  continuation,
272
305
  ]),
273
306
  );
@@ -321,25 +354,59 @@ export function runPrompt<
321
354
  pendingStopBoundary = undefined;
322
355
  };
323
356
 
324
- // Accumulate the model's output content per step so telemetry can record
325
- // `gen_ai.output.messages` and reporters can log what was actually said.
357
+ // Accumulate the model response until its step boundary. Harness runtimes
358
+ // may execute tools before emitting `finish-step`, so tool lifecycle
359
+ // notifications and consumer-visible tool outcomes are held until then.
326
360
  let stepText = '';
327
361
  let stepReasoning = '';
328
- let stepToolCalls: TurnContentPart[] = [];
362
+ let stepToolCalls: ContentPart<TOOLS>[] = [];
363
+ let stepProviderToolResults: ContentPart<TOOLS>[] = [];
364
+ let stepApprovalRequests: ContentPart<TOOLS>[] = [];
365
+ let bufferedToolOutcomes: Array<() => void> = [];
366
+ const toolExecutions = new Map<
367
+ string,
368
+ {
369
+ toolCall: TypedToolCall<TOOLS>;
370
+ toolOutput?: TypedToolResult<TOOLS> | TypedToolError<TOOLS>;
371
+ toolExecutionMs?: number;
372
+ }
373
+ >();
374
+ const publishToolExecutions = async (): Promise<void> => {
375
+ for (const execution of toolExecutions.values()) {
376
+ if (execution.toolOutput == null) continue;
377
+ await lifecycle.toolExecutionStart({
378
+ toolCall: execution.toolCall,
379
+ });
380
+ await lifecycle.toolExecutionEnd({
381
+ toolCall: execution.toolCall,
382
+ toolOutput: execution.toolOutput,
383
+ toolExecutionMs: execution.toolExecutionMs ?? 0,
384
+ });
385
+ }
386
+ for (const publish of bufferedToolOutcomes) publish();
387
+ toolExecutions.clear();
388
+ bufferedToolOutcomes = [];
389
+ };
329
390
  let expectedStepToolCallCount: number | undefined;
330
391
  let observedStepToolCallCount = 0;
331
392
  let pauseAfterStepToolCalls = false;
332
- const buildStepContent = (): TurnContentPart[] => {
333
- const parts: TurnContentPart[] = [];
393
+ const buildModelCallContent = (): ContentPart<TOOLS>[] => {
394
+ const parts: ContentPart<TOOLS>[] = [];
334
395
  if (stepText) parts.push({ type: 'text', text: stepText });
335
396
  if (stepReasoning) parts.push({ type: 'reasoning', text: stepReasoning });
336
397
  parts.push(...stepToolCalls);
398
+ parts.push(...stepApprovalRequests);
399
+ parts.push(...stepProviderToolResults);
337
400
  return parts;
338
401
  };
339
402
  const resetStepContent = (): void => {
340
403
  stepText = '';
341
404
  stepReasoning = '';
342
405
  stepToolCalls = [];
406
+ stepProviderToolResults = [];
407
+ stepApprovalRequests = [];
408
+ bufferedToolOutcomes = [];
409
+ toolExecutions.clear();
343
410
  expectedStepToolCallCount = undefined;
344
411
  observedStepToolCallCount = 0;
345
412
  pauseAfterStepToolCalls = false;
@@ -366,13 +433,13 @@ export function runPrompt<
366
433
  usage: LanguageModelV4Usage;
367
434
  providerMetadata: ProviderMetadata | undefined;
368
435
  }): Promise<StepResult<TOOLS, RUNTIME_CONTEXT>> => {
369
- await telemetry.stepFinish({
370
- finishReason: input.finishReason,
371
- usage: input.usage,
436
+ await lifecycle.languageModelCallEnd({
437
+ finishReason: input.finishReason.unified,
438
+ usage: asLanguageModelUsage(input.usage),
372
439
  providerMetadata: input.providerMetadata,
373
- content: buildStepContent(),
440
+ content: buildModelCallContent(),
374
441
  });
375
- resetStepContent();
442
+ await publishToolExecutions();
376
443
  const step = result.finishStep({
377
444
  finishReason: input.finishReason,
378
445
  usage: input.usage,
@@ -380,6 +447,8 @@ export function runPrompt<
380
447
  warnings: [],
381
448
  });
382
449
  completedSteps.push(step);
450
+ await lifecycle.stepEnd(step);
451
+ resetStepContent();
383
452
  return step;
384
453
  };
385
454
  const finishForHostInputPause = async (options: {
@@ -392,10 +461,12 @@ export function runPrompt<
392
461
  usage: zeroUsage,
393
462
  providerMetadata: undefined,
394
463
  });
464
+ } else {
465
+ await publishToolExecutions();
395
466
  }
396
- await telemetry.end({
397
- finishReason: toolCallsFinishReason,
398
- usage: zeroUsage,
467
+ await lifecycle.end({
468
+ steps: completedSteps,
469
+ usage: asLanguageModelUsage(zeroUsage),
399
470
  });
400
471
  await result.finish();
401
472
  };
@@ -404,14 +475,16 @@ export function runPrompt<
404
475
  toolCall: ToolCallTextStreamPart;
405
476
  isAutomatic?: boolean;
406
477
  }): void => {
407
- result.enqueue({
478
+ const part = {
408
479
  type: 'tool-approval-request',
409
480
  approvalId: approval.approvalId,
410
481
  toolCall: approval.toolCall,
411
482
  ...(approval.isAutomatic !== undefined
412
483
  ? { isAutomatic: approval.isAutomatic }
413
484
  : {}),
414
- } as TextStreamPart<TOOLS>);
485
+ } as TextStreamPart<TOOLS>;
486
+ result.enqueue(part);
487
+ stepApprovalRequests.push(part as ContentPart<TOOLS>);
415
488
  };
416
489
  const enqueueAutomaticApprovalResponse = (input: {
417
490
  approvalId: string;
@@ -433,15 +506,16 @@ export function runPrompt<
433
506
  };
434
507
  const enqueueApprovalResponse = (
435
508
  approval: HarnessV1PendingToolApproval,
436
- continuation: HarnessAgentToolApprovalContinuation,
509
+ continuation: ToolApprovalResponse,
510
+ toolCall: ToolCallTextStreamPart,
437
511
  ): void => {
438
512
  result.enqueueContinuation({
439
513
  type: 'tool-approval-response',
440
514
  approvalId: approval.approvalId,
441
- toolCall: continuation.toolCall,
442
- approved: continuation.approvalResponse.approved,
443
- ...(continuation.approvalResponse.reason !== undefined
444
- ? { reason: continuation.approvalResponse.reason }
515
+ toolCall,
516
+ approved: continuation.approved,
517
+ ...(continuation.reason !== undefined
518
+ ? { reason: continuation.reason }
445
519
  : {}),
446
520
  ...(approval.providerExecuted !== undefined
447
521
  ? { providerExecuted: approval.providerExecuted }
@@ -457,6 +531,9 @@ export function runPrompt<
457
531
  toolCallId: options.toolCall.toolCallId,
458
532
  toolName: options.toolCall.toolName,
459
533
  input: options.toolCall.input,
534
+ ...(options.toolCall.providerMetadata !== undefined
535
+ ? { providerOptions: options.toolCall.providerMetadata }
536
+ : {}),
460
537
  } satisfies HarnessV1PendingToolResult);
461
538
  pendingResultsByToolCallId.set(pendingResult.toolCallId, pendingResult);
462
539
  onPendingToolResult(pendingResult);
@@ -464,19 +541,28 @@ export function runPrompt<
464
541
  };
465
542
  const processPendingToolResultContinuation = async (
466
543
  pendingResult: HarnessV1PendingToolResult,
467
- continuation: HarnessAgentToolResultContinuation,
544
+ continuation: ToolResultPart,
468
545
  ): Promise<void> => {
546
+ const result = unwrapToolResultOutput(continuation);
469
547
  onToolResultSettled(pendingResult.toolCallId);
470
548
  pendingResultsByToolCallId.delete(pendingResult.toolCallId);
471
549
  settledHostToolCallIds.add(pendingResult.toolCallId);
472
550
  await control.submitToolResult({
473
551
  toolCallId: pendingResult.toolCallId,
474
- output: continuation.output,
475
- isError: continuation.isError,
552
+ output: result.output,
553
+ isError: result.isError,
554
+ toolResult: {
555
+ ...continuation,
556
+ toolName: pendingResult.toolName,
557
+ ...(continuation.providerOptions == null &&
558
+ pendingResult.providerOptions != null
559
+ ? { providerOptions: pendingResult.providerOptions }
560
+ : {}),
561
+ },
476
562
  });
477
563
  };
478
564
  const enqueueHostToolOutcome = (options: {
479
- toolCall: HarnessAgentToolApprovalContinuation['toolCall'];
565
+ toolCall: ToolCallTextStreamPart;
480
566
  outcome: HostToolOutcome;
481
567
  }): void => {
482
568
  if (options.outcome.ok) {
@@ -512,9 +598,30 @@ export function runPrompt<
512
598
  };
513
599
  const processPendingApprovalContinuation = async (
514
600
  approval: HarnessV1PendingToolApproval,
515
- continuation: HarnessAgentToolApprovalContinuation,
601
+ continuation: ToolApprovalResponse,
516
602
  ): Promise<'continued' | 'awaiting-tool-result'> => {
517
- enqueueApprovalResponse(approval, continuation);
603
+ const rawToolCall =
604
+ rawToolCallsByToolCallId.get(approval.toolCallId) ??
605
+ ({
606
+ type: 'tool-call',
607
+ toolCallId: approval.toolCallId,
608
+ toolName: approval.toolName,
609
+ input: approval.input,
610
+ providerExecuted: approval.providerExecuted,
611
+ nativeName: approval.nativeName,
612
+ } satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
613
+ const parsedInput = await safeParseJSON({ text: rawToolCall.input });
614
+ const toolCall: ToolCallTextStreamPart = {
615
+ type: 'tool-call',
616
+ toolCallId: rawToolCall.toolCallId,
617
+ toolName: rawToolCall.toolName,
618
+ input: parsedInput.success ? parsedInput.value : rawToolCall.input,
619
+ ...(rawToolCall.providerExecuted !== undefined
620
+ ? { providerExecuted: rawToolCall.providerExecuted }
621
+ : {}),
622
+ };
623
+
624
+ enqueueApprovalResponse(approval, continuation, toolCall);
518
625
  onToolApprovalSettled(approval.approvalId);
519
626
  pendingApprovalsByApprovalId.delete(approval.approvalId);
520
627
  pendingApprovalsByToolCallId.delete(approval.toolCallId);
@@ -528,43 +635,33 @@ export function runPrompt<
528
635
  }
529
636
  await control.submitToolApproval({
530
637
  approvalId: approval.approvalId,
531
- approved: continuation.approvalResponse.approved,
532
- reason: continuation.approvalResponse.reason,
638
+ approved: continuation.approved,
639
+ reason: continuation.reason,
533
640
  });
534
641
  return 'continued';
535
642
  }
536
643
 
537
644
  settledHostToolCallIds.add(approval.toolCallId);
538
- if (!continuation.approvalResponse.approved) {
645
+ if (!continuation.approved) {
539
646
  await control.submitToolResult({
540
647
  toolCallId: approval.toolCallId,
541
648
  output: {
542
649
  type: 'execution-denied',
543
- reason: continuation.approvalResponse.reason,
650
+ reason: continuation.reason,
544
651
  },
545
652
  });
546
653
  return 'continued';
547
654
  }
548
655
 
549
- const rawToolCall =
550
- rawToolCallsByToolCallId.get(approval.toolCallId) ??
551
- ({
552
- type: 'tool-call',
553
- toolCallId: approval.toolCallId,
554
- toolName: approval.toolName,
555
- input: approval.input,
556
- } satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
557
-
558
- await telemetry.start(input.model);
559
- await telemetry.toolStart({
560
- toolCallId: rawToolCall.toolCallId,
561
- toolName: rawToolCall.toolName,
562
- input: rawToolCall.input,
656
+ await lifecycle.start(input.model);
657
+ toolExecutions.set(rawToolCall.toolCallId, {
658
+ toolCall: toolCall as TypedToolCall<TOOLS>,
563
659
  });
660
+ const executionStartedAt = Date.now();
564
661
  const execution = await maybeExecuteHostTool({
565
662
  event: rawToolCall,
566
663
  tools: activeTools,
567
- wrappedExecuteTool: telemetry.executeTool,
664
+ wrappedExecuteTool: lifecycle.executeTool,
568
665
  sandboxSession: input.sandboxSession,
569
666
  abortSignal: input.abortSignal,
570
667
  control,
@@ -581,14 +678,16 @@ export function runPrompt<
581
678
  },
582
679
  input.sessionWorkDir,
583
680
  ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
584
- result.enqueue({
585
- type: 'tool-result',
586
- toolCallId: rawToolCall.toolCallId,
587
- toolName: rawToolCall.toolName,
588
- input: undefined,
589
- output: stripped.result,
590
- preliminary: true,
591
- } as TextStreamPart<TOOLS>);
681
+ bufferedToolOutcomes.push(() => {
682
+ result.enqueue({
683
+ type: 'tool-result',
684
+ toolCallId: rawToolCall.toolCallId,
685
+ toolName: rawToolCall.toolName,
686
+ input: undefined,
687
+ output: stripped.result,
688
+ preliminary: true,
689
+ } as TextStreamPart<TOOLS>);
690
+ });
592
691
  },
593
692
  });
594
693
  if (!execution.executed) {
@@ -596,11 +695,19 @@ export function runPrompt<
596
695
  await finishForHostInputPause({ completeCurrentStep: false });
597
696
  return 'awaiting-tool-result';
598
697
  }
599
- enqueueHostToolOutcome({
600
- toolCall: continuation.toolCall,
698
+ const toolExecution = toolExecutions.get(rawToolCall.toolCallId)!;
699
+ toolExecution.toolOutput = toToolOutput({
700
+ toolCall,
601
701
  outcome: execution.outcome,
602
702
  });
603
- await telemetry.toolEnd(rawToolCall.toolCallId, execution.outcome);
703
+ toolExecution.toolExecutionMs = Date.now() - executionStartedAt;
704
+ bufferedToolOutcomes.push(() => {
705
+ enqueueHostToolOutcome({
706
+ toolCall,
707
+ outcome: execution.outcome,
708
+ });
709
+ });
710
+ await publishToolExecutions();
604
711
  return 'continued';
605
712
  };
606
713
 
@@ -659,9 +766,12 @@ export function runPrompt<
659
766
  ).some(Boolean)
660
767
  ) {
661
768
  await input.onStopConditionMet?.();
662
- const { finishReason, usage } = pendingStopBoundary;
769
+ const { usage } = pendingStopBoundary;
663
770
  releasePendingStopBoundary();
664
- await telemetry.end({ finishReason, usage });
771
+ await lifecycle.end({
772
+ steps: completedSteps,
773
+ usage: asLanguageModelUsage(usage),
774
+ });
665
775
  await result.finish();
666
776
  return;
667
777
  } else {
@@ -672,7 +782,9 @@ export function runPrompt<
672
782
  // Begin the operation span on stream-start, using the runtime-resolved
673
783
  // model the adapter reports (falling back to the requested turn model).
674
784
  if (value.type === 'stream-start') {
675
- await telemetry.start(value.modelId ?? input.model);
785
+ const modelId = value.modelId ?? input.model;
786
+ if (modelId != null) result.setModelId(modelId);
787
+ await lifecycle.start(modelId);
676
788
  }
677
789
 
678
790
  // Open a step span lazily before the first content of each step.
@@ -682,7 +794,7 @@ export function runPrompt<
682
794
  value.type !== 'finish' &&
683
795
  value.type !== 'error'
684
796
  ) {
685
- await telemetry.ensureStepOpen();
797
+ await lifecycle.ensureStepOpen();
686
798
  }
687
799
 
688
800
  if (
@@ -730,6 +842,7 @@ export function runPrompt<
730
842
 
731
843
  if (displayValue.type === 'finish-step' && closingResumedStep) {
732
844
  closingResumedStep = false;
845
+ await publishToolExecutions();
733
846
  resetStepContent();
734
847
  result.discardCurrentStepContent();
735
848
  continue;
@@ -780,7 +893,7 @@ export function runPrompt<
780
893
  // Telemetry and stderr diagnostics keep the raw error (absolute
781
894
  // paths help debugging); the consumer-facing settle uses the
782
895
  // workDir-stripped one, like every other forwarded part.
783
- await telemetry.error(value.error);
896
+ await lifecycle.error(value.error);
784
897
  // A turn the caller itself aborted ends with an error-shaped part by
785
898
  // construction; diagnosing the caller's own signal to stderr reads
786
899
  // as a malfunction. `settleFailure` below still reports it as an
@@ -797,12 +910,16 @@ export function runPrompt<
797
910
  return;
798
911
  }
799
912
 
800
- // Forward to consumer as soon as possible.
801
- for (const part of translateStreamPart<TOOLS>(
913
+ const translatedParts = translateStreamPart<TOOLS>(
802
914
  displayValue,
803
915
  translateOptions,
804
- )) {
805
- result.enqueue(part);
916
+ );
917
+ if (value.type === 'tool-result') {
918
+ bufferedToolOutcomes.push(() => {
919
+ for (const part of translatedParts) result.enqueue(part);
920
+ });
921
+ } else {
922
+ for (const part of translatedParts) result.enqueue(part);
806
923
  }
807
924
 
808
925
  // Tool-call validation lives here (not in translateStreamPart) because
@@ -818,38 +935,48 @@ export function runPrompt<
818
935
  result.enqueue(parsed);
819
936
  }
820
937
 
821
- // Accumulate output content for telemetry / reporters.
822
938
  if (value.type === 'text-delta') {
823
939
  stepText += value.delta;
824
940
  } else if (value.type === 'reasoning-delta') {
825
941
  stepReasoning += value.delta;
826
942
  }
827
943
 
828
- // Telemetry: a tool execution begins on its `tool-call`.
829
944
  if (value.type === 'tool-call') {
830
945
  observedStepToolCallCount += 1;
831
946
  expectedStepToolCallCount ??= value.stepToolCallCount;
832
- stepToolCalls.push({
833
- type: 'tool-call',
834
- toolCallId: value.toolCallId,
835
- toolName: value.toolName,
836
- input: value.input,
837
- });
838
- await telemetry.toolStart({
839
- toolCallId: value.toolCallId,
840
- toolName: value.toolName,
841
- input: value.input,
842
- });
947
+ const toolCall = toolCallsByToolCallId.get(value.toolCallId);
948
+ if (toolCall != null) {
949
+ stepToolCalls.push(toolCall as ContentPart<TOOLS>);
950
+ toolExecutions.set(value.toolCallId, {
951
+ toolCall: toolCall as TypedToolCall<TOOLS>,
952
+ });
953
+ }
843
954
  }
844
955
 
845
- // Telemetry: close a tool span when its provider-executed result lands.
846
956
  if (value.type === 'tool-result') {
847
- await telemetry.toolEnd(
848
- value.toolCallId,
849
- value.isError
850
- ? { ok: false, error: value.result }
851
- : { ok: true, output: value.result },
852
- );
957
+ const execution = toolExecutions.get(value.toolCallId);
958
+ if (execution != null && execution.toolOutput == null) {
959
+ execution.toolOutput = value.isError
960
+ ? ({
961
+ ...execution.toolCall,
962
+ type: 'tool-error',
963
+ error: value.result,
964
+ } as TypedToolError<TOOLS>)
965
+ : ({
966
+ ...execution.toolCall,
967
+ type: 'tool-result',
968
+ output: value.result,
969
+ } as TypedToolResult<TOOLS>);
970
+ }
971
+ if (
972
+ rawToolCallsByToolCallId.get(value.toolCallId)?.providerExecuted ===
973
+ true &&
974
+ execution?.toolOutput != null
975
+ ) {
976
+ stepProviderToolResults.push(
977
+ execution.toolOutput as ContentPart<TOOLS>,
978
+ );
979
+ }
853
980
  }
854
981
 
855
982
  if (value.type === 'tool-approval-request') {
@@ -925,9 +1052,9 @@ export function runPrompt<
925
1052
  if (value.type === 'finish') {
926
1053
  await waitForOutstandingHostToolExecutions();
927
1054
  finalFinish = value;
928
- await telemetry.end({
929
- finishReason: value.finishReason,
930
- usage: value.totalUsage,
1055
+ await lifecycle.end({
1056
+ steps: completedSteps,
1057
+ usage: asLanguageModelUsage(value.totalUsage),
931
1058
  });
932
1059
  }
933
1060
 
@@ -940,7 +1067,16 @@ export function runPrompt<
940
1067
  `Harness '${input.harness.harnessId}' could not find parsed tool call '${toolCall.toolCallId}' for custom tool approval.`,
941
1068
  );
942
1069
  }
943
- if (!hasTool({ tools: activeTools, toolName: toolCall.toolName })) {
1070
+ const isClientExecutedBuiltin =
1071
+ toolCall.toolName === 'askUserQuestions' &&
1072
+ Object.prototype.hasOwnProperty.call(
1073
+ input.harness.builtinTools,
1074
+ toolCall.toolName,
1075
+ );
1076
+ if (
1077
+ !isClientExecutedBuiltin &&
1078
+ !hasTool({ tools: activeTools, toolName: toolCall.toolName })
1079
+ ) {
944
1080
  const output = {
945
1081
  type: 'execution-denied',
946
1082
  reason: getHarnessV1BuiltinToolFilteringDenialReason({
@@ -951,9 +1087,27 @@ export function runPrompt<
951
1087
  toolCallId: toolCall.toolCallId,
952
1088
  output,
953
1089
  });
954
- await telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
1090
+ const execution = toolExecutions.get(toolCall.toolCallId);
1091
+ if (execution != null) {
1092
+ execution.toolOutput = toToolOutput({
1093
+ toolCall: parsedToolCall,
1094
+ outcome: { ok: true, output },
1095
+ });
1096
+ }
955
1097
  continue;
956
1098
  }
1099
+ if (isClientExecutedBuiltin) {
1100
+ recordPendingToolResult({ toolCall });
1101
+ if (
1102
+ expectedStepToolCallCount != null &&
1103
+ observedStepToolCallCount < expectedStepToolCallCount
1104
+ ) {
1105
+ pauseAfterStepToolCalls = true;
1106
+ continue;
1107
+ }
1108
+ await finishForHostInputPause({ completeCurrentStep: true });
1109
+ return;
1110
+ }
957
1111
  const customToolApprovalDecision = resolveCustomToolApproval({
958
1112
  toolName: toolCall.toolName,
959
1113
  toolApproval: input.toolApproval,
@@ -980,7 +1134,13 @@ export function runPrompt<
980
1134
  toolCallId: toolCall.toolCallId,
981
1135
  output,
982
1136
  });
983
- await telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
1137
+ const execution = toolExecutions.get(toolCall.toolCallId);
1138
+ if (execution != null) {
1139
+ execution.toolOutput = toToolOutput({
1140
+ toolCall: parsedToolCall,
1141
+ outcome: { ok: true, output },
1142
+ });
1143
+ }
984
1144
  continue;
985
1145
  }
986
1146
  const pendingApproval =
@@ -1056,10 +1216,11 @@ export function runPrompt<
1056
1216
  }
1057
1217
  startHostToolExecution(
1058
1218
  (async () => {
1219
+ const executionStartedAt = Date.now();
1059
1220
  const execution = await maybeExecuteHostTool({
1060
1221
  event: toolCall,
1061
1222
  tools: activeTools,
1062
- wrappedExecuteTool: telemetry.executeTool,
1223
+ wrappedExecuteTool: lifecycle.executeTool,
1063
1224
  sandboxSession: input.sandboxSession,
1064
1225
  abortSignal: input.abortSignal,
1065
1226
  control,
@@ -1084,14 +1245,16 @@ export function runPrompt<
1084
1245
  },
1085
1246
  input.sessionWorkDir,
1086
1247
  ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
1087
- result.enqueue({
1088
- type: 'tool-result',
1089
- toolCallId: toolCall.toolCallId,
1090
- toolName: toolCall.toolName,
1091
- input: undefined,
1092
- output: stripped.result,
1093
- preliminary: true,
1094
- } as TextStreamPart<TOOLS>);
1248
+ bufferedToolOutcomes.push(() => {
1249
+ result.enqueue({
1250
+ type: 'tool-result',
1251
+ toolCallId: toolCall.toolCallId,
1252
+ toolName: toolCall.toolName,
1253
+ input: undefined,
1254
+ output: stripped.result,
1255
+ preliminary: true,
1256
+ } as TextStreamPart<TOOLS>);
1257
+ });
1095
1258
  },
1096
1259
  });
1097
1260
  if (!execution.executed) {
@@ -1099,7 +1262,14 @@ export function runPrompt<
1099
1262
  `Harness '${input.harness.harnessId}' could not execute host tool '${toolCall.toolName}'.`,
1100
1263
  );
1101
1264
  }
1102
- await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
1265
+ const toolExecution = toolExecutions.get(toolCall.toolCallId);
1266
+ if (toolExecution != null) {
1267
+ toolExecution.toolOutput = toToolOutput({
1268
+ toolCall: parsedToolCall,
1269
+ outcome: execution.outcome,
1270
+ });
1271
+ toolExecution.toolExecutionMs = Date.now() - executionStartedAt;
1272
+ }
1103
1273
  })(),
1104
1274
  );
1105
1275
  }
@@ -1136,7 +1306,7 @@ export function runPrompt<
1136
1306
  } catch {
1137
1307
  // Preserve the error that stopped the reader loop.
1138
1308
  }
1139
- await telemetry.error(err);
1309
+ await lifecycle.error(err);
1140
1310
  logBridgeError({
1141
1311
  harnessId: input.harness.harnessId,
1142
1312
  sessionId: input.session.sessionId,
@@ -1167,6 +1337,23 @@ type HostToolExecution =
1167
1337
  | { executed: false }
1168
1338
  | { executed: true; outcome: HostToolOutcome };
1169
1339
 
1340
+ function toToolOutput<TOOLS extends ToolSet>(input: {
1341
+ toolCall: ToolCallTextStreamPart;
1342
+ outcome: HostToolOutcome;
1343
+ }): TypedToolResult<TOOLS> | TypedToolError<TOOLS> {
1344
+ return input.outcome.ok
1345
+ ? ({
1346
+ ...input.toolCall,
1347
+ type: 'tool-result',
1348
+ output: input.outcome.output,
1349
+ } as TypedToolResult<TOOLS>)
1350
+ : ({
1351
+ ...input.toolCall,
1352
+ type: 'tool-error',
1353
+ error: input.outcome.error,
1354
+ } as TypedToolError<TOOLS>);
1355
+ }
1356
+
1170
1357
  function asToolCallTextStreamPart<TOOLS extends ToolSet>(input: {
1171
1358
  part: TextStreamPart<TOOLS>;
1172
1359
  }): ToolCallTextStreamPart {
@@ -1198,7 +1385,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
1198
1385
  async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
1199
1386
  event: { toolCallId: string; toolName: string; input: string };
1200
1387
  tools: TOOLS;
1201
- wrappedExecuteTool: TurnTelemetry['executeTool'];
1388
+ wrappedExecuteTool: TurnLifecycle<ToolSet, Context>['executeTool'];
1202
1389
  sandboxSession: SandboxSession;
1203
1390
  abortSignal: AbortSignal | undefined;
1204
1391
  control: HarnessV1PromptControl;