@ai-sdk/harness 1.0.39 → 1.0.41

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.
@@ -9,10 +9,17 @@ import type {
9
9
  HarnessAgentSkill,
10
10
  } from './harness-agent-types';
11
11
  import type {
12
+ Arrayable,
13
+ Context,
12
14
  Experimental_SandboxSession as SandboxSession,
13
15
  ToolSet,
14
16
  } from '@ai-sdk/provider-utils';
15
- import type { ActiveTools, TelemetryOptions, ToolApprovalStatus } from 'ai';
17
+ import type {
18
+ ActiveTools,
19
+ StopCondition,
20
+ TelemetryOptions,
21
+ ToolApprovalStatus,
22
+ } from 'ai';
16
23
  import type { HarnessAllTools } from './harness-agent-tool-types';
17
24
 
18
25
  export type HarnessAgentToolApprovalConfiguration = Readonly<
@@ -91,6 +98,7 @@ type HarnessAgentToolFilteringSettings<TOOLS extends ToolSet> =
91
98
  export type HarnessAgentSettings<
92
99
  THarness extends HarnessAgentAdapter<any> = HarnessAgentAdapter,
93
100
  TUserTools extends ToolSet = {},
101
+ RUNTIME_CONTEXT extends Context = Context,
94
102
  > = {
95
103
  /**
96
104
  * The harness adapter driving the underlying agent runtime. Its
@@ -130,6 +138,22 @@ export type HarnessAgentSettings<
130
138
  */
131
139
  readonly instructions?: string;
132
140
 
141
+ /**
142
+ * Conditions that stop the current result after a completed harness tool
143
+ * step that can continue into another model step. The underlying turn remains
144
+ * unfinished and can be suspended and continued.
145
+ *
146
+ * A terminal text-only step finishes naturally and is not stopped early.
147
+ *
148
+ * When omitted, the harness runs until the turn naturally finishes or pauses.
149
+ */
150
+ readonly stopWhen?: Arrayable<
151
+ StopCondition<
152
+ NoInfer<HarnessAllTools<THarness, TUserTools>>,
153
+ RUNTIME_CONTEXT
154
+ >
155
+ >;
156
+
133
157
  /**
134
158
  * Built-in tool permission mode. Defaults to `'allow-all'`, preserving the
135
159
  * existing bypass-permissions behavior unless users opt in.
@@ -6,6 +6,7 @@ import type {
6
6
  HarnessV1SandboxProvider,
7
7
  } from '../v1';
8
8
  import {
9
+ asArray,
9
10
  asSchema,
10
11
  generateId,
11
12
  type Context,
@@ -19,6 +20,7 @@ import type {
19
20
  GenerateTextResult,
20
21
  ReasoningFileOutput,
21
22
  ReasoningOutput,
23
+ StopCondition,
22
24
  StreamTextResult,
23
25
  } from 'ai';
24
26
  import type {
@@ -133,7 +135,14 @@ export class HarnessAgent<
133
135
  */
134
136
  readonly tools: HarnessAllTools<THarness, TUserTools>;
135
137
 
136
- private readonly settings: HarnessAgentSettings<THarness, TUserTools>;
138
+ private readonly settings: HarnessAgentSettings<
139
+ THarness,
140
+ TUserTools,
141
+ RUNTIME_CONTEXT
142
+ >;
143
+ private readonly stopConditions: Array<
144
+ StopCondition<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT>
145
+ >;
137
146
  private readonly sandboxConfig: HarnessAgentSandboxConfig;
138
147
  private readonly activeUserTools: TUserTools;
139
148
  private readonly builtinToolFiltering:
@@ -141,10 +150,14 @@ export class HarnessAgent<
141
150
  | undefined;
142
151
  private readonly permissionMode: HarnessAgentPermissionMode;
143
152
 
144
- constructor(settings: HarnessAgentSettings<THarness, TUserTools>) {
153
+ constructor(
154
+ settings: HarnessAgentSettings<THarness, TUserTools, RUNTIME_CONTEXT>,
155
+ ) {
145
156
  const sandboxConfig = resolveSandboxConfig(settings);
146
157
  validateSandboxBootstrapSettings(sandboxConfig);
147
158
  this.settings = settings;
159
+ this.stopConditions =
160
+ settings.stopWhen == null ? [] : asArray(settings.stopWhen);
148
161
  this.sandboxConfig = sandboxConfig;
149
162
  this.id = settings.id;
150
163
  const userTools = settings.tools ?? ({} as TUserTools);
@@ -528,6 +541,7 @@ export class HarnessAgent<
528
541
  runtimeContext: input.runtimeContext,
529
542
  abortSignal: input.abortSignal,
530
543
  telemetry: this.settings.telemetry,
544
+ stopConditions: this.stopConditions,
531
545
  toolApprovalContinuations: input.turnInput.toolApprovalContinuations,
532
546
  toolResultContinuations: input.turnInput.toolResultContinuations,
533
547
  });
@@ -546,6 +560,7 @@ export class HarnessAgent<
546
560
  runtimeContext: input.runtimeContext,
547
561
  abortSignal: input.abortSignal,
548
562
  telemetry: this.settings.telemetry,
563
+ stopConditions: this.stopConditions,
549
564
  });
550
565
  }
551
566
 
@@ -32,6 +32,7 @@ import type {
32
32
  ContentPart,
33
33
  ProviderMetadata,
34
34
  StepResult,
35
+ StopCondition,
35
36
  TelemetryOptions,
36
37
  TextStreamPart,
37
38
  } from 'ai';
@@ -41,9 +42,14 @@ import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-set
41
42
  import { HarnessStreamTextResult } from './harness-stream-text-result';
42
43
  import { translateStreamPart } from './translate-stream-part';
43
44
  import { stripWorkDir } from './strip-work-dir';
44
- import { createTurnTelemetry, type TurnContentPart } from './turn-telemetry';
45
+ import {
46
+ createTurnTelemetry,
47
+ type TurnContentPart,
48
+ type TurnTelemetry,
49
+ } from './turn-telemetry';
45
50
  import { resolveCustomToolApproval } from './permission-mode';
46
51
  import { logBridgeError } from '../../utils/bridge-diagnostics';
52
+ import { pinSandboxChannelEventCheckpoint } from '../../utils/sandbox-channel';
47
53
 
48
54
  /**
49
55
  * Drive one prompt turn end-to-end:
@@ -81,6 +87,7 @@ export function runPrompt<
81
87
  runtimeContext: RUNTIME_CONTEXT;
82
88
  abortSignal: AbortSignal | undefined;
83
89
  telemetry?: TelemetryOptions | undefined;
90
+ stopConditions?: ReadonlyArray<StopCondition<TOOLS, RUNTIME_CONTEXT>>;
84
91
  toolApproval?: HarnessAgentToolApprovalConfiguration | undefined;
85
92
  pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
86
93
  pendingToolResults?: readonly HarnessV1PendingToolResult[];
@@ -96,6 +103,7 @@ export function runPrompt<
96
103
  onToolResultSettled?: (toolCallId: string) => void;
97
104
  onTurnFinished?: () => void;
98
105
  onTurnFailed?: () => void;
106
+ onStopConditionMet?: () => Promise<void>;
99
107
  }): {
100
108
  result: HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT>;
101
109
  done: Promise<void>;
@@ -176,7 +184,7 @@ export function runPrompt<
176
184
  },
177
185
  });
178
186
  } catch (err) {
179
- telemetry.error(err);
187
+ await telemetry.error(err);
180
188
  logBridgeError({
181
189
  harnessId: input.harness.harnessId,
182
190
  sessionId: input.session.sessionId,
@@ -220,9 +228,21 @@ export function runPrompt<
220
228
  );
221
229
  const settledHostToolCallIds = new Set<string>();
222
230
  let closingResumedStep = false;
231
+ let pendingStopBoundary:
232
+ | {
233
+ finishReason: LanguageModelV4FinishReason;
234
+ usage: LanguageModelV4Usage;
235
+ releaseCheckpoint: (() => void) | undefined;
236
+ }
237
+ | undefined;
223
238
  let finalFinish:
224
239
  | Extract<HarnessV1StreamPart, { type: 'finish' }>
225
240
  | undefined;
241
+ const completedSteps: Array<StepResult<TOOLS, RUNTIME_CONTEXT>> = [];
242
+ const releasePendingStopBoundary = (): void => {
243
+ pendingStopBoundary?.releaseCheckpoint?.();
244
+ pendingStopBoundary = undefined;
245
+ };
226
246
 
227
247
  // Accumulate the model's output content per step so telemetry can record
228
248
  // `gen_ai.output.messages` and reporters can log what was actually said.
@@ -258,36 +278,38 @@ export function runPrompt<
258
278
  unified: 'tool-calls',
259
279
  raw: undefined,
260
280
  };
261
- const completeStep = (input: {
281
+ const completeStep = async (input: {
262
282
  finishReason: LanguageModelV4FinishReason;
263
283
  usage: LanguageModelV4Usage;
264
284
  providerMetadata: ProviderMetadata | undefined;
265
- }): StepResult<TOOLS, RUNTIME_CONTEXT> => {
266
- telemetry.stepFinish({
285
+ }): Promise<StepResult<TOOLS, RUNTIME_CONTEXT>> => {
286
+ await telemetry.stepFinish({
267
287
  finishReason: input.finishReason,
268
288
  usage: input.usage,
269
289
  providerMetadata: input.providerMetadata,
270
290
  content: buildStepContent(),
271
291
  });
272
292
  resetStepContent();
273
- return result.finishStep({
293
+ const step = result.finishStep({
274
294
  finishReason: input.finishReason,
275
295
  usage: input.usage,
276
296
  providerMetadata: input.providerMetadata,
277
297
  warnings: [],
278
298
  });
299
+ completedSteps.push(step);
300
+ return step;
279
301
  };
280
302
  const finishForHostInputPause = async (options: {
281
303
  completeCurrentStep: boolean;
282
304
  }): Promise<void> => {
283
305
  if (options.completeCurrentStep) {
284
- completeStep({
306
+ await completeStep({
285
307
  finishReason: toolCallsFinishReason,
286
308
  usage: zeroUsage,
287
309
  providerMetadata: undefined,
288
310
  });
289
311
  }
290
- telemetry.end({
312
+ await telemetry.end({
291
313
  finishReason: toolCallsFinishReason,
292
314
  usage: zeroUsage,
293
315
  });
@@ -413,9 +435,16 @@ export function runPrompt<
413
435
  input: approval.input,
414
436
  } satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
415
437
 
438
+ await telemetry.start(input.session.modelId);
439
+ await telemetry.toolStart({
440
+ toolCallId: rawToolCall.toolCallId,
441
+ toolName: rawToolCall.toolName,
442
+ input: rawToolCall.input,
443
+ });
416
444
  const execution = await maybeExecuteHostTool({
417
445
  event: rawToolCall,
418
446
  tools: activeTools,
447
+ wrappedExecuteTool: telemetry.executeTool,
419
448
  sandboxSession: input.sandboxSession,
420
449
  abortSignal: input.abortSignal,
421
450
  control,
@@ -447,7 +476,7 @@ export function runPrompt<
447
476
  await finishForHostInputPause({ completeCurrentStep: false });
448
477
  return 'awaiting-tool-result';
449
478
  }
450
- telemetry.toolEnd(rawToolCall.toolCallId, execution.outcome);
479
+ await telemetry.toolEnd(rawToolCall.toolCallId, execution.outcome);
451
480
  return 'continued';
452
481
  };
453
482
 
@@ -478,13 +507,39 @@ export function runPrompt<
478
507
 
479
508
  while (true) {
480
509
  const { value, done } = await reader.read();
481
- if (done) break;
510
+ if (done) {
511
+ releasePendingStopBoundary();
512
+ break;
513
+ }
482
514
  if (value == null) continue;
483
515
 
516
+ if (pendingStopBoundary != null) {
517
+ if (value.type === 'finish') {
518
+ releasePendingStopBoundary();
519
+ } else if (
520
+ (
521
+ await Promise.all(
522
+ input.stopConditions!.map(condition =>
523
+ condition({ steps: completedSteps }),
524
+ ),
525
+ )
526
+ ).some(Boolean)
527
+ ) {
528
+ await input.onStopConditionMet?.();
529
+ const { finishReason, usage } = pendingStopBoundary;
530
+ releasePendingStopBoundary();
531
+ await telemetry.end({ finishReason, usage });
532
+ await result.finish();
533
+ return;
534
+ } else {
535
+ releasePendingStopBoundary();
536
+ }
537
+ }
538
+
484
539
  // Begin the operation span on stream-start, using the runtime-resolved
485
540
  // model the adapter reports (falling back to the session's model).
486
541
  if (value.type === 'stream-start') {
487
- telemetry.start(value.modelId ?? input.session.modelId);
542
+ await telemetry.start(value.modelId ?? input.session.modelId);
488
543
  }
489
544
 
490
545
  // Open a step span lazily before the first content of each step.
@@ -494,7 +549,7 @@ export function runPrompt<
494
549
  value.type !== 'finish' &&
495
550
  value.type !== 'error'
496
551
  ) {
497
- telemetry.ensureStepOpen();
552
+ await telemetry.ensureStepOpen();
498
553
  }
499
554
 
500
555
  /*
@@ -565,7 +620,7 @@ export function runPrompt<
565
620
  // Telemetry and stderr diagnostics keep the raw error (absolute
566
621
  // paths help debugging); the consumer-facing settle uses the
567
622
  // workDir-stripped one, like every other forwarded part.
568
- telemetry.error(value.error);
623
+ await telemetry.error(value.error);
569
624
  logBridgeError({
570
625
  harnessId: input.harness.harnessId,
571
626
  sessionId: input.session.sessionId,
@@ -609,7 +664,7 @@ export function runPrompt<
609
664
  toolName: value.toolName,
610
665
  input: value.input,
611
666
  });
612
- telemetry.toolStart({
667
+ await telemetry.toolStart({
613
668
  toolCallId: value.toolCallId,
614
669
  toolName: value.toolName,
615
670
  input: value.input,
@@ -618,7 +673,7 @@ export function runPrompt<
618
673
 
619
674
  // Telemetry: close a tool span when its provider-executed result lands.
620
675
  if (value.type === 'tool-result') {
621
- telemetry.toolEnd(
676
+ await telemetry.toolEnd(
622
677
  value.toolCallId,
623
678
  value.isError
624
679
  ? { ok: false, error: value.result }
@@ -681,16 +736,23 @@ export function runPrompt<
681
736
 
682
737
  // Drive step boundaries.
683
738
  if (value.type === 'finish-step') {
684
- completeStep({
739
+ await completeStep({
685
740
  finishReason: value.finishReason,
686
741
  usage: value.usage,
687
742
  providerMetadata: value.harnessMetadata,
688
743
  });
744
+ if (input.stopConditions != null && input.stopConditions.length > 0) {
745
+ pendingStopBoundary = {
746
+ finishReason: value.finishReason,
747
+ usage: value.usage,
748
+ releaseCheckpoint: pinSandboxChannelEventCheckpoint(value),
749
+ };
750
+ }
689
751
  }
690
752
 
691
753
  if (value.type === 'finish') {
692
754
  finalFinish = value;
693
- telemetry.end({
755
+ await telemetry.end({
694
756
  finishReason: value.finishReason,
695
757
  usage: value.totalUsage,
696
758
  });
@@ -716,7 +778,7 @@ export function runPrompt<
716
778
  toolCallId: toolCall.toolCallId,
717
779
  output,
718
780
  });
719
- telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
781
+ await telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
720
782
  continue;
721
783
  }
722
784
  const customToolApprovalDecision = resolveCustomToolApproval({
@@ -745,7 +807,7 @@ export function runPrompt<
745
807
  toolCallId: toolCall.toolCallId,
746
808
  output,
747
809
  });
748
- telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
810
+ await telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
749
811
  continue;
750
812
  }
751
813
  const pendingApproval =
@@ -803,6 +865,7 @@ export function runPrompt<
803
865
  const execution = await maybeExecuteHostTool({
804
866
  event: toolCall,
805
867
  tools: activeTools,
868
+ wrappedExecuteTool: telemetry.executeTool,
806
869
  sandboxSession: input.sandboxSession,
807
870
  abortSignal: input.abortSignal,
808
871
  control,
@@ -842,7 +905,7 @@ export function runPrompt<
842
905
  await finishForHostInputPause({ completeCurrentStep: true });
843
906
  return;
844
907
  }
845
- telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
908
+ await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
846
909
  }
847
910
  }
848
911
  if (finalFinish != null) {
@@ -860,7 +923,7 @@ export function runPrompt<
860
923
  : undefined,
861
924
  );
862
925
  } catch (err) {
863
- telemetry.error(err);
926
+ await telemetry.error(err);
864
927
  logBridgeError({
865
928
  harnessId: input.harness.harnessId,
866
929
  sessionId: input.session.sessionId,
@@ -869,6 +932,7 @@ export function runPrompt<
869
932
  });
870
933
  settleFailure(err);
871
934
  } finally {
935
+ releasePendingStopBoundary();
872
936
  reader.releaseLock();
873
937
  }
874
938
  })();
@@ -921,6 +985,7 @@ function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
921
985
  async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
922
986
  event: { toolCallId: string; toolName: string; input: string };
923
987
  tools: TOOLS;
988
+ wrappedExecuteTool: TurnTelemetry['executeTool'];
924
989
  sandboxSession: SandboxSession;
925
990
  abortSignal: AbortSignal | undefined;
926
991
  control: HarnessV1PromptControl;
@@ -949,25 +1014,31 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
949
1014
  * back to the model — preliminary values are surfaced to the consumer
950
1015
  * stream alone, matching how the AI SDK treats `onPreliminaryToolResult`.
951
1016
  */
952
- let output: unknown;
953
- const stream = executeTool({
954
- tool,
955
- input: args as never,
956
- options: {
957
- toolCallId: input.event.toolCallId,
958
- messages: [],
959
- abortSignal: input.abortSignal,
960
- context: undefined as never,
961
- experimental_sandbox: input.sandboxSession,
1017
+ const output = await input.wrappedExecuteTool({
1018
+ toolCallId: input.event.toolCallId,
1019
+ execute: async () => {
1020
+ let output: unknown;
1021
+ const stream = executeTool({
1022
+ tool,
1023
+ input: args as never,
1024
+ options: {
1025
+ toolCallId: input.event.toolCallId,
1026
+ messages: [],
1027
+ abortSignal: input.abortSignal,
1028
+ context: undefined as never,
1029
+ experimental_sandbox: input.sandboxSession,
1030
+ },
1031
+ });
1032
+ for await (const part of stream) {
1033
+ if (part.type === 'preliminary') {
1034
+ input.onPreliminaryResult(part.output);
1035
+ } else {
1036
+ output = part.output;
1037
+ }
1038
+ }
1039
+ return output;
962
1040
  },
963
1041
  });
964
- for await (const part of stream) {
965
- if (part.type === 'preliminary') {
966
- input.onPreliminaryResult(part.output);
967
- } else {
968
- output = part.output;
969
- }
970
- }
971
1042
 
972
1043
  await input.control.submitToolResult({
973
1044
  toolCallId: input.event.toolCallId,
@@ -1009,6 +1080,7 @@ export async function validateToolCall<TOOLS extends ToolSet>(args: {
1009
1080
  ...(event.providerExecuted !== undefined
1010
1081
  ? { providerExecuted: event.providerExecuted }
1011
1082
  : {}),
1083
+ ...(event.dynamic !== undefined ? { dynamic: event.dynamic } : {}),
1012
1084
  ...(event.providerMetadata !== undefined
1013
1085
  ? { providerMetadata: event.providerMetadata }
1014
1086
  : {}),