@ai-sdk/harness 1.0.92 → 1.0.94

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.
@@ -1,6 +1,5 @@
1
1
  import { HarnessCapabilityUnsupportedError } from '../errors/harness-capability-unsupported-error';
2
2
  import type {
3
- HarnessV1Bootstrap,
4
3
  HarnessV1BuiltinToolFiltering,
5
4
  HarnessV1JSONSchema,
6
5
  HarnessV1NetworkSandboxSession,
@@ -10,6 +9,7 @@ import {
10
9
  asArray,
11
10
  asSchema,
12
11
  generateId,
12
+ validateTypes,
13
13
  type Context,
14
14
  type Experimental_SandboxSession as SandboxSession,
15
15
  type ModelMessage,
@@ -38,6 +38,7 @@ import type {
38
38
  HarnessAgentPermissionMode,
39
39
  HarnessAgentPrompt,
40
40
  HarnessAgentResumeSessionState,
41
+ HarnessAgentSkill,
41
42
  HarnessAgentToolSpec,
42
43
  } from './harness-agent-types';
43
44
  import {
@@ -84,6 +85,54 @@ export interface HarnessAgentCallExtensions {
84
85
  session: HarnessAgentSession;
85
86
  }
86
87
 
88
+ type HarnessAgentContinueTurnInput = {
89
+ toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
90
+ toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
91
+ };
92
+
93
+ type PreparedHarnessAgentTurnSettings<
94
+ THarness extends HarnessAgentAdapter<any>,
95
+ TUserTools extends ToolSet,
96
+ > = {
97
+ model: string | undefined;
98
+ skills: ReadonlyArray<HarnessAgentSkill>;
99
+ instructions: string | undefined;
100
+ tools: HarnessAllTools<THarness, TUserTools>;
101
+ activeTools: TUserTools;
102
+ toolSpecs: HarnessAgentToolSpec[];
103
+ builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
104
+ };
105
+
106
+ type PreparedHarnessAgentPromptTurnInput<
107
+ THarness extends HarnessAgentAdapter<any>,
108
+ TUserTools extends ToolSet,
109
+ > = PreparedHarnessAgentTurnSettings<THarness, TUserTools> & {
110
+ prompt: HarnessAgentPrompt;
111
+ };
112
+
113
+ type PreparedHarnessAgentContinueTurnInput<
114
+ THarness extends HarnessAgentAdapter<any>,
115
+ TUserTools extends ToolSet,
116
+ > = PreparedHarnessAgentTurnSettings<THarness, TUserTools> & {
117
+ toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
118
+ toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
119
+ };
120
+
121
+ type HarnessAgentTurnResult<
122
+ THarness extends HarnessAgentAdapter<any>,
123
+ TUserTools extends ToolSet,
124
+ RUNTIME_CONTEXT extends Context,
125
+ OUTPUT extends Output,
126
+ > = {
127
+ result: StreamTextResult<
128
+ HarnessAllTools<THarness, TUserTools>,
129
+ RUNTIME_CONTEXT,
130
+ OUTPUT
131
+ >;
132
+ done: Promise<void>;
133
+ ready: Promise<void>;
134
+ };
135
+
87
136
  /**
88
137
  * AI SDK `Agent` implementation that drives a third-party agent runtime
89
138
  * through a harness adapter (Claude Code, Codex, …).
@@ -124,8 +173,9 @@ export class HarnessAgent<
124
173
  TUserTools extends ToolSet = {},
125
174
  RUNTIME_CONTEXT extends Context = Context,
126
175
  OUTPUT extends Output = never,
176
+ CALL_OPTIONS = never,
127
177
  > implements Agent<
128
- never,
178
+ CALL_OPTIONS,
129
179
  HarnessAllTools<THarness, TUserTools>,
130
180
  RUNTIME_CONTEXT,
131
181
  OUTPUT
@@ -145,13 +195,13 @@ export class HarnessAgent<
145
195
  THarness,
146
196
  TUserTools,
147
197
  RUNTIME_CONTEXT,
148
- OUTPUT
198
+ OUTPUT,
199
+ CALL_OPTIONS
149
200
  >;
150
201
  private readonly stopConditions: Array<
151
202
  StopCondition<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT>
152
203
  >;
153
204
  private readonly sandboxConfig: HarnessAgentSandboxConfig;
154
- private readonly activeUserTools: TUserTools;
155
205
  private readonly builtinToolFiltering:
156
206
  | HarnessV1BuiltinToolFiltering
157
207
  | undefined;
@@ -162,7 +212,8 @@ export class HarnessAgent<
162
212
  THarness,
163
213
  TUserTools,
164
214
  RUNTIME_CONTEXT,
165
- OUTPUT
215
+ OUTPUT,
216
+ CALL_OPTIONS
166
217
  >,
167
218
  ) {
168
219
  const sandboxConfig = resolveSandboxConfig(settings);
@@ -187,7 +238,6 @@ export class HarnessAgent<
187
238
  activeTools: settings.activeTools,
188
239
  inactiveTools: settings.inactiveTools,
189
240
  });
190
- this.activeUserTools = toolFiltering.activeUserTools;
191
241
  this.builtinToolFiltering = toolFiltering.builtinToolFiltering;
192
242
  if (
193
243
  Object.keys(settings.harness.builtinTools).length > 0 &&
@@ -329,6 +379,7 @@ export class HarnessAgent<
329
379
  );
330
380
  }
331
381
 
382
+ const recipe = await harness.getBootstrap?.({ abortSignal });
332
383
  if (isResumedSession) {
333
384
  if (sandboxProvider.resumeSession == null) {
334
385
  throw new HarnessCapabilityUnsupportedError({
@@ -348,16 +399,34 @@ export class HarnessAgent<
348
399
  sessionId,
349
400
  workDir: this.sandboxConfig.workDir,
350
401
  });
351
- } else {
352
- // The logic in this clause applies the bootstrap plan, including both the harness
353
- // bootstrap recipe and agent specific sandbox configuration.
354
- // The logic matches largely what `prepareHarnessSandboxTemplate()` and
355
- // `prepareSandboxForHarness()` do, so they will have to remain aligned.
356
- let recipe: HarnessV1Bootstrap | undefined;
357
- if (harness.getBootstrap != null) {
358
- recipe = await harness.getBootstrap({ abortSignal });
359
- }
360
402
 
403
+ // Ensure the harness bootstrap recipe on resumed sessions too. The
404
+ // marker is keyed by recipe identity, so a resume whose bootstrap is
405
+ // already current costs one file read, while a resume into a sandbox
406
+ // bootstrapped by an older adapter build — a snapshot that outlived
407
+ // the harness version that made it — would otherwise keep running a
408
+ // stale bridge against a newer host, silently missing whatever the
409
+ // newer protocol added.
410
+ if (recipe != null) {
411
+ const recipeIdentity = await hashHarnessBootstrap(recipe);
412
+ try {
413
+ await applyBootstrapRecipe({
414
+ session: resumedSandboxSession.restricted(),
415
+ recipe,
416
+ identity: recipeIdentity,
417
+ defaultWorkingDirectory:
418
+ resumedSandboxSession.defaultWorkingDirectory,
419
+ abortSignal,
420
+ });
421
+ } catch (err) {
422
+ await cleanupAfterStartFailure({
423
+ sandboxSession,
424
+ ownsSandboxLifecycle,
425
+ });
426
+ throw err;
427
+ }
428
+ }
429
+ } else {
361
430
  // Defines the hashes based on both harness bootstrap recipe and
362
431
  // consumer-defined onBootstrap callback.
363
432
  const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
@@ -432,7 +501,6 @@ export class HarnessAgent<
432
501
  try {
433
502
  const baseStartOptions = {
434
503
  sessionId,
435
- skills: this.settings.skills,
436
504
  resumeFrom: validatedResumeFrom,
437
505
  continueFrom: effectiveContinueFrom,
438
506
  permissionMode: this.permissionMode,
@@ -455,6 +523,7 @@ export class HarnessAgent<
455
523
  toolApproval: this.settings.toolApproval,
456
524
  pendingToolApprovals: effectiveContinueFrom?.pendingToolApprovals,
457
525
  pendingToolResults: effectiveContinueFrom?.pendingToolResults,
526
+ turnSettings: effectiveContinueFrom?.turnSettings,
458
527
  turnState:
459
528
  effectiveContinueFrom == null
460
529
  ? 'idle'
@@ -477,7 +546,7 @@ export class HarnessAgent<
477
546
 
478
547
  async generate(
479
548
  options: AgentCallParameters<
480
- never,
549
+ CALL_OPTIONS,
481
550
  HarnessAllTools<THarness, TUserTools>,
482
551
  RUNTIME_CONTEXT
483
552
  > &
@@ -489,23 +558,24 @@ export class HarnessAgent<
489
558
  OUTPUT
490
559
  >
491
560
  > {
492
- const turnInput = this._resolveTurnInput(options);
561
+ const continueTurnInput = this._resolveContinueTurnInput(options);
493
562
  const runtimeContext = {} as RUNTIME_CONTEXT;
494
- const responseFormat = await this._resolveResponseFormat();
495
- const { result, done } = this._startTurn({
496
- session: options.session,
497
- turnInput,
498
- runtimeContext,
499
- abortSignal: options.abortSignal,
500
- responseFormat,
501
- });
563
+ const { result, done } =
564
+ continueTurnInput == null
565
+ ? await this._startPromptTurn({ options, runtimeContext })
566
+ : await this._startContinueTurn({
567
+ session: options.session,
568
+ turnInput: continueTurnInput,
569
+ runtimeContext,
570
+ abortSignal: options.abortSignal,
571
+ });
502
572
  await done;
503
573
  return this._toGenerateResult(result);
504
574
  }
505
575
 
506
576
  async stream(
507
577
  options: AgentStreamParameters<
508
- never,
578
+ CALL_OPTIONS,
509
579
  HarnessAllTools<THarness, TUserTools>,
510
580
  RUNTIME_CONTEXT
511
581
  > &
@@ -517,16 +587,18 @@ export class HarnessAgent<
517
587
  OUTPUT
518
588
  >
519
589
  > {
520
- const turnInput = this._resolveTurnInput(options);
590
+ const continueTurnInput = this._resolveContinueTurnInput(options);
521
591
  const runtimeContext = {} as RUNTIME_CONTEXT;
522
- const responseFormat = await this._resolveResponseFormat();
523
- const { result } = this._startTurn({
524
- session: options.session,
525
- turnInput,
526
- runtimeContext,
527
- abortSignal: options.abortSignal,
528
- responseFormat,
529
- });
592
+ const { result, ready } =
593
+ continueTurnInput == null
594
+ ? await this._startPromptTurn({ options, runtimeContext })
595
+ : await this._startContinueTurn({
596
+ session: options.session,
597
+ turnInput: continueTurnInput,
598
+ runtimeContext,
599
+ abortSignal: options.abortSignal,
600
+ });
601
+ await ready;
530
602
  return result;
531
603
  }
532
604
 
@@ -548,18 +620,14 @@ export class HarnessAgent<
548
620
  >
549
621
  > {
550
622
  const runtimeContext = {} as RUNTIME_CONTEXT;
551
- const responseFormat = await this._resolveResponseFormat();
552
-
553
- const { result, done } = this._startTurn({
623
+ const { result, done } = await this._startContinueTurn({
554
624
  session: options.session,
555
625
  turnInput: {
556
- mode: 'continue',
557
626
  toolApprovalContinuations: options.toolApprovalContinuations ?? [],
558
627
  toolResultContinuations: options.toolResultContinuations ?? [],
559
628
  },
560
629
  runtimeContext,
561
630
  abortSignal: options.abortSignal,
562
- responseFormat,
563
631
  });
564
632
  await done;
565
633
  return this._toGenerateResult(result);
@@ -586,19 +654,16 @@ export class HarnessAgent<
586
654
  >
587
655
  > {
588
656
  const runtimeContext = {} as RUNTIME_CONTEXT;
589
- const responseFormat = await this._resolveResponseFormat();
590
-
591
- const { result } = this._startTurn({
657
+ const { result, ready } = await this._startContinueTurn({
592
658
  session: options.session,
593
659
  turnInput: {
594
- mode: 'continue',
595
660
  toolApprovalContinuations: options.toolApprovalContinuations ?? [],
596
661
  toolResultContinuations: options.toolResultContinuations ?? [],
597
662
  },
598
663
  runtimeContext,
599
664
  abortSignal: options.abortSignal,
600
- responseFormat,
601
665
  });
666
+ await ready;
602
667
  return result;
603
668
  }
604
669
 
@@ -618,88 +683,91 @@ export class HarnessAgent<
618
683
 
619
684
  // ─── Internals ──────────────────────────────────────────────────────
620
685
 
621
- private _startTurn(input: {
622
- session: HarnessAgentSession;
623
- turnInput:
624
- | { mode: 'prompt'; prompt: HarnessAgentPrompt }
625
- | {
626
- mode: 'continue';
627
- toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
628
- toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
629
- };
686
+ private async _startPromptTurn(input: {
687
+ options: AgentCallParameters<
688
+ CALL_OPTIONS,
689
+ HarnessAllTools<THarness, TUserTools>,
690
+ RUNTIME_CONTEXT
691
+ > &
692
+ HarnessAgentCallExtensions;
630
693
  runtimeContext: RUNTIME_CONTEXT;
631
- abortSignal: AbortSignal | undefined;
632
- responseFormat: HarnessV1ResponseFormat | undefined;
633
- }): {
634
- result: StreamTextResult<
694
+ }): Promise<
695
+ HarnessAgentTurnResult<THarness, TUserTools, RUNTIME_CONTEXT, OUTPUT>
696
+ > {
697
+ const turnInput = await this._preparePromptTurnInput(input.options);
698
+ const responseFormat = await this._resolveResponseFormat();
699
+
700
+ return input.options.session.promptTurn<
635
701
  HarnessAllTools<THarness, TUserTools>,
636
702
  RUNTIME_CONTEXT,
637
703
  OUTPUT
638
- >;
639
- done: Promise<void>;
640
- } {
641
- if (input.turnInput.mode === 'continue') {
642
- return input.session.continueTurn<
643
- HarnessAllTools<THarness, TUserTools>,
644
- RUNTIME_CONTEXT,
645
- OUTPUT
646
- >({
647
- instructions: this.settings.instructions,
648
- tools: this.tools,
649
- activeTools: this.activeUserTools,
650
- toolSpecs: this._toToolSpecs(),
651
- builtinToolFiltering: this.builtinToolFiltering,
704
+ >({
705
+ ...this._buildTurnOptions({
706
+ turnSettings: turnInput,
652
707
  runtimeContext: input.runtimeContext,
653
- abortSignal: input.abortSignal,
654
- responseFormat: input.responseFormat,
655
- output: this.settings.output,
656
- telemetry: this.settings.telemetry,
657
- stopConditions: this.stopConditions,
658
- toolApprovalContinuations: input.turnInput.toolApprovalContinuations,
659
- toolResultContinuations: input.turnInput.toolResultContinuations,
660
- });
661
- }
708
+ abortSignal: input.options.abortSignal,
709
+ responseFormat,
710
+ }),
711
+ prompt: turnInput.prompt,
712
+ });
713
+ }
662
714
 
663
- return input.session.promptTurn<
715
+ private async _startContinueTurn(input: {
716
+ session: HarnessAgentSession;
717
+ turnInput: HarnessAgentContinueTurnInput;
718
+ runtimeContext: RUNTIME_CONTEXT;
719
+ abortSignal: AbortSignal | undefined;
720
+ }): Promise<
721
+ HarnessAgentTurnResult<THarness, TUserTools, RUNTIME_CONTEXT, OUTPUT>
722
+ > {
723
+ const turnInput = this._prepareContinueTurnInput(input.turnInput);
724
+ const responseFormat = await this._resolveResponseFormat();
725
+
726
+ return input.session.continueTurn<
664
727
  HarnessAllTools<THarness, TUserTools>,
665
728
  RUNTIME_CONTEXT,
666
729
  OUTPUT
667
730
  >({
668
- prompt: input.turnInput.prompt,
669
- instructions: this.settings.instructions,
670
- tools: this.tools,
671
- activeTools: this.activeUserTools,
672
- toolSpecs: this._toToolSpecs(),
673
- builtinToolFiltering: this.builtinToolFiltering,
731
+ ...this._buildTurnOptions({
732
+ turnSettings: turnInput,
733
+ runtimeContext: input.runtimeContext,
734
+ abortSignal: input.abortSignal,
735
+ responseFormat,
736
+ }),
737
+ toolApprovalContinuations: turnInput.toolApprovalContinuations,
738
+ toolResultContinuations: turnInput.toolResultContinuations,
739
+ });
740
+ }
741
+
742
+ private _buildTurnOptions(input: {
743
+ turnSettings: PreparedHarnessAgentTurnSettings<THarness, TUserTools>;
744
+ runtimeContext: RUNTIME_CONTEXT;
745
+ abortSignal: AbortSignal | undefined;
746
+ responseFormat: HarnessV1ResponseFormat | undefined;
747
+ }) {
748
+ return {
749
+ model: input.turnSettings.model,
750
+ skills: input.turnSettings.skills,
751
+ instructions: input.turnSettings.instructions,
752
+ tools: input.turnSettings.tools,
753
+ activeTools: input.turnSettings.activeTools,
754
+ toolSpecs: input.turnSettings.toolSpecs,
755
+ builtinToolFiltering: input.turnSettings.builtinToolFiltering,
674
756
  runtimeContext: input.runtimeContext,
675
757
  abortSignal: input.abortSignal,
676
758
  responseFormat: input.responseFormat,
677
759
  output: this.settings.output,
678
760
  telemetry: this.settings.telemetry,
679
761
  stopConditions: this.stopConditions,
680
- });
762
+ };
681
763
  }
682
764
 
683
- /*
684
- * Reduce AI SDK input to the single user message the harness should run
685
- * for this turn. The harness session owns prior-turn state (system
686
- * prompt, assistant turns, tool results) — we never replay it. A bare
687
- * string is forwarded as-is; a message array is collapsed to its last
688
- * `role: 'user'` entry. Inputs whose only messages are non-user (system,
689
- * assistant, tool) have no fresh user input and are rejected.
690
- */
691
- private _resolveTurnInput(options: {
765
+ private _resolveContinueTurnInput(options: {
692
766
  prompt?: string | ModelMessage[];
693
767
  messages?: ModelMessage[];
694
- }):
695
- | { mode: 'prompt'; prompt: HarnessAgentPrompt }
696
- | {
697
- mode: 'continue';
698
- toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
699
- toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
700
- } {
768
+ }): HarnessAgentContinueTurnInput | undefined {
701
769
  if (typeof options.prompt === 'string') {
702
- return { mode: 'prompt', prompt: options.prompt };
770
+ return undefined;
703
771
  }
704
772
  const messages = Array.isArray(options.prompt)
705
773
  ? options.prompt
@@ -714,15 +782,36 @@ export class HarnessAgent<
714
782
  toolResultContinuations.length > 0
715
783
  ) {
716
784
  return {
717
- mode: 'continue',
718
785
  toolApprovalContinuations,
719
786
  toolResultContinuations,
720
787
  };
721
788
  }
789
+ }
790
+ return undefined;
791
+ }
792
+
793
+ /*
794
+ * Reduce AI SDK input to the single user message the harness should run
795
+ * for this turn. The harness session owns prior-turn state (system
796
+ * prompt, assistant turns, tool results) — we never replay it. A bare
797
+ * string is forwarded as-is; a message array is collapsed to its last
798
+ * `role: 'user'` entry. Inputs whose only messages are non-user (system,
799
+ * assistant, tool) have no fresh user input and are rejected.
800
+ */
801
+ private _resolvePromptTurnInput(options: {
802
+ prompt?: string | ModelMessage[];
803
+ messages?: ModelMessage[];
804
+ }): HarnessAgentPrompt {
805
+ if (typeof options.prompt === 'string') {
806
+ return options.prompt;
807
+ }
808
+ const messages = Array.isArray(options.prompt)
809
+ ? options.prompt
810
+ : options.messages;
811
+ if (Array.isArray(messages)) {
722
812
  for (let i = messages.length - 1; i >= 0; i--) {
723
813
  const message = messages[i];
724
- if (message?.role === 'user')
725
- return { mode: 'prompt', prompt: message };
814
+ if (message?.role === 'user') return message;
726
815
  }
727
816
  throw new Error(
728
817
  'HarnessAgent: messages must contain at least one `role: "user"` entry.',
@@ -731,15 +820,142 @@ export class HarnessAgent<
731
820
  throw new Error('HarnessAgent: either `prompt` or `messages` is required.');
732
821
  }
733
822
 
823
+ private async _preparePromptTurnInput(
824
+ options: AgentCallParameters<
825
+ CALL_OPTIONS,
826
+ HarnessAllTools<THarness, TUserTools>,
827
+ RUNTIME_CONTEXT
828
+ > &
829
+ HarnessAgentCallExtensions,
830
+ ): Promise<PreparedHarnessAgentPromptTurnInput<THarness, TUserTools>> {
831
+ let callOptions = options;
832
+ if (
833
+ this.settings.callOptionsSchema != null &&
834
+ options.options !== undefined
835
+ ) {
836
+ const validatedOptions = await validateTypes({
837
+ value: options.options,
838
+ schema: this.settings.callOptionsSchema,
839
+ context: { field: 'options' },
840
+ });
841
+ callOptions = {
842
+ ...options,
843
+ options: validatedOptions,
844
+ } as unknown as typeof options;
845
+ }
846
+
847
+ const {
848
+ session: _session,
849
+ abortSignal: _abortSignal,
850
+ timeout: _timeout,
851
+ onStart: _onStart,
852
+ experimental_onStart: _experimentalOnStart,
853
+ onStepStart: _onStepStart,
854
+ experimental_onStepStart: _experimentalOnStepStart,
855
+ onToolExecutionStart: _onToolExecutionStart,
856
+ experimental_onToolCallStart: _experimentalOnToolCallStart,
857
+ onToolExecutionEnd: _onToolExecutionEnd,
858
+ experimental_onToolCallFinish: _experimentalOnToolCallFinish,
859
+ onStepEnd: _onStepEnd,
860
+ onStepFinish: _onStepFinish,
861
+ onEnd: _onEnd,
862
+ onFinish: _onFinish,
863
+ experimental_sandbox: _experimentalSandbox,
864
+ ...promptOptions
865
+ } = callOptions;
866
+
867
+ const baseCallArgs = {
868
+ model: this.settings.model,
869
+ skills: this.settings.skills,
870
+ instructions: this.settings.instructions,
871
+ tools: this.settings.tools,
872
+ ...promptOptions,
873
+ };
874
+ const preparedCallArgs =
875
+ (await this.settings.prepareCall?.(
876
+ baseCallArgs as Parameters<
877
+ NonNullable<
878
+ HarnessAgentSettings<
879
+ THarness,
880
+ TUserTools,
881
+ RUNTIME_CONTEXT,
882
+ OUTPUT,
883
+ CALL_OPTIONS
884
+ >['prepareCall']
885
+ >
886
+ >[0],
887
+ )) ?? baseCallArgs;
888
+ if (this._resolveContinueTurnInput(preparedCallArgs) != null) {
889
+ throw new Error(
890
+ 'HarnessAgent.prepareCall must return a fresh user prompt, not a tool continuation.',
891
+ );
892
+ }
893
+
894
+ return {
895
+ prompt: this._resolvePromptTurnInput(preparedCallArgs),
896
+ ...this._prepareTurnSettings({
897
+ model: preparedCallArgs.model,
898
+ skills: preparedCallArgs.skills,
899
+ instructions: preparedCallArgs.instructions,
900
+ tools: preparedCallArgs.tools,
901
+ }),
902
+ };
903
+ }
904
+
905
+ private _prepareContinueTurnInput(options: {
906
+ toolApprovalContinuations?: readonly HarnessAgentToolApprovalContinuation[];
907
+ toolResultContinuations?: readonly HarnessAgentToolResultContinuation[];
908
+ }): PreparedHarnessAgentContinueTurnInput<THarness, TUserTools> {
909
+ return {
910
+ ...this._prepareTurnSettings({
911
+ model: this.settings.model,
912
+ skills: this.settings.skills,
913
+ instructions: this.settings.instructions,
914
+ tools: this.settings.tools,
915
+ }),
916
+ toolApprovalContinuations: options.toolApprovalContinuations ?? [],
917
+ toolResultContinuations: options.toolResultContinuations ?? [],
918
+ };
919
+ }
920
+
921
+ private _prepareTurnSettings(options: {
922
+ model?: string;
923
+ skills?: ReadonlyArray<HarnessAgentSkill>;
924
+ instructions?: string;
925
+ tools?: TUserTools;
926
+ }): PreparedHarnessAgentTurnSettings<THarness, TUserTools> {
927
+ const userTools = options.tools ?? ({} as TUserTools);
928
+ const tools = {
929
+ ...this.settings.harness.builtinTools,
930
+ ...userTools,
931
+ } as HarnessAllTools<THarness, TUserTools>;
932
+ const toolFiltering = resolveHarnessAgentToolFiltering({
933
+ harness: this.settings.harness,
934
+ userTools,
935
+ allTools: tools,
936
+ activeTools: this.settings.activeTools,
937
+ inactiveTools: this.settings.inactiveTools,
938
+ });
939
+ return {
940
+ model: options.model,
941
+ skills: options.skills ?? [],
942
+ instructions: options.instructions,
943
+ tools,
944
+ activeTools: toolFiltering.activeUserTools,
945
+ toolSpecs: this._toToolSpecs(toolFiltering.activeUserTools),
946
+ builtinToolFiltering: toolFiltering.builtinToolFiltering,
947
+ };
948
+ }
949
+
734
950
  /*
735
951
  * Wire-format projection of user-defined tools only. Harness builtins are
736
952
  * executed by the runtime and the bridge already knows about them — we
737
953
  * never re-declare them over the wire.
738
954
  */
739
- private _toToolSpecs(): HarnessAgentToolSpec[] {
955
+ private _toToolSpecs(activeUserTools: TUserTools): HarnessAgentToolSpec[] {
740
956
  const specs: HarnessAgentToolSpec[] = [];
741
957
  for (const [name, tool] of Object.entries(
742
- this.activeUserTools as Record<string, unknown>,
958
+ activeUserTools as Record<string, unknown>,
743
959
  )) {
744
960
  const t = tool as {
745
961
  description?: string;
@@ -104,5 +104,8 @@ export async function validateLifecycleStateData<
104
104
  ...(state.pendingToolResults !== undefined
105
105
  ? { pendingToolResults: state.pendingToolResults }
106
106
  : {}),
107
+ ...(state.turnSettings !== undefined
108
+ ? { turnSettings: state.turnSettings }
109
+ : {}),
107
110
  } as STATE;
108
111
  }