@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.
- 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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JsonValue } from '../core/types.js';
|
|
2
|
+
import type { StepContext } from './program.js';
|
|
3
|
+
/** A bounded window must expose new bytes, rather than slide through already read text. */
|
|
4
|
+
export declare function isUnreadFileWindow(ctx: Pick<StepContext<unknown>, 'results'>, call: {
|
|
5
|
+
toolName: JsonValue;
|
|
6
|
+
input: JsonValue;
|
|
7
|
+
}, refs: readonly string[]): boolean;
|
|
8
|
+
/** Count actual new windows/version changes, not arbitrary reads at positive offsets. */
|
|
9
|
+
export declare function isReadProgress(ctx: Pick<StepContext<unknown>, 'results'>, ref: string, priorRefs: readonly string[]): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { normalize } from 'node:path';
|
|
2
|
+
const pathKey = (value) => typeof value === 'string' ? normalize(value).replaceAll('\\', '/') : undefined;
|
|
3
|
+
const limitBytes = 3000;
|
|
4
|
+
const row = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
5
|
+
const integer = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
6
|
+
/** A bounded window must expose new bytes, rather than slide through already read text. */
|
|
7
|
+
export function isUnreadFileWindow(ctx, call, refs) {
|
|
8
|
+
if (call.toolName !== 'fs.read')
|
|
9
|
+
return false;
|
|
10
|
+
const { path, offset = 0, startLine, maxBytes = limitBytes } = row(call.input);
|
|
11
|
+
if (typeof path !== 'string' || !integer(offset) || !integer(maxBytes) || maxBytes === 0)
|
|
12
|
+
return false;
|
|
13
|
+
const size = Math.min(maxBytes, limitBytes);
|
|
14
|
+
const ordered = [...new Set(refs)].sort((a, b) => Number(a.match(/\d+$/)?.[0] ?? 0) - Number(b.match(/\d+$/)?.[0] ?? 0));
|
|
15
|
+
const observations = ordered.flatMap((ref, order) => {
|
|
16
|
+
const meta = ctx.results.meta(ref), body = row(ctx.results.read(ref)), args = row(meta?.toolArguments);
|
|
17
|
+
if (meta?.outcomeStatus !== 'succeeded' || !['fs.read', 'fs.write', 'fs.apply_patch', 'fs.apply_patches', 'fs.stage'].includes(meta.toolName ?? ''))
|
|
18
|
+
return [];
|
|
19
|
+
if (pathKey(body.path) !== pathKey(path) && pathKey(body.target) !== pathKey(path) && pathKey(args.path) !== pathKey(path))
|
|
20
|
+
return [];
|
|
21
|
+
if (meta.toolName === 'fs.stage' && body.committed !== true)
|
|
22
|
+
return [];
|
|
23
|
+
return [{ body, args, read: meta.toolName === 'fs.read', order }];
|
|
24
|
+
});
|
|
25
|
+
const latest = observations.at(-1);
|
|
26
|
+
if (latest && typeof latest.body.hash !== 'string')
|
|
27
|
+
return false;
|
|
28
|
+
const pages = observations.filter(o => o.read && o.body.hash === latest?.body.hash);
|
|
29
|
+
if (startLine !== undefined) {
|
|
30
|
+
if (!integer(startLine) || startLine < 1 || offset !== 0 || pages.some(p => integer(p.args.startLine) && startLine >= p.args.startLine && startLine <= p.args.startLine + (typeof p.body.content === 'string' ? p.body.content.split('\n').length - (p.body.content.endsWith('\n') ? 2 : 1) : 0)))
|
|
31
|
+
return false;
|
|
32
|
+
// Search anchors are exact observed locations, not model-invented line numbers.
|
|
33
|
+
// A later write invalidates older anchors, even when the read result was retained.
|
|
34
|
+
const lastWrite = observations.filter(o => !o.read && o.body.changed !== false).at(-1)?.order ?? -1;
|
|
35
|
+
return ordered.some((ref, order) => {
|
|
36
|
+
if (order <= lastWrite)
|
|
37
|
+
return false;
|
|
38
|
+
const meta = ctx.results.meta(ref), body = row(ctx.results.read(ref));
|
|
39
|
+
return meta?.toolName === 'fs.search' && meta.outcomeStatus === 'succeeded' && Array.isArray(body.matches) && body.matches.some(m => pathKey(row(m).path) === pathKey(path) && row(m).line === startLine);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!latest || pages.length === 0)
|
|
43
|
+
return offset === 0;
|
|
44
|
+
const ranges = pages.flatMap(({ body }) => {
|
|
45
|
+
const start = body.offset ?? 0;
|
|
46
|
+
const end = integer(body.nextOffset) ? body.nextOffset : typeof body.content === 'string' ? start + Buffer.byteLength(body.content) : undefined;
|
|
47
|
+
return integer(start) && integer(end) && end > start ? [[start, end]] : [];
|
|
48
|
+
}).sort((a, b) => a[0] - b[0]);
|
|
49
|
+
const merged = [];
|
|
50
|
+
for (const range of ranges) {
|
|
51
|
+
const last = merged.at(-1);
|
|
52
|
+
if (last && range[0] <= last[1])
|
|
53
|
+
last[1] = Math.max(last[1], range[1]);
|
|
54
|
+
else
|
|
55
|
+
merged.push([...range]);
|
|
56
|
+
}
|
|
57
|
+
const eof = pages.flatMap(({ body }) => body.nextOffset === null && typeof body.content === 'string' && integer(body.offset ?? 0) ? [(body.offset ?? 0) + Buffer.byteLength(body.content)] : []);
|
|
58
|
+
const end = Math.min(offset + size, ...eof);
|
|
59
|
+
if (!Number.isSafeInteger(end) || end <= offset)
|
|
60
|
+
return false;
|
|
61
|
+
const covered = merged.reduce((n, [start, stop]) => n + Math.max(0, Math.min(stop, end) - Math.max(start, offset)), 0);
|
|
62
|
+
if (covered >= end - offset)
|
|
63
|
+
return false;
|
|
64
|
+
if (pages.some(({ body }) => body.nextOffset === offset))
|
|
65
|
+
return true;
|
|
66
|
+
// A little context overlap is useful, but 1-byte shifts cannot defeat the gate.
|
|
67
|
+
return merged.some(([start, stop]) => offset >= start && offset < stop && end > stop) && covered <= Math.min(512, Math.floor((end - offset) / 4));
|
|
68
|
+
}
|
|
69
|
+
/** Count actual new windows/version changes, not arbitrary reads at positive offsets. */
|
|
70
|
+
export function isReadProgress(ctx, ref, priorRefs) {
|
|
71
|
+
const meta = ctx.results.meta(ref), body = row(ctx.results.read(ref)), args = row(meta?.toolArguments);
|
|
72
|
+
if (meta?.toolName !== 'fs.read' || meta.outcomeStatus !== 'succeeded')
|
|
73
|
+
return false;
|
|
74
|
+
const path = body.path ?? args.path;
|
|
75
|
+
const priorFile = [...new Set(priorRefs)].sort((a, b) => Number(b.match(/\d+$/)?.[0] ?? 0) - Number(a.match(/\d+$/)?.[0] ?? 0)).find(prior => {
|
|
76
|
+
const m = ctx.results.meta(prior), b = row(ctx.results.read(prior)), a = row(m?.toolArguments);
|
|
77
|
+
return m?.outcomeStatus === 'succeeded' && ['fs.read', 'fs.write', 'fs.apply_patch', 'fs.apply_patches', 'fs.stage'].includes(m.toolName ?? '') && (m.toolName !== 'fs.stage' || b.committed === true) && pathKey(b.path ?? b.target ?? a.path) === pathKey(path);
|
|
78
|
+
});
|
|
79
|
+
if (priorFile && typeof body.hash === 'string' && typeof row(ctx.results.read(priorFile)).hash === 'string' && body.hash !== row(ctx.results.read(priorFile)).hash)
|
|
80
|
+
return true;
|
|
81
|
+
if (!priorFile && args.startLine === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
return isUnreadFileWindow(ctx, { toolName: 'fs.read', input: { ...args, path, ...(args.startLine === undefined ? { offset: body.offset ?? args.offset ?? 0 } : {}) } }, priorRefs);
|
|
84
|
+
}
|
package/dist/dsl/templates.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface ReActLaneConfig {
|
|
|
20
20
|
maxTurns?: number;
|
|
21
21
|
maxTruncationRetries?: number;
|
|
22
22
|
maxToolsPerTurn?: number;
|
|
23
|
+
prepareQuestion?: (toolName: string, input: JsonValue, ctx: StepContext<JsonValue>) => JsonValue;
|
|
24
|
+
onHumanReply?: (question: JsonValue, answer: JsonValue, ctx: StepContext<JsonValue>) => NextStepTarget<JsonValue> | undefined;
|
|
25
|
+
/** Bound investigation-only rounds for an implementation run; pagination is exempt. */
|
|
26
|
+
maxReadOnlyRounds?: number | ((ctx: StepContext<JsonValue>) => number | undefined);
|
|
23
27
|
serialTools?: string[];
|
|
24
28
|
stopAfterFirstSerialTool?: boolean;
|
|
25
29
|
/** Return a durable token when a new attempt should get a fresh turn budget. */
|
package/dist/dsl/templates.js
CHANGED
|
@@ -4,7 +4,7 @@ export function defineReActLane(config) {
|
|
|
4
4
|
const onFinish = config.onFinish ?? (config.outputSchema === undefined
|
|
5
5
|
? { text: (resultRef) => ({ complete: { value: { textRef: resultRef } } }) }
|
|
6
6
|
: { text: (resultRef) => ({ complete: { value: { textRef: resultRef } } }), structured: { schema: config.outputSchema, onParsed: (value) => ({ complete: { value: value } }) } });
|
|
7
|
-
builder.addReActLoopStep('react', { ...(config.task === undefined ? {} : { task: config.task }), instruction: config.instruction, ...(config.inputs === undefined ? {} : { inputs: config.inputs }), ...(config.toolAllow === undefined ? {} : { toolAllow: config.toolAllow }), ...(config.maxToolsPerTurn === undefined ? {} : { maxToolsPerTurn: config.maxToolsPerTurn }), ...(config.serialTools === undefined ? {} : { serialTools: config.serialTools }), ...(config.stopAfterFirstSerialTool === undefined ? {} : { stopAfterFirstSerialTool: config.stopAfterFirstSerialTool }), ...(config.maxTruncationRetries === undefined ? {} : { maxTruncationRetries: config.maxTruncationRetries }), ...(config.maxTurns === undefined ? {} : { maxTurns: config.maxTurns }), ...(config.resetTurnsOnEntry === undefined ? {} : { resetTurnsOnEntry: config.resetTurnsOnEntry }), ...(config.outputSchema === undefined ? {} : { outputSchema: config.outputSchema }), ...(config.requirements === undefined ? {} : { requirements: config.requirements }), ...(config.toolApproval === undefined ? {} : { toolApproval: config.toolApproval }), onFinish });
|
|
7
|
+
builder.addReActLoopStep('react', { ...(config.task === undefined ? {} : { task: config.task }), instruction: config.instruction, ...(config.prepareQuestion === undefined ? {} : { prepareQuestion: config.prepareQuestion }), ...(config.onHumanReply === undefined ? {} : { onHumanReply: config.onHumanReply }), ...(config.inputs === undefined ? {} : { inputs: config.inputs }), ...(config.toolAllow === undefined ? {} : { toolAllow: config.toolAllow }), ...(config.maxToolsPerTurn === undefined ? {} : { maxToolsPerTurn: config.maxToolsPerTurn }), ...(config.maxReadOnlyRounds === undefined ? {} : { maxReadOnlyRounds: config.maxReadOnlyRounds }), ...(config.serialTools === undefined ? {} : { serialTools: config.serialTools }), ...(config.stopAfterFirstSerialTool === undefined ? {} : { stopAfterFirstSerialTool: config.stopAfterFirstSerialTool }), ...(config.maxTruncationRetries === undefined ? {} : { maxTruncationRetries: config.maxTruncationRetries }), ...(config.maxTurns === undefined ? {} : { maxTurns: config.maxTurns }), ...(config.resetTurnsOnEntry === undefined ? {} : { resetTurnsOnEntry: config.resetTurnsOnEntry }), ...(config.outputSchema === undefined ? {} : { outputSchema: config.outputSchema }), ...(config.requirements === undefined ? {} : { requirements: config.requirements }), ...(config.toolApproval === undefined ? {} : { toolApproval: config.toolApproval }), onFinish });
|
|
8
8
|
config.extend?.(builder);
|
|
9
9
|
});
|
|
10
10
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/models/router.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JsonValue, LLMRequestProjection, PrivacyLabel, ProvenanceRef } from '../core/types.js';
|
|
2
2
|
export type ReasoningLevel = 'low' | 'medium' | 'high';
|
|
3
3
|
export interface ModelCapabilities {
|
|
4
|
+
imageInput?: boolean;
|
|
4
5
|
toolCalling?: boolean;
|
|
5
6
|
structuredOutput?: boolean;
|
|
6
7
|
reasoning?: ReasoningLevel;
|
|
@@ -33,6 +34,7 @@ export interface ModelAdapter {
|
|
|
33
34
|
request: LLMRequestProjection;
|
|
34
35
|
signal: AbortSignal;
|
|
35
36
|
onObservation?: (chunk: string) => void;
|
|
37
|
+
onReasoning?: (chunk: string) => void;
|
|
36
38
|
model?: string;
|
|
37
39
|
outputSchema?: JsonValue;
|
|
38
40
|
maxOutputTokens?: number;
|
package/dist/models/router.js
CHANGED
|
@@ -6,7 +6,7 @@ function isModelCandidate(value) {
|
|
|
6
6
|
if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities))
|
|
7
7
|
return false;
|
|
8
8
|
const modelCapabilities = capabilities;
|
|
9
|
-
return typeof candidate.id === 'string' && candidate.id.length > 0 && typeof candidate.providerId === 'string' && candidate.providerId.length > 0 && Array.isArray(candidate.tasks) && candidate.tasks.length > 0 && candidate.tasks.every((task) => typeof task === 'string' && task.length > 0) && typeof candidate.priority === 'number' && Number.isFinite(candidate.priority) && Number.isInteger(modelCapabilities.maxContextTokens) && modelCapabilities.maxContextTokens > 0 && (modelCapabilities.maxOutputTokens === undefined || (Number.isInteger(modelCapabilities.maxOutputTokens) && modelCapabilities.maxOutputTokens > 0)) && (modelCapabilities.local === undefined || typeof modelCapabilities.local === 'boolean') && (modelCapabilities.toolCalling === undefined || typeof modelCapabilities.toolCalling === 'boolean') && (modelCapabilities.structuredOutput === undefined || typeof modelCapabilities.structuredOutput === 'boolean') && (modelCapabilities.reasoning === undefined || ['low', 'medium', 'high'].includes(String(modelCapabilities.reasoning))) && (candidate.adapter === undefined || (typeof candidate.adapter === 'object' && candidate.adapter !== null && typeof candidate.adapter.executeAttempt === 'function'));
|
|
9
|
+
return typeof candidate.id === 'string' && candidate.id.length > 0 && typeof candidate.providerId === 'string' && candidate.providerId.length > 0 && Array.isArray(candidate.tasks) && candidate.tasks.length > 0 && candidate.tasks.every((task) => typeof task === 'string' && task.length > 0) && typeof candidate.priority === 'number' && Number.isFinite(candidate.priority) && Number.isInteger(modelCapabilities.maxContextTokens) && modelCapabilities.maxContextTokens > 0 && (modelCapabilities.maxOutputTokens === undefined || (Number.isInteger(modelCapabilities.maxOutputTokens) && modelCapabilities.maxOutputTokens > 0)) && (modelCapabilities.imageInput === undefined || typeof modelCapabilities.imageInput === 'boolean') && (modelCapabilities.local === undefined || typeof modelCapabilities.local === 'boolean') && (modelCapabilities.toolCalling === undefined || typeof modelCapabilities.toolCalling === 'boolean') && (modelCapabilities.structuredOutput === undefined || typeof modelCapabilities.structuredOutput === 'boolean') && (modelCapabilities.reasoning === undefined || ['low', 'medium', 'high'].includes(String(modelCapabilities.reasoning))) && (candidate.adapter === undefined || (typeof candidate.adapter === 'object' && candidate.adapter !== null && typeof candidate.adapter.executeAttempt === 'function'));
|
|
10
10
|
}
|
|
11
11
|
export class InMemoryModelRegistry {
|
|
12
12
|
candidates = [];
|
|
@@ -21,7 +21,7 @@ export class InMemoryModelRegistry {
|
|
|
21
21
|
}
|
|
22
22
|
/** Conservative admission estimate used before a provider attempt is started. */
|
|
23
23
|
export function estimateProjectionTokens(projection) {
|
|
24
|
-
return Math.ceil(Buffer.byteLength(JSON.stringify(projection.blocks), 'utf8') / 4);
|
|
24
|
+
return Math.ceil(Buffer.byteLength(JSON.stringify(projection.blocks), 'utf8') / 4) + (projection.images?.length ?? 0) * 4096;
|
|
25
25
|
}
|
|
26
26
|
export class ModelRouter {
|
|
27
27
|
registry;
|
|
@@ -6,7 +6,7 @@ import { PulseSession } from '../dsl/session.js';
|
|
|
6
6
|
import type { ProgramRef } from '../dsl/templates.js';
|
|
7
7
|
import { FactInbox, ObservationInbox, type FactInboxDedupeArchive, type FactInboxDedupeArchiveBatch } from '../core/inbox.js';
|
|
8
8
|
import { EffectOutbox } from '../storage/outbox.js';
|
|
9
|
-
import { type RuntimePersistenceBackend, type RuntimePersistenceSnapshot } from '../storage/persistence.js';
|
|
9
|
+
import { type RuntimePersistenceBackend, type RuntimePersistenceCompatibility, type RuntimePersistenceSnapshot } from '../storage/persistence.js';
|
|
10
10
|
import { type RuntimeLogSink, type RuntimeSessionStore, type SessionLogExport, type SessionLogExportOptions } from '../storage/session.js';
|
|
11
11
|
import { ResourceLockManager } from './locks.js';
|
|
12
12
|
import { type Mutation } from '../core/mutations.js';
|
|
@@ -186,6 +186,11 @@ export interface RuntimeConfig {
|
|
|
186
186
|
hostPolicy?: ModelHostPolicy;
|
|
187
187
|
modelRouter?: ModelRouter;
|
|
188
188
|
tools?: RuntimeToolRegistry;
|
|
189
|
+
recoveryBuildMigration?: {
|
|
190
|
+
from: string;
|
|
191
|
+
to: string;
|
|
192
|
+
};
|
|
193
|
+
recoveryFingerprints?: RuntimePersistenceCompatibility['fingerprints'];
|
|
189
194
|
toolVersions?: Record<string, string>;
|
|
190
195
|
policyVersion?: string;
|
|
191
196
|
routerVersion?: string;
|
|
@@ -298,6 +303,8 @@ export declare class PulseRuntime {
|
|
|
298
303
|
private readonly enforcingRecoveryPrograms;
|
|
299
304
|
private readonly toolVersions;
|
|
300
305
|
private readonly recoveryCompatibility;
|
|
306
|
+
private readonly recoveryBuildMigration;
|
|
307
|
+
private readonly recoveryFingerprints;
|
|
301
308
|
private readonly policyVersion;
|
|
302
309
|
private readonly routerVersion;
|
|
303
310
|
private readonly budget;
|
|
@@ -381,6 +388,13 @@ export declare class PulseRuntime {
|
|
|
381
388
|
private wakeError;
|
|
382
389
|
private tickBudget;
|
|
383
390
|
static restore(backend: RuntimePersistenceBackend, config?: Omit<RuntimeConfig, 'persistence'>): Promise<PulseRuntime>;
|
|
391
|
+
private lazyResultCache;
|
|
392
|
+
private lazyResultSizes;
|
|
393
|
+
private lazyResultBytes;
|
|
394
|
+
private lazyResultRefs;
|
|
395
|
+
private installLazyResults;
|
|
396
|
+
/** Only the active step's referenced bodies enter its pure snapshot. Older refs remain available through Host inspection. */
|
|
397
|
+
private stepState;
|
|
384
398
|
constructor(config?: RuntimeConfig);
|
|
385
399
|
register(program: LaneProgram): void;
|
|
386
400
|
/** Register the program used for urgent, concurrent human interactions. */
|
|
@@ -416,6 +430,7 @@ export declare class PulseRuntime {
|
|
|
416
430
|
private assertRecoveryPrograms;
|
|
417
431
|
private persistenceCompatibility;
|
|
418
432
|
private assertRecoveryCompatibility;
|
|
433
|
+
private assertRecoveryFingerprints;
|
|
419
434
|
private currentToolVersions;
|
|
420
435
|
private tryEmit;
|
|
421
436
|
private rejectHostCommand;
|
|
@@ -12,6 +12,7 @@ import { EffectOutbox } from '../storage/outbox.js';
|
|
|
12
12
|
import { exportRuntimeCheckpoint, exportRuntimePersistence, externalizeRuntimeResultBodies, externalizeRuntimeSnapshotBodies, hydrateRuntimeResultBodies, hydrateRuntimeSnapshotBodies, importRuntimePersistence, validateRuntimePersistenceSnapshot, withRuntimePersistenceIntegrity } from '../storage/persistence.js';
|
|
13
13
|
import { exportRuntimeLog, exportRuntimeLogTo, exportWarmStartSession } from '../storage/session.js';
|
|
14
14
|
import { ResourceLockManager } from './locks.js';
|
|
15
|
+
import { extractToolImage } from '../core/tool-image.js';
|
|
15
16
|
import { appendRuntimeEvent, normalizeRuntimeEvent } from '../core/events.js';
|
|
16
17
|
import { apply, forkRuntimeStateForAdmission } from '../core/mutations.js';
|
|
17
18
|
import { ContextMerger } from '../context/merger.js';
|
|
@@ -25,6 +26,12 @@ import { runtimeErrorFromCause } from '../core/errors.js';
|
|
|
25
26
|
import { RuntimeToolRegistry } from '../tools/registry.js';
|
|
26
27
|
import { SchedulerDecisionCoordinator, schedulerDecisionCandidateFromLane } from './decision.js';
|
|
27
28
|
import { HumanArbitrationCoordinator, ruleHumanArbitration } from './human-arbitration.js';
|
|
29
|
+
function repeatedControlFailure(error, count) {
|
|
30
|
+
const details = { lastError: error, consecutiveControlErrors: count };
|
|
31
|
+
if (error.code === 'CONTEXT_TOO_LARGE')
|
|
32
|
+
return { code: error.code, message: `${error.message} Configure historyCompaction with an Effect-backed summary, or restore a checkpoint with a smaller history before continuing.`, details };
|
|
33
|
+
return { code: 'CONTROL_ERROR_LOOP', message: 'Lane exceeded the consecutive control error limit.', details };
|
|
34
|
+
}
|
|
28
35
|
function encodeEffectExecution(execution) {
|
|
29
36
|
const encoded = structuredClone(execution);
|
|
30
37
|
const artifact = execution.artifact;
|
|
@@ -461,7 +468,7 @@ export class PulseRuntime {
|
|
|
461
468
|
results = {
|
|
462
469
|
get: (resultRef) => {
|
|
463
470
|
const result = this.state.results.get(resultRef);
|
|
464
|
-
return result === undefined ? undefined : structuredClone(result);
|
|
471
|
+
return result === undefined ? undefined : { ...structuredClone(result), ...(result.value === undefined ? {} : { value: structuredClone(result.value) }) };
|
|
465
472
|
},
|
|
466
473
|
};
|
|
467
474
|
shuttingDown = false;
|
|
@@ -480,6 +487,8 @@ export class PulseRuntime {
|
|
|
480
487
|
enforcingRecoveryPrograms;
|
|
481
488
|
toolVersions;
|
|
482
489
|
recoveryCompatibility;
|
|
490
|
+
recoveryBuildMigration;
|
|
491
|
+
recoveryFingerprints;
|
|
483
492
|
policyVersion;
|
|
484
493
|
routerVersion;
|
|
485
494
|
budget;
|
|
@@ -567,15 +576,97 @@ export class PulseRuntime {
|
|
|
567
576
|
if (loaded?.snapshotBodies === 'external' && backend.snapshotStore === undefined)
|
|
568
577
|
throw new Error('RUNTIME_SNAPSHOT_STORE_REQUIRED');
|
|
569
578
|
const withSnapshots = loaded === undefined || backend.snapshotStore === undefined ? loaded : await hydrateRuntimeSnapshotBodies(loaded, backend.snapshotStore);
|
|
570
|
-
const snapshot = withSnapshots === undefined || backend.resultStore === undefined ? withSnapshots : await hydrateRuntimeResultBodies(withSnapshots, backend.resultStore);
|
|
579
|
+
const snapshot = withSnapshots === undefined || backend.resultStore === undefined || backend.lazyResultBodies && backend.resultStore.loadSync ? withSnapshots : await hydrateRuntimeResultBodies(withSnapshots, backend.resultStore);
|
|
571
580
|
if (snapshot?.resultBodies === 'external' && backend.resultStore === undefined)
|
|
572
581
|
throw new Error('RUNTIME_RESULT_STORE_REQUIRED');
|
|
573
582
|
const restoredConfig = snapshot === undefined ? config : { ...config, persistence: snapshot, ...(config.persistenceBackend === undefined || loaded?.integrity?.digest === undefined ? {} : { persistenceExpectedDigest: loaded.integrity.digest }) };
|
|
574
|
-
|
|
583
|
+
const runtime = new PulseRuntime({
|
|
575
584
|
...restoredConfig,
|
|
576
585
|
...(restoredConfig.factInboxDedupeArchive === undefined && backend.factInboxDedupeArchive !== undefined ? { factInboxDedupeArchive: backend.factInboxDedupeArchive } : {}),
|
|
577
586
|
...(restoredConfig.sessionStore === undefined && backend.sessionStore !== undefined ? { sessionStore: backend.sessionStore } : {}),
|
|
578
587
|
});
|
|
588
|
+
if (snapshot?.compatibility?.fingerprints)
|
|
589
|
+
runtime.assertRecoveryFingerprints(snapshot.compatibility);
|
|
590
|
+
if (backend.lazyResultBodies && backend.resultStore?.loadSync)
|
|
591
|
+
runtime.installLazyResults(backend, snapshot?.externalResultRefs ?? []);
|
|
592
|
+
return runtime;
|
|
593
|
+
}
|
|
594
|
+
lazyResultCache = new Map();
|
|
595
|
+
lazyResultSizes = new Map();
|
|
596
|
+
lazyResultBytes = 0;
|
|
597
|
+
lazyResultRefs = new Set();
|
|
598
|
+
installLazyResults(backend, persistedRefs) {
|
|
599
|
+
const store = backend.resultStore;
|
|
600
|
+
if (!store?.loadSync)
|
|
601
|
+
return;
|
|
602
|
+
for (const [ref, result] of this.state.results) {
|
|
603
|
+
if (this.lazyResultRefs.has(ref) || !persistedRefs.includes(ref))
|
|
604
|
+
continue;
|
|
605
|
+
this.lazyResultRefs.add(ref);
|
|
606
|
+
delete result.value;
|
|
607
|
+
Object.defineProperty(result, 'value', { configurable: true, enumerable: false, get: () => {
|
|
608
|
+
let value = this.lazyResultCache.get(ref);
|
|
609
|
+
if (value === undefined) {
|
|
610
|
+
value = store.loadSync(ref);
|
|
611
|
+
if (value === undefined)
|
|
612
|
+
throw new Error(`RUNTIME_RESULT_NOT_FOUND:${ref}`);
|
|
613
|
+
if (result.contentHash && contentHash(value) !== result.contentHash)
|
|
614
|
+
throw new Error(`RUNTIME_RESULT_INTEGRITY_MISMATCH:${ref}`);
|
|
615
|
+
this.lazyResultCache.set(ref, value);
|
|
616
|
+
const bytes = Buffer.byteLength(stableSerialize(value));
|
|
617
|
+
this.lazyResultSizes.set(ref, bytes);
|
|
618
|
+
this.lazyResultBytes += bytes;
|
|
619
|
+
}
|
|
620
|
+
while ((this.lazyResultBytes > 4_000_000 || this.lazyResultCache.size > 128) && this.lazyResultCache.size > 1) {
|
|
621
|
+
const oldest = this.lazyResultCache.keys().next().value;
|
|
622
|
+
this.lazyResultBytes -= this.lazyResultSizes.get(oldest) ?? 0;
|
|
623
|
+
this.lazyResultCache.delete(oldest);
|
|
624
|
+
this.lazyResultSizes.delete(oldest);
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
} });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/** Only the active step's referenced bodies enter its pure snapshot. Older refs remain available through Host inspection. */
|
|
631
|
+
stepState(lane) {
|
|
632
|
+
const copy = structuredClone(this.state);
|
|
633
|
+
if (!this.lazyResultRefs.size)
|
|
634
|
+
return copy;
|
|
635
|
+
const refs = new Set();
|
|
636
|
+
const collect = (value) => {
|
|
637
|
+
if (typeof value === 'string') {
|
|
638
|
+
if (this.state.results.has(value))
|
|
639
|
+
refs.add(value);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
for (const item of value)
|
|
644
|
+
collect(item);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (value && typeof value === 'object')
|
|
648
|
+
for (const item of Object.values(value))
|
|
649
|
+
collect(item);
|
|
650
|
+
};
|
|
651
|
+
collect(lane.resume.locals);
|
|
652
|
+
collect(lane.context.state);
|
|
653
|
+
collect(lane.pendingResumeInput);
|
|
654
|
+
collect(lane.pendingHumanInputs);
|
|
655
|
+
collect([...(lane.visibleResultRefs ?? [])].slice(-64));
|
|
656
|
+
collect(lane.context.history.slice(-64));
|
|
657
|
+
const agent = this.state.agents.get(lane.agentId);
|
|
658
|
+
if (agent) {
|
|
659
|
+
collect(agent.globalVersions.get(agent.latestGlobalVersion));
|
|
660
|
+
collect(agent.globalVersions.get(lane.contextSnapshotVersion));
|
|
661
|
+
}
|
|
662
|
+
for (const ref of refs) {
|
|
663
|
+
const source = this.state.results.get(ref), target = copy.results.get(ref);
|
|
664
|
+
if (source && target && source.value !== undefined) {
|
|
665
|
+
target.value = structuredClone(source.value);
|
|
666
|
+
collect(target.value);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return copy;
|
|
579
670
|
}
|
|
580
671
|
constructor(config = {}) {
|
|
581
672
|
validateRuntimeConfig(config);
|
|
@@ -588,6 +679,8 @@ export class PulseRuntime {
|
|
|
588
679
|
this.modelRouter = config.modelRouter ?? new ModelRouter(this.models, config.hostPolicy);
|
|
589
680
|
this.tools = config.tools ?? new RuntimeToolRegistry();
|
|
590
681
|
this.toolVersions = { ...(config.toolVersions ?? {}) };
|
|
682
|
+
this.recoveryBuildMigration = config.recoveryBuildMigration;
|
|
683
|
+
this.recoveryFingerprints = config.recoveryFingerprints;
|
|
591
684
|
this.policyVersion = config.policyVersion;
|
|
592
685
|
this.routerVersion = config.routerVersion;
|
|
593
686
|
this.state = restored?.state ?? createRuntimeState(config.maxTotalLanes ?? 64, { ...(config.maxQueuedEffects === undefined ? {} : { maxQueuedEffects: config.maxQueuedEffects }), ...(config.maxRunning === undefined ? {} : { maxRunning: config.maxRunning }), ...(config.forkAffinity === undefined ? {} : { forkAffinity: config.forkAffinity }), ...(config.historySoftTokens === undefined ? {} : { historySoftTokens: config.historySoftTokens }), ...(config.historyHardTokens === undefined ? {} : { historyHardTokens: config.historyHardTokens }), ...(config.maxResultSummaryBytes === undefined ? {} : { maxResultSummaryBytes: config.maxResultSummaryBytes }), ...(config.trustedSanitizerIds === undefined ? {} : { trustedSanitizerIds: config.trustedSanitizerIds }) });
|
|
@@ -1101,6 +1194,10 @@ export class PulseRuntime {
|
|
|
1101
1194
|
return compacted;
|
|
1102
1195
|
}
|
|
1103
1196
|
async persist(backend) {
|
|
1197
|
+
if (backend.eventArchive && backend.checkpointEventInterval && this.state.events.length >= backend.checkpointEventInterval) {
|
|
1198
|
+
await this.checkpoint(backend);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1104
1201
|
const persistedPolicy = this.storagePolicy.clone();
|
|
1105
1202
|
persistedPolicy.markPersisted();
|
|
1106
1203
|
// The saved state is already authoritative: restore does not replay the
|
|
@@ -1122,6 +1219,8 @@ export class PulseRuntime {
|
|
|
1122
1219
|
if (journalWatermark !== undefined && journalWatermark > this.mutationLog.watermark && journalWatermark <= this.mutationLog.lastSequence)
|
|
1123
1220
|
this.mutationLog.truncateThrough(journalWatermark);
|
|
1124
1221
|
}
|
|
1222
|
+
if (backend.lazyResultBodies && backend.resultStore?.loadSync)
|
|
1223
|
+
this.installLazyResults(backend, snapshot.externalResultRefs ?? []);
|
|
1125
1224
|
this.storagePolicy.markPersisted();
|
|
1126
1225
|
this.markArtifactsPersisted();
|
|
1127
1226
|
this.syncStoragePolicy();
|
|
@@ -1169,11 +1268,14 @@ export class PulseRuntime {
|
|
|
1169
1268
|
}
|
|
1170
1269
|
}
|
|
1171
1270
|
const eventWatermark = options.compactEventsThrough ?? this.state.events.at(-1)?.seq;
|
|
1271
|
+
let archivedEventIds = [];
|
|
1172
1272
|
if (backend.eventArchive !== undefined && eventWatermark !== undefined) {
|
|
1173
1273
|
const fromSeq = (this.state.eventsCompactedThrough ?? 0) + 1;
|
|
1174
1274
|
const events = this.state.events.filter((event) => event.seq >= fromSeq && event.seq <= eventWatermark);
|
|
1175
1275
|
if (events.length)
|
|
1176
1276
|
await backend.eventArchive.append(events);
|
|
1277
|
+
archivedEventIds = events.map(event => event.id);
|
|
1278
|
+
persistedPolicy.forgetArchivedEvents(archivedEventIds);
|
|
1177
1279
|
}
|
|
1178
1280
|
const exported = exportRuntimeCheckpoint(this.persistenceState(), this.mutationLog, this.outbox, this.quarantine, persistedPolicy, eventWatermark === undefined ? {} : { compactEventsThrough: eventWatermark }, this.factInbox.snapshot(), this.persistenceCompatibility());
|
|
1179
1281
|
const archived = backend.eventArchive === undefined || eventWatermark === undefined ? exported : withRuntimePersistenceIntegrity({ ...exported, eventArchive: { through: eventWatermark } });
|
|
@@ -1185,7 +1287,6 @@ export class PulseRuntime {
|
|
|
1185
1287
|
this.persistenceDigest = snapshot.integrity?.digest;
|
|
1186
1288
|
this.storagePolicy.markPersisted();
|
|
1187
1289
|
this.markArtifactsPersisted();
|
|
1188
|
-
this.syncStoragePolicy();
|
|
1189
1290
|
const watermark = snapshot.checkpoint?.logWatermark ?? 0;
|
|
1190
1291
|
if (watermark > 0 && this.mutationLog.lastSequence >= watermark)
|
|
1191
1292
|
this.mutationLog.truncateThrough(watermark);
|
|
@@ -1193,6 +1294,10 @@ export class PulseRuntime {
|
|
|
1193
1294
|
this.state.events = this.state.events.filter((event) => event.seq > eventWatermark);
|
|
1194
1295
|
this.state.eventsCompactedThrough = Math.max(this.state.eventsCompactedThrough ?? 0, eventWatermark);
|
|
1195
1296
|
}
|
|
1297
|
+
this.storagePolicy.forgetArchivedEvents(archivedEventIds);
|
|
1298
|
+
this.syncStoragePolicy();
|
|
1299
|
+
if (backend.lazyResultBodies && backend.resultStore?.loadSync)
|
|
1300
|
+
this.installLazyResults(backend, snapshot.externalResultRefs ?? []);
|
|
1196
1301
|
this.schedulePersistence();
|
|
1197
1302
|
return snapshot;
|
|
1198
1303
|
}
|
|
@@ -1242,6 +1347,7 @@ export class PulseRuntime {
|
|
|
1242
1347
|
schemaVersion: 1,
|
|
1243
1348
|
programVersions: Object.fromEntries([...this.programs.entries()].map(([key, program]) => [key, program.version])),
|
|
1244
1349
|
toolVersions,
|
|
1350
|
+
...(this.recoveryFingerprints ? { fingerprints: this.recoveryFingerprints } : {}),
|
|
1245
1351
|
...(this.policyVersion === undefined ? {} : { policyVersion: this.policyVersion }),
|
|
1246
1352
|
...(this.routerVersion === undefined ? {} : { routerVersion: this.routerVersion }),
|
|
1247
1353
|
};
|
|
@@ -1252,6 +1358,7 @@ export class PulseRuntime {
|
|
|
1252
1358
|
if (!program || program.version !== version)
|
|
1253
1359
|
throw new Error(`PROGRAM_VERSION_UNAVAILABLE:${key}`);
|
|
1254
1360
|
}
|
|
1361
|
+
this.assertRecoveryFingerprints(expected);
|
|
1255
1362
|
const toolVersions = this.currentToolVersions();
|
|
1256
1363
|
for (const [name, version] of Object.entries(expected.toolVersions))
|
|
1257
1364
|
if (toolVersions[name] !== version)
|
|
@@ -1261,6 +1368,19 @@ export class PulseRuntime {
|
|
|
1261
1368
|
if (expected.routerVersion !== undefined && this.routerVersion !== expected.routerVersion)
|
|
1262
1369
|
throw new Error(`ROUTER_VERSION_UNAVAILABLE:${expected.routerVersion}`);
|
|
1263
1370
|
}
|
|
1371
|
+
assertRecoveryFingerprints(expected) {
|
|
1372
|
+
if (expected.fingerprints) {
|
|
1373
|
+
if (!this.recoveryFingerprints)
|
|
1374
|
+
throw new Error('RECOVERY_FINGERPRINT_REQUIRED');
|
|
1375
|
+
if (expected.fingerprints.build !== this.recoveryFingerprints.build && (this.recoveryBuildMigration?.from !== expected.fingerprints.build || this.recoveryBuildMigration?.to !== this.recoveryFingerprints.build))
|
|
1376
|
+
throw new Error(`RECOVERY_BUILD_MIGRATION_REQUIRED:from=${expected.fingerprints.build}:to=${this.recoveryFingerprints.build}`);
|
|
1377
|
+
for (const [name, hash] of Object.entries(expected.fingerprints.tools))
|
|
1378
|
+
if (this.recoveryFingerprints.tools[name] !== hash)
|
|
1379
|
+
throw new Error(`TOOL_SCHEMA_INCOMPATIBLE:${name}`);
|
|
1380
|
+
if (expected.fingerprints.policy !== this.recoveryFingerprints.policy)
|
|
1381
|
+
throw new Error('RECOVERY_POLICY_INCOMPATIBLE');
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1264
1384
|
currentToolVersions() {
|
|
1265
1385
|
return { ...this.toolVersions, ...Object.fromEntries(this.tools.list().map((manifest) => [manifest.name, manifest.version])) };
|
|
1266
1386
|
}
|
|
@@ -1355,7 +1475,10 @@ export class PulseRuntime {
|
|
|
1355
1475
|
value = null;
|
|
1356
1476
|
artifact = artifactOutput(detailed.output);
|
|
1357
1477
|
}
|
|
1358
|
-
|
|
1478
|
+
const image = extractToolImage(value);
|
|
1479
|
+
value = image.value;
|
|
1480
|
+
artifact = image.artifact ?? artifact;
|
|
1481
|
+
return { value, ...(artifact === undefined ? {} : { artifact }), ...(detailed.normalized === undefined ? {} : { normalized: image.artifact ? value : strictJsonValue(detailed.normalized) }), ...(detailed.summary === undefined ? {} : { summary: strictJsonValue(detailed.summary) }), sideEffectState: isSideEffectful(definition.manifest.sideEffectPolicy) ? 'applied' : 'none', executionState: 'succeeded', status: 'succeeded', ...(executionRef === undefined ? {} : { executionRef }), metadata: { toolVersion: detailed.manifest.version, retrySafety: detailed.manifest.retrySafety, defaultTimeoutMs: detailed.manifest.defaultTimeoutMs, observationCount: observations.length, ...(artifact === undefined ? {} : { artifactMediaType: artifact.mediaType }) }, ...(observations.length ? { observations } : {}) };
|
|
1359
1482
|
}
|
|
1360
1483
|
catch (cause) {
|
|
1361
1484
|
if (signal.aborted && isSideEffectful(definition.manifest.sideEffectPolicy))
|
|
@@ -1443,7 +1566,7 @@ export class PulseRuntime {
|
|
|
1443
1566
|
this.mutationLog.append(transactionId, mutations, this.state.now);
|
|
1444
1567
|
}
|
|
1445
1568
|
enqueueFact(fact, eventId, urgent = false) {
|
|
1446
|
-
const candidateInbox = FactInbox.fromSnapshot(this.factInbox.snapshot());
|
|
1569
|
+
const candidateInbox = FactInbox.fromSnapshot(this.factInbox.snapshot(), this.factInboxDedupeArchive ? { dedupeArchive: this.factInboxDedupeArchive } : {});
|
|
1447
1570
|
if (!(urgent ? candidateInbox.enqueueUrgent(fact, eventId) : candidateInbox.enqueue(fact, eventId)))
|
|
1448
1571
|
return false;
|
|
1449
1572
|
const candidatePolicy = this.storagePolicy.clone();
|
|
@@ -1908,7 +2031,7 @@ export class PulseRuntime {
|
|
|
1908
2031
|
continue;
|
|
1909
2032
|
}
|
|
1910
2033
|
let output;
|
|
1911
|
-
const stepContext = { lane: stepLane, state:
|
|
2034
|
+
const stepContext = { lane: stepLane, state: this.stepState(lane), ...(lane.pendingResumeInput ? { resumeInput: structuredClone(lane.pendingResumeInput) } : {}), ...(lane.pendingHumanInputs?.length ? { humanInputs: structuredClone(lane.pendingHumanInputs) } : {}), now: this.state.now, observe: (event) => { this.observationInbox.enqueue({ ...event, agentId: lane.agentId, laneId: lane.id, timestamp: this.state.now }); this.notifyActivity(); } };
|
|
1912
2035
|
try {
|
|
1913
2036
|
output = withPureStepGuard(() => lane.series || program.seriesMember ? this.seriesStep(program, stepContext, lane.series) : program.step(stepContext));
|
|
1914
2037
|
}
|
|
@@ -1942,7 +2065,7 @@ export class PulseRuntime {
|
|
|
1942
2065
|
const consecutive = (lane.consecutiveControlErrors ?? 0) + 1;
|
|
1943
2066
|
const controlInput = { type: 'control_error', error: rejection, ...(lane.pendingResumeInput ? { original: lane.pendingResumeInput } : {}) };
|
|
1944
2067
|
if (consecutive >= this.maxConsecutiveControlErrors)
|
|
1945
|
-
this.failLane(lane,
|
|
2068
|
+
this.failLane(lane, repeatedControlFailure(rejection, consecutive));
|
|
1946
2069
|
else
|
|
1947
2070
|
this.commitLaneControlInput(lane, controlInput, { type: 'step.rejected', laneId: lane.id, data: rejection }, { consecutiveControlErrors: consecutive });
|
|
1948
2071
|
progressed++;
|
|
@@ -1957,7 +2080,7 @@ export class PulseRuntime {
|
|
|
1957
2080
|
}
|
|
1958
2081
|
else {
|
|
1959
2082
|
if (consecutive >= this.maxConsecutiveControlErrors)
|
|
1960
|
-
this.failLane(lane,
|
|
2083
|
+
this.failLane(lane, repeatedControlFailure(result.rejection, consecutive));
|
|
1961
2084
|
else
|
|
1962
2085
|
this.commitLaneControlInput(lane, controlInput, { type: 'step.rejected', laneId: lane.id, data: result.rejection }, { consecutiveControlErrors: consecutive });
|
|
1963
2086
|
}
|
|
@@ -2501,7 +2624,7 @@ export class PulseRuntime {
|
|
|
2501
2624
|
const artifact = prepareArtifactPublication(this.state, { ...execution.artifact, laneId: effect.ownerLaneId, derivedFrom: [...(effect.derivedFrom ?? []), ...(execution.artifact.derivedFrom ?? [])] });
|
|
2502
2625
|
publishedArtifact = artifact;
|
|
2503
2626
|
resultDerivedFrom = [...resultDerivedFrom, { kind: 'artifact', ref: artifact.ref }];
|
|
2504
|
-
effectiveExecution = { ...effectiveExecution, value: { artifactRef: artifact.ref }, privacy: artifact.privacy, ...(artifact.privacyTaints === undefined ? {} : { privacyTaints: artifact.privacyTaints }) };
|
|
2627
|
+
effectiveExecution = { ...effectiveExecution, value: { ...(effectiveExecution.value && typeof effectiveExecution.value === 'object' && !Array.isArray(effectiveExecution.value) ? effectiveExecution.value : {}), artifactRef: artifact.ref }, privacy: artifact.privacy, ...(artifact.privacyTaints === undefined ? {} : { privacyTaints: artifact.privacyTaints }) };
|
|
2505
2628
|
}
|
|
2506
2629
|
catch (cause) {
|
|
2507
2630
|
effectiveExecution = { ...effectiveExecution, status: 'failed', executionState: 'failed', error: runtimeErrorFromCause(cause, 'ARTIFACT_PUBLICATION_FAILED') };
|
|
@@ -2543,6 +2666,8 @@ export class PulseRuntime {
|
|
|
2543
2666
|
effect.retryPolicy = { maxAttempts: Math.max(1, eligible), initialBackoffMs: 0, maxBackoffMs: 0, jitter: false };
|
|
2544
2667
|
}
|
|
2545
2668
|
const attemptMetadata = execution.metadata && typeof execution.metadata === 'object' && !Array.isArray(execution.metadata) ? execution.metadata : undefined;
|
|
2669
|
+
if (attempt && typeof attemptMetadata?.providerAttempts === 'number')
|
|
2670
|
+
attempt.providerAttempts = attemptMetadata.providerAttempts;
|
|
2546
2671
|
const selectedModel = attemptMetadata?.selected && typeof attemptMetadata.selected === 'object' && !Array.isArray(attemptMetadata.selected) ? attemptMetadata.selected : undefined;
|
|
2547
2672
|
if (attempt && typeof selectedModel?.id === 'string')
|
|
2548
2673
|
attempt.modelId = selectedModel.id;
|
|
@@ -3621,6 +3746,20 @@ export class PulseRuntime {
|
|
|
3621
3746
|
...(observation.outcome?.resultRef === undefined ? [] : [observation.outcome.resultRef]),
|
|
3622
3747
|
...(observation.outcome?.rejectedOutputRefs ?? []),
|
|
3623
3748
|
]);
|
|
3749
|
+
// A failed/cancelled child may have durable tool effects without a
|
|
3750
|
+
// completion ResultRef. Expose only direct same-agent child receipts.
|
|
3751
|
+
for (const observation of Object.values(observations)) {
|
|
3752
|
+
if (observation.state === 'pending' || observation.target.kind !== 'lane')
|
|
3753
|
+
continue;
|
|
3754
|
+
const child = this.state.lanes.get(observation.target.id);
|
|
3755
|
+
if (!child || child.ownerLaneId !== nextLane.id || child.agentId !== nextLane.agentId)
|
|
3756
|
+
continue;
|
|
3757
|
+
for (const effectId of child.ownedEffectIds) {
|
|
3758
|
+
const effect = this.state.effects.get(effectId);
|
|
3759
|
+
if (effect?.kind === 'tool' && effect.outcome?.resultRef)
|
|
3760
|
+
refs.push(effect.outcome.resultRef);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3624
3763
|
if (!refs.length)
|
|
3625
3764
|
return;
|
|
3626
3765
|
if (nextLane.visibleResultRefs)
|
|
@@ -38,10 +38,16 @@ export interface RuntimePersistenceCompatibility {
|
|
|
38
38
|
toolVersions: Record<string, string>;
|
|
39
39
|
policyVersion?: string;
|
|
40
40
|
routerVersion?: string;
|
|
41
|
+
fingerprints?: {
|
|
42
|
+
build: string;
|
|
43
|
+
tools: Record<string, string>;
|
|
44
|
+
policy: string;
|
|
45
|
+
};
|
|
41
46
|
}
|
|
42
47
|
export interface RuntimeResultStore {
|
|
43
48
|
save(ref: string, value: JsonValue): Promise<void>;
|
|
44
49
|
load(ref: string): Promise<JsonValue | undefined>;
|
|
50
|
+
loadSync?(ref: string): JsonValue | undefined;
|
|
45
51
|
}
|
|
46
52
|
export interface RuntimeSnapshotStore {
|
|
47
53
|
save(ref: string, value: JsonValue): Promise<void>;
|
|
@@ -57,6 +63,7 @@ export declare class FileRuntimeContentStore implements RuntimeResultStore, Runt
|
|
|
57
63
|
constructor(directory: string);
|
|
58
64
|
save(ref: string, value: JsonValue): Promise<void>;
|
|
59
65
|
load(ref: string): Promise<JsonValue | undefined>;
|
|
66
|
+
loadSync(ref: string): JsonValue | undefined;
|
|
60
67
|
private pathFor;
|
|
61
68
|
private readEnvelope;
|
|
62
69
|
private withLock;
|
|
@@ -70,6 +77,7 @@ export declare class SqliteRuntimeContentStore implements RuntimeResultStore, Ru
|
|
|
70
77
|
constructor(filePath: string, namespace: 'result' | 'snapshot');
|
|
71
78
|
save(ref: string, value: JsonValue): Promise<void>;
|
|
72
79
|
load(ref: string): Promise<JsonValue | undefined>;
|
|
80
|
+
loadSync(ref: string): JsonValue | undefined;
|
|
73
81
|
close(): Promise<void>;
|
|
74
82
|
private open;
|
|
75
83
|
private enqueue;
|
|
@@ -127,10 +135,13 @@ export declare class SqliteRuntimeFactInboxDedupeArchive implements FactInboxDed
|
|
|
127
135
|
private enqueue;
|
|
128
136
|
}
|
|
129
137
|
export interface RuntimePersistenceBackend {
|
|
138
|
+
close?(): Promise<void>;
|
|
130
139
|
load(): Promise<RuntimePersistenceSnapshot | undefined>;
|
|
131
140
|
save(snapshot: RuntimePersistenceSnapshot, expectedDigest?: string): Promise<void>;
|
|
132
141
|
sessionStore?: RuntimeSessionStore;
|
|
133
142
|
resultStore?: RuntimeResultStore;
|
|
143
|
+
lazyResultBodies?: boolean;
|
|
144
|
+
checkpointEventInterval?: number;
|
|
134
145
|
snapshotStore?: RuntimeSnapshotStore;
|
|
135
146
|
eventArchive?: RuntimeEventArchive;
|
|
136
147
|
factInboxDedupeArchive?: FactInboxDedupeArchiveWriter;
|
|
@@ -102,6 +102,20 @@ export class FileRuntimeContentStore {
|
|
|
102
102
|
throw new Error('RUNTIME_CONTENT_REF_COLLISION');
|
|
103
103
|
return structuredClone(envelope.value);
|
|
104
104
|
}
|
|
105
|
+
loadSync(ref) {
|
|
106
|
+
let envelope;
|
|
107
|
+
try {
|
|
108
|
+
envelope = JSON.parse(readFileSync(this.pathFor(ref), 'utf8'));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error.code === 'ENOENT')
|
|
112
|
+
return undefined;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (envelope.schemaVersion !== 1 || envelope.ref !== ref || envelope.value === undefined)
|
|
116
|
+
throw new Error('INVALID_RUNTIME_CONTENT');
|
|
117
|
+
return envelope.value;
|
|
118
|
+
}
|
|
105
119
|
pathFor(ref) { return join(this.directory, `${createHash('sha256').update(ref).digest('hex')}.json`); }
|
|
106
120
|
async readEnvelope(path) {
|
|
107
121
|
try {
|
|
@@ -176,6 +190,10 @@ export class SqliteRuntimeContentStore {
|
|
|
176
190
|
return structuredClone(JSON.parse(row.payload));
|
|
177
191
|
});
|
|
178
192
|
}
|
|
193
|
+
loadSync(ref) {
|
|
194
|
+
const row = this.open().prepare('SELECT payload FROM runtime_content WHERE namespace = ? AND ref = ?').get(this.namespace, ref);
|
|
195
|
+
return row && typeof row.payload === 'string' ? JSON.parse(row.payload) : undefined;
|
|
196
|
+
}
|
|
179
197
|
async close() {
|
|
180
198
|
await this.enqueue(async () => {
|
|
181
199
|
this.database?.close();
|
|
@@ -888,8 +906,10 @@ export async function externalizeRuntimeResultBodies(snapshot, store) {
|
|
|
888
906
|
const states = [copy.state, ...(copy.checkpoint === undefined ? [] : [copy.checkpoint.state])];
|
|
889
907
|
for (const session of states) {
|
|
890
908
|
for (const [ref, result] of session.state.results) {
|
|
891
|
-
if (result.value === undefined)
|
|
909
|
+
if (result.value === undefined) {
|
|
910
|
+
refs.add(ref);
|
|
892
911
|
continue;
|
|
912
|
+
}
|
|
893
913
|
await store.save(ref, result.value);
|
|
894
914
|
delete result.value;
|
|
895
915
|
refs.add(ref);
|