@ai-sdk/harness 1.0.150 → 1.0.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.150",
3
+ "version": "1.0.152",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@ai-sdk/provider": "4.0.27",
48
- "@ai-sdk/provider-utils": "5.0.59",
49
- "ai": "7.0.139"
48
+ "@ai-sdk/provider-utils": "5.0.60",
49
+ "ai": "7.0.141"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@ai-sdk/otel": "1.0.139",
61
+ "@ai-sdk/otel": "1.0.141",
62
62
  "@opentelemetry/sdk-trace-base": "2.7.1",
63
63
  "@types/node": "22.19.19",
64
64
  "@types/ws": "^8.5.13",
@@ -14,6 +14,9 @@ import type {
14
14
  * locally to find the original tool call before it can resume the paused turn.
15
15
  * Responses that already have a tool result are ignored, because those
16
16
  * approvals were already consumed by a prior continuation.
17
+ * An `execution-denied` result is synthesized by `convertToModelMessages`
18
+ * for a newly denied approval; it does not mean the harness has received
19
+ * the decision, so that approval response must still continue the turn.
17
20
  */
18
21
  export function collectHarnessAgentToolApprovalContinuations(input: {
19
22
  messages: readonly ModelMessage[];
@@ -38,7 +41,10 @@ export function collectHarnessAgentToolApprovalContinuations(input: {
38
41
 
39
42
  const toolResultIds = new Set<string>();
40
43
  for (const part of lastMessage.content) {
41
- if (part.type === 'tool-result') {
44
+ if (
45
+ part.type === 'tool-result' &&
46
+ part.output.type !== 'execution-denied'
47
+ ) {
42
48
  toolResultIds.add(part.toolCallId);
43
49
  }
44
50
  }
@@ -4,6 +4,7 @@ import {
4
4
  type AssistantModelMessage,
5
5
  type Context,
6
6
  type ToolModelMessage,
7
+ type ToolResultOutput,
7
8
  type ToolSet,
8
9
  } from '@ai-sdk/provider-utils';
9
10
  import {
@@ -11,6 +12,7 @@ import {
11
12
  asLanguageModelUsage,
12
13
  createAsyncIterableStream,
13
14
  createNullLanguageModelUsage,
15
+ createToolModelOutput,
14
16
  DefaultStepResult,
15
17
  toResponseMessages,
16
18
  } from 'ai/internal';
@@ -173,6 +175,7 @@ export class HarnessStreamTextResult<
173
175
  private stepStarted = false;
174
176
 
175
177
  private readonly tools: TOOLS;
178
+ private readonly toolModelOutputs = new Map<string, ToolResultOutput>();
176
179
  private readonly runtimeContext: RUNTIME_CONTEXT;
177
180
  private readonly toolsContext: InferToolSetContext<TOOLS>;
178
181
  private readonly providerName: string;
@@ -188,6 +191,13 @@ export class HarnessStreamTextResult<
188
191
  private aggregateWarnings: CallWarning[] = [];
189
192
  private settled = false;
190
193
 
194
+ setToolModelOutput(input: {
195
+ toolCallId: string;
196
+ output: ToolResultOutput;
197
+ }): void {
198
+ this.toolModelOutputs.set(input.toolCallId, input.output);
199
+ }
200
+
191
201
  constructor(options: {
192
202
  tools: TOOLS;
193
203
  runtimeContext: RUNTIME_CONTEXT;
@@ -512,13 +522,26 @@ export class HarnessStreamTextResult<
512
522
  this._response.resolve(finalStep.response);
513
523
  this._providerMetadata.resolve(this.finalProviderMetadata);
514
524
 
525
+ const responseTools: ToolSet = {};
526
+ for (const [name, tool] of Object.entries(this.tools)) {
527
+ responseTools[name] = {
528
+ ...tool,
529
+ toModelOutput: (options: {
530
+ toolCallId: string;
531
+ input: unknown;
532
+ output: unknown;
533
+ }) =>
534
+ this.toolModelOutputs.get(options.toolCallId) ??
535
+ createToolModelOutput({ ...options, tool, errorMode: 'none' }),
536
+ };
537
+ }
515
538
  const responseMessages: Array<AssistantModelMessage | ToolModelMessage> =
516
539
  [];
517
540
  for (const step of this.stepsBuffer) {
518
541
  responseMessages.push(
519
542
  ...(await toResponseMessages<TOOLS>({
520
543
  content: step.content,
521
- tools: this.tools,
544
+ tools: responseTools as TOOLS,
522
545
  })),
523
546
  );
524
547
  }
@@ -24,6 +24,7 @@ import {
24
24
  type Experimental_SandboxSession as SandboxSession,
25
25
  type ToolApprovalResponse,
26
26
  type ToolResultPart,
27
+ type ToolResultOutput,
27
28
  type ToolSet,
28
29
  } from '@ai-sdk/provider-utils';
29
30
  import {
@@ -36,6 +37,7 @@ import {
36
37
  addLanguageModelUsage,
37
38
  asLanguageModelUsage,
38
39
  createNullLanguageModelUsage,
40
+ createToolModelOutput,
39
41
  parseToolCall,
40
42
  validateToolContext,
41
43
  } from 'ai/internal';
@@ -68,6 +70,8 @@ import {
68
70
  import { resolveCustomToolApproval } from './permission-mode';
69
71
  import { logBridgeError } from '../../utils/bridge-diagnostics';
70
72
  import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
73
+ import { normalizeHarnessToolModelOutput } from '../../utils/normalize-harness-tool-model-output';
74
+ import { HarnessCapabilityUnsupportedError } from '../../errors/harness-capability-unsupported-error';
71
75
 
72
76
  const invalidToolInputMessage = 'Tool input validation failed.';
73
77
 
@@ -424,9 +428,24 @@ export function runPrompt<
424
428
  toolExecutions.clear();
425
429
  bufferedToolOutcomes = [];
426
430
  };
427
- let expectedStepToolCallCount: number | undefined;
428
- let observedStepToolCallCount = 0;
429
- let pauseAfterStepToolCalls = false;
431
+ type FinishStepPart = Extract<HarnessV1StreamPart, { type: 'finish-step' }>;
432
+ /*
433
+ * An approval request or a pending client tool result does not end the
434
+ * response right away: the rest of the step (other tool calls, native
435
+ * results, host tool executions) still has to be surfaced. The pause
436
+ * happens at the next `finish-step`, once every host tool started in
437
+ * this response has been echoed back by the runtime.
438
+ */
439
+ let hostInputPauseRequested = false;
440
+ let deferredPauseFinishStep: FinishStepPart | undefined;
441
+ /*
442
+ * A `finish-step` that arrives while host tools started in this step have
443
+ * not been echoed back is the adapter signalling that the runtime waits on
444
+ * host input, not the end of the step. It is remembered so the step can
445
+ * still be closed if the turn finishes without another `finish-step`.
446
+ */
447
+ let ignoredFinishStep: FinishStepPart | undefined;
448
+ const hostToolCallIdsAwaitingEcho = new Set<string>();
430
449
  const buildModelCallContent = (): ContentPart<TOOLS>[] => {
431
450
  const parts: ContentPart<TOOLS>[] = [];
432
451
  if (stepText) parts.push({ type: 'text', text: stepText });
@@ -444,9 +463,7 @@ export function runPrompt<
444
463
  stepApprovalRequests = [];
445
464
  bufferedToolOutcomes = [];
446
465
  toolExecutions.clear();
447
- expectedStepToolCallCount = undefined;
448
- observedStepToolCallCount = 0;
449
- pauseAfterStepToolCalls = false;
466
+ ignoredFinishStep = undefined;
450
467
  };
451
468
  const zeroUsage: LanguageModelV4Usage = {
452
469
  inputTokens: {
@@ -487,25 +504,27 @@ export function runPrompt<
487
504
  resetStepContent();
488
505
  return step;
489
506
  };
490
- const finishForHostInputPause = async (options: {
491
- completeCurrentStep: boolean;
492
- }): Promise<void> => {
507
+ const finishForHostInputPause = async (): Promise<void> => {
493
508
  await waitForOutstandingHostToolExecutions();
494
- if (options.completeCurrentStep) {
495
- await completeStep({
496
- finishReason: toolCallsFinishReason,
497
- usage: zeroUsage,
498
- providerMetadata: undefined,
499
- });
500
- } else {
501
- await publishToolExecutions();
502
- }
509
+ await publishToolExecutions();
503
510
  await lifecycle.end({
504
511
  steps: completedSteps,
505
512
  usage: asLanguageModelUsage(zeroUsage),
506
513
  });
507
514
  await result.finish();
508
515
  };
516
+ const pauseForHostInput = async (
517
+ finishStep: FinishStepPart | undefined,
518
+ ): Promise<void> => {
519
+ await waitForOutstandingHostToolExecutions();
520
+ closingResumedStep = false;
521
+ await completeStep({
522
+ finishReason: toolCallsFinishReason,
523
+ usage: finishStep?.usage ?? zeroUsage,
524
+ providerMetadata: finishStep?.harnessMetadata,
525
+ });
526
+ await finishForHostInputPause();
527
+ };
509
528
  const enqueueApprovalRequest = (approval: {
510
529
  approvalId: string;
511
530
  toolCall: ToolCallTextStreamPart;
@@ -612,7 +631,37 @@ export function runPrompt<
612
631
  };
613
632
  const submitToolResult: HarnessV1PromptControl['submitToolResult'] =
614
633
  async submission => {
634
+ if (submission.toolResult != null) {
635
+ let output: ToolResultOutput;
636
+ try {
637
+ output = normalizeHarnessToolModelOutput({
638
+ output: submission.toolResult.output,
639
+ });
640
+ } catch (error) {
641
+ if (
642
+ !continuationsByToolCallId.has(submission.toolCallId) ||
643
+ !HarnessCapabilityUnsupportedError.isInstance(error)
644
+ ) {
645
+ throw error;
646
+ }
647
+ output = {
648
+ type: 'text',
649
+ value: JSON.stringify(submission.output) ?? 'null',
650
+ };
651
+ }
652
+ submission = {
653
+ ...submission,
654
+ toolResult: { ...submission.toolResult, output },
655
+ };
656
+ result.setToolModelOutput({
657
+ toolCallId: submission.toolCallId,
658
+ output,
659
+ });
660
+ }
615
661
  if (!input.isTurnSuspending?.()) {
662
+ if (!settledHostToolCallIds.has(submission.toolCallId)) {
663
+ hostToolCallIdsAwaitingEcho.add(submission.toolCallId);
664
+ }
616
665
  return control.submitToolResult(submission);
617
666
  }
618
667
  const toolCall =
@@ -811,7 +860,7 @@ export function runPrompt<
811
860
  });
812
861
  if (!execution.executed) {
813
862
  recordPendingToolResult({ toolCall: rawToolCall });
814
- await finishForHostInputPause({ completeCurrentStep: false });
863
+ await finishForHostInputPause();
815
864
  return 'awaiting-tool-result';
816
865
  }
817
866
  const toolExecution = toolExecutions.get(rawToolCall.toolCallId)!;
@@ -855,11 +904,10 @@ export function runPrompt<
855
904
 
856
905
  while (true) {
857
906
  if (
858
- pauseAfterStepToolCalls &&
859
- expectedStepToolCallCount != null &&
860
- observedStepToolCallCount >= expectedStepToolCallCount
907
+ deferredPauseFinishStep != null &&
908
+ hostToolCallIdsAwaitingEcho.size === 0
861
909
  ) {
862
- await finishForHostInputPause({ completeCurrentStep: true });
910
+ await pauseForHostInput(deferredPauseFinishStep);
863
911
  return;
864
912
  }
865
913
 
@@ -877,6 +925,9 @@ export function runPrompt<
877
925
  result.enqueueRaw(value.rawValue);
878
926
  continue;
879
927
  }
928
+ if (value.type === 'tool-result') {
929
+ hostToolCallIdsAwaitingEcho.delete(value.toolCallId);
930
+ }
880
931
 
881
932
  if (pendingStopBoundary != null) {
882
933
  if (value.type === 'finish') {
@@ -967,6 +1018,45 @@ export function runPrompt<
967
1018
  if (settledHostInputReplay || settledBuiltinApprovalReplay) {
968
1019
  continue;
969
1020
  }
1021
+ if (value.type === 'finish-step') {
1022
+ if (hostInputPauseRequested) {
1023
+ await waitForOutstandingHostToolExecutions();
1024
+ if (hostToolCallIdsAwaitingEcho.size === 0) {
1025
+ await pauseForHostInput(value);
1026
+ return;
1027
+ }
1028
+ deferredPauseFinishStep = value;
1029
+ continue;
1030
+ }
1031
+ if (hostToolCallIdsAwaitingEcho.size > 0) {
1032
+ ignoredFinishStep = value;
1033
+ continue;
1034
+ }
1035
+ ignoredFinishStep = undefined;
1036
+ }
1037
+ if (value.type === 'finish') {
1038
+ if (hostInputPauseRequested) {
1039
+ await pauseForHostInput(deferredPauseFinishStep);
1040
+ return;
1041
+ }
1042
+ if (ignoredFinishStep != null) {
1043
+ const finishStep = ignoredFinishStep;
1044
+ ignoredFinishStep = undefined;
1045
+ await waitForOutstandingHostToolExecutions();
1046
+ if (closingResumedStep) {
1047
+ closingResumedStep = false;
1048
+ await publishToolExecutions();
1049
+ resetStepContent();
1050
+ result.discardCurrentStepContent();
1051
+ } else {
1052
+ await completeStep({
1053
+ finishReason: finishStep.finishReason,
1054
+ usage: finishStep.usage,
1055
+ providerMetadata: finishStep.harnessMetadata,
1056
+ });
1057
+ }
1058
+ }
1059
+ }
970
1060
  if (displayValue.type === 'finish-step' && closingResumedStep) {
971
1061
  closingResumedStep = false;
972
1062
  await publishToolExecutions();
@@ -997,6 +1087,12 @@ export function runPrompt<
997
1087
  `Harness '${input.harness.harnessId}' emitted a built-in tool approval request but does not support approval responses.`,
998
1088
  );
999
1089
  }
1090
+ /*
1091
+ * The runtime may report a step boundary while it waits for this
1092
+ * decision. The decision is answered here, so that boundary must
1093
+ * not end the step before the denied call's result arrives.
1094
+ */
1095
+ hostToolCallIdsAwaitingEcho.add(displayValue.toolCallId);
1000
1096
  await control.submitToolApproval({
1001
1097
  approvalId: displayValue.approvalId,
1002
1098
  approved: false,
@@ -1103,8 +1199,6 @@ export function runPrompt<
1103
1199
  }
1104
1200
 
1105
1201
  if (value.type === 'tool-call') {
1106
- observedStepToolCallCount += 1;
1107
- expectedStepToolCallCount ??= value.stepToolCallCount;
1108
1202
  const toolCall = toolCallsByToolCallId.get(value.toolCallId);
1109
1203
  if (toolCall != null) {
1110
1204
  stepToolCalls.push(toolCall as ContentPart<TOOLS>);
@@ -1204,8 +1298,8 @@ export function runPrompt<
1204
1298
  approvalId: pendingApproval.approvalId,
1205
1299
  toolCall,
1206
1300
  });
1207
- await finishForHostInputPause({ completeCurrentStep: true });
1208
- return;
1301
+ hostInputPauseRequested = true;
1302
+ continue;
1209
1303
  }
1210
1304
 
1211
1305
  // Drive step boundaries.
@@ -1273,15 +1367,8 @@ export function runPrompt<
1273
1367
  }
1274
1368
  if (isClientExecutedBuiltin) {
1275
1369
  recordPendingToolResult({ toolCall });
1276
- if (
1277
- expectedStepToolCallCount != null &&
1278
- observedStepToolCallCount < expectedStepToolCallCount
1279
- ) {
1280
- pauseAfterStepToolCalls = true;
1281
- continue;
1282
- }
1283
- await finishForHostInputPause({ completeCurrentStep: true });
1284
- return;
1370
+ hostInputPauseRequested = true;
1371
+ continue;
1285
1372
  }
1286
1373
  if (validatedHostToolCall == null) {
1287
1374
  throw new Error(
@@ -1396,28 +1483,15 @@ export function runPrompt<
1396
1483
  approvalId: pendingApproval.approvalId,
1397
1484
  toolCall: pendingParsedToolCall,
1398
1485
  });
1399
- if (
1400
- expectedStepToolCallCount != null &&
1401
- observedStepToolCallCount < expectedStepToolCallCount
1402
- ) {
1403
- pauseAfterStepToolCalls = true;
1404
- continue;
1405
- }
1406
- await finishForHostInputPause({ completeCurrentStep: true });
1407
- return;
1486
+ hostInputPauseRequested = true;
1487
+ continue;
1408
1488
  }
1409
1489
  if (!isExecutableTool(activeTools[toolCall.toolName])) {
1410
1490
  recordPendingToolResult({ toolCall });
1411
- if (
1412
- expectedStepToolCallCount != null &&
1413
- observedStepToolCallCount < expectedStepToolCallCount
1414
- ) {
1415
- pauseAfterStepToolCalls = true;
1416
- continue;
1417
- }
1418
- await finishForHostInputPause({ completeCurrentStep: true });
1419
- return;
1491
+ hostInputPauseRequested = true;
1492
+ continue;
1420
1493
  }
1494
+ hostToolCallIdsAwaitingEcho.add(toolCall.toolCallId);
1421
1495
  startHostToolExecution(
1422
1496
  (async () => {
1423
1497
  const toolExecution = toolExecutions.get(toolCall.toolCallId);
@@ -1484,6 +1558,10 @@ export function runPrompt<
1484
1558
  );
1485
1559
  }
1486
1560
  }
1561
+ if (hostInputPauseRequested && input.isTurnSuspending?.() !== true) {
1562
+ await pauseForHostInput(deferredPauseFinishStep);
1563
+ return;
1564
+ }
1487
1565
  await waitForOutstandingHostToolExecutions();
1488
1566
  const isTurnSuspending = input.isTurnSuspending?.() === true;
1489
1567
  if (isTurnSuspending) {
@@ -1698,6 +1776,22 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
1698
1776
  await input.submitToolResult({
1699
1777
  toolCallId: input.event.toolCallId,
1700
1778
  output,
1779
+ ...(tool.toModelOutput == null
1780
+ ? {}
1781
+ : {
1782
+ toolResult: {
1783
+ type: 'tool-result' as const,
1784
+ toolCallId: input.event.toolCallId,
1785
+ toolName: input.event.toolName,
1786
+ output: await createToolModelOutput({
1787
+ toolCallId: input.event.toolCallId,
1788
+ input: input.parsedToolCall.input,
1789
+ output,
1790
+ tool,
1791
+ errorMode: 'none',
1792
+ }),
1793
+ },
1794
+ }),
1701
1795
  });
1702
1796
  return { executed: true, outcome: { ok: true, output } };
1703
1797
  } catch (err) {
@@ -15,6 +15,10 @@ import type { ToolResultPart } from '@ai-sdk/provider-utils';
15
15
  import { WebSocketServer, type WebSocket } from 'ws';
16
16
 
17
17
  export { HarnessBridgeCapabilityUnsupportedError } from './harness-bridge-capability-unsupported-error';
18
+ export {
19
+ convertHarnessToolModelOutput,
20
+ type HarnessToolModelOutputContent,
21
+ } from '../utils/convert-harness-tool-model-output';
18
22
 
19
23
  export type BridgeState = 'init' | 'waiting' | 'running' | 'draining' | 'done';
20
24
 
@@ -0,0 +1,66 @@
1
+ import type { ToolResultOutput } from '@ai-sdk/provider-utils';
2
+ import { HarnessBridgeCapabilityUnsupportedError } from '../bridge/harness-bridge-capability-unsupported-error';
3
+
4
+ export type HarnessToolModelOutputContent =
5
+ | { type: 'text'; text: string }
6
+ | { type: 'image'; data: string; mediaType: string };
7
+
8
+ type ToolModelOutputContentPart = Extract<
9
+ ToolResultOutput,
10
+ { type: 'content' }
11
+ >['value'][number];
12
+
13
+ function getNormalizedInlineImage(
14
+ part: ToolModelOutputContentPart,
15
+ ): { data: string; mediaType: string } | undefined {
16
+ if (
17
+ part.type === 'file' &&
18
+ part.mediaType.startsWith('image/') &&
19
+ part.mediaType !== 'image/*' &&
20
+ part.data.type === 'data' &&
21
+ typeof part.data.data === 'string'
22
+ ) {
23
+ return { data: part.data.data, mediaType: part.mediaType };
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function convertHarnessToolModelOutput({
29
+ output,
30
+ }: {
31
+ output: ToolResultOutput;
32
+ }): { content: HarnessToolModelOutputContent[]; isError: boolean } {
33
+ switch (output.type) {
34
+ case 'text':
35
+ case 'error-text':
36
+ return {
37
+ content: [{ type: 'text', text: output.value }],
38
+ isError: output.type === 'error-text',
39
+ };
40
+ case 'json':
41
+ case 'error-json':
42
+ return {
43
+ content: [{ type: 'text', text: JSON.stringify(output.value) }],
44
+ isError: output.type === 'error-json',
45
+ };
46
+ case 'execution-denied':
47
+ return {
48
+ content: [
49
+ { type: 'text', text: output.reason ?? 'Tool execution denied.' },
50
+ ],
51
+ isError: true,
52
+ };
53
+ case 'content':
54
+ return {
55
+ content: output.value.map(part => {
56
+ if (part.type === 'text') return { type: 'text', text: part.text };
57
+ const image = getNormalizedInlineImage(part);
58
+ if (image != null) return { type: 'image', ...image };
59
+ throw new HarnessBridgeCapabilityUnsupportedError({
60
+ message: `Harnesses support only text and inline images in tool model output; unsupported content '${part.type}'.`,
61
+ });
62
+ }),
63
+ isError: false,
64
+ };
65
+ }
66
+ }
@@ -71,3 +71,8 @@ export {
71
71
  export { resolveSandboxDefaultWorkingDirectory } from './resolve-sandbox-default-working-directory';
72
72
  export { getRestrictedSandboxSession } from './get-restricted-sandbox-session';
73
73
  export { sleep } from './sleep';
74
+ export { normalizeHarnessToolModelOutput } from './normalize-harness-tool-model-output';
75
+ export {
76
+ convertHarnessToolModelOutput,
77
+ type HarnessToolModelOutputContent,
78
+ } from './convert-harness-tool-model-output';
@@ -0,0 +1,66 @@
1
+ import {
2
+ convertInlineFileDataToUint8Array,
3
+ convertUint8ArrayToBase64,
4
+ detectMediaType,
5
+ getTopLevelMediaType,
6
+ isFullMediaType,
7
+ type ToolResultOutput,
8
+ } from '@ai-sdk/provider-utils';
9
+ import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
10
+
11
+ export function normalizeHarnessToolModelOutput({
12
+ output,
13
+ }: {
14
+ output: ToolResultOutput;
15
+ }): ToolResultOutput {
16
+ try {
17
+ const normalized: ToolResultOutput =
18
+ output.type !== 'content'
19
+ ? output
20
+ : {
21
+ ...output,
22
+ value: output.value.map(part => {
23
+ if (part.type === 'text') return part;
24
+ const file =
25
+ part.type === 'image-data' || part.type === 'file-data'
26
+ ? {
27
+ ...part,
28
+ type: 'file' as const,
29
+ data: { type: 'data' as const, data: part.data },
30
+ }
31
+ : part;
32
+ if (
33
+ file.type !== 'file' ||
34
+ file.data.type !== 'data' ||
35
+ getTopLevelMediaType(file.mediaType) !== 'image'
36
+ ) {
37
+ throw new Error(
38
+ `Unsupported content '${part.type}'${file.type === 'file' ? ` (${file.mediaType}, ${file.data.type})` : ''}.`,
39
+ );
40
+ }
41
+ const data =
42
+ typeof file.data.data === 'string'
43
+ ? file.data.data
44
+ : convertUint8ArrayToBase64(
45
+ convertInlineFileDataToUint8Array(file.data),
46
+ );
47
+ const mediaType = isFullMediaType(file.mediaType)
48
+ ? file.mediaType
49
+ : detectMediaType({ data, topLevelType: 'image' });
50
+ if (mediaType == null)
51
+ throw new Error('Cannot detect the inline image media type.');
52
+ return {
53
+ ...file,
54
+ mediaType,
55
+ data: { type: 'data' as const, data },
56
+ };
57
+ }),
58
+ };
59
+ return normalized;
60
+ } catch (cause) {
61
+ throw new HarnessCapabilityUnsupportedError({
62
+ message: `Harnesses support only text and inline images in tool model output. ${cause instanceof Error ? cause.message : String(cause)}`,
63
+ cause,
64
+ });
65
+ }
66
+ }
@@ -13,6 +13,9 @@ export type HarnessV1PromptControl = {
13
13
  /**
14
14
  * Provide a result for a `tool-call` the adapter emitted. The adapter
15
15
  * forwards the result to the underlying runtime so the model can continue.
16
+ * `output` is the raw execution result for consumer-facing events. When
17
+ * `toolResult` is present, its output is the model-facing representation
18
+ * and takes precedence when constructing the runtime's tool response.
16
19
  */
17
20
  submitToolResult(input: {
18
21
  toolCallId: string;
@@ -65,11 +65,6 @@ export type HarnessV1StreamPart =
65
65
  // when it differs from the wire `toolName` (e.g. `toolName: 'bash'`,
66
66
  // `nativeName: 'Bash'`).
67
67
  //
68
- // `stepToolCallCount` lets adapters that know a step's complete tool-call
69
- // set up front report its cardinality. The host uses it to collect every
70
- // approval/result request from the step before pausing; adapters that cannot
71
- // know the count omit it and retain pause-on-first behavior.
72
- //
73
68
  // Whether the call was executed by the underlying runtime (Claude Code's
74
69
  // built-in `Bash`, Codex's `shell`) vs. needs host dispatch is signalled by
75
70
  // the standard `providerExecuted` field on `LanguageModelV4ToolCall` —
@@ -79,16 +74,21 @@ export type HarnessV1StreamPart =
79
74
  | Extract<LanguageModelV4StreamPart, { type: 'tool-input-end' }>
80
75
  | (LanguageModelV4ToolCall & {
81
76
  nativeName?: string;
82
- /**
83
- * Total tool calls in the current model step, when known before tool
84
- * execution begins. Populate this on every tool call in the step.
85
- */
86
- stepToolCallCount?: number;
87
77
  })
88
78
  | LanguageModelV4ToolApprovalRequest
89
79
  | LanguageModelV4ToolResult
90
80
 
91
81
  // Step boundary inside a multi-step turn.
82
+ //
83
+ // Adapters also emit `finish-step` when the runtime cannot make progress
84
+ // without host input: every unsettled tool call of the step waits for
85
+ // `submitToolResult`, `submitToolApproval`, or a question answer. The
86
+ // runtime's step stays open, and the adapter emits another `finish-step`
87
+ // once those calls settle. Such a `finish-step` is only emitted when a
88
+ // waiting call was not already covered by an earlier one in the same step.
89
+ // The host may pause the turn there for pending approvals or client tool
90
+ // results, and ignores it while host tools it started have not been echoed
91
+ // back as `tool-result` parts yet.
92
92
  | {
93
93
  type: 'finish-step';
94
94
  finishReason: LanguageModelV4FinishReason;
@@ -304,7 +304,6 @@ export const harnessV1ToolCallPartSchema = z.object({
304
304
  dynamic: z.boolean().optional(),
305
305
  providerMetadata: harnessV1ProviderMetadataSchema.optional(),
306
306
  nativeName: z.string().optional(),
307
- stepToolCallCount: z.number().int().positive().optional(),
308
307
  });
309
308
 
310
309
  export const harnessV1ToolApprovalRequestPartSchema = z.object({