@hunterzhu/pulse-runtime 0.5.1 → 0.6.0

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,9 @@
1
1
  import type { AgentRecord, ArtifactRef, ConversationMessage, LaneRecord, LLMRequestProjection, PrivacyLabel, PrivacyTaint, ResultRef, RuntimeState, JsonValue, ContextDelta } from '../core/types.js';
2
2
  export interface ContextBuildInput {
3
3
  explicitContext?: boolean;
4
+ /** Include this lane's append-only history without implicitly exposing Global/Lane state. */
5
+ includeLaneHistory?: boolean;
6
+ historySeqs?: readonly number[];
4
7
  agent: AgentRecord;
5
8
  lane: LaneRecord;
6
9
  resultRefs?: ResultRef[];
@@ -32,6 +32,13 @@ function projectResultValue(result) {
32
32
  originalBytes,
33
33
  };
34
34
  }
35
+ function visibleModelOutput(value) {
36
+ if (Array.isArray(value))
37
+ return value.map(visibleModelOutput);
38
+ if (value && typeof value === 'object')
39
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !['providerContinuation', 'reasoningContent', 'reasoning_content'].includes(key)).map(([key, item]) => [key, visibleModelOutput(item)]));
40
+ return value;
41
+ }
35
42
  function buildProviderHistory(state, history, visible) {
36
43
  if (!history.length)
37
44
  return undefined;
@@ -110,7 +117,9 @@ export class ContextBuilder {
110
117
  const global = input.agent.globalVersions.get(input.lane.contextSnapshotVersion);
111
118
  if (global === undefined)
112
119
  throw new Error('UNKNOWN_CONTEXT_VERSION');
113
- const resultRefs = input.resultRefs ?? [];
120
+ const history = (input.includeLaneHistory || !input.explicitContext ? input.lane.context.history : []).filter(record => input.historySeqs === undefined || input.historySeqs.includes(record.seq));
121
+ const historyRefs = input.includeLaneHistory ? [...new Set(history.flatMap(r => r.resultRefs))].filter(ref => this.state.results.has(ref) && (input.lane.visibleResultRefs === undefined || input.lane.visibleResultRefs.has(ref))) : [];
122
+ const resultRefs = [...new Set([...(input.resultRefs ?? []), ...historyRefs])];
114
123
  const results = resultRefs.map((ref) => {
115
124
  const result = this.state.results.get(ref);
116
125
  if (!result)
@@ -119,7 +128,18 @@ export class ContextBuilder {
119
128
  throw new Error(`RESULT_NOT_VISIBLE:${ref}`);
120
129
  return result;
121
130
  });
122
- const artifactRefs = input.artifactRefs ?? [];
131
+ const imageRefs = [...(input.explicitContext ? [] : input.lane.context.history.flatMap(record => record.resultRefs).flatMap(ref => {
132
+ if (input.lane.visibleResultRefs && !input.lane.visibleResultRefs.has(ref))
133
+ return [];
134
+ const result = this.state.results.get(ref);
135
+ return result ? [result] : [];
136
+ })), ...results].flatMap(result => {
137
+ const value = result.value;
138
+ const ref = value && typeof value === 'object' && !Array.isArray(value) ? value.artifactRef : undefined;
139
+ const artifact = typeof ref === 'string' ? this.state.artifacts.get(ref) : undefined;
140
+ return artifact && (artifact.mediaType === 'image/png' || artifact.mediaType === 'image/jpeg') ? [artifact.ref] : [];
141
+ });
142
+ const artifactRefs = [...new Set([...(input.artifactRefs ?? []), ...[...new Set(imageRefs)].slice(-2)])];
123
143
  const artifacts = artifactRefs.map((ref) => {
124
144
  const artifact = this.state.artifacts.get(ref);
125
145
  if (!artifact)
@@ -146,23 +166,42 @@ export class ContextBuilder {
146
166
  ...effectiveArtifacts.flatMap((item) => (item.artifact.privacyTaints ?? []).map((taint) => ({ path: [item.artifact.ref, ...taint.path], privacy: taint.privacy }))),
147
167
  ];
148
168
  const conversation = input.conversation ?? [];
149
- const providerHistory = input.explicitContext ? undefined : buildProviderHistory(this.state, input.lane.context.history, input.lane.visibleResultRefs);
169
+ const providerHistory = input.explicitContext ? undefined : buildProviderHistory(this.state, history, input.lane.visibleResultRefs);
170
+ const emitted = new Set();
171
+ const historyResult = (ref) => {
172
+ const result = this.state.results.get(ref);
173
+ const effectId = result.effectId ?? (result.producer?.kind === 'effect' ? result.producer.id : undefined);
174
+ return effectId && this.state.effects.get(effectId)?.kind === 'llm'
175
+ ? projectResultValue({ value: visibleModelOutput(result.value ?? null), ...(result.summary === undefined ? {} : { summary: visibleModelOutput(result.summary) }) }) : projectResultValue(result);
176
+ };
177
+ const historyBlock = { kind: 'history', content: history.map(record => {
178
+ const output = record.output && typeof record.output === 'object' && !Array.isArray(record.output) ? record.output : undefined;
179
+ return { seq: record.seq, ...(record.effectId === undefined ? {} : { effectId: record.effectId }), instruction: record.instruction, resultRefs: record.resultRefs,
180
+ ...(record.resultSelection === undefined ? {} : { resultSelection: record.resultSelection }), ...(record.result === undefined ? {} : { result: record.result }),
181
+ ...(record.findings === undefined ? {} : { findings: record.findings }),
182
+ output: input.includeLaneHistory && output ? visibleModelOutput(output) : record.output,
183
+ ...(input.includeLaneHistory ? { evidence: record.resultRefs.filter(ref => historyRefs.includes(ref) && !emitted.has(ref) && !!emitted.add(ref)).map(ref => ({ ref, ...historyResult(ref) })) } : {}),
184
+ privacy: record.privacy, ...(record.privacyTaints === undefined ? {} : { privacyTaints: record.privacyTaints }) };
185
+ }) };
150
186
  const contextSpec = { globalSnapshotVersion: input.lane.contextSnapshotVersion, laneSnapshotVersion: input.lane.context.version, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: input.eventIds ?? [], toolSetId: input.toolSetId, instruction: input.instruction, ...(conversation.length ? { conversation: structuredClone(conversation) } : {}), ...(providerHistory === undefined ? {} : { providerHistory }), privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
151
187
  const prefixBlocks = [
152
188
  { kind: 'system', content: input.system ?? '' },
153
189
  { kind: 'policy', content: input.policy ?? {} },
154
190
  { kind: 'tools', content: input.tools ?? {} },
155
191
  { kind: 'global', content: input.explicitContext ? {} : global },
156
- ...(conversation.length ? [{ kind: 'conversation', content: structuredClone(conversation) }] : []),
157
- { kind: 'history', content: (input.explicitContext ? [] : input.lane.context.history).map((record) => ({ seq: record.seq, ...(record.effectId === undefined ? {} : { effectId: record.effectId }), instruction: record.instruction, resultRefs: record.resultRefs, ...(record.resultSelection === undefined ? {} : { resultSelection: record.resultSelection }), ...(record.result === undefined ? {} : { result: record.result }), ...(record.findings === undefined ? {} : { findings: record.findings }), output: record.output, privacy: record.privacy, ...(record.privacyTaints === undefined ? {} : { privacyTaints: record.privacyTaints }) })) },
192
+ ...(!input.includeLaneHistory && conversation.length ? [{ kind: 'conversation', content: structuredClone(conversation) }] : []),
193
+ historyBlock,
158
194
  ];
159
195
  const correlatedRefs = new Set((providerHistory ?? []).flatMap((message) => message.role === 'tool' ? [message.resultRef] : []));
160
- const remainingResults = results.filter((result) => !correlatedRefs.has(result.id));
161
- const blocks = [...prefixBlocks, ...(providerHistory === undefined ? [] : [{ kind: 'provider_history', content: providerHistory }]), { kind: 'lane', content: input.explicitContext ? {} : input.lane.context.state }, { kind: 'events', content: input.eventIds ?? [] }, { kind: 'results', content: remainingResults.map((result) => {
196
+ const remainingResults = results.filter((result) => !correlatedRefs.has(result.id) && (!emitted.has(result.id) || input.resultRefs?.includes(result.id)));
197
+ const blocks = [...prefixBlocks, ...(input.includeLaneHistory && conversation.length ? [{ kind: 'conversation', content: structuredClone(conversation) }] : []), ...(providerHistory === undefined ? [] : [{ kind: 'provider_history', content: providerHistory }]), { kind: 'lane', content: input.explicitContext ? {} : input.lane.context.state }, { kind: 'events', content: input.eventIds ?? [] }, { kind: 'results', content: remainingResults.map((result) => {
198
+ if (emitted.has(result.id))
199
+ return { id: result.id, fromHistory: true };
162
200
  const projected = projectResultValue(result);
163
201
  return { id: result.id, value: projected.value, ...(projected.summarized ? { summarized: true, ...(projected.originalBytes === undefined ? {} : { originalBytes: projected.originalBytes }) } : {}), ...(result.privacyTaints === undefined ? {} : { privacyTaints: result.privacyTaints.map((taint) => ({ path: [...taint.path], privacy: taint.privacy })) }) };
164
202
  }) }, { kind: 'artifacts', content: artifacts.map((artifact) => ({ ref: artifact.ref, mediaType: artifact.mediaType, sizeBytes: artifact.sizeBytes, contentHash: artifact.contentHash })) }, { kind: 'instruction', content: input.instruction }];
165
- return { contextSpec, blocks, prefixHash: hash(prefixBlocks), projectionHash: hash(blocks), builderVersion: this.version, policyVersion: '1', toolSetVersion: input.toolSetId, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
203
+ const images = artifacts.filter(artifact => artifact.mediaType === 'image/png' || artifact.mediaType === 'image/jpeg').slice(-2).map(artifact => ({ ref: artifact.ref, mediaType: artifact.mediaType }));
204
+ return { contextSpec, blocks, ...(images.length ? { images } : {}), prefixHash: hash(prefixBlocks), projectionHash: hash(blocks), builderVersion: this.version, policyVersion: '1', toolSetVersion: input.toolSetId, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
166
205
  }
167
206
  }
168
207
  export function appendHistory(lane, record) {
@@ -0,0 +1,9 @@
1
+ import type { JsonValue } from './types.js';
2
+ /** Extract an explicit tool image before JSON history/result summarization. */
3
+ export declare function extractToolImage(value: JsonValue): {
4
+ value: JsonValue;
5
+ artifact?: {
6
+ mediaType: string;
7
+ content: Uint8Array;
8
+ };
9
+ };
@@ -0,0 +1,13 @@
1
+ /** Extract an explicit tool image before JSON history/result summarization. */
2
+ export function extractToolImage(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !value.pulseImage)
4
+ return { value };
5
+ const image = value.pulseImage;
6
+ if (typeof image !== 'object' || Array.isArray(image) || !['image/png', 'image/jpeg'].includes(String(image.mediaType)) || typeof image.data !== 'string' || image.data.length > 4 * 1024 * 1024 || !/^[A-Za-z0-9+/]*={0,2}$/.test(image.data))
7
+ throw new Error('INVALID_TOOL_IMAGE');
8
+ const bytes = Buffer.from(image.data, 'base64');
9
+ if (bytes.length < 16 || (image.mediaType === 'image/png' ? bytes.readUInt32BE(0) !== 0x89504e47 : bytes.readUInt16BE(0) !== 0xffd8))
10
+ throw new Error('INVALID_TOOL_IMAGE');
11
+ const { pulseImage: _image, ...metadata } = value;
12
+ return { value: metadata, artifact: { mediaType: image.mediaType, content: bytes } };
13
+ }
@@ -205,6 +205,8 @@ export interface EffectRecord {
205
205
  locks?: ResourceLockSpec[];
206
206
  }
207
207
  export interface AttemptRecord {
208
+ /** Actual provider requests admitted by the Host, including transport retries. */
209
+ providerAttempts?: number;
208
210
  id: string;
209
211
  effectId: EffectId;
210
212
  modelId?: string;
@@ -491,6 +493,8 @@ export interface MergeProposal {
491
493
  }
492
494
  export interface LaneStepOutput {
493
495
  contextDelta?: ContextDelta;
496
+ /** Optional lane update committed atomically alongside a global ContextDelta. */
497
+ laneContextDelta?: ContextDelta;
494
498
  adoptCommittedContext?: boolean;
495
499
  actions: RuntimeAction[];
496
500
  next: ResumePoint;
@@ -535,6 +539,12 @@ export type ProviderHistoryMessage = {
535
539
  resultRef: ResultRef;
536
540
  };
537
541
  export interface LLMRequestProjection {
542
+ /** Resolved, visible image artifacts. Never serialize these into text history. */
543
+ images?: Array<{
544
+ ref: ArtifactRef;
545
+ mediaType: 'image/png' | 'image/jpeg';
546
+ data?: string;
547
+ }>;
538
548
  contextSpec: LLMContextSpec;
539
549
  blocks: Array<{
540
550
  kind: 'system' | 'policy' | 'tools' | 'global' | 'conversation' | 'history' | 'provider_history' | 'lane' | 'events' | 'results' | 'artifacts' | 'instruction';
@@ -0,0 +1,11 @@
1
+ import type { JsonValue } from '../core/types.js';
2
+ import type { StepContext } from './program.js';
3
+ export interface ProposedEvidenceCall {
4
+ toolName: string;
5
+ input: JsonValue;
6
+ }
7
+ /** Pure proposal filter. Host freshness checks remain mandatory at verification. */
8
+ export declare function reusableEvidence(ctx: Pick<StepContext, 'results'>, calls: ProposedEvidenceCall[], refs: string[]): {
9
+ refs: string[];
10
+ checksOnly: boolean;
11
+ } | undefined;
@@ -0,0 +1,50 @@
1
+ const row = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
2
+ /** Pure proposal filter. Host freshness checks remain mandatory at verification. */
3
+ export function reusableEvidence(ctx, calls, refs) {
4
+ if (!calls.length || calls.some(c => !['fs.read', 'shell.exec'].includes(c.toolName)))
5
+ return undefined;
6
+ const ordered = [...new Set(refs)].sort((a, b) => Number(a.match(/\d+$/)?.[0] ?? 0) - Number(b.match(/\d+$/)?.[0] ?? 0));
7
+ const latestAnswer = [...ordered].reverse().find(ref => ctx.results.meta(ref)?.effectKind === 'human');
8
+ const versions = new Map();
9
+ for (const ref of ordered) {
10
+ const meta = ctx.results.meta(ref), v = row(ctx.results.read(ref)), args = row(meta?.toolArguments);
11
+ if (!meta?.toolName?.startsWith('fs.'))
12
+ continue;
13
+ if (meta.toolName === 'fs.stage' && v.committed !== true)
14
+ continue;
15
+ const path = v.path ?? v.target ?? args.path;
16
+ if (typeof path === 'string' && ['fs.read', 'fs.write', 'fs.apply_patch', 'fs.apply_patches', 'fs.stage'].includes(meta.toolName))
17
+ versions.set(path, typeof v.hash === 'string' ? v.hash : undefined);
18
+ }
19
+ const matched = [];
20
+ for (const call of calls) {
21
+ const input = row(call.input);
22
+ const newestCheck = call.toolName === 'shell.exec' ? [...ordered].reverse().find(ref => {
23
+ const meta = ctx.results.meta(ref), args = row(meta?.toolArguments);
24
+ return meta?.toolName === call.toolName && args.command === input.command && JSON.stringify(args.args ?? []) === JSON.stringify(input.args ?? []) && (args.cwd ?? '.') === (input.cwd ?? '.');
25
+ }) : undefined;
26
+ const ref = [...ordered].reverse().find(ref => {
27
+ const meta = ctx.results.meta(ref), v = row(ctx.results.read(ref)), args = row(meta?.toolArguments);
28
+ if (meta?.toolName !== call.toolName || meta.outcomeStatus !== 'succeeded')
29
+ return false;
30
+ if (latestAnswer && ordered.indexOf(ref) < ordered.indexOf(latestAnswer))
31
+ return false;
32
+ if (call.toolName === 'fs.read')
33
+ return typeof v.hash === 'string' && v.path === input.path && (v.offset ?? 0) === (input.offset ?? 0) && versions.get(v.path) === v.hash;
34
+ if (ref !== newestCheck)
35
+ return false; // A later failure cannot fall back to an old pass.
36
+ const receipt = row(v.checkReceipt);
37
+ if (v.code !== 0 || v.timedOut || v.aborted || v.truncated || receipt.stable !== true || receipt.definitionChanged !== false)
38
+ return false;
39
+ if (args.command !== input.command || JSON.stringify(args.args ?? []) !== JSON.stringify(input.args ?? []) || (args.cwd ?? '.') !== (input.cwd ?? '.'))
40
+ return false;
41
+ if (!receipt.files || !Object.keys(receipt.files).length)
42
+ return false;
43
+ return Object.entries(receipt.files).every(([path, hash]) => !versions.has(path) || versions.get(path) === hash);
44
+ });
45
+ if (!ref)
46
+ return undefined;
47
+ matched.push(ref);
48
+ }
49
+ return { refs: [...new Set(matched)], checksOnly: calls.every(c => c.toolName === 'shell.exec') };
50
+ }
@@ -2,3 +2,4 @@ export * from './program.js';
2
2
  export * from './templates.js';
3
3
  export * from './session.js';
4
4
  export * from './context-proxy.js';
5
+ export { isUnreadFileWindow, isReadProgress } from './read-continuation.js';
package/dist/dsl/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './program.js';
2
2
  export * from './templates.js';
3
3
  export * from './session.js';
4
4
  export * from './context-proxy.js';
5
+ export { isUnreadFileWindow, isReadProgress } from './read-continuation.js';
@@ -0,0 +1,33 @@
1
+ import type { JsonValue } from '../core/types.js';
2
+ import type { StepContext } from './program.js';
3
+ export interface PatchRecovery {
4
+ path: string;
5
+ workspaceRoot?: string;
6
+ currentHash: string;
7
+ reason: string;
8
+ attempts: number;
9
+ corrected: boolean;
10
+ afterRef: number;
11
+ findHashes: string[];
12
+ missingFindHashes: string[];
13
+ readPending: boolean;
14
+ suggestedRead: {
15
+ path: string;
16
+ offset: number;
17
+ maxBytes: number;
18
+ };
19
+ }
20
+ export declare function readPatchRecoveries(value: unknown): PatchRecovery[];
21
+ export declare function rememberPatchFailures(prior: PatchRecovery[], failures: Array<{
22
+ code: string;
23
+ details?: JsonValue | undefined;
24
+ }>, afterRef: number): PatchRecovery[];
25
+ export declare function refreshPatchRecoveries(ctx: Pick<StepContext, 'results'>, records: PatchRecovery[], refs: string[]): PatchRecovery[];
26
+ export declare function isPatchRecoveryRead(call: {
27
+ toolName: JsonValue;
28
+ input: JsonValue;
29
+ }, records: PatchRecovery[]): boolean;
30
+ export declare function stalePatchRecord(call: {
31
+ toolName: string;
32
+ input: JsonValue;
33
+ }, records: PatchRecovery[]): PatchRecovery | undefined;
@@ -0,0 +1,51 @@
1
+ import { normalize, relative, resolve, isAbsolute } from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ const fragmentHash = (text) => createHash('sha256').update(text).digest('hex');
4
+ const row = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
5
+ const hash = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
6
+ const order = (ref) => Number(ref.match(/\d+$/)?.[0] ?? 0);
7
+ const pathMatches = (record, path) => typeof path === 'string' && (record.workspaceRoot ? relative(record.workspaceRoot, resolve(record.workspaceRoot, path)) : normalize(path)).replaceAll('\\', '/') === normalize(record.path).replaceAll('\\', '/');
8
+ export function readPatchRecoveries(value) {
9
+ return structuredClone(Array.isArray(value) ? value.filter(v => typeof v?.path === 'string' && hash(v?.currentHash) && Array.isArray(v?.findHashes) && v.findHashes.length <= 64 && v.findHashes.every(hash) && Array.isArray(v?.missingFindHashes) && v.missingFindHashes.every((h) => v.findHashes.includes(h)) && typeof v.corrected === 'boolean' && typeof v.readPending === 'boolean' && (v.workspaceRoot === undefined || typeof v.workspaceRoot === 'string' && isAbsolute(v.workspaceRoot)) && Number.isSafeInteger(v?.attempts) && Number.isSafeInteger(v?.afterRef) && v?.suggestedRead?.path === v.path && Number.isSafeInteger(v?.suggestedRead?.offset) && v.suggestedRead.offset >= 0 && Number.isSafeInteger(v.suggestedRead.maxBytes) && v.suggestedRead.maxBytes > 0 && v.suggestedRead.maxBytes <= 3000).slice(-8) : []);
10
+ }
11
+ export function rememberPatchFailures(prior, failures, afterRef) {
12
+ const result = structuredClone(prior);
13
+ for (const failure of failures) {
14
+ const d = row(failure.details), read = row(d.suggestedRead);
15
+ if (!['FILE_BASELINE_CONFLICT', 'PATCH_CONTEXT_NOT_FOUND', 'PATCH_CONTEXT_AMBIGUOUS', 'PATCH_RANGES_OVERLAP'].includes(failure.code) || d.kind !== 'patch-conflict' || !['fs.apply_patch', 'fs.apply_patches'].includes(d.toolName) || typeof d.path !== 'string' || !hash(d.currentHash) || !Array.isArray(d.findHashes) || !d.findHashes.length || d.findHashes.length > 16 || !d.findHashes.every(hash) || !Array.isArray(d.missingFindHashes) || !d.missingFindHashes.every(h => d.findHashes.includes(h)) || read.path !== d.path || !Number.isSafeInteger(read.offset) || read.offset < 0 || !Number.isSafeInteger(read.maxBytes) || read.maxBytes < 1 || read.maxBytes > 3000)
16
+ continue;
17
+ const previous = result.find(r => r.path === d.path && r.currentHash === d.currentHash);
18
+ const next = { path: d.path, ...(typeof d.workspaceRoot === 'string' && isAbsolute(d.workspaceRoot) ? { workspaceRoot: d.workspaceRoot } : previous?.workspaceRoot ? { workspaceRoot: previous.workspaceRoot } : {}), currentHash: d.currentHash, reason: failure.code, attempts: (previous?.attempts ?? 0) + 1, corrected: previous?.corrected ?? false, afterRef,
19
+ findHashes: [...new Set([...(previous?.findHashes ?? []), ...d.findHashes])].slice(-64), missingFindHashes: [...new Set([...(previous?.missingFindHashes ?? []), ...d.missingFindHashes])].slice(-64), readPending: true, suggestedRead: { path: read.path, offset: read.offset, maxBytes: read.maxBytes } };
20
+ const index = result.findIndex(r => r.path === d.path);
21
+ if (index >= 0)
22
+ result.splice(index, 1);
23
+ result.push(next);
24
+ }
25
+ return result.slice(-8);
26
+ }
27
+ export function refreshPatchRecoveries(ctx, records, refs) {
28
+ return records.flatMap(record => {
29
+ const newest = [...new Set(refs)].sort((a, b) => order(b) - order(a)).find(ref => {
30
+ const meta = ctx.results.meta(ref), body = row(ctx.results.read(ref)), args = row(meta?.toolArguments);
31
+ return order(ref) > record.afterRef && meta?.outcomeStatus === 'succeeded' && ['fs.read', 'fs.write', 'fs.apply_patch', 'fs.apply_patches', 'fs.stage'].includes(meta.toolName ?? '') && pathMatches(record, body.path ?? body.target ?? args.path) && hash(body.hash) && (meta.toolName !== 'fs.stage' || body.committed === true);
32
+ });
33
+ if (!newest)
34
+ return [record];
35
+ const body = row(ctx.results.read(newest)), meta = ctx.results.meta(newest);
36
+ if (body.hash !== record.currentHash)
37
+ return [];
38
+ return [{ ...record, afterRef: order(newest), readPending: meta?.toolName === 'fs.read' ? false : record.readPending }];
39
+ });
40
+ }
41
+ export function isPatchRecoveryRead(call, records) {
42
+ const a = row(call.input);
43
+ return call.toolName === 'fs.read' && a.startLine === undefined && records.some(r => r.readPending && pathMatches(r, a.path) && (a.offset ?? 0) === r.suggestedRead.offset && Math.min(a.maxBytes ?? 3000, 3000) === r.suggestedRead.maxBytes);
44
+ }
45
+ export function stalePatchRecord(call, records) {
46
+ if (!['fs.apply_patch', 'fs.apply_patches'].includes(call.toolName))
47
+ return undefined;
48
+ const a = row(call.input), patches = call.toolName === 'fs.apply_patch' ? [a] : Array.isArray(a.patches) ? a.patches.map(row) : [];
49
+ const finds = patches.filter(p => typeof p.find === 'string').map(p => fragmentHash(p.find));
50
+ return records.find(r => pathMatches(r, a.path) && (finds.some(h => r.missingFindHashes.includes(h)) || a.expectedHash !== r.currentHash && finds.some(h => r.findHashes.includes(h))));
51
+ }
@@ -63,6 +63,8 @@ export interface ResultMeta {
63
63
  outcomeStatus?: string;
64
64
  sideEffectPolicy?: 'none' | 'read' | 'write' | 'external';
65
65
  toolName?: string;
66
+ toolArguments?: JsonValue;
67
+ humanQuestion?: JsonValue;
66
68
  toolCommand?: string;
67
69
  toolExitCode?: number;
68
70
  ref: ResultRef;
@@ -92,6 +94,13 @@ export interface StepContext<TState = JsonValue> {
92
94
  summary(ref: ResultRef): JsonValue | undefined;
93
95
  read(ref: ResultRef): JsonValue | undefined;
94
96
  };
97
+ /** Evidence of a settled direct child, even when it produced no completion result. */
98
+ settledLaneEvidence(laneId: LaneId): {
99
+ evidenceRefs: ResultRef[];
100
+ modelEffectIds: string[];
101
+ modelCalls: number;
102
+ laneState?: JsonValue;
103
+ } | undefined;
95
104
  mergeProposals: ReadonlyArray<MergeProposal>;
96
105
  mutateLane(mutator: (draft: TState) => void): void;
97
106
  proposeGlobal(delta: {
@@ -155,6 +164,17 @@ interface AffinityAdviceGroup {
155
164
  keys: string[];
156
165
  signals: string[];
157
166
  }
167
+ /** Keep assertions verbatim; strip only timing syntax from recognized test reporters. */
168
+ export declare function stableCheckOutput(text: string): string;
169
+ /** Timing metadata is not new evidence; preserve the command's actual output and exit code. */
170
+ export declare function toolEvidenceHash(meta: Pick<ResultMeta, 'toolName' | 'toolArguments'> | undefined, value: JsonValue): string;
171
+ /** Stable identity includes the actual decision, never the prompt alone. */
172
+ export declare function interactionIdentity(tool: string, input: JsonValue): {
173
+ exact: string;
174
+ semantic?: string;
175
+ };
176
+ /** The model-facing surface is bounded; full answers stay in durable results. */
177
+ export declare function confirmedInteractionSurface(answers: JsonValue[], scopes?: string[]): JsonValue;
158
178
  /** A stop response that still contains tool-call markup was not executed. */
159
179
  export declare function looksLikeUnexecutedToolRequest(text: string): boolean;
160
180
  /** Models often wrap the real arguments, put a path in title, or pack a shell command line into one string. */
@@ -165,6 +185,7 @@ export declare class StepBuilder<TState = JsonValue> {
165
185
  version: string;
166
186
  system?: string;
167
187
  explicitContext?: boolean;
188
+ includeLaneHistory?: boolean;
168
189
  toolSet?: string;
169
190
  state?: z.ZodType<TState>;
170
191
  historyCompaction?: HistoryCompactionOptions;
@@ -183,6 +204,7 @@ export declare class StepBuilder<TState = JsonValue> {
183
204
  version: string;
184
205
  system?: string;
185
206
  explicitContext?: boolean;
207
+ includeLaneHistory?: boolean;
186
208
  toolSet?: string;
187
209
  state?: z.ZodType<TState>;
188
210
  historyCompaction?: HistoryCompactionOptions;
@@ -221,10 +243,15 @@ export declare class StepBuilder<TState = JsonValue> {
221
243
  maxTurns?: number;
222
244
  maxTruncationRetries?: number;
223
245
  maxToolsPerTurn?: number;
246
+ maxReadOnlyRounds?: number | ((ctx: StepContext<TState>) => number | undefined);
224
247
  serialTools?: string[];
225
248
  stopAfterFirstSerialTool?: boolean;
226
249
  blockReadOnlyTools?: (ctx: StepContext<TState>) => boolean;
250
+ preserveReadContinuations?: boolean;
227
251
  scopeToolCallsToEffect?: boolean;
252
+ reuseSettledEvidence?: boolean;
253
+ onEvidenceReused?: (refs: ResultRef[], ctx: StepContext<TState>) => void;
254
+ onReuseEvidence?: (refs: ResultRef[], ctx: StepContext<TState>) => NextStepTarget<TState>;
228
255
  resetTurnsOnEntry?: (ctx: StepContext<TState>) => string | number | undefined;
229
256
  outputSchema?: ZodTypeAny;
230
257
  requirements?: Record<string, JsonValue>;
@@ -240,6 +267,8 @@ export declare class StepBuilder<TState = JsonValue> {
240
267
  };
241
268
  };
242
269
  onMaxTurns?: (ctx: StepContext<TState>) => NextStepTarget<TState>;
270
+ prepareQuestion?: (toolName: string, input: JsonValue, ctx: StepContext<TState>) => JsonValue;
271
+ onHumanReply?: (question: JsonValue, answer: JsonValue, ctx: StepContext<TState>) => NextStepTarget<TState> | undefined;
243
272
  onError?: (error: RuntimeError, ctx: StepContext<TState>) => NextStepTarget<TState>;
244
273
  }): this;
245
274
  addParallelStep(name: string, options: {
@@ -335,6 +364,7 @@ export declare function defineLaneProgram<TState = JsonValue>(config: {
335
364
  version: string;
336
365
  system?: string;
337
366
  explicitContext?: boolean;
367
+ includeLaneHistory?: boolean;
338
368
  toolSet?: string;
339
369
  state?: z.ZodType<TState>;
340
370
  historyCompaction?: HistoryCompactionOptions;