@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.
package/dist/index.d.ts CHANGED
@@ -297,6 +297,11 @@ type HarnessV1PendingToolApproval = {
297
297
  readonly providerExecuted?: boolean;
298
298
  readonly nativeName?: string;
299
299
  };
300
+ type HarnessV1PendingToolResult = {
301
+ readonly toolCallId: string;
302
+ readonly toolName: string;
303
+ readonly input: string;
304
+ };
300
305
  type HarnessV1LifecycleStateBase = {
301
306
  /**
302
307
  * Identifier of the harness that produced this state. Used by adapters to
@@ -339,6 +344,11 @@ type HarnessV1ContinueTurnState = HarnessV1LifecycleStateBase & {
339
344
  * without the harness framework owning storage.
340
345
  */
341
346
  readonly pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
347
+ /**
348
+ * Framework-owned client tool calls that are waiting for a caller-provided
349
+ * result before the underlying turn can continue.
350
+ */
351
+ readonly pendingToolResults?: readonly HarnessV1PendingToolResult[];
342
352
  };
343
353
  type HarnessV1LifecycleState = HarnessV1ResumeSessionState | HarnessV1ContinueTurnState;
344
354
 
@@ -1584,4 +1594,4 @@ declare class HarnessCapabilityUnsupportedError extends HarnessError {
1584
1594
  static isInstance(error: unknown): error is HarnessCapabilityUnsupportedError;
1585
1595
  }
1586
1596
 
1587
- export { HARNESS_V1_BUILTIN_TOOLS, HARNESS_V1_BUILTIN_TOOL_NAMES, HarnessCapabilityUnsupportedError, HarnessError, type HarnessV1, type HarnessV1Bootstrap, type HarnessV1BootstrapCommand, type HarnessV1BootstrapFile, type HarnessV1BridgeDebugEvent, type HarnessV1BridgeOutboundMessage, type HarnessV1BridgeReady, type HarnessV1BridgeSandboxLog, type HarnessV1BridgeToolWire, type HarnessV1BuiltinTool, type HarnessV1BuiltinToolFiltering, type HarnessV1BuiltinToolName, type HarnessV1BuiltinToolUseKind, type HarnessV1CallWarning, type HarnessV1ContinueTurnOptions, type HarnessV1ContinueTurnState, type HarnessV1DebugConfig, type HarnessV1DebugLevel, type HarnessV1Diagnostic, type HarnessV1LifecycleState, type HarnessV1Metadata, type HarnessV1NetworkPolicy, type HarnessV1NetworkSandboxSession, type HarnessV1Observability, type HarnessV1PendingToolApproval, type HarnessV1PermissionMode, type HarnessV1Prompt, type HarnessV1PromptControl, type HarnessV1PromptTurnOptions, type HarnessV1ResumeSessionState, type HarnessV1SandboxProvider, type HarnessV1Session, type HarnessV1Skill, type HarnessV1StartOptions, type HarnessV1StreamPart, type HarnessV1ToolSpec, commonTool, getHarnessV1BuiltinToolFilteringDenialReason, harnessV1BridgeAbortInboundSchema, harnessV1BridgeBuiltinToolFilteringSchema, harnessV1BridgeDebugEventSchema, harnessV1BridgeDetachInboundSchema, harnessV1BridgeDetachSchema, harnessV1BridgeHelloSchema, harnessV1BridgeInboundCommandSchemas, harnessV1BridgeOutboundMessageSchema, harnessV1BridgePermissionModeSchema, harnessV1BridgeReadySchema, harnessV1BridgeResumeInboundSchema, harnessV1BridgeSandboxLogSchema, harnessV1BridgeShutdownInboundSchema, harnessV1BridgeStartBaseSchema, harnessV1BridgeThreadSchema, harnessV1BridgeToolApprovalResponseInboundSchema, harnessV1BridgeToolResultInboundSchema, harnessV1BridgeToolWireSchema, harnessV1BridgeUserMessageInboundSchema, harnessV1DebugConfigSchema, harnessV1DebugLevelSchema, harnessV1DiagnosticFromBridgeFrame, harnessV1ErrorPartSchema, harnessV1FileChangePartSchema, harnessV1FinishPartSchema, harnessV1FinishStepPartSchema, harnessV1RawPartSchema, harnessV1ReasoningDeltaPartSchema, harnessV1ReasoningEndPartSchema, harnessV1ReasoningStartPartSchema, harnessV1StreamPartSchema, harnessV1StreamStartPartSchema, harnessV1TextDeltaPartSchema, harnessV1TextEndPartSchema, harnessV1TextStartPartSchema, harnessV1ToolApprovalRequestPartSchema, harnessV1ToolCallPartSchema, harnessV1ToolResultPartSchema, isHarnessV1BuiltinToolIncluded };
1597
+ export { HARNESS_V1_BUILTIN_TOOLS, HARNESS_V1_BUILTIN_TOOL_NAMES, HarnessCapabilityUnsupportedError, HarnessError, type HarnessV1, type HarnessV1Bootstrap, type HarnessV1BootstrapCommand, type HarnessV1BootstrapFile, type HarnessV1BridgeDebugEvent, type HarnessV1BridgeOutboundMessage, type HarnessV1BridgeReady, type HarnessV1BridgeSandboxLog, type HarnessV1BridgeToolWire, type HarnessV1BuiltinTool, type HarnessV1BuiltinToolFiltering, type HarnessV1BuiltinToolName, type HarnessV1BuiltinToolUseKind, type HarnessV1CallWarning, type HarnessV1ContinueTurnOptions, type HarnessV1ContinueTurnState, type HarnessV1DebugConfig, type HarnessV1DebugLevel, type HarnessV1Diagnostic, type HarnessV1LifecycleState, type HarnessV1Metadata, type HarnessV1NetworkPolicy, type HarnessV1NetworkSandboxSession, type HarnessV1Observability, type HarnessV1PendingToolApproval, type HarnessV1PendingToolResult, type HarnessV1PermissionMode, type HarnessV1Prompt, type HarnessV1PromptControl, type HarnessV1PromptTurnOptions, type HarnessV1ResumeSessionState, type HarnessV1SandboxProvider, type HarnessV1Session, type HarnessV1Skill, type HarnessV1StartOptions, type HarnessV1StreamPart, type HarnessV1ToolSpec, commonTool, getHarnessV1BuiltinToolFilteringDenialReason, harnessV1BridgeAbortInboundSchema, harnessV1BridgeBuiltinToolFilteringSchema, harnessV1BridgeDebugEventSchema, harnessV1BridgeDetachInboundSchema, harnessV1BridgeDetachSchema, harnessV1BridgeHelloSchema, harnessV1BridgeInboundCommandSchemas, harnessV1BridgeOutboundMessageSchema, harnessV1BridgePermissionModeSchema, harnessV1BridgeReadySchema, harnessV1BridgeResumeInboundSchema, harnessV1BridgeSandboxLogSchema, harnessV1BridgeShutdownInboundSchema, harnessV1BridgeStartBaseSchema, harnessV1BridgeThreadSchema, harnessV1BridgeToolApprovalResponseInboundSchema, harnessV1BridgeToolResultInboundSchema, harnessV1BridgeToolWireSchema, harnessV1BridgeUserMessageInboundSchema, harnessV1DebugConfigSchema, harnessV1DebugLevelSchema, harnessV1DiagnosticFromBridgeFrame, harnessV1ErrorPartSchema, harnessV1FileChangePartSchema, harnessV1FinishPartSchema, harnessV1FinishStepPartSchema, harnessV1RawPartSchema, harnessV1ReasoningDeltaPartSchema, harnessV1ReasoningEndPartSchema, harnessV1ReasoningStartPartSchema, harnessV1StreamPartSchema, harnessV1StreamStartPartSchema, harnessV1TextDeltaPartSchema, harnessV1TextEndPartSchema, harnessV1TextStartPartSchema, harnessV1ToolApprovalRequestPartSchema, harnessV1ToolCallPartSchema, harnessV1ToolResultPartSchema, isHarnessV1BuiltinToolIncluded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
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.3",
48
48
  "@ai-sdk/provider-utils": "5.0.11",
49
- "ai": "7.0.31"
49
+ "ai": "7.0.32"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "ws": "^8.21.0",
@@ -11,11 +11,13 @@ import type {
11
11
  HarnessAgentAdapterSession,
12
12
  HarnessAgentContinueTurnState,
13
13
  HarnessAgentPendingToolApproval,
14
+ HarnessAgentPendingToolResult,
14
15
  HarnessAgentPrompt,
15
16
  HarnessAgentResumeSessionState,
16
17
  HarnessAgentToolSpec,
17
18
  } from './harness-agent-types';
18
19
  import type { HarnessAgentToolApprovalContinuation } from './harness-agent-tool-approval-continuation';
20
+ import type { HarnessAgentToolResultContinuation } from './harness-agent-tool-result-continuation';
19
21
  import { releaseBridgePort } from './internal/bridge-port-registry';
20
22
  import { validateLifecycleStateData } from './internal/lifecycle-state-validation';
21
23
  import { runPrompt } from './internal/run-prompt';
@@ -34,6 +36,7 @@ type HarnessAgentTurnState =
34
36
  | 'idle'
35
37
  | 'running'
36
38
  | 'awaiting-approval'
39
+ | 'awaiting-tool-result'
37
40
  | 'suspended';
38
41
 
39
42
  /**
@@ -71,6 +74,10 @@ export class HarnessAgentSession {
71
74
  string,
72
75
  HarnessAgentPendingToolApproval
73
76
  >();
77
+ private readonly pendingToolResults = new Map<
78
+ string,
79
+ HarnessAgentPendingToolResult
80
+ >();
74
81
  private sessionState: HarnessAgentSessionState = 'active';
75
82
  private turnState: HarnessAgentTurnState;
76
83
  private turnSequence = 0;
@@ -92,6 +99,7 @@ export class HarnessAgentSession {
92
99
  sessionWorkDir: string;
93
100
  toolApproval: HarnessAgentToolApprovalConfiguration | undefined;
94
101
  pendingToolApprovals?: readonly HarnessAgentPendingToolApproval[];
102
+ pendingToolResults?: readonly HarnessAgentPendingToolResult[];
95
103
  turnState?: HarnessAgentTurnState;
96
104
  }) {
97
105
  this.sessionId = options.sessionId;
@@ -105,9 +113,16 @@ export class HarnessAgentSession {
105
113
  for (const approval of options.pendingToolApprovals ?? []) {
106
114
  this.pendingToolApprovals.set(approval.approvalId, approval);
107
115
  }
116
+ for (const pendingResult of options.pendingToolResults ?? []) {
117
+ this.pendingToolResults.set(pendingResult.toolCallId, pendingResult);
118
+ }
108
119
  this.turnState =
109
120
  options.turnState ??
110
- (this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle');
121
+ (this.pendingToolApprovals.size > 0
122
+ ? 'awaiting-approval'
123
+ : this.pendingToolResults.size > 0
124
+ ? 'awaiting-tool-result'
125
+ : 'idle');
111
126
  this.isResume = options.underlyingSession.isResume;
112
127
  }
113
128
 
@@ -135,6 +150,10 @@ export class HarnessAgentSession {
135
150
  return this.sessionWorkDir;
136
151
  }
137
152
 
153
+ hasUnfinishedTurn(): boolean {
154
+ return this.turnState !== 'idle';
155
+ }
156
+
138
157
  promptTurn<TOOLS extends ToolSet, RUNTIME_CONTEXT extends Context>(options: {
139
158
  prompt: HarnessAgentPrompt;
140
159
  instructions: string | undefined;
@@ -167,6 +186,7 @@ export class HarnessAgentSession {
167
186
  telemetry: options.telemetry,
168
187
  toolApproval: this.toolApproval,
169
188
  pendingToolApprovals: this.getPendingToolApprovals(),
189
+ pendingToolResults: this.getPendingToolResults(),
170
190
  onPendingToolApproval: approval => {
171
191
  this.pendingToolApprovals.set(approval.approvalId, approval);
172
192
  this.markAwaitingApprovalIfActive();
@@ -174,11 +194,20 @@ export class HarnessAgentSession {
174
194
  onToolApprovalSettled: approvalId => {
175
195
  this.pendingToolApprovals.delete(approvalId);
176
196
  },
197
+ onPendingToolResult: pendingResult => {
198
+ this.pendingToolResults.set(pendingResult.toolCallId, pendingResult);
199
+ this.markAwaitingToolResultIfActive();
200
+ },
201
+ onToolResultSettled: toolCallId => {
202
+ this.pendingToolResults.delete(toolCallId);
203
+ },
177
204
  onTurnFinished: () => {
178
205
  this.finishTrackedTurn({ turnId });
179
206
  },
207
+ onTurnFailed: () => {
208
+ this.finishTrackedTurn({ turnId });
209
+ },
180
210
  });
181
- this.trackTurnCompletion({ done: turn.done, turnId });
182
211
  return turn;
183
212
  } catch (error) {
184
213
  this.finishTrackedTurn({ turnId });
@@ -201,6 +230,9 @@ export class HarnessAgentSession {
201
230
  toolApprovalContinuations?:
202
231
  | readonly HarnessAgentToolApprovalContinuation[]
203
232
  | undefined;
233
+ toolResultContinuations?:
234
+ | readonly HarnessAgentToolResultContinuation[]
235
+ | undefined;
204
236
  }): HarnessAgentTurnResult<TOOLS, RUNTIME_CONTEXT> {
205
237
  const session = this.requireReusableSession();
206
238
  this.requireContinuableTurn();
@@ -223,7 +255,9 @@ export class HarnessAgentSession {
223
255
  telemetry: options.telemetry,
224
256
  toolApproval: this.toolApproval,
225
257
  pendingToolApprovals: this.getPendingToolApprovals(),
258
+ pendingToolResults: this.getPendingToolResults(),
226
259
  toolApprovalContinuations: options.toolApprovalContinuations,
260
+ toolResultContinuations: options.toolResultContinuations,
227
261
  onPendingToolApproval: approval => {
228
262
  this.pendingToolApprovals.set(approval.approvalId, approval);
229
263
  this.markAwaitingApprovalIfActive();
@@ -231,11 +265,20 @@ export class HarnessAgentSession {
231
265
  onToolApprovalSettled: approvalId => {
232
266
  this.pendingToolApprovals.delete(approvalId);
233
267
  },
268
+ onPendingToolResult: pendingResult => {
269
+ this.pendingToolResults.set(pendingResult.toolCallId, pendingResult);
270
+ this.markAwaitingToolResultIfActive();
271
+ },
272
+ onToolResultSettled: toolCallId => {
273
+ this.pendingToolResults.delete(toolCallId);
274
+ },
234
275
  onTurnFinished: () => {
235
276
  this.finishTrackedTurn({ turnId });
236
277
  },
278
+ onTurnFailed: () => {
279
+ this.finishTrackedTurn({ turnId });
280
+ },
237
281
  });
238
- this.trackTurnCompletion({ done: turn.done, turnId });
239
282
  return turn;
240
283
  } catch (error) {
241
284
  this.finishTrackedTurn({ turnId });
@@ -382,11 +425,16 @@ export class HarnessAgentSession {
382
425
  return Array.from(this.pendingToolApprovals.values());
383
426
  }
384
427
 
385
- private addPendingToolApprovals(
428
+ private getPendingToolResults(): readonly HarnessAgentPendingToolResult[] {
429
+ return Array.from(this.pendingToolResults.values());
430
+ }
431
+
432
+ private addPendingToolState(
386
433
  state: HarnessAgentContinueTurnState,
387
434
  ): HarnessAgentContinueTurnState {
388
435
  const pendingToolApprovals = this.getPendingToolApprovals();
389
- if (pendingToolApprovals.length === 0) {
436
+ const pendingToolResults = this.getPendingToolResults();
437
+ if (pendingToolApprovals.length === 0 && pendingToolResults.length === 0) {
390
438
  return {
391
439
  type: state.type,
392
440
  harnessId: state.harnessId,
@@ -396,7 +444,8 @@ export class HarnessAgentSession {
396
444
  }
397
445
  return {
398
446
  ...state,
399
- pendingToolApprovals,
447
+ ...(pendingToolApprovals.length > 0 ? { pendingToolApprovals } : {}),
448
+ ...(pendingToolResults.length > 0 ? { pendingToolResults } : {}),
400
449
  };
401
450
  }
402
451
 
@@ -410,7 +459,7 @@ export class HarnessAgentSession {
410
459
  expectedType: 'continue-turn',
411
460
  });
412
461
  this.turnState = 'suspended';
413
- return this.addPendingToolApprovals(validated);
462
+ return this.addPendingToolState(validated);
414
463
  }
415
464
 
416
465
  private toResumeStateWithContinuation(options: {
@@ -441,6 +490,7 @@ export class HarnessAgentSession {
441
490
  private requireContinuableTurn(): void {
442
491
  if (
443
492
  this.turnState === 'awaiting-approval' ||
493
+ this.turnState === 'awaiting-tool-result' ||
444
494
  this.turnState === 'suspended'
445
495
  ) {
446
496
  return;
@@ -461,6 +511,12 @@ export class HarnessAgentSession {
461
511
  }
462
512
  }
463
513
 
514
+ private markAwaitingToolResultIfActive(): void {
515
+ if (this.sessionState === 'active') {
516
+ this.turnState = 'awaiting-tool-result';
517
+ }
518
+ }
519
+
464
520
  private startTrackedTurn(): number {
465
521
  const turnId = ++this.turnSequence;
466
522
  this.activeTurnSequence = turnId;
@@ -468,22 +524,12 @@ export class HarnessAgentSession {
468
524
  return turnId;
469
525
  }
470
526
 
471
- private trackTurnCompletion(options: {
472
- done: Promise<void>;
473
- turnId: number;
474
- }): void {
475
- void Promise.resolve(options.done)
476
- .finally(() => {
477
- this.finishTrackedTurn({ turnId: options.turnId });
478
- })
479
- .catch(() => {});
480
- }
481
-
482
527
  private finishTrackedTurn(options: { turnId: number }): void {
483
528
  if (this.sessionState !== 'active') return;
484
529
  if (this.activeTurnSequence !== options.turnId) return;
485
- this.turnState =
486
- this.pendingToolApprovals.size > 0 ? 'awaiting-approval' : 'idle';
530
+ this.pendingToolApprovals.clear();
531
+ this.pendingToolResults.clear();
532
+ this.turnState = 'idle';
487
533
  }
488
534
 
489
535
  private endLocalHandle(options: {
@@ -0,0 +1,62 @@
1
+ import type { ModelMessage, ToolResultOutput } from '@ai-sdk/provider-utils';
2
+
3
+ export type HarnessAgentToolResultContinuation = {
4
+ readonly toolCallId: string;
5
+ readonly output: unknown;
6
+ readonly isError?: boolean;
7
+ };
8
+
9
+ /**
10
+ * Extract client-provided tool results from the trailing tool message and
11
+ * convert AI SDK model-output wrappers back into values a harness runtime can
12
+ * return from its pending host-tool invocation.
13
+ */
14
+ export function collectHarnessAgentToolResultContinuations(input: {
15
+ messages: readonly ModelMessage[];
16
+ }): readonly HarnessAgentToolResultContinuation[] {
17
+ const lastMessage = input.messages.at(-1);
18
+ if (lastMessage?.role !== 'tool') return [];
19
+
20
+ return lastMessage.content
21
+ .filter(part => part.type === 'tool-result')
22
+ .map(part =>
23
+ toToolResultContinuation({
24
+ toolCallId: part.toolCallId,
25
+ output: part.output,
26
+ }),
27
+ );
28
+ }
29
+
30
+ function toToolResultContinuation(input: {
31
+ toolCallId: string;
32
+ output: ToolResultOutput;
33
+ }): HarnessAgentToolResultContinuation {
34
+ switch (input.output.type) {
35
+ case 'text':
36
+ case 'json':
37
+ return {
38
+ toolCallId: input.toolCallId,
39
+ output: input.output.value,
40
+ };
41
+ case 'error-text':
42
+ case 'error-json':
43
+ return {
44
+ toolCallId: input.toolCallId,
45
+ output: input.output.value,
46
+ isError: true,
47
+ };
48
+ case 'execution-denied':
49
+ return {
50
+ toolCallId: input.toolCallId,
51
+ output: {
52
+ type: input.output.type,
53
+ reason: input.output.reason,
54
+ },
55
+ };
56
+ case 'content':
57
+ return {
58
+ toolCallId: input.toolCallId,
59
+ output: input.output,
60
+ };
61
+ }
62
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  HarnessV1ContinueTurnState,
10
10
  HarnessV1LifecycleState,
11
11
  HarnessV1PendingToolApproval,
12
+ HarnessV1PendingToolResult,
12
13
  HarnessV1PermissionMode,
13
14
  HarnessV1Prompt,
14
15
  HarnessV1PromptControl,
@@ -45,6 +46,7 @@ export type HarnessAgentLifecycleState = HarnessV1LifecycleState;
45
46
  export type HarnessAgentResumeSessionState = HarnessV1ResumeSessionState;
46
47
  export type HarnessAgentContinueTurnState = HarnessV1ContinueTurnState;
47
48
  export type HarnessAgentPendingToolApproval = HarnessV1PendingToolApproval;
49
+ export type HarnessAgentPendingToolResult = HarnessV1PendingToolResult;
48
50
 
49
51
  export type HarnessAgentSkill = HarnessV1Skill;
50
52
  export type HarnessAgentPermissionMode = HarnessV1PermissionMode;
@@ -39,6 +39,10 @@ import {
39
39
  collectHarnessAgentToolApprovalContinuations,
40
40
  type HarnessAgentToolApprovalContinuation,
41
41
  } from './harness-agent-tool-approval-continuation';
42
+ import {
43
+ collectHarnessAgentToolResultContinuations,
44
+ type HarnessAgentToolResultContinuation,
45
+ } from './harness-agent-tool-result-continuation';
42
46
  import { applyBootstrapRecipe } from './internal/bootstrap-recipe';
43
47
  import {
44
48
  acquireBridgePort,
@@ -348,13 +352,17 @@ export class HarnessAgent<
348
352
  sessionWorkDir,
349
353
  toolApproval: this.settings.toolApproval,
350
354
  pendingToolApprovals: effectiveContinueFrom?.pendingToolApprovals,
355
+ pendingToolResults: effectiveContinueFrom?.pendingToolResults,
351
356
  turnState:
352
357
  effectiveContinueFrom == null
353
358
  ? 'idle'
354
359
  : effectiveContinueFrom.pendingToolApprovals != null &&
355
360
  effectiveContinueFrom.pendingToolApprovals.length > 0
356
361
  ? 'awaiting-approval'
357
- : 'suspended',
362
+ : effectiveContinueFrom.pendingToolResults != null &&
363
+ effectiveContinueFrom.pendingToolResults.length > 0
364
+ ? 'awaiting-tool-result'
365
+ : 'suspended',
358
366
  });
359
367
  } catch (error) {
360
368
  await cleanupAfterStartFailure({
@@ -426,6 +434,7 @@ export class HarnessAgent<
426
434
  async continueGenerate(options: {
427
435
  session: HarnessAgentSession;
428
436
  toolApprovalContinuations?: readonly HarnessAgentToolApprovalContinuation[];
437
+ toolResultContinuations?: readonly HarnessAgentToolResultContinuation[];
429
438
  abortSignal?: AbortSignal;
430
439
  }): Promise<
431
440
  GenerateTextResult<
@@ -441,6 +450,7 @@ export class HarnessAgent<
441
450
  turnInput: {
442
451
  mode: 'continue',
443
452
  toolApprovalContinuations: options.toolApprovalContinuations ?? [],
453
+ toolResultContinuations: options.toolResultContinuations ?? [],
444
454
  },
445
455
  runtimeContext,
446
456
  abortSignal: options.abortSignal,
@@ -460,6 +470,7 @@ export class HarnessAgent<
460
470
  async continueStream(options: {
461
471
  session: HarnessAgentSession;
462
472
  toolApprovalContinuations?: readonly HarnessAgentToolApprovalContinuation[];
473
+ toolResultContinuations?: readonly HarnessAgentToolResultContinuation[];
463
474
  abortSignal?: AbortSignal;
464
475
  }): Promise<
465
476
  StreamTextResult<
@@ -475,6 +486,7 @@ export class HarnessAgent<
475
486
  turnInput: {
476
487
  mode: 'continue',
477
488
  toolApprovalContinuations: options.toolApprovalContinuations ?? [],
489
+ toolResultContinuations: options.toolResultContinuations ?? [],
478
490
  },
479
491
  runtimeContext,
480
492
  abortSignal: options.abortSignal,
@@ -491,6 +503,7 @@ export class HarnessAgent<
491
503
  | {
492
504
  mode: 'continue';
493
505
  toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
506
+ toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
494
507
  };
495
508
  runtimeContext: RUNTIME_CONTEXT;
496
509
  abortSignal: AbortSignal | undefined;
@@ -516,6 +529,7 @@ export class HarnessAgent<
516
529
  abortSignal: input.abortSignal,
517
530
  telemetry: this.settings.telemetry,
518
531
  toolApprovalContinuations: input.turnInput.toolApprovalContinuations,
532
+ toolResultContinuations: input.turnInput.toolResultContinuations,
519
533
  });
520
534
  }
521
535
 
@@ -579,6 +593,7 @@ export class HarnessAgent<
579
593
  | {
580
594
  mode: 'continue';
581
595
  toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
596
+ toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
582
597
  } {
583
598
  if (typeof options.prompt === 'string') {
584
599
  return { mode: 'prompt', prompt: options.prompt };
@@ -589,10 +604,16 @@ export class HarnessAgent<
589
604
  if (Array.isArray(messages)) {
590
605
  const toolApprovalContinuations =
591
606
  collectHarnessAgentToolApprovalContinuations({ messages });
592
- if (toolApprovalContinuations.length > 0) {
607
+ const toolResultContinuations =
608
+ collectHarnessAgentToolResultContinuations({ messages });
609
+ if (
610
+ toolApprovalContinuations.length > 0 ||
611
+ toolResultContinuations.length > 0
612
+ ) {
593
613
  return {
594
614
  mode: 'continue',
595
615
  toolApprovalContinuations,
616
+ toolResultContinuations,
596
617
  };
597
618
  }
598
619
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -141,6 +141,7 @@ export class HarnessStreamTextResult<
141
141
  private currentStepContent: ContentPart<TOOLS>[] = [];
142
142
  private currentStepWarnings: CallWarning[] = [];
143
143
  private stepNumber = 0;
144
+ private stepStarted = false;
144
145
 
145
146
  private readonly tools: TOOLS;
146
147
  private readonly runtimeContext: RUNTIME_CONTEXT;
@@ -173,6 +174,13 @@ export class HarnessStreamTextResult<
173
174
  const baseStream = new ReadableStream<TextStreamPart<TOOLS>>({
174
175
  start(c) {
175
176
  controllerRef = c;
177
+ // Send the message-level start event as the first part, mirroring
178
+ // `streamText`. Downstream UI message stream consumers depend on it:
179
+ // `toUIMessageStream`'s persistence mode injects the response message
180
+ // id into this part (it never synthesizes one), so without it
181
+ // `useChat` clients keep a locally generated assistant message id
182
+ // that diverges from the id the server persists under.
183
+ c.enqueue({ type: 'start' });
176
184
  },
177
185
  });
178
186
  this.fullStreamController = controllerRef;
@@ -198,10 +206,30 @@ export class HarnessStreamTextResult<
198
206
  * into the current step's content array where applicable.
199
207
  */
200
208
  enqueue(part: TextStreamPart<TOOLS>): void {
209
+ this.startStep();
201
210
  this.fullStreamController.enqueue(part);
202
211
  this.appendToCurrentStepContent(part);
203
212
  }
204
213
 
214
+ /**
215
+ * Push a continuation input into the consumer stream without attributing it
216
+ * to the next model step. Approval responses and client tool results arrive
217
+ * between model calls and therefore must not create or alter a StepResult.
218
+ */
219
+ enqueueContinuation(part: TextStreamPart<TOOLS>): void {
220
+ this.fullStreamController.enqueue(part);
221
+ }
222
+
223
+ /**
224
+ * Drop content replayed while a suspended host-input pause closes its
225
+ * already-recorded model step.
226
+ */
227
+ discardCurrentStepContent(): void {
228
+ this.currentStepContent = [];
229
+ this.currentStepWarnings = [];
230
+ this.stepStarted = false;
231
+ }
232
+
205
233
  /**
206
234
  * Mark the end of a step. Builds a `StepResult` from the accumulated
207
235
  * content and records it in the steps array. Accepts the V4-shaped
@@ -213,7 +241,9 @@ export class HarnessStreamTextResult<
213
241
  usage: LanguageModelV4Usage;
214
242
  providerMetadata: ProviderMetadata | undefined;
215
243
  warnings: CallWarning[];
216
- }): void {
244
+ }): StepResult<TOOLS, RUNTIME_CONTEXT> {
245
+ this.startStep();
246
+
217
247
  const normalizedUsage = asLanguageModelUsage(input.usage);
218
248
  const finishReason = input.finishReason.unified;
219
249
  const rawFinishReason = input.finishReason.raw;
@@ -267,6 +297,20 @@ export class HarnessStreamTextResult<
267
297
  this.stepNumber += 1;
268
298
  this.currentStepContent = [];
269
299
  this.currentStepWarnings = [];
300
+ this.stepStarted = false;
301
+
302
+ return step;
303
+ }
304
+
305
+ private startStep(): void {
306
+ if (this.stepStarted) return;
307
+
308
+ this.stepStarted = true;
309
+ this.fullStreamController.enqueue({
310
+ type: 'start-step',
311
+ request: {},
312
+ warnings: this.currentStepWarnings,
313
+ } as TextStreamPart<TOOLS>);
270
314
  }
271
315
 
272
316
  /**
@@ -278,7 +322,20 @@ export class HarnessStreamTextResult<
278
322
  providerMetadata: ProviderMetadata | undefined;
279
323
  }): Promise<void> {
280
324
  if (this.settled) return;
281
- this.settled = true;
325
+
326
+ /*
327
+ * Do not flush trailing content that has not been captured by a
328
+ * `finish-step`. A terminal `finish` closes the turn but is not a semantic
329
+ * step boundary, so buffered content indicates an invalid harness stream.
330
+ */
331
+ if (this.currentStepContent.length > 0) {
332
+ this.fail(
333
+ new Error(
334
+ 'HarnessAgent: received terminal finish with unclosed step content. Harness adapters must emit `finish-step` before `finish`.',
335
+ ),
336
+ );
337
+ return;
338
+ }
282
339
 
283
340
  if (input != null) {
284
341
  this.finalFinishReason = input.finishReason.unified;
@@ -287,39 +344,7 @@ export class HarnessStreamTextResult<
287
344
  this.accumulatedUsage = asLanguageModelUsage(input.totalUsage);
288
345
  }
289
346
 
290
- // Flush any trailing content not yet captured by a finish-step. We
291
- // construct the step directly here (the public `finishStep` takes V4
292
- // shapes; we already have AI SDK shapes at this point).
293
- if (this.currentStepContent.length > 0) {
294
- const trailingStep = new DefaultStepResult<TOOLS, RUNTIME_CONTEXT>({
295
- callId: generateId(),
296
- stepNumber: this.stepNumber,
297
- provider: this.providerName,
298
- modelId: this.modelId,
299
- runtimeContext: this.runtimeContext,
300
- toolsContext: this.toolsContext,
301
- content: this.currentStepContent,
302
- finishReason: this.finalFinishReason,
303
- rawFinishReason: this.finalRawFinishReason,
304
- usage: createNullLanguageModelUsage(),
305
- performance: createEmptyPerformance(),
306
- warnings:
307
- this.currentStepWarnings.length > 0
308
- ? this.currentStepWarnings
309
- : undefined,
310
- request: {},
311
- response: {
312
- id: generateId(),
313
- timestamp: new Date(),
314
- modelId: this.modelId,
315
- messages: [],
316
- },
317
- providerMetadata: this.finalProviderMetadata,
318
- });
319
- this.stepsBuffer.push(trailingStep);
320
- this.currentStepContent = [];
321
- this.currentStepWarnings = [];
322
- }
347
+ this.settled = true;
323
348
 
324
349
  const finalStep =
325
350
  this.stepsBuffer.length > 0
@@ -46,6 +46,16 @@ export async function validateLifecycleStateData<
46
46
  'Resume session state cannot contain pending tool approvals; unfinished turns must be stored as `continueFrom`.',
47
47
  });
48
48
  }
49
+ if (
50
+ state.type === 'resume-session' &&
51
+ 'pendingToolResults' in state &&
52
+ state.pendingToolResults !== undefined
53
+ ) {
54
+ throw new HarnessError({
55
+ message:
56
+ 'Resume session state cannot contain pending tool results; unfinished turns must be stored as `continueFrom`.',
57
+ });
58
+ }
49
59
 
50
60
  const data =
51
61
  harness.lifecycleStateSchema == null
@@ -91,5 +101,8 @@ export async function validateLifecycleStateData<
91
101
  ...(state.pendingToolApprovals !== undefined
92
102
  ? { pendingToolApprovals: state.pendingToolApprovals }
93
103
  : {}),
104
+ ...(state.pendingToolResults !== undefined
105
+ ? { pendingToolResults: state.pendingToolResults }
106
+ : {}),
94
107
  } as STATE;
95
108
  }