@hunterzhu/pulse-runtime 0.5.0 → 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.
- package/dist/context/builder.d.ts +3 -0
- package/dist/context/builder.js +47 -8
- package/dist/core/tool-image.d.ts +9 -0
- package/dist/core/tool-image.js +13 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/dsl/evidence-reuse.d.ts +11 -0
- package/dist/dsl/evidence-reuse.js +50 -0
- package/dist/dsl/index.d.ts +1 -0
- package/dist/dsl/index.js +1 -0
- package/dist/dsl/patch-recovery.d.ts +33 -0
- package/dist/dsl/patch-recovery.js +51 -0
- package/dist/dsl/program.d.ts +30 -0
- package/dist/dsl/program.js +371 -49
- package/dist/dsl/read-continuation.d.ts +9 -0
- package/dist/dsl/read-continuation.js +84 -0
- package/dist/dsl/templates.d.ts +4 -0
- package/dist/dsl/templates.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/router.d.ts +2 -0
- package/dist/models/router.js +2 -2
- package/dist/scheduler/runtime.d.ts +16 -1
- package/dist/scheduler/runtime.js +149 -10
- package/dist/storage/persistence.d.ts +11 -0
- package/dist/storage/persistence.js +21 -1
- package/dist/storage/policy.d.ts +2 -0
- package/dist/storage/policy.js +8 -0
- package/dist/transitions/validate.js +22 -17
- package/package.json +1 -1
|
@@ -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[];
|
package/dist/context/builder.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/dsl/index.d.ts
CHANGED
package/dist/dsl/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/dsl/program.d.ts
CHANGED
|
@@ -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;
|