@ai-sdk/harness 1.0.123 → 1.0.124
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 +9 -0
- package/dist/agent/index.js +145 -15
- package/dist/agent/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/internal/run-prompt.ts +134 -19
- package/src/agent/internal/strip-work-dir.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.124",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ai-sdk/provider": "4.0.18",
|
|
48
48
|
"@ai-sdk/provider-utils": "5.0.47",
|
|
49
|
-
"ai": "7.0.
|
|
49
|
+
"ai": "7.0.114"
|
|
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.114",
|
|
62
62
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
63
63
|
"@types/node": "22.19.19",
|
|
64
64
|
"@types/ws": "^8.5.13",
|
|
@@ -53,7 +53,11 @@ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-set
|
|
|
53
53
|
import { HarnessStreamTextResult } from './harness-stream-text-result';
|
|
54
54
|
import { getOwn } from './get-own';
|
|
55
55
|
import { translateStreamPart } from './translate-stream-part';
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
createToolInputWorkDirStripper,
|
|
58
|
+
stripParsedToolInputWorkDir,
|
|
59
|
+
stripWorkDir,
|
|
60
|
+
} from './strip-work-dir';
|
|
57
61
|
import {
|
|
58
62
|
createTurnLifecycle,
|
|
59
63
|
type HarnessAgentLifecycleCallbacks,
|
|
@@ -63,6 +67,8 @@ import { resolveCustomToolApproval } from './permission-mode';
|
|
|
63
67
|
import { logBridgeError } from '../../utils/bridge-diagnostics';
|
|
64
68
|
import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
|
|
65
69
|
|
|
70
|
+
const invalidToolInputMessage = 'Tool input validation failed.';
|
|
71
|
+
|
|
66
72
|
function unwrapToolResultOutput(toolResult: ToolResultPart): {
|
|
67
73
|
output: unknown;
|
|
68
74
|
isError?: boolean;
|
|
@@ -647,7 +653,9 @@ export function runPrompt<
|
|
|
647
653
|
continuation: ToolApprovalResponse,
|
|
648
654
|
): Promise<'continued' | 'awaiting-tool-result'> => {
|
|
649
655
|
const rawToolCall =
|
|
650
|
-
|
|
656
|
+
(approval.kind === 'builtin'
|
|
657
|
+
? rawToolCallsByToolCallId.get(approval.toolCallId)
|
|
658
|
+
: undefined) ??
|
|
651
659
|
({
|
|
652
660
|
type: 'tool-call',
|
|
653
661
|
toolCallId: approval.toolCallId,
|
|
@@ -656,16 +664,31 @@ export function runPrompt<
|
|
|
656
664
|
providerExecuted: approval.providerExecuted,
|
|
657
665
|
nativeName: approval.nativeName,
|
|
658
666
|
} satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
667
|
+
let validatedToolCall: ToolCallTextStreamPart | undefined;
|
|
668
|
+
let toolCall: ToolCallTextStreamPart;
|
|
669
|
+
if (approval.kind === 'custom' && continuation.approved) {
|
|
670
|
+
validatedToolCall = asToolCallTextStreamPart({
|
|
671
|
+
part: await validateToolCall({
|
|
672
|
+
event: rawToolCall,
|
|
673
|
+
tools: input.tools,
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
toolCall = displayHostToolCall({
|
|
677
|
+
toolCall: validatedToolCall,
|
|
678
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
const parsedInput = await safeParseJSON({ text: rawToolCall.input });
|
|
682
|
+
toolCall = {
|
|
683
|
+
type: 'tool-call',
|
|
684
|
+
toolCallId: rawToolCall.toolCallId,
|
|
685
|
+
toolName: rawToolCall.toolName,
|
|
686
|
+
input: parsedInput.success ? parsedInput.value : rawToolCall.input,
|
|
687
|
+
...(rawToolCall.providerExecuted !== undefined
|
|
688
|
+
? { providerExecuted: rawToolCall.providerExecuted }
|
|
689
|
+
: {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
669
692
|
|
|
670
693
|
enqueueApprovalResponse(approval, continuation, toolCall);
|
|
671
694
|
onToolApprovalSettled(approval.approvalId);
|
|
@@ -699,6 +722,29 @@ export function runPrompt<
|
|
|
699
722
|
return 'continued';
|
|
700
723
|
}
|
|
701
724
|
|
|
725
|
+
if (validatedToolCall == null) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Harness '${input.harness.harnessId}' could not validate approved host tool '${approval.toolName}'.`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (validatedToolCall.invalid) {
|
|
732
|
+
const error = new Error(invalidToolInputMessage);
|
|
733
|
+
await submitToolResult({
|
|
734
|
+
toolCallId: approval.toolCallId,
|
|
735
|
+
output: { error: invalidToolInputMessage },
|
|
736
|
+
isError: true,
|
|
737
|
+
});
|
|
738
|
+
bufferedToolOutcomes.push(() => {
|
|
739
|
+
enqueueHostToolOutcome({
|
|
740
|
+
toolCall,
|
|
741
|
+
outcome: { ok: false, error },
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
await publishToolExecutions();
|
|
745
|
+
return 'continued';
|
|
746
|
+
}
|
|
747
|
+
|
|
702
748
|
await lifecycle.start(input.model);
|
|
703
749
|
toolExecutions.set(rawToolCall.toolCallId, {
|
|
704
750
|
toolCall: toolCall as TypedToolCall<TOOLS>,
|
|
@@ -706,6 +752,7 @@ export function runPrompt<
|
|
|
706
752
|
const executionStartedAt = Date.now();
|
|
707
753
|
const execution = await maybeExecuteHostTool({
|
|
708
754
|
event: rawToolCall,
|
|
755
|
+
parsedToolCall: validatedToolCall,
|
|
709
756
|
tools: activeTools,
|
|
710
757
|
toolsContext,
|
|
711
758
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -886,7 +933,6 @@ export function runPrompt<
|
|
|
886
933
|
if (settledHostInputReplay || settledBuiltinApprovalReplay) {
|
|
887
934
|
continue;
|
|
888
935
|
}
|
|
889
|
-
|
|
890
936
|
if (displayValue.type === 'finish-step' && closingResumedStep) {
|
|
891
937
|
closingResumedStep = false;
|
|
892
938
|
await publishToolExecutions();
|
|
@@ -971,15 +1017,34 @@ export function runPrompt<
|
|
|
971
1017
|
|
|
972
1018
|
// Tool-call validation lives here (not in translateStreamPart) because
|
|
973
1019
|
// schema parsing is async and needs the merged tool set in scope.
|
|
1020
|
+
let validatedHostToolCall: ToolCallTextStreamPart | undefined;
|
|
974
1021
|
if (displayValue.type === 'tool-call') {
|
|
1022
|
+
const isHostTool =
|
|
1023
|
+
value.type === 'tool-call' &&
|
|
1024
|
+
!value.providerExecuted &&
|
|
1025
|
+
hasTool({ tools: activeTools, toolName: value.toolName }) &&
|
|
1026
|
+
!(
|
|
1027
|
+
value.toolName === 'askUserQuestions' &&
|
|
1028
|
+
hasTool({
|
|
1029
|
+
tools: input.harness.builtinTools,
|
|
1030
|
+
toolName: value.toolName,
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
975
1033
|
const parsed = await validateToolCall<TOOLS>({
|
|
976
|
-
event: displayValue,
|
|
1034
|
+
event: isHostTool ? value : displayValue,
|
|
977
1035
|
tools: input.tools,
|
|
978
1036
|
});
|
|
979
1037
|
const parsedToolCall = asToolCallTextStreamPart({ part: parsed });
|
|
1038
|
+
validatedHostToolCall = isHostTool ? parsedToolCall : undefined;
|
|
1039
|
+
const displayToolCall = isHostTool
|
|
1040
|
+
? displayHostToolCall({
|
|
1041
|
+
toolCall: parsedToolCall,
|
|
1042
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1043
|
+
})
|
|
1044
|
+
: parsedToolCall;
|
|
980
1045
|
rawToolCallsByToolCallId.set(displayValue.toolCallId, displayValue);
|
|
981
|
-
toolCallsByToolCallId.set(displayValue.toolCallId,
|
|
982
|
-
result.enqueue(
|
|
1046
|
+
toolCallsByToolCallId.set(displayValue.toolCallId, displayToolCall);
|
|
1047
|
+
result.enqueue(displayToolCall as TextStreamPart<TOOLS>);
|
|
983
1048
|
}
|
|
984
1049
|
|
|
985
1050
|
if (value.type === 'text-delta') {
|
|
@@ -1155,6 +1220,31 @@ export function runPrompt<
|
|
|
1155
1220
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
1156
1221
|
return;
|
|
1157
1222
|
}
|
|
1223
|
+
if (validatedHostToolCall == null) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`Harness '${input.harness.harnessId}' could not validate host tool '${toolCall.toolName}'.`,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (validatedHostToolCall.invalid) {
|
|
1229
|
+
settledHostToolCallIds.add(toolCall.toolCallId);
|
|
1230
|
+
toolExecutions.delete(toolCall.toolCallId);
|
|
1231
|
+
bufferedToolOutcomes.push(() => {
|
|
1232
|
+
result.enqueue({
|
|
1233
|
+
type: 'tool-error',
|
|
1234
|
+
toolCallId: toolCall.toolCallId,
|
|
1235
|
+
toolName: toolCall.toolName,
|
|
1236
|
+
input: parsedToolCall.input,
|
|
1237
|
+
error: new Error(invalidToolInputMessage),
|
|
1238
|
+
dynamic: true,
|
|
1239
|
+
} as TextStreamPart<TOOLS>);
|
|
1240
|
+
});
|
|
1241
|
+
await submitToolResult({
|
|
1242
|
+
toolCallId: toolCall.toolCallId,
|
|
1243
|
+
output: { error: invalidToolInputMessage },
|
|
1244
|
+
isError: true,
|
|
1245
|
+
});
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1158
1248
|
const customToolApprovalDecision = resolveCustomToolApproval({
|
|
1159
1249
|
toolName: toolCall.toolName,
|
|
1160
1250
|
toolApproval: input.toolApproval,
|
|
@@ -1266,6 +1356,7 @@ export function runPrompt<
|
|
|
1266
1356
|
const executionStartedAt = Date.now();
|
|
1267
1357
|
const execution = await maybeExecuteHostTool({
|
|
1268
1358
|
event: toolCall,
|
|
1359
|
+
parsedToolCall: validatedHostToolCall,
|
|
1269
1360
|
tools: activeTools,
|
|
1270
1361
|
toolsContext,
|
|
1271
1362
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -1414,6 +1505,22 @@ function asToolCallTextStreamPart<TOOLS extends ToolSet>(input: {
|
|
|
1414
1505
|
return input.part as ToolCallTextStreamPart;
|
|
1415
1506
|
}
|
|
1416
1507
|
|
|
1508
|
+
function displayHostToolCall(input: {
|
|
1509
|
+
toolCall: ToolCallTextStreamPart;
|
|
1510
|
+
sessionWorkDir: string;
|
|
1511
|
+
}): ToolCallTextStreamPart {
|
|
1512
|
+
return {
|
|
1513
|
+
...input.toolCall,
|
|
1514
|
+
input: stripParsedToolInputWorkDir({
|
|
1515
|
+
value: input.toolCall.input,
|
|
1516
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1517
|
+
}),
|
|
1518
|
+
...(input.toolCall.invalid
|
|
1519
|
+
? { error: new Error(invalidToolInputMessage) }
|
|
1520
|
+
: {}),
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1417
1524
|
type ToolCallTextStreamPart = {
|
|
1418
1525
|
readonly type: 'tool-call';
|
|
1419
1526
|
readonly toolCallId: string;
|
|
@@ -1433,6 +1540,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
|
|
|
1433
1540
|
|
|
1434
1541
|
async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
1435
1542
|
event: { toolCallId: string; toolName: string; input: string };
|
|
1543
|
+
parsedToolCall: ToolCallTextStreamPart;
|
|
1436
1544
|
tools: TOOLS;
|
|
1437
1545
|
toolsContext: InferToolSetContext<TOOLS>;
|
|
1438
1546
|
wrappedExecuteTool: TurnLifecycle<ToolSet, Context>['executeTool'];
|
|
@@ -1450,8 +1558,15 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1450
1558
|
|
|
1451
1559
|
if (!isExecutableTool(tool)) return { executed: false };
|
|
1452
1560
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1561
|
+
if (input.parsedToolCall.invalid) {
|
|
1562
|
+
const error = new Error(invalidToolInputMessage);
|
|
1563
|
+
await input.submitToolResult({
|
|
1564
|
+
toolCallId: input.event.toolCallId,
|
|
1565
|
+
output: { error: invalidToolInputMessage },
|
|
1566
|
+
isError: true,
|
|
1567
|
+
});
|
|
1568
|
+
return { executed: true, outcome: { ok: false, error } };
|
|
1569
|
+
}
|
|
1455
1570
|
|
|
1456
1571
|
let context: unknown;
|
|
1457
1572
|
try {
|
|
@@ -1489,7 +1604,7 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
|
1489
1604
|
let output: unknown;
|
|
1490
1605
|
const stream = executeTool({
|
|
1491
1606
|
tool,
|
|
1492
|
-
input:
|
|
1607
|
+
input: input.parsedToolCall.input as never,
|
|
1493
1608
|
options: {
|
|
1494
1609
|
toolCallId: input.event.toolCallId,
|
|
1495
1610
|
messages: [],
|
|
@@ -120,6 +120,63 @@ export function stripWorkDir(
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function stripParsedToolInputWorkDir({
|
|
124
|
+
value,
|
|
125
|
+
sessionWorkDir,
|
|
126
|
+
}: {
|
|
127
|
+
value: unknown;
|
|
128
|
+
sessionWorkDir: string;
|
|
129
|
+
}): unknown {
|
|
130
|
+
if (sessionWorkDir.length === 0) return value;
|
|
131
|
+
|
|
132
|
+
const projected = new WeakMap<object, unknown>();
|
|
133
|
+
const pending: Array<{
|
|
134
|
+
source: object;
|
|
135
|
+
target: Record<string, unknown> | unknown[];
|
|
136
|
+
}> = [];
|
|
137
|
+
|
|
138
|
+
const project = (item: unknown): unknown => {
|
|
139
|
+
if (typeof item === 'string') return stripString(item, sessionWorkDir);
|
|
140
|
+
if (item === null || typeof item !== 'object') return item;
|
|
141
|
+
if (
|
|
142
|
+
!Array.isArray(item) &&
|
|
143
|
+
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
144
|
+
Object.getPrototypeOf(item) !== null
|
|
145
|
+
) {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
if (projected.has(item)) return projected.get(item);
|
|
149
|
+
|
|
150
|
+
const target: Record<string, unknown> | unknown[] = Array.isArray(item)
|
|
151
|
+
? new Array(item.length)
|
|
152
|
+
: {};
|
|
153
|
+
projected.set(item, target);
|
|
154
|
+
pending.push({ source: item, target });
|
|
155
|
+
return target;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = project(value);
|
|
159
|
+
while (pending.length > 0) {
|
|
160
|
+
const { source, target } = pending.pop()!;
|
|
161
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
162
|
+
for (let index = 0; index < source.length; index++) {
|
|
163
|
+
if (index in source) target[index] = project(source[index]);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for (const [key, item] of Object.entries(source)) {
|
|
167
|
+
Object.defineProperty(target, key, {
|
|
168
|
+
value: project(item),
|
|
169
|
+
enumerable: true,
|
|
170
|
+
writable: true,
|
|
171
|
+
configurable: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
/**
|
|
124
181
|
* Replace occurrences of the working directory in a string. A reference to the
|
|
125
182
|
* directory followed by a separator becomes workspace-relative
|