@ai-sdk/harness 1.0.91 → 1.0.93

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,53 @@ 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
+ skills: ReadonlyArray<HarnessAgentSkill>;
98
+ instructions: string | undefined;
99
+ tools: HarnessAllTools<THarness, TUserTools>;
100
+ activeTools: TUserTools;
101
+ toolSpecs: HarnessAgentToolSpec[];
102
+ builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
103
+ };
104
+
105
+ type PreparedHarnessAgentPromptTurnInput<
106
+ THarness extends HarnessAgentAdapter<any>,
107
+ TUserTools extends ToolSet,
108
+ > = PreparedHarnessAgentTurnSettings<THarness, TUserTools> & {
109
+ prompt: HarnessAgentPrompt;
110
+ };
111
+
112
+ type PreparedHarnessAgentContinueTurnInput<
113
+ THarness extends HarnessAgentAdapter<any>,
114
+ TUserTools extends ToolSet,
115
+ > = PreparedHarnessAgentTurnSettings<THarness, TUserTools> & {
116
+ toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
117
+ toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
118
+ };
119
+
120
+ type HarnessAgentTurnResult<
121
+ THarness extends HarnessAgentAdapter<any>,
122
+ TUserTools extends ToolSet,
123
+ RUNTIME_CONTEXT extends Context,
124
+ OUTPUT extends Output,
125
+ > = {
126
+ result: StreamTextResult<
127
+ HarnessAllTools<THarness, TUserTools>,
128
+ RUNTIME_CONTEXT,
129
+ OUTPUT
130
+ >;
131
+ done: Promise<void>;
132
+ ready: Promise<void>;
133
+ };
134
+
87
135
  /**
88
136
  * AI SDK `Agent` implementation that drives a third-party agent runtime
89
137
  * through a harness adapter (Claude Code, Codex, …).
@@ -124,8 +172,9 @@ export class HarnessAgent<
124
172
  TUserTools extends ToolSet = {},
125
173
  RUNTIME_CONTEXT extends Context = Context,
126
174
  OUTPUT extends Output = never,
175
+ CALL_OPTIONS = never,
127
176
  > implements Agent<
128
- never,
177
+ CALL_OPTIONS,
129
178
  HarnessAllTools<THarness, TUserTools>,
130
179
  RUNTIME_CONTEXT,
131
180
  OUTPUT
@@ -145,13 +194,13 @@ export class HarnessAgent<
145
194
  THarness,
146
195
  TUserTools,
147
196
  RUNTIME_CONTEXT,
148
- OUTPUT
197
+ OUTPUT,
198
+ CALL_OPTIONS
149
199
  >;
150
200
  private readonly stopConditions: Array<
151
201
  StopCondition<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT>
152
202
  >;
153
203
  private readonly sandboxConfig: HarnessAgentSandboxConfig;
154
- private readonly activeUserTools: TUserTools;
155
204
  private readonly builtinToolFiltering:
156
205
  | HarnessV1BuiltinToolFiltering
157
206
  | undefined;
@@ -162,7 +211,8 @@ export class HarnessAgent<
162
211
  THarness,
163
212
  TUserTools,
164
213
  RUNTIME_CONTEXT,
165
- OUTPUT
214
+ OUTPUT,
215
+ CALL_OPTIONS
166
216
  >,
167
217
  ) {
168
218
  const sandboxConfig = resolveSandboxConfig(settings);
@@ -187,7 +237,6 @@ export class HarnessAgent<
187
237
  activeTools: settings.activeTools,
188
238
  inactiveTools: settings.inactiveTools,
189
239
  });
190
- this.activeUserTools = toolFiltering.activeUserTools;
191
240
  this.builtinToolFiltering = toolFiltering.builtinToolFiltering;
192
241
  if (
193
242
  Object.keys(settings.harness.builtinTools).length > 0 &&
@@ -329,6 +378,7 @@ export class HarnessAgent<
329
378
  );
330
379
  }
331
380
 
381
+ const recipe = await harness.getBootstrap?.({ abortSignal });
332
382
  if (isResumedSession) {
333
383
  if (sandboxProvider.resumeSession == null) {
334
384
  throw new HarnessCapabilityUnsupportedError({
@@ -348,16 +398,34 @@ export class HarnessAgent<
348
398
  sessionId,
349
399
  workDir: this.sandboxConfig.workDir,
350
400
  });
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
401
 
402
+ // Ensure the harness bootstrap recipe on resumed sessions too. The
403
+ // marker is keyed by recipe identity, so a resume whose bootstrap is
404
+ // already current costs one file read, while a resume into a sandbox
405
+ // bootstrapped by an older adapter build — a snapshot that outlived
406
+ // the harness version that made it — would otherwise keep running a
407
+ // stale bridge against a newer host, silently missing whatever the
408
+ // newer protocol added.
409
+ if (recipe != null) {
410
+ const recipeIdentity = await hashHarnessBootstrap(recipe);
411
+ try {
412
+ await applyBootstrapRecipe({
413
+ session: resumedSandboxSession.restricted(),
414
+ recipe,
415
+ identity: recipeIdentity,
416
+ defaultWorkingDirectory:
417
+ resumedSandboxSession.defaultWorkingDirectory,
418
+ abortSignal,
419
+ });
420
+ } catch (err) {
421
+ await cleanupAfterStartFailure({
422
+ sandboxSession,
423
+ ownsSandboxLifecycle,
424
+ });
425
+ throw err;
426
+ }
427
+ }
428
+ } else {
361
429
  // Defines the hashes based on both harness bootstrap recipe and
362
430
  // consumer-defined onBootstrap callback.
363
431
  const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
@@ -431,8 +499,8 @@ export class HarnessAgent<
431
499
 
432
500
  try {
433
501
  const baseStartOptions = {
502
+ model: this.settings.model,
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,90 @@ 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
+ skills: input.turnSettings.skills,
750
+ instructions: input.turnSettings.instructions,
751
+ tools: input.turnSettings.tools,
752
+ activeTools: input.turnSettings.activeTools,
753
+ toolSpecs: input.turnSettings.toolSpecs,
754
+ builtinToolFiltering: input.turnSettings.builtinToolFiltering,
674
755
  runtimeContext: input.runtimeContext,
675
756
  abortSignal: input.abortSignal,
676
757
  responseFormat: input.responseFormat,
677
758
  output: this.settings.output,
678
759
  telemetry: this.settings.telemetry,
679
760
  stopConditions: this.stopConditions,
680
- });
761
+ };
681
762
  }
682
763
 
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: {
764
+ private _resolveContinueTurnInput(options: {
692
765
  prompt?: string | ModelMessage[];
693
766
  messages?: ModelMessage[];
694
- }):
695
- | { mode: 'prompt'; prompt: HarnessAgentPrompt }
696
- | {
697
- mode: 'continue';
698
- toolApprovalContinuations: readonly HarnessAgentToolApprovalContinuation[];
699
- toolResultContinuations: readonly HarnessAgentToolResultContinuation[];
700
- } {
767
+ }): HarnessAgentContinueTurnInput | undefined {
701
768
  if (typeof options.prompt === 'string') {
702
- return { mode: 'prompt', prompt: options.prompt };
769
+ return undefined;
703
770
  }
704
771
  const messages = Array.isArray(options.prompt)
705
772
  ? options.prompt
@@ -714,15 +781,36 @@ export class HarnessAgent<
714
781
  toolResultContinuations.length > 0
715
782
  ) {
716
783
  return {
717
- mode: 'continue',
718
784
  toolApprovalContinuations,
719
785
  toolResultContinuations,
720
786
  };
721
787
  }
788
+ }
789
+ return undefined;
790
+ }
791
+
792
+ /*
793
+ * Reduce AI SDK input to the single user message the harness should run
794
+ * for this turn. The harness session owns prior-turn state (system
795
+ * prompt, assistant turns, tool results) — we never replay it. A bare
796
+ * string is forwarded as-is; a message array is collapsed to its last
797
+ * `role: 'user'` entry. Inputs whose only messages are non-user (system,
798
+ * assistant, tool) have no fresh user input and are rejected.
799
+ */
800
+ private _resolvePromptTurnInput(options: {
801
+ prompt?: string | ModelMessage[];
802
+ messages?: ModelMessage[];
803
+ }): HarnessAgentPrompt {
804
+ if (typeof options.prompt === 'string') {
805
+ return options.prompt;
806
+ }
807
+ const messages = Array.isArray(options.prompt)
808
+ ? options.prompt
809
+ : options.messages;
810
+ if (Array.isArray(messages)) {
722
811
  for (let i = messages.length - 1; i >= 0; i--) {
723
812
  const message = messages[i];
724
- if (message?.role === 'user')
725
- return { mode: 'prompt', prompt: message };
813
+ if (message?.role === 'user') return message;
726
814
  }
727
815
  throw new Error(
728
816
  'HarnessAgent: messages must contain at least one `role: "user"` entry.',
@@ -731,15 +819,137 @@ export class HarnessAgent<
731
819
  throw new Error('HarnessAgent: either `prompt` or `messages` is required.');
732
820
  }
733
821
 
822
+ private async _preparePromptTurnInput(
823
+ options: AgentCallParameters<
824
+ CALL_OPTIONS,
825
+ HarnessAllTools<THarness, TUserTools>,
826
+ RUNTIME_CONTEXT
827
+ > &
828
+ HarnessAgentCallExtensions,
829
+ ): Promise<PreparedHarnessAgentPromptTurnInput<THarness, TUserTools>> {
830
+ let callOptions = options;
831
+ if (
832
+ this.settings.callOptionsSchema != null &&
833
+ options.options !== undefined
834
+ ) {
835
+ const validatedOptions = await validateTypes({
836
+ value: options.options,
837
+ schema: this.settings.callOptionsSchema,
838
+ context: { field: 'options' },
839
+ });
840
+ callOptions = {
841
+ ...options,
842
+ options: validatedOptions,
843
+ } as unknown as typeof options;
844
+ }
845
+
846
+ const {
847
+ session: _session,
848
+ abortSignal: _abortSignal,
849
+ timeout: _timeout,
850
+ onStart: _onStart,
851
+ experimental_onStart: _experimentalOnStart,
852
+ onStepStart: _onStepStart,
853
+ experimental_onStepStart: _experimentalOnStepStart,
854
+ onToolExecutionStart: _onToolExecutionStart,
855
+ experimental_onToolCallStart: _experimentalOnToolCallStart,
856
+ onToolExecutionEnd: _onToolExecutionEnd,
857
+ experimental_onToolCallFinish: _experimentalOnToolCallFinish,
858
+ onStepEnd: _onStepEnd,
859
+ onStepFinish: _onStepFinish,
860
+ onEnd: _onEnd,
861
+ onFinish: _onFinish,
862
+ experimental_sandbox: _experimentalSandbox,
863
+ ...promptOptions
864
+ } = callOptions;
865
+
866
+ const baseCallArgs = {
867
+ skills: this.settings.skills,
868
+ instructions: this.settings.instructions,
869
+ tools: this.settings.tools,
870
+ ...promptOptions,
871
+ };
872
+ const preparedCallArgs =
873
+ (await this.settings.prepareCall?.(
874
+ baseCallArgs as Parameters<
875
+ NonNullable<
876
+ HarnessAgentSettings<
877
+ THarness,
878
+ TUserTools,
879
+ RUNTIME_CONTEXT,
880
+ OUTPUT,
881
+ CALL_OPTIONS
882
+ >['prepareCall']
883
+ >
884
+ >[0],
885
+ )) ?? baseCallArgs;
886
+ if (this._resolveContinueTurnInput(preparedCallArgs) != null) {
887
+ throw new Error(
888
+ 'HarnessAgent.prepareCall must return a fresh user prompt, not a tool continuation.',
889
+ );
890
+ }
891
+
892
+ return {
893
+ prompt: this._resolvePromptTurnInput(preparedCallArgs),
894
+ ...this._prepareTurnSettings({
895
+ skills: preparedCallArgs.skills,
896
+ instructions: preparedCallArgs.instructions,
897
+ tools: preparedCallArgs.tools,
898
+ }),
899
+ };
900
+ }
901
+
902
+ private _prepareContinueTurnInput(options: {
903
+ toolApprovalContinuations?: readonly HarnessAgentToolApprovalContinuation[];
904
+ toolResultContinuations?: readonly HarnessAgentToolResultContinuation[];
905
+ }): PreparedHarnessAgentContinueTurnInput<THarness, TUserTools> {
906
+ return {
907
+ ...this._prepareTurnSettings({
908
+ skills: this.settings.skills,
909
+ instructions: this.settings.instructions,
910
+ tools: this.settings.tools,
911
+ }),
912
+ toolApprovalContinuations: options.toolApprovalContinuations ?? [],
913
+ toolResultContinuations: options.toolResultContinuations ?? [],
914
+ };
915
+ }
916
+
917
+ private _prepareTurnSettings(options: {
918
+ skills?: ReadonlyArray<HarnessAgentSkill>;
919
+ instructions?: string;
920
+ tools?: TUserTools;
921
+ }): PreparedHarnessAgentTurnSettings<THarness, TUserTools> {
922
+ const userTools = options.tools ?? ({} as TUserTools);
923
+ const tools = {
924
+ ...this.settings.harness.builtinTools,
925
+ ...userTools,
926
+ } as HarnessAllTools<THarness, TUserTools>;
927
+ const toolFiltering = resolveHarnessAgentToolFiltering({
928
+ harness: this.settings.harness,
929
+ userTools,
930
+ allTools: tools,
931
+ activeTools: this.settings.activeTools,
932
+ inactiveTools: this.settings.inactiveTools,
933
+ });
934
+ return {
935
+ skills: options.skills ?? [],
936
+ instructions: options.instructions,
937
+ tools,
938
+ activeTools: toolFiltering.activeUserTools,
939
+ toolSpecs: this._toToolSpecs(toolFiltering.activeUserTools),
940
+ builtinToolFiltering: toolFiltering.builtinToolFiltering,
941
+ };
942
+ }
943
+
734
944
  /*
735
945
  * Wire-format projection of user-defined tools only. Harness builtins are
736
946
  * executed by the runtime and the bridge already knows about them — we
737
947
  * never re-declare them over the wire.
738
948
  */
739
- private _toToolSpecs(): HarnessAgentToolSpec[] {
949
+ private _toToolSpecs(activeUserTools: TUserTools): HarnessAgentToolSpec[] {
740
950
  const specs: HarnessAgentToolSpec[] = [];
741
951
  for (const [name, tool] of Object.entries(
742
- this.activeUserTools as Record<string, unknown>,
952
+ activeUserTools as Record<string, unknown>,
743
953
  )) {
744
954
  const t = tool as {
745
955
  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
  }
@@ -9,6 +9,7 @@ import {
9
9
  type HarnessV1PromptControl,
10
10
  type HarnessV1ResponseFormat,
11
11
  type HarnessV1Session,
12
+ type HarnessV1Skill,
12
13
  type HarnessV1StreamPart,
13
14
  type HarnessV1ToolSpec,
14
15
  } from '../../v1';
@@ -80,6 +81,7 @@ export function runPrompt<
80
81
  mode?: 'prompt' | 'continue';
81
82
  /** Required for `mode: 'prompt'`; absent for `mode: 'continue'`. */
82
83
  prompt?: HarnessV1Prompt;
84
+ skills?: ReadonlyArray<HarnessV1Skill>;
83
85
  instructions: string | undefined;
84
86
  tools: TOOLS;
85
87
  activeTools?: ToolSet;
@@ -179,6 +181,7 @@ export function runPrompt<
179
181
  input.mode === 'continue'
180
182
  ? emit =>
181
183
  input.session.doContinueTurn({
184
+ skills: input.skills ?? [],
182
185
  responseFormat: input.responseFormat,
183
186
  tools: input.toolSpecs,
184
187
  instructions: input.instructions,
@@ -193,6 +196,7 @@ export function runPrompt<
193
196
  }
194
197
  return input.session.doPromptTurn({
195
198
  prompt: input.prompt,
199
+ skills: input.skills ?? [],
196
200
  responseFormat: input.responseFormat,
197
201
  tools: input.toolSpecs,
198
202
  instructions: input.instructions,
@@ -734,12 +738,18 @@ export function runPrompt<
734
738
  // paths help debugging); the consumer-facing settle uses the
735
739
  // workDir-stripped one, like every other forwarded part.
736
740
  await telemetry.error(value.error);
737
- logBridgeError({
738
- harnessId: input.harness.harnessId,
739
- sessionId: input.session.sessionId,
740
- context: 'harness stream error',
741
- error: value.error,
742
- });
741
+ // A turn the caller itself aborted ends with an error-shaped part by
742
+ // construction; diagnosing the caller's own signal to stderr reads
743
+ // as a malfunction. `settleFailure` below still reports it as an
744
+ // abort to the consumer.
745
+ if (!input.abortSignal?.aborted) {
746
+ logBridgeError({
747
+ harnessId: input.harness.harnessId,
748
+ sessionId: input.session.sessionId,
749
+ context: 'harness stream error',
750
+ error: value.error,
751
+ });
752
+ }
743
753
  settleFailure(displayValue.error);
744
754
  return;
745
755
  }