@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.
- package/CHANGELOG.md +20 -0
- package/dist/agent/index.d.ts +6 -5
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +263 -70
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.d.ts +18 -1
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +51 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +24 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +164 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/harness-agent-tool-approval-continuation.ts +7 -1
- package/src/agent/internal/harness-stream-text-result.ts +143 -27
- package/src/agent/internal/run-prompt.ts +155 -58
- package/src/agent/internal/translate-stream-part.ts +20 -3
- package/src/bridge/index.ts +4 -0
- package/src/utils/convert-harness-tool-model-output.ts +66 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/normalize-harness-tool-model-output.ts +66 -0
- package/src/v1/harness-v1-prompt-control.ts +3 -0
- package/src/v1/harness-v1-stream-part.ts +10 -11
|
@@ -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
|
-
(
|
|
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:
|
|
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:
|
|
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
|
: {}),
|
package/src/bridge/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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({
|