@ai-sdk/harness 1.0.36 → 1.0.37

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.
@@ -4,6 +4,7 @@ import {
4
4
  type HarnessV1,
5
5
  type HarnessV1BuiltinToolFiltering,
6
6
  type HarnessV1PendingToolApproval,
7
+ type HarnessV1PendingToolResult,
7
8
  type HarnessV1Prompt,
8
9
  type HarnessV1PromptControl,
9
10
  type HarnessV1Session,
@@ -26,8 +27,15 @@ import type {
26
27
  LanguageModelV4Usage,
27
28
  } from '@ai-sdk/provider';
28
29
  import { parseToolCall } from 'ai/internal';
29
- import type { ContentPart, TelemetryOptions, TextStreamPart } from 'ai';
30
+ import type {
31
+ ContentPart,
32
+ ProviderMetadata,
33
+ StepResult,
34
+ TelemetryOptions,
35
+ TextStreamPart,
36
+ } from 'ai';
30
37
  import type { HarnessAgentToolApprovalContinuation } from '../harness-agent-tool-approval-continuation';
38
+ import type { HarnessAgentToolResultContinuation } from '../harness-agent-tool-result-continuation';
31
39
  import type { HarnessAgentToolApprovalConfiguration } from '../harness-agent-settings';
32
40
  import { HarnessStreamTextResult } from './harness-stream-text-result';
33
41
  import { translateStreamPart } from './translate-stream-part';
@@ -74,12 +82,19 @@ export function runPrompt<
74
82
  telemetry?: TelemetryOptions | undefined;
75
83
  toolApproval?: HarnessAgentToolApprovalConfiguration | undefined;
76
84
  pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
85
+ pendingToolResults?: readonly HarnessV1PendingToolResult[];
77
86
  toolApprovalContinuations?:
78
87
  | readonly HarnessAgentToolApprovalContinuation[]
79
88
  | undefined;
89
+ toolResultContinuations?:
90
+ | readonly HarnessAgentToolResultContinuation[]
91
+ | undefined;
80
92
  onPendingToolApproval?: (approval: HarnessV1PendingToolApproval) => void;
81
93
  onToolApprovalSettled?: (approvalId: string) => void;
94
+ onPendingToolResult?: (pendingResult: HarnessV1PendingToolResult) => void;
95
+ onToolResultSettled?: (toolCallId: string) => void;
82
96
  onTurnFinished?: () => void;
97
+ onTurnFailed?: () => void;
83
98
  }): {
84
99
  result: HarnessStreamTextResult<TOOLS, RUNTIME_CONTEXT>;
85
100
  done: Promise<void>;
@@ -93,8 +108,11 @@ export function runPrompt<
93
108
  sessionId: input.session.sessionId,
94
109
  });
95
110
  const pendingToolApprovals = input.pendingToolApprovals ?? [];
111
+ const pendingToolResults = input.pendingToolResults ?? [];
96
112
  const onPendingToolApproval = input.onPendingToolApproval ?? (() => {});
97
113
  const onToolApprovalSettled = input.onToolApprovalSettled ?? (() => {});
114
+ const onPendingToolResult = input.onPendingToolResult ?? (() => {});
115
+ const onToolResultSettled = input.onToolResultSettled ?? (() => {});
98
116
  const activeTools = input.activeTools ?? input.tools;
99
117
 
100
118
  const telemetry = createTurnTelemetry({
@@ -141,6 +159,7 @@ export function runPrompt<
141
159
  context: 'failed to start harness turn',
142
160
  error: err,
143
161
  });
162
+ input.onTurnFailed?.();
144
163
  result.fail(err);
145
164
  return;
146
165
  }
@@ -164,7 +183,20 @@ export function runPrompt<
164
183
  continuation,
165
184
  ]),
166
185
  );
167
- const settledApprovalToolCallIds = new Set<string>();
186
+ const pendingResultsByToolCallId = new Map(
187
+ pendingToolResults.map(pendingResult => [
188
+ pendingResult.toolCallId,
189
+ pendingResult,
190
+ ]),
191
+ );
192
+ const continuationsByToolCallId = new Map(
193
+ (input.toolResultContinuations ?? []).map(continuation => [
194
+ continuation.toolCallId,
195
+ continuation,
196
+ ]),
197
+ );
198
+ const settledHostToolCallIds = new Set<string>();
199
+ let closingResumedStep = false;
168
200
  let finalFinish:
169
201
  | Extract<HarnessV1StreamPart, { type: 'finish' }>
170
202
  | undefined;
@@ -203,19 +235,35 @@ export function runPrompt<
203
235
  unified: 'tool-calls',
204
236
  raw: undefined,
205
237
  };
206
- const finishForToolApprovalPause = async (): Promise<void> => {
238
+ const completeStep = (input: {
239
+ finishReason: LanguageModelV4FinishReason;
240
+ usage: LanguageModelV4Usage;
241
+ providerMetadata: ProviderMetadata | undefined;
242
+ }): StepResult<TOOLS, RUNTIME_CONTEXT> => {
207
243
  telemetry.stepFinish({
208
- finishReason: toolCallsFinishReason,
209
- usage: zeroUsage,
244
+ finishReason: input.finishReason,
245
+ usage: input.usage,
246
+ providerMetadata: input.providerMetadata,
210
247
  content: buildStepContent(),
211
248
  });
212
249
  resetStepContent();
213
- result.finishStep({
214
- finishReason: toolCallsFinishReason,
215
- usage: zeroUsage,
216
- providerMetadata: undefined,
250
+ return result.finishStep({
251
+ finishReason: input.finishReason,
252
+ usage: input.usage,
253
+ providerMetadata: input.providerMetadata,
217
254
  warnings: [],
218
255
  });
256
+ };
257
+ const finishForHostInputPause = async (options: {
258
+ completeCurrentStep: boolean;
259
+ }): Promise<void> => {
260
+ if (options.completeCurrentStep) {
261
+ completeStep({
262
+ finishReason: toolCallsFinishReason,
263
+ usage: zeroUsage,
264
+ providerMetadata: undefined,
265
+ });
266
+ }
219
267
  telemetry.end({
220
268
  finishReason: toolCallsFinishReason,
221
269
  usage: zeroUsage,
@@ -258,7 +306,7 @@ export function runPrompt<
258
306
  approval: HarnessV1PendingToolApproval,
259
307
  continuation: HarnessAgentToolApprovalContinuation,
260
308
  ): void => {
261
- result.enqueue({
309
+ result.enqueueContinuation({
262
310
  type: 'tool-approval-response',
263
311
  approvalId: approval.approvalId,
264
312
  toolCall: continuation.toolCall,
@@ -271,15 +319,42 @@ export function runPrompt<
271
319
  : {}),
272
320
  } as TextStreamPart<TOOLS>);
273
321
  };
322
+ const recordPendingToolResult = (options: {
323
+ toolCall: Extract<HarnessV1StreamPart, { type: 'tool-call' }>;
324
+ }): HarnessV1PendingToolResult => {
325
+ const pendingResult =
326
+ pendingResultsByToolCallId.get(options.toolCall.toolCallId) ??
327
+ ({
328
+ toolCallId: options.toolCall.toolCallId,
329
+ toolName: options.toolCall.toolName,
330
+ input: options.toolCall.input,
331
+ } satisfies HarnessV1PendingToolResult);
332
+ pendingResultsByToolCallId.set(pendingResult.toolCallId, pendingResult);
333
+ onPendingToolResult(pendingResult);
334
+ return pendingResult;
335
+ };
336
+ const processPendingToolResultContinuation = async (
337
+ pendingResult: HarnessV1PendingToolResult,
338
+ continuation: HarnessAgentToolResultContinuation,
339
+ ): Promise<void> => {
340
+ onToolResultSettled(pendingResult.toolCallId);
341
+ pendingResultsByToolCallId.delete(pendingResult.toolCallId);
342
+ settledHostToolCallIds.add(pendingResult.toolCallId);
343
+ await control.submitToolResult({
344
+ toolCallId: pendingResult.toolCallId,
345
+ output: continuation.output,
346
+ isError: continuation.isError,
347
+ });
348
+ };
274
349
  const processPendingApprovalContinuation = async (
275
350
  approval: HarnessV1PendingToolApproval,
276
351
  continuation: HarnessAgentToolApprovalContinuation,
277
- ): Promise<void> => {
352
+ ): Promise<'continued' | 'awaiting-tool-result'> => {
278
353
  enqueueApprovalResponse(approval, continuation);
279
354
  onToolApprovalSettled(approval.approvalId);
280
355
  pendingApprovalsByApprovalId.delete(approval.approvalId);
281
356
  pendingApprovalsByToolCallId.delete(approval.toolCallId);
282
- settledApprovalToolCallIds.add(approval.toolCallId);
357
+ settledHostToolCallIds.add(approval.toolCallId);
283
358
 
284
359
  if (approval.kind === 'builtin') {
285
360
  if (control.submitToolApproval == null) {
@@ -292,7 +367,7 @@ export function runPrompt<
292
367
  approved: continuation.approvalResponse.approved,
293
368
  reason: continuation.approvalResponse.reason,
294
369
  });
295
- return;
370
+ return 'continued';
296
371
  }
297
372
 
298
373
  if (!continuation.approvalResponse.approved) {
@@ -303,7 +378,7 @@ export function runPrompt<
303
378
  reason: continuation.approvalResponse.reason,
304
379
  },
305
380
  });
306
- return;
381
+ return 'continued';
307
382
  }
308
383
 
309
384
  const rawToolCall =
@@ -315,7 +390,7 @@ export function runPrompt<
315
390
  input: approval.input,
316
391
  } satisfies Extract<HarnessV1StreamPart, { type: 'tool-call' }>);
317
392
 
318
- const outcome = await maybeExecuteHostTool({
393
+ const execution = await maybeExecuteHostTool({
319
394
  event: rawToolCall,
320
395
  tools: activeTools,
321
396
  sandboxSession: input.sandboxSession,
@@ -344,14 +419,37 @@ export function runPrompt<
344
419
  } as TextStreamPart<TOOLS>);
345
420
  },
346
421
  });
347
- telemetry.toolEnd(rawToolCall.toolCallId, outcome);
422
+ if (!execution.executed) {
423
+ recordPendingToolResult({ toolCall: rawToolCall });
424
+ await finishForHostInputPause({ completeCurrentStep: false });
425
+ return 'awaiting-tool-result';
426
+ }
427
+ telemetry.toolEnd(rawToolCall.toolCallId, execution.outcome);
428
+ return 'continued';
348
429
  };
349
430
 
350
431
  try {
351
432
  for (const approval of pendingToolApprovals) {
352
433
  const continuation = continuationsByApprovalId.get(approval.approvalId);
353
434
  if (continuation != null) {
354
- await processPendingApprovalContinuation(approval, continuation);
435
+ const outcome = await processPendingApprovalContinuation(
436
+ approval,
437
+ continuation,
438
+ );
439
+ if (outcome === 'awaiting-tool-result') return;
440
+ closingResumedStep = true;
441
+ }
442
+ }
443
+ for (const pendingResult of pendingToolResults) {
444
+ const continuation = continuationsByToolCallId.get(
445
+ pendingResult.toolCallId,
446
+ );
447
+ if (continuation != null) {
448
+ await processPendingToolResultContinuation(
449
+ pendingResult,
450
+ continuation,
451
+ );
452
+ closingResumedStep = true;
355
453
  }
356
454
  }
357
455
 
@@ -383,12 +481,20 @@ export function runPrompt<
383
481
  * against the sandbox root, so the strip is display-only.
384
482
  */
385
483
  const displayValue = stripWorkDir(value, input.sessionWorkDir);
386
- const settledApprovalToolCallReplay =
387
- displayValue.type === 'tool-call' &&
388
- !displayValue.providerExecuted &&
389
- settledApprovalToolCallIds.has(displayValue.toolCallId);
484
+ const settledHostInputReplay =
485
+ (displayValue.type === 'tool-call' ||
486
+ displayValue.type === 'tool-result' ||
487
+ displayValue.type === 'tool-approval-request') &&
488
+ settledHostToolCallIds.has(displayValue.toolCallId);
390
489
 
391
- if (settledApprovalToolCallReplay) {
490
+ if (settledHostInputReplay) {
491
+ continue;
492
+ }
493
+
494
+ if (displayValue.type === 'finish-step' && closingResumedStep) {
495
+ closingResumedStep = false;
496
+ resetStepContent();
497
+ result.discardCurrentStepContent();
392
498
  continue;
393
499
  }
394
500
 
@@ -510,10 +616,12 @@ export function runPrompt<
510
616
  pendingApproval.approvalId,
511
617
  );
512
618
  if (continuation != null) {
513
- await processPendingApprovalContinuation(
619
+ const outcome = await processPendingApprovalContinuation(
514
620
  pendingApproval,
515
621
  continuation,
516
622
  );
623
+ if (outcome === 'awaiting-tool-result') return;
624
+ closingResumedStep = true;
517
625
  continue;
518
626
  }
519
627
 
@@ -522,24 +630,16 @@ export function runPrompt<
522
630
  approvalId: pendingApproval.approvalId,
523
631
  toolCall,
524
632
  });
525
- await finishForToolApprovalPause();
633
+ await finishForHostInputPause({ completeCurrentStep: true });
526
634
  return;
527
635
  }
528
636
 
529
637
  // Drive step boundaries.
530
638
  if (value.type === 'finish-step') {
531
- telemetry.stepFinish({
639
+ completeStep({
532
640
  finishReason: value.finishReason,
533
641
  usage: value.usage,
534
642
  providerMetadata: value.harnessMetadata,
535
- content: buildStepContent(),
536
- });
537
- resetStepContent();
538
- result.finishStep({
539
- finishReason: value.finishReason,
540
- usage: value.usage,
541
- providerMetadata: value.harnessMetadata,
542
- warnings: [],
543
643
  });
544
644
  }
545
645
 
@@ -631,10 +731,12 @@ export function runPrompt<
631
731
  pendingApproval.approvalId,
632
732
  );
633
733
  if (continuation != null) {
634
- await processPendingApprovalContinuation(
734
+ const outcome = await processPendingApprovalContinuation(
635
735
  pendingApproval,
636
736
  continuation,
637
737
  );
738
+ if (outcome === 'awaiting-tool-result') return;
739
+ closingResumedStep = true;
638
740
  continue;
639
741
  }
640
742
  const pendingParsedToolCall = toolCallsByToolCallId.get(
@@ -650,10 +752,10 @@ export function runPrompt<
650
752
  approvalId: pendingApproval.approvalId,
651
753
  toolCall: pendingParsedToolCall,
652
754
  });
653
- await finishForToolApprovalPause();
755
+ await finishForHostInputPause({ completeCurrentStep: true });
654
756
  return;
655
757
  }
656
- const outcome = await maybeExecuteHostTool({
758
+ const execution = await maybeExecuteHostTool({
657
759
  event: toolCall,
658
760
  tools: activeTools,
659
761
  sandboxSession: input.sandboxSession,
@@ -690,7 +792,12 @@ export function runPrompt<
690
792
  } as TextStreamPart<TOOLS>);
691
793
  },
692
794
  });
693
- telemetry.toolEnd(toolCall.toolCallId, outcome);
795
+ if (!execution.executed) {
796
+ recordPendingToolResult({ toolCall });
797
+ await finishForHostInputPause({ completeCurrentStep: true });
798
+ return;
799
+ }
800
+ telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
694
801
  }
695
802
 
696
803
  if (value.type === 'error') {
@@ -701,11 +808,16 @@ export function runPrompt<
701
808
  context: 'harness stream error',
702
809
  error: value.error,
703
810
  });
811
+ input.onTurnFailed?.();
704
812
  result.fail(value.error);
705
813
  return;
706
814
  }
707
815
  }
708
- input.onTurnFinished?.();
816
+ if (finalFinish != null) {
817
+ input.onTurnFinished?.();
818
+ } else {
819
+ input.onTurnFailed?.();
820
+ }
709
821
  await result.finish(
710
822
  finalFinish
711
823
  ? {
@@ -723,6 +835,7 @@ export function runPrompt<
723
835
  context: 'harness turn failed',
724
836
  error: err,
725
837
  });
838
+ input.onTurnFailed?.();
726
839
  result.fail(err);
727
840
  } finally {
728
841
  reader.releaseLock();
@@ -742,6 +855,10 @@ type HostToolOutcome =
742
855
  | { ok: true; output: unknown }
743
856
  | { ok: false; error: unknown };
744
857
 
858
+ type HostToolExecution =
859
+ | { executed: false }
860
+ | { executed: true; outcome: HostToolOutcome };
861
+
745
862
  function asToolCallTextStreamPart<TOOLS extends ToolSet>(input: {
746
863
  part: TextStreamPart<TOOLS>;
747
864
  }): ToolCallTextStreamPart {
@@ -782,10 +899,10 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
782
899
  * stream. Never called for a plain (non-generator) `execute`.
783
900
  */
784
901
  onPreliminaryResult: (output: unknown) => void;
785
- }): Promise<HostToolOutcome> {
902
+ }): Promise<HostToolExecution> {
786
903
  const tool = input.tools[input.event.toolName];
787
904
 
788
- if (!isExecutableTool(tool)) return { ok: true, output: undefined };
905
+ if (!isExecutableTool(tool)) return { executed: false };
789
906
 
790
907
  const parsed = await safeParseJSON({ text: input.event.input });
791
908
  const args = parsed.success ? parsed.value : input.event.input;
@@ -825,14 +942,14 @@ async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
825
942
  toolCallId: input.event.toolCallId,
826
943
  output,
827
944
  });
828
- return { ok: true, output };
945
+ return { executed: true, outcome: { ok: true, output } };
829
946
  } catch (err) {
830
947
  await input.control.submitToolResult({
831
948
  toolCallId: input.event.toolCallId,
832
949
  output: { error: String(err) },
833
950
  isError: true,
834
951
  });
835
- return { ok: false, error: err };
952
+ return { executed: true, outcome: { ok: false, error: err } };
836
953
  }
837
954
  }
838
955
 
@@ -29,8 +29,9 @@ export function translateStreamPart<TOOLS extends ToolSet>(
29
29
  ): ReadonlyArray<TextStreamPart<TOOLS>> {
30
30
  switch (event.type) {
31
31
  case 'stream-start':
32
- // The agent emits its own `start` part with normalized warnings;
33
- // the harness-level start signal is consumed internally.
32
+ // The agent emits its own `start` part as the first chunk of the
33
+ // stream (see `HarnessStreamTextResult`); the harness-level start
34
+ // signal is consumed internally.
34
35
  return [];
35
36
 
36
37
  case 'text-start':
@@ -10,6 +10,12 @@ export type HarnessV1PendingToolApproval = {
10
10
  readonly nativeName?: string;
11
11
  };
12
12
 
13
+ export type HarnessV1PendingToolResult = {
14
+ readonly toolCallId: string;
15
+ readonly toolName: string;
16
+ readonly input: string;
17
+ };
18
+
13
19
  type HarnessV1LifecycleStateBase = {
14
20
  /**
15
21
  * Identifier of the harness that produced this state. Used by adapters to
@@ -58,6 +64,12 @@ export type HarnessV1ContinueTurnState = HarnessV1LifecycleStateBase & {
58
64
  * without the harness framework owning storage.
59
65
  */
60
66
  readonly pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
67
+
68
+ /**
69
+ * Framework-owned client tool calls that are waiting for a caller-provided
70
+ * result before the underlying turn can continue.
71
+ */
72
+ readonly pendingToolResults?: readonly HarnessV1PendingToolResult[];
61
73
  };
62
74
 
63
75
  export type HarnessV1LifecycleState =
package/src/v1/index.ts CHANGED
@@ -30,6 +30,7 @@ export type {
30
30
  HarnessV1ContinueTurnState,
31
31
  HarnessV1LifecycleState,
32
32
  HarnessV1PendingToolApproval,
33
+ HarnessV1PendingToolResult,
33
34
  HarnessV1ResumeSessionState,
34
35
  } from './harness-v1-lifecycle-state';
35
36
  export type {