@hunterzhu/pulse-runtime 0.5.1 → 0.7.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 +33 -0
- package/dist/dsl/program.js +480 -104
- package/dist/dsl/read-continuation.d.ts +9 -0
- package/dist/dsl/read-continuation.js +84 -0
- package/dist/dsl/templates.d.ts +7 -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 +155 -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/tools/registry.d.ts +1 -0
- package/dist/tools/registry.js +4 -1
- 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,13 +20,20 @@ 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
|
+
validateQuestion?: (toolName: string, input: JsonValue, ctx: StepContext<JsonValue>) => string | undefined;
|
|
25
|
+
onHumanReply?: (question: JsonValue, answer: JsonValue, ctx: StepContext<JsonValue>) => NextStepTarget<JsonValue> | undefined;
|
|
26
|
+
/** Bound investigation-only rounds for an implementation run; pagination is exempt. */
|
|
27
|
+
maxReadOnlyRounds?: number | ((ctx: StepContext<JsonValue>) => number | undefined);
|
|
23
28
|
serialTools?: string[];
|
|
24
29
|
stopAfterFirstSerialTool?: boolean;
|
|
25
30
|
/** Return a durable token when a new attempt should get a fresh turn budget. */
|
|
26
31
|
resetTurnsOnEntry?: (ctx: StepContext<JsonValue>) => string | number | undefined;
|
|
27
32
|
outputSchema?: ZodTypeAny;
|
|
28
33
|
requirements?: Record<string, JsonValue>;
|
|
34
|
+
manualApprovalFallback?: boolean;
|
|
29
35
|
toolApproval?: {
|
|
36
|
+
requiresApproval?: (calls: JsonValue) => boolean;
|
|
30
37
|
prompt: string | ((calls: JsonValue, ctx: StepContext<JsonValue>) => string);
|
|
31
38
|
onDenied?: (reason: string, ctx: StepContext<JsonValue>) => NextStepTarget<JsonValue>;
|
|
32
39
|
};
|
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.manualApprovalFallback ? { manualApprovalFallback: true } : {}), ...(config.prepareQuestion === undefined ? {} : { prepareQuestion: config.prepareQuestion }), ...(config.validateQuestion === undefined ? {} : { validateQuestion: config.validateQuestion }), ...(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
|
}
|
|
@@ -2493,6 +2616,12 @@ export class PulseRuntime {
|
|
|
2493
2616
|
}
|
|
2494
2617
|
let effectiveExecution = execution;
|
|
2495
2618
|
let outputError = error ?? execution.error;
|
|
2619
|
+
// A timeout remains a failure even when an aborted executor returns a generic cancellation.
|
|
2620
|
+
const timeoutReason = effect.cancelRequested?.reason;
|
|
2621
|
+
if (timeoutReason === 'ATTEMPT_TIMEOUT' || timeoutReason === 'TIMEOUT') {
|
|
2622
|
+
outputError = { code: timeoutReason, message: `Effect exceeded its execution time limit (${timeoutReason}).`, retryable: false };
|
|
2623
|
+
effectiveExecution = { ...execution, status: 'failed', executionState: 'failed', error: outputError };
|
|
2624
|
+
}
|
|
2496
2625
|
let resultDerivedFrom = [...(effect.derivedFrom ?? [])];
|
|
2497
2626
|
let publishedArtifact;
|
|
2498
2627
|
const rawInput = effect.input && typeof effect.input === 'object' && !Array.isArray(effect.input) ? effect.input : {};
|
|
@@ -2501,7 +2630,7 @@ export class PulseRuntime {
|
|
|
2501
2630
|
const artifact = prepareArtifactPublication(this.state, { ...execution.artifact, laneId: effect.ownerLaneId, derivedFrom: [...(effect.derivedFrom ?? []), ...(execution.artifact.derivedFrom ?? [])] });
|
|
2502
2631
|
publishedArtifact = artifact;
|
|
2503
2632
|
resultDerivedFrom = [...resultDerivedFrom, { kind: 'artifact', ref: artifact.ref }];
|
|
2504
|
-
effectiveExecution = { ...effectiveExecution, value: { artifactRef: artifact.ref }, privacy: artifact.privacy, ...(artifact.privacyTaints === undefined ? {} : { privacyTaints: artifact.privacyTaints }) };
|
|
2633
|
+
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
2634
|
}
|
|
2506
2635
|
catch (cause) {
|
|
2507
2636
|
effectiveExecution = { ...effectiveExecution, status: 'failed', executionState: 'failed', error: runtimeErrorFromCause(cause, 'ARTIFACT_PUBLICATION_FAILED') };
|
|
@@ -2543,6 +2672,8 @@ export class PulseRuntime {
|
|
|
2543
2672
|
effect.retryPolicy = { maxAttempts: Math.max(1, eligible), initialBackoffMs: 0, maxBackoffMs: 0, jitter: false };
|
|
2544
2673
|
}
|
|
2545
2674
|
const attemptMetadata = execution.metadata && typeof execution.metadata === 'object' && !Array.isArray(execution.metadata) ? execution.metadata : undefined;
|
|
2675
|
+
if (attempt && typeof attemptMetadata?.providerAttempts === 'number')
|
|
2676
|
+
attempt.providerAttempts = attemptMetadata.providerAttempts;
|
|
2546
2677
|
const selectedModel = attemptMetadata?.selected && typeof attemptMetadata.selected === 'object' && !Array.isArray(attemptMetadata.selected) ? attemptMetadata.selected : undefined;
|
|
2547
2678
|
if (attempt && typeof selectedModel?.id === 'string')
|
|
2548
2679
|
attempt.modelId = selectedModel.id;
|
|
@@ -3621,6 +3752,20 @@ export class PulseRuntime {
|
|
|
3621
3752
|
...(observation.outcome?.resultRef === undefined ? [] : [observation.outcome.resultRef]),
|
|
3622
3753
|
...(observation.outcome?.rejectedOutputRefs ?? []),
|
|
3623
3754
|
]);
|
|
3755
|
+
// A failed/cancelled child may have durable tool effects without a
|
|
3756
|
+
// completion ResultRef. Expose only direct same-agent child receipts.
|
|
3757
|
+
for (const observation of Object.values(observations)) {
|
|
3758
|
+
if (observation.state === 'pending' || observation.target.kind !== 'lane')
|
|
3759
|
+
continue;
|
|
3760
|
+
const child = this.state.lanes.get(observation.target.id);
|
|
3761
|
+
if (!child || child.ownerLaneId !== nextLane.id || child.agentId !== nextLane.agentId)
|
|
3762
|
+
continue;
|
|
3763
|
+
for (const effectId of child.ownedEffectIds) {
|
|
3764
|
+
const effect = this.state.effects.get(effectId);
|
|
3765
|
+
if (effect?.kind === 'tool' && effect.outcome?.resultRef)
|
|
3766
|
+
refs.push(effect.outcome.resultRef);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3624
3769
|
if (!refs.length)
|
|
3625
3770
|
return;
|
|
3626
3771
|
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;
|