@ai-sdk/harness 1.0.132 → 1.0.133

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness",
3
- "version": "1.0.132",
3
+ "version": "1.0.133",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -17,6 +17,7 @@ import type {
17
17
  HarnessV1BuiltinToolFiltering,
18
18
  HarnessV1NetworkSandboxSession,
19
19
  HarnessV1PromptControl,
20
+ HarnessV1ReadHistoryResult,
20
21
  HarnessV1ResponseFormat,
21
22
  HarnessV1Skill,
22
23
  HarnessV1TurnSettings,
@@ -482,22 +483,53 @@ export class HarnessAgentSession {
482
483
  );
483
484
  }
484
485
  const session = this.underlyingSession;
485
- try {
486
- if (this.turnState !== 'idle') {
487
- return this.toResumeStateWithContinuation({
488
- continueFrom: await this.finalizeCurrentTurnSuspension({ session }),
489
- });
490
- }
491
- const raw = await session.doDetach();
492
- const validated = await validateLifecycleStateData({
493
- harness: this.harness,
494
- state: raw,
495
- expectedType: 'resume-session',
486
+ const state =
487
+ this.turnState !== 'idle'
488
+ ? this.toResumeStateWithContinuation({
489
+ continueFrom: await this.finalizeCurrentTurnSuspension({ session }),
490
+ })
491
+ : await validateLifecycleStateData({
492
+ harness: this.harness,
493
+ state: await session.doDetach(),
494
+ expectedType: 'resume-session',
495
+ });
496
+ this.endLocalHandle({ sessionState: 'detached' });
497
+ return state;
498
+ }
499
+
500
+ /**
501
+ * Read the conversation history the runtime itself persisted, normalized
502
+ * by the adapter. Includes exchanges that happened outside this process —
503
+ * the same conversation continued interactively in the agent's own CLI,
504
+ * for instance — which the live event stream never saw.
505
+ *
506
+ * Pass a previous result's `cursor` as `since` to read only the delta. A
507
+ * conversation with no recorded messages yet resolves to an empty
508
+ * `messages` array.
509
+ *
510
+ * Throws `HarnessCapabilityUnsupportedError` when the adapter does not
511
+ * implement history reads, and
512
+ * `HarnessHistoryUnavailableError` when the adapter supports them but
513
+ * cannot reach the runtime's store from this environment.
514
+ */
515
+ async readHistory(options?: {
516
+ since?: string;
517
+ }): Promise<HarnessV1ReadHistoryResult> {
518
+ if (this.sessionState !== 'active' || this.underlyingSession == null) {
519
+ throw new Error(
520
+ `Harness session ${this.sessionId} is not active and cannot read history.`,
521
+ );
522
+ }
523
+ const session = this.underlyingSession;
524
+ if (typeof session.doReadHistory !== 'function') {
525
+ throw new HarnessCapabilityUnsupportedError({
526
+ harnessId: this.harness.harnessId,
527
+ message: `Harness '${this.harness.harnessId}' does not support reading the runtime's conversation history.`,
496
528
  });
497
- return validated;
498
- } finally {
499
- this.endLocalHandle({ sessionState: 'detached' });
500
529
  }
530
+ return await session.doReadHistory({
531
+ ...(options?.since != null ? { since: options.since } : {}),
532
+ });
501
533
  }
502
534
 
503
535
  /**
@@ -366,34 +366,47 @@ export function runPrompt<
366
366
  pendingStopBoundary = undefined;
367
367
  };
368
368
 
369
- // Accumulate the model response until its step boundary. Harness runtimes
370
- // may execute tools before emitting `finish-step`, so tool lifecycle
371
- // notifications and consumer-visible tool outcomes are held until then.
369
+ // Accumulate the model response until its step boundary. Tool lifecycle
370
+ // notifications and consumer-visible tool outcomes are published as each
371
+ // tool runs; `finish-step` remains the boundary for step accounting.
372
372
  let stepText = '';
373
373
  let stepReasoning = '';
374
374
  let stepToolCalls: ContentPart<TOOLS>[] = [];
375
375
  let stepProviderToolResults: ContentPart<TOOLS>[] = [];
376
376
  let stepApprovalRequests: ContentPart<TOOLS>[] = [];
377
377
  let bufferedToolOutcomes: Array<() => void> = [];
378
- const toolExecutions = new Map<
379
- string,
380
- {
381
- toolCall: TypedToolCall<TOOLS>;
382
- toolOutput?: TypedToolResult<TOOLS> | TypedToolError<TOOLS>;
383
- toolExecutionMs?: number;
384
- }
385
- >();
378
+ type ToolExecutionState = {
379
+ toolCall: TypedToolCall<TOOLS>;
380
+ toolOutput?: TypedToolResult<TOOLS> | TypedToolError<TOOLS>;
381
+ toolExecutionMs?: number;
382
+ executionStartedAt?: number;
383
+ startNotification?: Promise<void>;
384
+ endNotification?: Promise<void>;
385
+ };
386
+ const toolExecutions = new Map<string, ToolExecutionState>();
387
+ const publishToolExecutionStart = async (
388
+ execution: ToolExecutionState,
389
+ ): Promise<void> => {
390
+ execution.startNotification ??= lifecycle.toolExecutionStart({
391
+ toolCall: execution.toolCall,
392
+ });
393
+ await execution.startNotification;
394
+ };
395
+ const publishToolExecutionEnd = async (
396
+ execution: ToolExecutionState,
397
+ ): Promise<void> => {
398
+ if (execution.toolOutput == null) return;
399
+ await publishToolExecutionStart(execution);
400
+ execution.endNotification ??= lifecycle.toolExecutionEnd({
401
+ toolCall: execution.toolCall,
402
+ toolOutput: execution.toolOutput,
403
+ toolExecutionMs: execution.toolExecutionMs ?? 0,
404
+ });
405
+ await execution.endNotification;
406
+ };
386
407
  const publishToolExecutions = async (): Promise<void> => {
387
408
  for (const execution of toolExecutions.values()) {
388
- if (execution.toolOutput == null) continue;
389
- await lifecycle.toolExecutionStart({
390
- toolCall: execution.toolCall,
391
- });
392
- await lifecycle.toolExecutionEnd({
393
- toolCall: execution.toolCall,
394
- toolOutput: execution.toolOutput,
395
- toolExecutionMs: execution.toolExecutionMs ?? 0,
396
- });
409
+ await publishToolExecutionEnd(execution);
397
410
  }
398
411
  for (const publish of bufferedToolOutcomes) publish();
399
412
  toolExecutions.clear();
@@ -749,6 +762,9 @@ export function runPrompt<
749
762
  toolExecutions.set(rawToolCall.toolCallId, {
750
763
  toolCall: toolCall as TypedToolCall<TOOLS>,
751
764
  });
765
+ await publishToolExecutionStart(
766
+ toolExecutions.get(rawToolCall.toolCallId)!,
767
+ );
752
768
  const executionStartedAt = Date.now();
753
769
  const execution = await maybeExecuteHostTool({
754
770
  event: rawToolCall,
@@ -772,16 +788,14 @@ export function runPrompt<
772
788
  },
773
789
  input.sessionWorkDir,
774
790
  ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
775
- bufferedToolOutcomes.push(() => {
776
- result.enqueue({
777
- type: 'tool-result',
778
- toolCallId: rawToolCall.toolCallId,
779
- toolName: rawToolCall.toolName,
780
- input: undefined,
781
- output: stripped.result,
782
- preliminary: true,
783
- } as TextStreamPart<TOOLS>);
784
- });
791
+ result.enqueue({
792
+ type: 'tool-result',
793
+ toolCallId: rawToolCall.toolCallId,
794
+ toolName: rawToolCall.toolName,
795
+ input: undefined,
796
+ output: stripped.result,
797
+ preliminary: true,
798
+ } as TextStreamPart<TOOLS>);
785
799
  },
786
800
  });
787
801
  if (!execution.executed) {
@@ -795,13 +809,11 @@ export function runPrompt<
795
809
  outcome: execution.outcome,
796
810
  });
797
811
  toolExecution.toolExecutionMs = Date.now() - executionStartedAt;
798
- bufferedToolOutcomes.push(() => {
799
- enqueueHostToolOutcome({
800
- toolCall,
801
- outcome: execution.outcome,
802
- });
812
+ await publishToolExecutionEnd(toolExecution);
813
+ enqueueHostToolOutcome({
814
+ toolCall,
815
+ outcome: execution.outcome,
803
816
  });
804
- await publishToolExecutions();
805
817
  return 'continued';
806
818
  };
807
819
 
@@ -1007,11 +1019,7 @@ export function runPrompt<
1007
1019
  displayValue,
1008
1020
  translateOptions,
1009
1021
  );
1010
- if (value.type === 'tool-result') {
1011
- bufferedToolOutcomes.push(() => {
1012
- for (const part of translatedParts) result.enqueue(part);
1013
- });
1014
- } else {
1022
+ if (value.type !== 'tool-result') {
1015
1023
  for (const part of translatedParts) result.enqueue(part);
1016
1024
  }
1017
1025
 
@@ -1059,9 +1067,13 @@ export function runPrompt<
1059
1067
  const toolCall = toolCallsByToolCallId.get(value.toolCallId);
1060
1068
  if (toolCall != null) {
1061
1069
  stepToolCalls.push(toolCall as ContentPart<TOOLS>);
1062
- toolExecutions.set(value.toolCallId, {
1070
+ const execution: ToolExecutionState = {
1063
1071
  toolCall: toolCall as TypedToolCall<TOOLS>,
1064
- });
1072
+ };
1073
+ toolExecutions.set(value.toolCallId, execution);
1074
+ if (value.providerExecuted === true) {
1075
+ await publishToolExecutionStart(execution);
1076
+ }
1065
1077
  }
1066
1078
  }
1067
1079
 
@@ -1080,6 +1092,13 @@ export function runPrompt<
1080
1092
  output: value.result,
1081
1093
  } as TypedToolResult<TOOLS>);
1082
1094
  }
1095
+ if (
1096
+ execution?.toolExecutionMs == null &&
1097
+ execution?.executionStartedAt != null
1098
+ ) {
1099
+ execution.toolExecutionMs =
1100
+ Date.now() - execution.executionStartedAt;
1101
+ }
1083
1102
  if (
1084
1103
  rawToolCallsByToolCallId.get(value.toolCallId)?.providerExecuted ===
1085
1104
  true &&
@@ -1089,6 +1108,10 @@ export function runPrompt<
1089
1108
  execution.toolOutput as ContentPart<TOOLS>,
1090
1109
  );
1091
1110
  }
1111
+ if (execution != null) {
1112
+ await publishToolExecutionEnd(execution);
1113
+ }
1114
+ for (const part of translatedParts) result.enqueue(part);
1092
1115
  }
1093
1116
 
1094
1117
  if (value.type === 'tool-approval-request') {
@@ -1267,16 +1290,20 @@ export function runPrompt<
1267
1290
  type: 'execution-denied',
1268
1291
  reason: customToolApprovalDecision.reason,
1269
1292
  };
1293
+ const execution = toolExecutions.get(toolCall.toolCallId);
1294
+ if (execution != null) {
1295
+ await publishToolExecutionStart(execution);
1296
+ }
1270
1297
  await submitToolResult({
1271
1298
  toolCallId: toolCall.toolCallId,
1272
1299
  output,
1273
1300
  });
1274
- const execution = toolExecutions.get(toolCall.toolCallId);
1275
1301
  if (execution != null) {
1276
1302
  execution.toolOutput = toToolOutput({
1277
1303
  toolCall: parsedToolCall,
1278
1304
  outcome: { ok: true, output },
1279
1305
  });
1306
+ await publishToolExecutionEnd(execution);
1280
1307
  }
1281
1308
  continue;
1282
1309
  }
@@ -1353,7 +1380,15 @@ export function runPrompt<
1353
1380
  }
1354
1381
  startHostToolExecution(
1355
1382
  (async () => {
1383
+ const toolExecution = toolExecutions.get(toolCall.toolCallId);
1384
+ if (toolExecution == null) {
1385
+ throw new Error(
1386
+ `Harness '${input.harness.harnessId}' could not track host tool '${toolCall.toolName}'.`,
1387
+ );
1388
+ }
1389
+ await publishToolExecutionStart(toolExecution);
1356
1390
  const executionStartedAt = Date.now();
1391
+ toolExecution.executionStartedAt = executionStartedAt;
1357
1392
  const execution = await maybeExecuteHostTool({
1358
1393
  event: toolCall,
1359
1394
  parsedToolCall: validatedHostToolCall,
@@ -1384,16 +1419,14 @@ export function runPrompt<
1384
1419
  },
1385
1420
  input.sessionWorkDir,
1386
1421
  ) as Extract<HarnessV1StreamPart, { type: 'tool-result' }>;
1387
- bufferedToolOutcomes.push(() => {
1388
- result.enqueue({
1389
- type: 'tool-result',
1390
- toolCallId: toolCall.toolCallId,
1391
- toolName: toolCall.toolName,
1392
- input: undefined,
1393
- output: stripped.result,
1394
- preliminary: true,
1395
- } as TextStreamPart<TOOLS>);
1396
- });
1422
+ result.enqueue({
1423
+ type: 'tool-result',
1424
+ toolCallId: toolCall.toolCallId,
1425
+ toolName: toolCall.toolName,
1426
+ input: undefined,
1427
+ output: stripped.result,
1428
+ preliminary: true,
1429
+ } as TextStreamPart<TOOLS>);
1397
1430
  },
1398
1431
  });
1399
1432
  if (!execution.executed) {
@@ -1401,14 +1434,12 @@ export function runPrompt<
1401
1434
  `Harness '${input.harness.harnessId}' could not execute host tool '${toolCall.toolName}'.`,
1402
1435
  );
1403
1436
  }
1404
- const toolExecution = toolExecutions.get(toolCall.toolCallId);
1405
- if (toolExecution != null) {
1406
- toolExecution.toolOutput = toToolOutput({
1407
- toolCall: parsedToolCall,
1408
- outcome: execution.outcome,
1409
- });
1410
- toolExecution.toolExecutionMs = Date.now() - executionStartedAt;
1411
- }
1437
+ toolExecution.toolOutput = toToolOutput({
1438
+ toolCall: parsedToolCall,
1439
+ outcome: execution.outcome,
1440
+ });
1441
+ toolExecution.toolExecutionMs = Date.now() - executionStartedAt;
1442
+ await publishToolExecutionEnd(toolExecution);
1412
1443
  })(),
1413
1444
  );
1414
1445
  }
@@ -0,0 +1,41 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+ import { HarnessError } from './harness-error';
3
+
4
+ const name = 'AI_HarnessHistoryUnavailableError';
5
+ const marker = `vercel.ai.error.${name}`;
6
+ const symbol = Symbol.for(marker);
7
+
8
+ /**
9
+ * Thrown by `readHistory` when the adapter supports history reads but cannot
10
+ * reach the runtime's store from the current environment — for example the
11
+ * store lives inside a remote sandbox, or the transcript directory is
12
+ * missing or unreadable.
13
+ *
14
+ * Distinct from `HarnessCapabilityUnsupportedError` (the adapter does not
15
+ * implement history reads at all) so hosts can retry or degrade differently.
16
+ * A conversation with no recorded messages yet is not an error; it resolves
17
+ * to an empty result instead.
18
+ */
19
+ export class HarnessHistoryUnavailableError extends HarnessError {
20
+ private readonly [symbol] = true;
21
+
22
+ readonly harnessId?: string;
23
+
24
+ constructor({
25
+ message,
26
+ harnessId,
27
+ cause,
28
+ }: {
29
+ message: string;
30
+ harnessId?: string;
31
+ cause?: unknown;
32
+ }) {
33
+ super({ message, cause });
34
+ Object.defineProperty(this, 'name', { value: name });
35
+ this.harnessId = harnessId;
36
+ }
37
+
38
+ static isInstance(error: unknown): error is HarnessHistoryUnavailableError {
39
+ return AISDKError.hasMarker(error, marker);
40
+ }
41
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './v1';
2
2
  export * from './errors/harness-error';
3
3
  export * from './errors/harness-capability-unsupported-error';
4
+ export * from './errors/harness-history-unavailable-error';
4
5
  export * from './errors/harness-sandbox-authentication-error';
@@ -28,14 +28,16 @@ export async function applyCredentialForwarding({
28
28
  return forwardedEnvironment;
29
29
  }
30
30
 
31
- export async function createSandboxCredentialEnvironment({
31
+ export async function resolveSandboxCredentialEnvironment({
32
32
  environment,
33
33
  credentialEnvironmentVariables,
34
34
  credentialForwarding,
35
+ previousSandboxCredentialEnvironment,
35
36
  }: {
36
37
  environment: Readonly<Record<string, string>>;
37
38
  credentialEnvironmentVariables: ReadonlyArray<string>;
38
39
  credentialForwarding: HarnessV1CredentialForwarding | undefined;
40
+ previousSandboxCredentialEnvironment?: Readonly<Record<string, string>>;
39
41
  }): Promise<Record<string, string>> {
40
42
  const sandboxCredentialEnvironment: Record<string, string> = {};
41
43
 
@@ -44,6 +46,18 @@ export async function createSandboxCredentialEnvironment({
44
46
  )) {
45
47
  if (environment[environmentVariableName] == null) continue;
46
48
 
49
+ if (
50
+ previousSandboxCredentialEnvironment != null &&
51
+ Object.prototype.hasOwnProperty.call(
52
+ previousSandboxCredentialEnvironment,
53
+ environmentVariableName,
54
+ )
55
+ ) {
56
+ sandboxCredentialEnvironment[environmentVariableName] =
57
+ previousSandboxCredentialEnvironment[environmentVariableName];
58
+ continue;
59
+ }
60
+
47
61
  const placeholder = generateSandboxCredentialPlaceholder();
48
62
  sandboxCredentialEnvironment[environmentVariableName] =
49
63
  credentialForwarding == null
@@ -28,7 +28,7 @@ export {
28
28
  export { isLinux, isMacOS, isWindows } from './os';
29
29
  export {
30
30
  applyCredentialForwarding,
31
- createSandboxCredentialEnvironment,
31
+ resolveSandboxCredentialEnvironment,
32
32
  } from './credential-forwarding';
33
33
  export {
34
34
  createCredentialRequestTransformation,
@@ -0,0 +1,150 @@
1
+ import type {
2
+ LanguageModelV4CustomPart,
3
+ LanguageModelV4FilePart,
4
+ LanguageModelV4ReasoningFilePart,
5
+ LanguageModelV4ReasoningPart,
6
+ LanguageModelV4TextPart,
7
+ LanguageModelV4ToolApprovalResponsePart,
8
+ LanguageModelV4ToolCallPart,
9
+ LanguageModelV4ToolResultOutput,
10
+ LanguageModelV4ToolResultPart,
11
+ } from '@ai-sdk/provider';
12
+ import type { HarnessV1Metadata } from './harness-v1-metadata';
13
+
14
+ type LanguageModelV4ToolResultContentPart = Extract<
15
+ LanguageModelV4ToolResultOutput,
16
+ { type: 'content' }
17
+ >['value'][number];
18
+
19
+ export type HarnessV1TextPart = Omit<
20
+ LanguageModelV4TextPart,
21
+ 'providerOptions'
22
+ >;
23
+
24
+ export type HarnessV1FilePart = Omit<
25
+ LanguageModelV4FilePart,
26
+ 'providerOptions'
27
+ >;
28
+
29
+ export type HarnessV1CustomPart = Omit<
30
+ LanguageModelV4CustomPart,
31
+ 'providerOptions'
32
+ >;
33
+
34
+ export type HarnessV1ReasoningPart = Omit<
35
+ LanguageModelV4ReasoningPart,
36
+ 'providerOptions'
37
+ >;
38
+
39
+ export type HarnessV1ReasoningFilePart = Omit<
40
+ LanguageModelV4ReasoningFilePart,
41
+ 'providerOptions'
42
+ >;
43
+
44
+ export type HarnessV1ToolCallPart = Omit<
45
+ LanguageModelV4ToolCallPart,
46
+ 'providerOptions'
47
+ > & {
48
+ nativeName?: string;
49
+ };
50
+
51
+ export type HarnessV1ToolResultOutput =
52
+ | Omit<
53
+ Extract<LanguageModelV4ToolResultOutput, { type: 'text' }>,
54
+ 'providerOptions'
55
+ >
56
+ | Omit<
57
+ Extract<LanguageModelV4ToolResultOutput, { type: 'json' }>,
58
+ 'providerOptions'
59
+ >
60
+ | Omit<
61
+ Extract<LanguageModelV4ToolResultOutput, { type: 'execution-denied' }>,
62
+ 'providerOptions'
63
+ >
64
+ | Omit<
65
+ Extract<LanguageModelV4ToolResultOutput, { type: 'error-text' }>,
66
+ 'providerOptions'
67
+ >
68
+ | Omit<
69
+ Extract<LanguageModelV4ToolResultOutput, { type: 'error-json' }>,
70
+ 'providerOptions'
71
+ >
72
+ | {
73
+ type: 'content';
74
+ value: Array<
75
+ | Omit<
76
+ Extract<LanguageModelV4ToolResultContentPart, { type: 'text' }>,
77
+ 'providerOptions'
78
+ >
79
+ | Omit<
80
+ Extract<LanguageModelV4ToolResultContentPart, { type: 'file' }>,
81
+ 'providerOptions'
82
+ >
83
+ | Omit<
84
+ Extract<LanguageModelV4ToolResultContentPart, { type: 'custom' }>,
85
+ 'providerOptions'
86
+ >
87
+ >;
88
+ };
89
+
90
+ export type HarnessV1ToolResultPart = Omit<
91
+ LanguageModelV4ToolResultPart,
92
+ 'output' | 'providerOptions'
93
+ > & {
94
+ output: HarnessV1ToolResultOutput;
95
+ };
96
+
97
+ export type HarnessV1ToolApprovalResponsePart = Omit<
98
+ LanguageModelV4ToolApprovalResponsePart,
99
+ 'providerOptions'
100
+ >;
101
+
102
+ export type HarnessV1MessagePart =
103
+ | HarnessV1TextPart
104
+ | HarnessV1FilePart
105
+ | HarnessV1CustomPart
106
+ | HarnessV1ReasoningPart
107
+ | HarnessV1ReasoningFilePart
108
+ | HarnessV1ToolCallPart
109
+ | HarnessV1ToolResultPart
110
+ | HarnessV1ToolApprovalResponsePart;
111
+
112
+ export type HarnessV1UserMessage = {
113
+ readonly role: 'user';
114
+ readonly content: Array<HarnessV1TextPart | HarnessV1FilePart>;
115
+ readonly at?: string;
116
+ readonly harnessMetadata?: HarnessV1Metadata;
117
+ };
118
+
119
+ export type HarnessV1AssistantMessage = {
120
+ readonly role: 'assistant';
121
+ readonly content: Array<
122
+ | HarnessV1TextPart
123
+ | HarnessV1FilePart
124
+ | HarnessV1CustomPart
125
+ | HarnessV1ReasoningPart
126
+ | HarnessV1ReasoningFilePart
127
+ | HarnessV1ToolCallPart
128
+ | HarnessV1ToolResultPart
129
+ >;
130
+ readonly at?: string;
131
+ readonly harnessMetadata?: HarnessV1Metadata;
132
+ };
133
+
134
+ export type HarnessV1ToolMessage = {
135
+ readonly role: 'tool';
136
+ readonly content: Array<
137
+ HarnessV1ToolResultPart | HarnessV1ToolApprovalResponsePart
138
+ >;
139
+ readonly at?: string;
140
+ readonly harnessMetadata?: HarnessV1Metadata;
141
+ };
142
+
143
+ /**
144
+ * A persisted harness message using V4 prompt content shapes and metadata
145
+ * scoped to the adapter that produced it.
146
+ */
147
+ export type HarnessV1Message =
148
+ | HarnessV1UserMessage
149
+ | HarnessV1AssistantMessage
150
+ | HarnessV1ToolMessage;
@@ -11,6 +11,7 @@ import type {
11
11
  HarnessV1TurnSettings,
12
12
  } from './harness-v1-lifecycle-state';
13
13
  import type { HarnessV1StreamPart } from './harness-v1-stream-part';
14
+ import type { HarnessV1Message } from './harness-v1-message';
14
15
  import type { HarnessV1BuiltinToolFiltering } from './harness-v1-tool-filtering';
15
16
 
16
17
  /**
@@ -90,6 +91,15 @@ export type HarnessV1StartOptions = {
90
91
  readonly sessionWorkDir: string;
91
92
  };
92
93
 
94
+ /**
95
+ * Result of `HarnessV1Session.doReadHistory`.
96
+ */
97
+ export type HarnessV1ReadHistoryResult = {
98
+ readonly messages: ReadonlyArray<HarnessV1Message>;
99
+ /** Opaque position; pass back as `since` to read only what follows. */
100
+ readonly cursor: string;
101
+ };
102
+
93
103
  /**
94
104
  * Options passed to `HarnessV1Session.doPromptTurn`.
95
105
  */
@@ -192,6 +202,34 @@ export type HarnessV1Session = {
192
202
  */
193
203
  doCompact(customInstructions?: string): PromiseLike<void>;
194
204
 
205
+ /**
206
+ * Read the conversation history the runtime itself persisted, normalized
207
+ * to `HarnessV1Message`.
208
+ *
209
+ * The session's history can grow outside the harness contract: the same
210
+ * runtime conversation may be continued interactively (`claude --resume`),
211
+ * by another process, or before this session attached. Hosts that render a
212
+ * continuous record of the conversation — not just the turns they drove —
213
+ * need to read that history back, and the runtime's own store is the only
214
+ * source that has it. The adapter owns its runtime's persistence format,
215
+ * so the read belongs here rather than in every host.
216
+ *
217
+ * `since` is the `cursor` from a previous read; the result then contains
218
+ * only messages recorded after it. The cursor is adapter-owned and opaque
219
+ * to the host.
220
+ *
221
+ * Optional capability: adapters that cannot read their runtime's store
222
+ * omit the method entirely, and the agent surfaces that as
223
+ * `HarnessCapabilityUnsupportedError`. An adapter that implements it but
224
+ * cannot reach the store from the current environment (e.g. it lives
225
+ * inside a remote sandbox) throws `HarnessHistoryUnavailableError`. A
226
+ * conversation with no recorded messages yet is not an error — it resolves
227
+ * to an empty `messages` array.
228
+ */
229
+ doReadHistory?(options: {
230
+ readonly since?: string;
231
+ }): PromiseLike<HarnessV1ReadHistoryResult>;
232
+
195
233
  /**
196
234
  * Continue the in-flight turn **without a new user prompt**, returning the
197
235
  * same control surface as `doPromptTurn`. Used to keep consuming a turn that
package/src/v1/index.ts CHANGED
@@ -13,9 +13,26 @@ export type {
13
13
  export type {
14
14
  HarnessV1ContinueTurnOptions,
15
15
  HarnessV1PromptTurnOptions,
16
+ HarnessV1ReadHistoryResult,
16
17
  HarnessV1Session,
17
18
  HarnessV1StartOptions,
18
19
  } from './harness-v1-session';
20
+ export type {
21
+ HarnessV1AssistantMessage,
22
+ HarnessV1CustomPart,
23
+ HarnessV1FilePart,
24
+ HarnessV1Message,
25
+ HarnessV1MessagePart,
26
+ HarnessV1ReasoningFilePart,
27
+ HarnessV1ReasoningPart,
28
+ HarnessV1TextPart,
29
+ HarnessV1ToolApprovalResponsePart,
30
+ HarnessV1ToolCallPart,
31
+ HarnessV1ToolMessage,
32
+ HarnessV1ToolResultOutput,
33
+ HarnessV1ToolResultPart,
34
+ HarnessV1UserMessage,
35
+ } from './harness-v1-message';
19
36
  export type { HarnessV1Observability } from './harness-v1-observability';
20
37
  export type { HarnessV1PromptControl } from './harness-v1-prompt-control';
21
38
  export type { HarnessV1CallWarning } from './harness-v1-call-warning';