@ai-sdk/harness 1.0.149 → 1.0.151

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.
@@ -29,6 +29,17 @@ import { generateId, type ToolSet } from '@ai-sdk/provider-utils';
29
29
  export function translateStreamPart<TOOLS extends ToolSet>(
30
30
  event: HarnessV1StreamPart,
31
31
  options: {
32
+ /**
33
+ * Returns the parsed tool call that produced a result. Harness tool-result
34
+ * events only carry the output, so their input and execution origin must
35
+ * be restored from the originating call.
36
+ */
37
+ getToolCall?: (toolCallId: string) =>
38
+ | {
39
+ input: unknown;
40
+ providerExecuted?: boolean;
41
+ }
42
+ | undefined;
32
43
  /**
33
44
  * Whether the tool call that produced this event ran inside the harness
34
45
  * runtime. Host tools travel the same `tool-result` events and their
@@ -119,9 +130,12 @@ export function translateStreamPart<TOOLS extends ToolSet>(
119
130
  return [];
120
131
 
121
132
  case 'tool-result':
133
+ const toolCall = options.getToolCall?.(event.toolCallId);
122
134
  if (
123
135
  event.isError === true &&
124
- (options.isProviderExecuted?.(event.toolCallId) ?? true)
136
+ (toolCall?.providerExecuted ??
137
+ options.isProviderExecuted?.(event.toolCallId) ??
138
+ true)
125
139
  ) {
126
140
  /*
127
141
  * `providerExecuted` decides whether the message survives:
@@ -134,7 +148,7 @@ export function translateStreamPart<TOOLS extends ToolSet>(
134
148
  type: 'tool-error',
135
149
  toolCallId: event.toolCallId,
136
150
  toolName: event.toolName,
137
- input: undefined,
151
+ input: toolCall?.input,
138
152
  error: event.result,
139
153
  providerExecuted: true,
140
154
  ...(event.dynamic !== undefined ? { dynamic: event.dynamic } : {}),
@@ -150,8 +164,11 @@ export function translateStreamPart<TOOLS extends ToolSet>(
150
164
  type: 'tool-result',
151
165
  toolCallId: event.toolCallId,
152
166
  toolName: event.toolName,
153
- input: undefined,
167
+ input: toolCall?.input,
154
168
  output: event.result,
169
+ ...(toolCall?.providerExecuted !== undefined
170
+ ? { providerExecuted: toolCall.providerExecuted }
171
+ : {}),
155
172
  ...(event.preliminary !== undefined
156
173
  ? { preliminary: event.preliminary }
157
174
  : {}),
@@ -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({