@ai-sdk/harness 1.0.0-canary.3 → 1.0.0-canary.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ai-sdk/harness
2
2
 
3
+ ## 1.0.0-canary.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 3d9a50c: feat(harness): implement harness adapters for Claude Code, Codex, Pi
8
+
3
9
  ## 1.0.0-canary.3
4
10
 
5
11
  ### Patch Changes
@@ -62,7 +62,7 @@ interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
62
62
  /**
63
63
  * Stable identifier for the underlying sandbox resource. Used by the
64
64
  * harness session manager as the durable lookup key for cross-process
65
- * resume — the framework persists this on the resume payload so a future
65
+ * resume — the framework persists this on lifecycle state so a future
66
66
  * process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
67
67
  * reach the same resource. Providers populate it from their native
68
68
  * identifier (Vercel: the sandbox name; just-bash: a UUID minted at
@@ -298,19 +298,10 @@ type HarnessV1PendingToolApproval = {
298
298
  readonly providerExecuted?: boolean;
299
299
  readonly nativeName?: string;
300
300
  };
301
- /**
302
- * Opaque payload returned by resumable session lifecycle methods and accepted
303
- * by a future `HarnessV1.doStart({ resumeFrom })` to resume the same
304
- * underlying session.
305
- *
306
- * The contents are entirely adapter-defined. Consumers (including
307
- * `HarnessAgent`) treat the value as opaque; adapters describe and validate
308
- * their own schemas via `HarnessV1.resumeStateSchema`.
309
- */
310
- type HarnessV1ResumeState = {
301
+ type HarnessV1LifecycleStateBase = {
311
302
  /**
312
303
  * Identifier of the harness that produced this state. Used by adapters to
313
- * refuse mismatched resume payloads.
304
+ * refuse mismatched payloads.
314
305
  */
315
306
  readonly harnessId: string;
316
307
  /**
@@ -324,11 +315,27 @@ type HarnessV1ResumeState = {
324
315
  readonly data: JSONValue;
325
316
  /**
326
317
  * Framework-owned pending approval records. These are intentionally outside
327
- * adapter-defined `data` so callers can persist the entire resume payload
318
+ * adapter-defined `data` so callers can persist the entire lifecycle payload
328
319
  * without the harness framework owning storage.
329
320
  */
330
321
  readonly pendingToolApprovals?: readonly HarnessV1PendingToolApproval[];
331
322
  };
323
+ /**
324
+ * Opaque payload returned by between-turn session lifecycle methods and
325
+ * accepted by a future `HarnessV1.doStart({ resumeFrom })` to resume the same
326
+ * underlying session before starting a new turn.
327
+ */
328
+ type HarnessV1ResumeSessionState = HarnessV1LifecycleStateBase & {
329
+ readonly type: 'resume-session';
330
+ };
331
+ /**
332
+ * Opaque payload returned by `doSuspendTurn` and accepted by a future
333
+ * `HarnessV1.doStart({ continueFrom })` to reconnect to the same session before
334
+ * continuing the interrupted turn.
335
+ */
336
+ type HarnessV1ContinueTurnState = HarnessV1LifecycleStateBase & {
337
+ readonly type: 'continue-turn';
338
+ };
332
339
 
333
340
  /**
334
341
  * A self-contained instruction bundle the underlying runtime can load into
@@ -515,11 +522,17 @@ type HarnessV1StartOptions = {
515
522
  */
516
523
  readonly skills?: ReadonlyArray<HarnessV1Skill>;
517
524
  /**
518
- * Optional resume payload returned by a prior session lifecycle method. When
519
- * provided, the adapter should resume the existing session rather than create
520
- * a fresh one.
525
+ * Optional resume payload returned by a prior between-turn session lifecycle
526
+ * method. When provided, the adapter should resume the existing session before
527
+ * accepting a new prompt.
528
+ */
529
+ readonly resumeFrom?: HarnessV1ResumeSessionState;
530
+ /**
531
+ * Optional continuation payload returned by `doSuspendTurn`. When provided,
532
+ * the adapter should resume the existing session in a shape ready for
533
+ * `doContinueTurn` rather than for a fresh prompt.
521
534
  */
522
- readonly resumeFrom?: HarnessV1ResumeState;
535
+ readonly continueFrom?: HarnessV1ContinueTurnState;
523
536
  /**
524
537
  * Approval policy for built-in adapter-native tool use. Custom host-executed
525
538
  * tools are approved by the framework before results are submitted back to
@@ -631,8 +644,8 @@ type HarnessV1Session = {
631
644
  */
632
645
  readonly sessionId: string;
633
646
  /**
634
- * Whether this session was created from a resume payload. Fresh sessions
635
- * report `false`; resumed sessions report `true`.
647
+ * Whether this session was created from `resumeFrom` or `continueFrom`. Fresh
648
+ * sessions report `false`; resumed sessions report `true`.
636
649
  */
637
650
  readonly isResume: boolean;
638
651
  /**
@@ -664,7 +677,7 @@ type HarnessV1Session = {
664
677
  * Continue the in-flight turn **without a new user prompt**, returning the
665
678
  * same control surface as `doPromptTurn`. Used to keep consuming a turn that
666
679
  * was interrupted at a process boundary (the workflow slice loop), after the
667
- * session itself has been resumed via `doStart({ resumeFrom })`:
680
+ * session itself has been resumed via `doStart({ continueFrom })`:
668
681
  *
669
682
  * - When the runtime's turn is still live and reachable (bridge `attach` /
670
683
  * `replay`), the adapter subscribes to its events and resolves `done` on
@@ -680,7 +693,7 @@ type HarnessV1Session = {
680
693
  doContinueTurn(options: HarnessV1ContinueTurnOptions): PromiseLike<HarnessV1PromptControl>;
681
694
  /**
682
695
  * Gracefully freeze the active turn **at a precise cursor while keeping the
683
- * runtime alive**, returning the resume payload.
696
+ * runtime alive**, returning the continuation payload.
684
697
  *
685
698
  * This is the slice-boundary primitive. The adapter stops host-side
686
699
  * consumption of the in-flight turn without telling the runtime to stop:
@@ -697,24 +710,24 @@ type HarnessV1Session = {
697
710
  * this is for an active turn at a slice boundary rather than a between-turn
698
711
  * session handoff. Required on every adapter.
699
712
  */
700
- doSuspendTurn(): PromiseLike<HarnessV1ResumeState>;
713
+ doSuspendTurn(): PromiseLike<HarnessV1ContinueTurnState>;
701
714
  /**
702
715
  * Detach from the underlying runtime without tearing it down, returning a
703
- * payload the host can later pass to `HarnessV1.doStart({ resumeFrom })`
704
- * to reconnect. After `doDetach`, no further methods on this session
705
- * instance may be called.
716
+ * payload the host can later pass to
717
+ * `HarnessV1.doStart({ resumeFrom })` to reconnect before a new turn. After
718
+ * `doDetach`, no further methods on this session instance may be called.
706
719
  *
707
720
  * Required. Adapters that cannot keep a live runtime parked still return the
708
- * best resume state they can while leaving the sandbox running.
721
+ * best resume session state they can while leaving the sandbox running.
709
722
  */
710
- doDetach(): PromiseLike<HarnessV1ResumeState>;
723
+ doDetach(): PromiseLike<HarnessV1ResumeSessionState>;
711
724
  /**
712
725
  * Persist enough state to resume later, then stop the underlying runtime.
713
726
  * After `doStop`, no further methods on this session instance may be called.
714
727
  */
715
- doStop(): PromiseLike<HarnessV1ResumeState>;
728
+ doStop(): PromiseLike<HarnessV1ResumeSessionState>;
716
729
  /**
717
- * Stop the underlying runtime without returning resume state. After
730
+ * Stop the underlying runtime without returning lifecycle state. After
718
731
  * `doDestroy`, no further methods on this session instance may be called.
719
732
  */
720
733
  doDestroy(): PromiseLike<void>;
@@ -763,12 +776,12 @@ type HarnessV1<TBuiltinTools extends ToolSet = ToolSet> = {
763
776
  */
764
777
  readonly supportsBuiltinToolApprovals?: boolean;
765
778
  /**
766
- * Optional schema for resume payloads returned by the session lifecycle.
767
- * When present, the adapter promises that exported state validated by this
768
- * schema can be re-imported in a future `doStart({ resumeFrom })` call.
769
- * Hosts use this to persist and re-hydrate resume payloads safely.
779
+ * Optional schema for the adapter-defined `data` payload returned by session
780
+ * lifecycle methods. When present, the adapter promises that exported state
781
+ * validated by this schema can be re-imported in a future
782
+ * `doStart({ resumeFrom })` or `doStart({ continueFrom })` call.
770
783
  */
771
- readonly resumeStateSchema?: FlexibleSchema<unknown>;
784
+ readonly lifecycleStateSchema?: FlexibleSchema<unknown>;
772
785
  /**
773
786
  * Optional bootstrap recipe. When defined, the harness session manager
774
787
  * computes a stable identity from the recipe, passes it (along with a
@@ -784,9 +797,9 @@ type HarnessV1<TBuiltinTools extends ToolSet = ToolSet> = {
784
797
  abortSignal?: AbortSignal;
785
798
  }) => PromiseLike<HarnessV1Bootstrap>;
786
799
  /**
787
- * Start a fresh session (or resume via `resumeFrom`). The host then issues
788
- * prompts against the returned session, ending with `doDetach`, `doStop`, or
789
- * `doDestroy`.
800
+ * Start a fresh session, resume a parked session via `resumeFrom`, or resume
801
+ * an interrupted turn via `continueFrom`. The host then issues prompts against
802
+ * the returned session, ending with `doDetach`, `doStop`, or `doDestroy`.
790
803
  */
791
804
  doStart(options: HarnessV1StartOptions): PromiseLike<HarnessV1Session>;
792
805
  };
@@ -1261,8 +1274,8 @@ declare class HarnessAgentSession {
1261
1274
  private readonly pendingToolApprovals;
1262
1275
  private stopped;
1263
1276
  /**
1264
- * Whether this session was created from a resume payload. Captured at
1265
- * construction so it survives lifecycle cleanup.
1277
+ * Whether this session was created from `resumeFrom` or `continueFrom`.
1278
+ * Captured at construction so it survives lifecycle cleanup.
1266
1279
  */
1267
1280
  readonly isResume: boolean;
1268
1281
  constructor(options: {
@@ -1311,13 +1324,13 @@ declare class HarnessAgentSession {
1311
1324
  * The runtime and sandbox keep running; this local session handle becomes
1312
1325
  * unusable.
1313
1326
  */
1314
- detach(): Promise<HarnessV1ResumeState>;
1327
+ detach(): Promise<HarnessV1ResumeSessionState>;
1315
1328
  /**
1316
1329
  * Persist enough state to resume later, then stop the runtime and sandbox.
1317
1330
  * Returns the resume state for a future
1318
1331
  * `agent.createSession({ sessionId, resumeFrom })` call.
1319
1332
  */
1320
- stop(): Promise<HarnessV1ResumeState>;
1333
+ stop(): Promise<HarnessV1ResumeSessionState>;
1321
1334
  /**
1322
1335
  * Stop the runtime and discard resumability. The sandbox is destroyed when
1323
1336
  * the provider supports destruction; otherwise it is stopped.
@@ -1325,8 +1338,8 @@ declare class HarnessAgentSession {
1325
1338
  destroy(): Promise<void>;
1326
1339
  /**
1327
1340
  * Gracefully freeze the active turn at the slice boundary and return the
1328
- * resume payload, **leaving the sandbox/runtime running** so the next process
1329
- * can resume. Resolves once the in-flight `stream()`/`continueTurn()`
1341
+ * continuation payload, **leaving the sandbox/runtime running** so the next
1342
+ * process can continue. Resolves once the in-flight `stream()`/`continueTurn()`
1330
1343
  * has cleanly wound down at a precise cursor (see
1331
1344
  * {@link HarnessV1Session.doSuspendTurn}).
1332
1345
  *
@@ -1336,7 +1349,7 @@ declare class HarnessAgentSession {
1336
1349
  * released, because bridge-backed adapters may still have a live bridge on
1337
1350
  * that port.
1338
1351
  */
1339
- suspendTurn(): Promise<HarnessV1ResumeState>;
1352
+ suspendTurn(): Promise<HarnessV1ContinueTurnState>;
1340
1353
  private getPendingToolApprovals;
1341
1354
  private withPendingToolApprovals;
1342
1355
  private endLocalHandle;
@@ -1381,7 +1394,9 @@ interface HarnessAgentCallExtensions {
1381
1394
  * - **Cross-process resume.** `createSession({ sessionId, resumeFrom })`
1382
1395
  * resumes from state previously returned by `session.detach()` or
1383
1396
  * `session.stop()`. The framework validates `resumeFrom` against the
1384
- * harness's `resumeStateSchema` before handing it to the adapter.
1397
+ * harness's `lifecycleStateSchema` before handing it to the adapter.
1398
+ * `createSession({ sessionId, continueFrom })` resumes from state returned
1399
+ * by `session.suspendTurn()` before `continueTurn()`.
1385
1400
  * - **Host tool execution.** User tools passed in `settings.tools` are
1386
1401
  * executed on the host whenever the underlying runtime calls them;
1387
1402
  * the result is fed back to the harness via `submitToolResult`.
@@ -1427,12 +1442,17 @@ declare class HarnessAgent<THarness extends HarnessV1<any> = HarnessV1, TUserToo
1427
1442
  sessionId?: string;
1428
1443
  /**
1429
1444
  * Resume payload returned by a prior `session.detach()` or
1430
- * `session.stop()`. Must be
1431
- * accompanied by the original `sessionId`; the framework
1432
- * validates it against `harness.resumeStateSchema` before handing
1433
- * it to the adapter.
1445
+ * `session.stop()`. Must be accompanied by the original `sessionId`; the
1446
+ * framework validates it against `harness.lifecycleStateSchema` before
1447
+ * handing it to the adapter.
1448
+ */
1449
+ resumeFrom?: HarnessV1ResumeSessionState;
1450
+ /**
1451
+ * Continuation payload returned by a prior `session.suspendTurn()`. Must be
1452
+ * accompanied by the original `sessionId`; the framework validates it before
1453
+ * handing it to the adapter.
1434
1454
  */
1435
- resumeFrom?: HarnessV1ResumeState;
1455
+ continueFrom?: HarnessV1ContinueTurnState;
1436
1456
  abortSignal?: AbortSignal;
1437
1457
  }): Promise<HarnessAgentSession>;
1438
1458
  generate(options: AgentCallParameters<never, HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT> & HarnessAgentCallExtensions): Promise<GenerateTextResult<HarnessAllTools<THarness, TUserTools>, RUNTIME_CONTEXT, never>>;
@@ -71,34 +71,40 @@ function releaseBridgePort(options) {
71
71
  entry.leases.delete(options.sessionId);
72
72
  }
73
73
 
74
- // src/agent/internal/resume-state-validation.ts
74
+ // src/agent/internal/lifecycle-state-validation.ts
75
75
  import { safeValidateTypes } from "@ai-sdk/provider-utils";
76
- async function validateResumeStateData(input) {
76
+ async function validateLifecycleStateData(input) {
77
77
  const { harness, state } = input;
78
+ if (state.type !== input.expectedType) {
79
+ throw new HarnessError({
80
+ message: `Lifecycle state has unexpected type '${state.type}'; expected '${input.expectedType}'.`
81
+ });
82
+ }
78
83
  if (state.specificationVersion !== "harness-v1") {
79
84
  throw new HarnessError({
80
- message: `Resume state has unexpected specificationVersion '${state.specificationVersion}'; expected 'harness-v1'.`
85
+ message: `Lifecycle state has unexpected specificationVersion '${state.specificationVersion}'; expected 'harness-v1'.`
81
86
  });
82
87
  }
83
88
  if (state.harnessId !== harness.harnessId) {
84
89
  throw new HarnessError({
85
- message: `Resume state was produced by harness '${state.harnessId}' but this agent uses '${harness.harnessId}'.`
90
+ message: `Lifecycle state was produced by harness '${state.harnessId}' but this agent uses '${harness.harnessId}'.`
86
91
  });
87
92
  }
88
- if (harness.resumeStateSchema == null) {
93
+ if (harness.lifecycleStateSchema == null) {
89
94
  return state;
90
95
  }
91
96
  const result = await safeValidateTypes({
92
97
  value: state.data,
93
- schema: harness.resumeStateSchema
98
+ schema: harness.lifecycleStateSchema
94
99
  });
95
100
  if (!result.success) {
96
101
  throw new HarnessError({
97
- message: `Resume state failed schema validation for harness '${harness.harnessId}': ${result.error.message}`,
102
+ message: `Lifecycle state failed schema validation for harness '${harness.harnessId}': ${result.error.message}`,
98
103
  cause: result.error
99
104
  });
100
105
  }
101
106
  return {
107
+ type: state.type,
102
108
  harnessId: state.harnessId,
103
109
  specificationVersion: state.specificationVersion,
104
110
  data: result.value,
@@ -1796,9 +1802,10 @@ var HarnessAgentSession = class {
1796
1802
  const session = this.underlyingSession;
1797
1803
  try {
1798
1804
  const raw = await session.doDetach();
1799
- const validated = await validateResumeStateData({
1805
+ const validated = await validateLifecycleStateData({
1800
1806
  harness: this.harness,
1801
- state: raw
1807
+ state: raw,
1808
+ expectedType: "resume-session"
1802
1809
  });
1803
1810
  return this.withPendingToolApprovals(validated);
1804
1811
  } finally {
@@ -1820,9 +1827,10 @@ var HarnessAgentSession = class {
1820
1827
  const sandboxSession = this.getSandboxSession();
1821
1828
  try {
1822
1829
  const raw = await session.doStop();
1823
- const validated = await validateResumeStateData({
1830
+ const validated = await validateLifecycleStateData({
1824
1831
  harness: this.harness,
1825
- state: raw
1832
+ state: raw,
1833
+ expectedType: "resume-session"
1826
1834
  });
1827
1835
  return this.withPendingToolApprovals(validated);
1828
1836
  } finally {
@@ -1852,8 +1860,8 @@ var HarnessAgentSession = class {
1852
1860
  }
1853
1861
  /**
1854
1862
  * Gracefully freeze the active turn at the slice boundary and return the
1855
- * resume payload, **leaving the sandbox/runtime running** so the next process
1856
- * can resume. Resolves once the in-flight `stream()`/`continueTurn()`
1863
+ * continuation payload, **leaving the sandbox/runtime running** so the next
1864
+ * process can continue. Resolves once the in-flight `stream()`/`continueTurn()`
1857
1865
  * has cleanly wound down at a precise cursor (see
1858
1866
  * {@link HarnessV1Session.doSuspendTurn}).
1859
1867
  *
@@ -1871,9 +1879,10 @@ var HarnessAgentSession = class {
1871
1879
  }
1872
1880
  const session = this.underlyingSession;
1873
1881
  const raw = await session.doSuspendTurn();
1874
- const validated = await validateResumeStateData({
1882
+ const validated = await validateLifecycleStateData({
1875
1883
  harness: this.harness,
1876
- state: raw
1884
+ state: raw,
1885
+ expectedType: "continue-turn"
1877
1886
  });
1878
1887
  this.stopped = true;
1879
1888
  this.underlyingSession = void 0;
@@ -1887,6 +1896,7 @@ var HarnessAgentSession = class {
1887
1896
  const pendingToolApprovals = this.getPendingToolApprovals();
1888
1897
  if (pendingToolApprovals.length === 0) {
1889
1898
  return {
1899
+ type: state.type,
1890
1900
  harnessId: state.harnessId,
1891
1901
  specificationVersion: state.specificationVersion,
1892
1902
  data: state.data
@@ -2159,17 +2169,32 @@ var HarnessAgent = class {
2159
2169
  * `session.destroy()`.
2160
2170
  */
2161
2171
  async createSession(options) {
2162
- var _a3;
2172
+ var _a3, _b3;
2163
2173
  const sessionId = (_a3 = options == null ? void 0 : options.sessionId) != null ? _a3 : generateId5();
2164
2174
  const resumeFrom = options == null ? void 0 : options.resumeFrom;
2175
+ const continueFrom = options == null ? void 0 : options.continueFrom;
2165
2176
  const abortSignal = options == null ? void 0 : options.abortSignal;
2166
2177
  const harness = this.settings.harness;
2167
2178
  const sandboxProvider = this.settings.sandbox;
2179
+ if (resumeFrom != null && continueFrom != null) {
2180
+ throw new Error(
2181
+ "HarnessAgent.createSession: pass either `resumeFrom` or `continueFrom`, not both."
2182
+ );
2183
+ }
2168
2184
  let validatedResumeFrom;
2169
2185
  if (resumeFrom != null) {
2170
- validatedResumeFrom = await validateResumeStateData({
2186
+ validatedResumeFrom = await validateLifecycleStateData({
2187
+ harness,
2188
+ state: resumeFrom,
2189
+ expectedType: "resume-session"
2190
+ });
2191
+ }
2192
+ let validatedContinueFrom;
2193
+ if (continueFrom != null) {
2194
+ validatedContinueFrom = await validateLifecycleStateData({
2171
2195
  harness,
2172
- state: resumeFrom
2196
+ state: continueFrom,
2197
+ expectedType: "continue-turn"
2173
2198
  });
2174
2199
  }
2175
2200
  let recipe;
@@ -2181,7 +2206,7 @@ var HarnessAgent = class {
2181
2206
  const acquiredSandboxSession = await this._acquireSandbox({
2182
2207
  sandboxProvider,
2183
2208
  sessionId,
2184
- isResume: validatedResumeFrom != null,
2209
+ isResume: validatedResumeFrom != null || validatedContinueFrom != null,
2185
2210
  recipe,
2186
2211
  identity,
2187
2212
  abortSignal
@@ -2228,6 +2253,7 @@ var HarnessAgent = class {
2228
2253
  sessionId,
2229
2254
  skills: this.settings.skills,
2230
2255
  resumeFrom: validatedResumeFrom,
2256
+ continueFrom: validatedContinueFrom,
2231
2257
  permissionMode: this.permissionMode,
2232
2258
  abortSignal,
2233
2259
  observability: buildObservability({ settings: this.settings })
@@ -2246,7 +2272,7 @@ var HarnessAgent = class {
2246
2272
  leasedBridgePort,
2247
2273
  sessionWorkDir,
2248
2274
  toolApproval: this.settings.toolApproval,
2249
- pendingToolApprovals: validatedResumeFrom == null ? void 0 : validatedResumeFrom.pendingToolApprovals
2275
+ pendingToolApprovals: (_b3 = validatedResumeFrom == null ? void 0 : validatedResumeFrom.pendingToolApprovals) != null ? _b3 : validatedContinueFrom == null ? void 0 : validatedContinueFrom.pendingToolApprovals
2250
2276
  });
2251
2277
  } catch (error) {
2252
2278
  await cleanupAfterStartFailure({