@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.123",
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.113"
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.113",
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 { createToolInputWorkDirStripper, stripWorkDir } from './strip-work-dir';
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
- rawToolCallsByToolCallId.get(approval.toolCallId) ??
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
- const parsedInput = await safeParseJSON({ text: rawToolCall.input });
660
- const toolCall: ToolCallTextStreamPart = {
661
- type: 'tool-call',
662
- toolCallId: rawToolCall.toolCallId,
663
- toolName: rawToolCall.toolName,
664
- input: parsedInput.success ? parsedInput.value : rawToolCall.input,
665
- ...(rawToolCall.providerExecuted !== undefined
666
- ? { providerExecuted: rawToolCall.providerExecuted }
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, parsedToolCall);
982
- result.enqueue(parsed);
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
- const parsed = await safeParseJSON({ text: input.event.input });
1454
- const args = parsed.success ? parsed.value : input.event.input;
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: args as never,
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