@ai-sdk/harness 1.0.70 → 1.0.71

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.70",
3
+ "version": "1.0.71",
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.7",
48
48
  "@ai-sdk/provider-utils": "5.0.27",
49
- "ai": "7.0.64"
49
+ "ai": "7.0.65"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -65,7 +65,7 @@
65
65
  "typescript": "5.8.3",
66
66
  "ws": "^8.21.0",
67
67
  "zod": "3.25.76",
68
- "@ai-sdk/otel": "1.0.64",
68
+ "@ai-sdk/otel": "1.0.65",
69
69
  "@vercel/ai-tsconfig": "0.0.0"
70
70
  },
71
71
  "engines": {
@@ -248,6 +248,25 @@ export function runPrompt<
248
248
  | Extract<HarnessV1StreamPart, { type: 'finish' }>
249
249
  | undefined;
250
250
  const completedSteps: Array<StepResult<TOOLS, RUNTIME_CONTEXT>> = [];
251
+ const outstandingHostToolExecutions: Promise<void>[] = [];
252
+ const startHostToolExecution = (execution: Promise<void>): void => {
253
+ outstandingHostToolExecutions.push(execution);
254
+ // The execution is joined at the next step boundary. Attach a rejection
255
+ // handler immediately so failures cannot become unhandled in the
256
+ // meantime; awaiting the original promise still propagates the failure.
257
+ void execution.catch(() => {});
258
+ };
259
+ const waitForOutstandingHostToolExecutions = async (): Promise<void> => {
260
+ if (outstandingHostToolExecutions.length === 0) return;
261
+ const executions = outstandingHostToolExecutions.splice(0);
262
+ const results = await Promise.allSettled(executions);
263
+ const failedExecution = results.find(
264
+ result => result.status === 'rejected',
265
+ );
266
+ if (failedExecution != null) {
267
+ throw failedExecution.reason;
268
+ }
269
+ };
251
270
  const releasePendingStopBoundary = (): void => {
252
271
  pendingStopBoundary?.releaseCheckpoint?.();
253
272
  pendingStopBoundary = undefined;
@@ -311,6 +330,7 @@ export function runPrompt<
311
330
  const finishForHostInputPause = async (options: {
312
331
  completeCurrentStep: boolean;
313
332
  }): Promise<void> => {
333
+ await waitForOutstandingHostToolExecutions();
314
334
  if (options.completeCurrentStep) {
315
335
  await completeStep({
316
336
  finishReason: toolCallsFinishReason,
@@ -665,6 +685,7 @@ export function runPrompt<
665
685
  * or a second `error` part from `fail`).
666
686
  */
667
687
  if (value.type === 'error' && displayValue.type === 'error') {
688
+ await waitForOutstandingHostToolExecutions();
668
689
  // Telemetry and stderr diagnostics keep the raw error (absolute
669
690
  // paths help debugging); the consumer-facing settle uses the
670
691
  // workDir-stripped one, like every other forwarded part.
@@ -784,6 +805,7 @@ export function runPrompt<
784
805
 
785
806
  // Drive step boundaries.
786
807
  if (value.type === 'finish-step') {
808
+ await waitForOutstandingHostToolExecutions();
787
809
  await completeStep({
788
810
  finishReason: value.finishReason,
789
811
  usage: value.usage,
@@ -799,6 +821,7 @@ export function runPrompt<
799
821
  }
800
822
 
801
823
  if (value.type === 'finish') {
824
+ await waitForOutstandingHostToolExecutions();
802
825
  finalFinish = value;
803
826
  await telemetry.end({
804
827
  finishReason: value.finishReason,
@@ -910,52 +933,62 @@ export function runPrompt<
910
933
  await finishForHostInputPause({ completeCurrentStep: true });
911
934
  return;
912
935
  }
913
- const execution = await maybeExecuteHostTool({
914
- event: toolCall,
915
- tools: activeTools,
916
- wrappedExecuteTool: telemetry.executeTool,
917
- sandboxSession: input.sandboxSession,
918
- abortSignal: input.abortSignal,
919
- control,
920
- onPreliminaryResult: preliminaryOutput => {
921
- /*
922
- * Project a `yield`ed value as a preliminary AI SDK
923
- * `tool-result` part. Unlike the final result — which is
924
- * submitted to the runtime, echoed back as a `tool-result`
925
- * event, and stripped on its way through the loop above —
926
- * preliminary values never reach the runtime, so strip the
927
- * working directory here to match the final result's projection.
928
- */
929
- const stripped = stripWorkDir(
930
- {
931
- type: 'tool-result',
932
- toolCallId: toolCall.toolCallId,
933
- toolName: toolCall.toolName,
934
- result: preliminaryOutput as Extract<
935
- HarnessV1StreamPart,
936
- { type: 'tool-result' }
937
- >['result'],
938
- },
939
- input.sessionWorkDir,
940
- ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
941
- result.enqueue({
942
- type: 'tool-result',
943
- toolCallId: toolCall.toolCallId,
944
- toolName: toolCall.toolName,
945
- input: undefined,
946
- output: stripped.result,
947
- preliminary: true,
948
- } as TextStreamPart<TOOLS>);
949
- },
950
- });
951
- if (!execution.executed) {
936
+ if (!isExecutableTool(activeTools[toolCall.toolName])) {
952
937
  recordPendingToolResult({ toolCall });
953
938
  await finishForHostInputPause({ completeCurrentStep: true });
954
939
  return;
955
940
  }
956
- await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
941
+ startHostToolExecution(
942
+ (async () => {
943
+ const execution = await maybeExecuteHostTool({
944
+ event: toolCall,
945
+ tools: activeTools,
946
+ wrappedExecuteTool: telemetry.executeTool,
947
+ sandboxSession: input.sandboxSession,
948
+ abortSignal: input.abortSignal,
949
+ control,
950
+ onPreliminaryResult: preliminaryOutput => {
951
+ /*
952
+ * Project a `yield`ed value as a preliminary AI SDK
953
+ * `tool-result` part. Unlike the final result — which is
954
+ * submitted to the runtime, echoed back as a `tool-result`
955
+ * event, and stripped on its way through the loop above —
956
+ * preliminary values never reach the runtime, so strip the
957
+ * working directory here to match the final result's projection.
958
+ */
959
+ const stripped = stripWorkDir(
960
+ {
961
+ type: 'tool-result',
962
+ toolCallId: toolCall.toolCallId,
963
+ toolName: toolCall.toolName,
964
+ result: preliminaryOutput as Extract<
965
+ HarnessV1StreamPart,
966
+ { type: 'tool-result' }
967
+ >['result'],
968
+ },
969
+ input.sessionWorkDir,
970
+ ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
971
+ result.enqueue({
972
+ type: 'tool-result',
973
+ toolCallId: toolCall.toolCallId,
974
+ toolName: toolCall.toolName,
975
+ input: undefined,
976
+ output: stripped.result,
977
+ preliminary: true,
978
+ } as TextStreamPart<TOOLS>);
979
+ },
980
+ });
981
+ if (!execution.executed) {
982
+ throw new Error(
983
+ `Harness '${input.harness.harnessId}' could not execute host tool '${toolCall.toolName}'.`,
984
+ );
985
+ }
986
+ await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
987
+ })(),
988
+ );
957
989
  }
958
990
  }
991
+ await waitForOutstandingHostToolExecutions();
959
992
  const isTurnSuspending = input.isTurnSuspending?.() === true;
960
993
  if (isTurnSuspending) {
961
994
  if (finalFinish == null) {
@@ -982,6 +1015,11 @@ export function runPrompt<
982
1015
  : undefined,
983
1016
  );
984
1017
  } catch (err) {
1018
+ try {
1019
+ await waitForOutstandingHostToolExecutions();
1020
+ } catch {
1021
+ // Preserve the error that stopped the reader loop.
1022
+ }
985
1023
  await telemetry.error(err);
986
1024
  logBridgeError({
987
1025
  harnessId: input.harness.harnessId,