@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/CHANGELOG.md +18 -0
- package/dist/agent/index.d.ts +6 -5
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +172 -44
- 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 +4 -4
- package/src/agent/harness-agent-tool-approval-continuation.ts +7 -1
- package/src/agent/internal/harness-stream-text-result.ts +24 -1
- package/src/agent/internal/run-prompt.ts +148 -54
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
49
|
-
"ai": "7.0.
|
|
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.
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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 (
|
|
491
|
-
completeCurrentStep: boolean;
|
|
492
|
-
}): Promise<void> => {
|
|
507
|
+
const finishForHostInputPause = async (): Promise<void> => {
|
|
493
508
|
await waitForOutstandingHostToolExecutions();
|
|
494
|
-
|
|
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(
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
observedStepToolCallCount >= expectedStepToolCallCount
|
|
907
|
+
deferredPauseFinishStep != null &&
|
|
908
|
+
hostToolCallIdsAwaitingEcho.size === 0
|
|
861
909
|
) {
|
|
862
|
-
await
|
|
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
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
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) {
|
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({
|