@hunterzhu/pulse-runtime 0.1.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 +68 -0
- package/dist/context/builder.js +127 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/merger.d.ts +25 -0
- package/dist/context/merger.js +125 -0
- package/dist/core/actions.d.ts +1 -0
- package/dist/core/actions.js +1 -0
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +36 -0
- package/dist/core/events.d.ts +10 -0
- package/dist/core/events.js +24 -0
- package/dist/core/factory.d.ts +35 -0
- package/dist/core/factory.js +27 -0
- package/dist/core/inbox.d.ts +119 -0
- package/dist/core/inbox.js +217 -0
- package/dist/core/mutations.d.ts +80 -0
- package/dist/core/mutations.js +127 -0
- package/dist/core/records.d.ts +1 -0
- package/dist/core/records.js +1 -0
- package/dist/core/types.d.ts +615 -0
- package/dist/core/types.js +109 -0
- package/dist/dependencies/graph.d.ts +25 -0
- package/dist/dependencies/graph.js +92 -0
- package/dist/dependencies/index.d.ts +1 -0
- package/dist/dependencies/index.js +1 -0
- package/dist/dsl/context-proxy.d.ts +20 -0
- package/dist/dsl/context-proxy.js +64 -0
- package/dist/dsl/index.d.ts +4 -0
- package/dist/dsl/index.js +4 -0
- package/dist/dsl/program.d.ts +314 -0
- package/dist/dsl/program.js +756 -0
- package/dist/dsl/session.d.ts +45 -0
- package/dist/dsl/session.js +93 -0
- package/dist/dsl/templates-index.d.ts +1 -0
- package/dist/dsl/templates-index.js +1 -0
- package/dist/dsl/templates.d.ts +85 -0
- package/dist/dsl/templates.js +110 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/scopes.d.ts +38 -0
- package/dist/lifecycle/scopes.js +50 -0
- package/dist/lifecycle/watchdog.d.ts +16 -0
- package/dist/lifecycle/watchdog.js +66 -0
- package/dist/models/actions.d.ts +10 -0
- package/dist/models/actions.js +68 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models/router.d.ts +187 -0
- package/dist/models/router.js +353 -0
- package/dist/scheduler/clock.d.ts +45 -0
- package/dist/scheduler/clock.js +92 -0
- package/dist/scheduler/decision.d.ts +72 -0
- package/dist/scheduler/decision.js +63 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +6 -0
- package/dist/scheduler/locks.d.ts +18 -0
- package/dist/scheduler/locks.js +106 -0
- package/dist/scheduler/ready-queue.d.ts +32 -0
- package/dist/scheduler/ready-queue.js +40 -0
- package/dist/scheduler/runtime.d.ts +486 -0
- package/dist/scheduler/runtime.js +3445 -0
- package/dist/scheduler/telemetry.d.ts +111 -0
- package/dist/scheduler/telemetry.js +177 -0
- package/dist/scheduler/worker.d.ts +158 -0
- package/dist/scheduler/worker.js +744 -0
- package/dist/storage/artifacts.d.ts +17 -0
- package/dist/storage/artifacts.js +90 -0
- package/dist/storage/findings.d.ts +12 -0
- package/dist/storage/findings.js +70 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/memory.d.ts +11 -0
- package/dist/storage/memory.js +21 -0
- package/dist/storage/mutation-log.d.ts +41 -0
- package/dist/storage/mutation-log.js +140 -0
- package/dist/storage/outbox.d.ts +30 -0
- package/dist/storage/outbox.js +59 -0
- package/dist/storage/persistence.d.ts +183 -0
- package/dist/storage/persistence.js +999 -0
- package/dist/storage/policy.d.ts +80 -0
- package/dist/storage/policy.js +268 -0
- package/dist/storage/session.d.ts +140 -0
- package/dist/storage/session.js +447 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +308 -0
- package/dist/transitions/index.d.ts +2 -0
- package/dist/transitions/index.js +1 -0
- package/dist/transitions/validate.d.ts +4 -0
- package/dist/transitions/validate.js +1118 -0
- package/package.json +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AgentRecord, ArtifactRef, LaneRecord, LLMRequestProjection, PrivacyLabel, PrivacyTaint, ResultRef, RuntimeState, JsonValue, ContextDelta } from '../core/types.js';
|
|
2
|
+
export interface ContextBuildInput {
|
|
3
|
+
agent: AgentRecord;
|
|
4
|
+
lane: LaneRecord;
|
|
5
|
+
resultRefs?: ResultRef[];
|
|
6
|
+
artifactRefs?: ArtifactRef[];
|
|
7
|
+
eventIds?: string[];
|
|
8
|
+
instruction: string;
|
|
9
|
+
system?: string;
|
|
10
|
+
policy?: JsonValue;
|
|
11
|
+
tools?: JsonValue;
|
|
12
|
+
toolSetId: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class ContextBuilder {
|
|
15
|
+
private readonly state;
|
|
16
|
+
readonly version = "1";
|
|
17
|
+
constructor(state: RuntimeState);
|
|
18
|
+
build(input: ContextBuildInput): LLMRequestProjection;
|
|
19
|
+
}
|
|
20
|
+
export declare function appendHistory(lane: LaneRecord, record: {
|
|
21
|
+
effectId?: string;
|
|
22
|
+
instruction: string;
|
|
23
|
+
resultRefs: string[];
|
|
24
|
+
resultSelection?: Array<{
|
|
25
|
+
ref: string;
|
|
26
|
+
rule: string;
|
|
27
|
+
hash: string;
|
|
28
|
+
}>;
|
|
29
|
+
result?: string;
|
|
30
|
+
findings?: string[];
|
|
31
|
+
output: JsonValue;
|
|
32
|
+
privacy: PrivacyLabel;
|
|
33
|
+
privacyTaints?: PrivacyTaint[];
|
|
34
|
+
}): LaneRecord;
|
|
35
|
+
/** A deterministic, conservative estimate used for admission rather than billing. */
|
|
36
|
+
export declare function estimateHistoryTokens(history: ReadonlyArray<{
|
|
37
|
+
seq: number;
|
|
38
|
+
instruction: string;
|
|
39
|
+
resultRefs: string[];
|
|
40
|
+
output: JsonValue;
|
|
41
|
+
privacy: PrivacyLabel;
|
|
42
|
+
}>): number;
|
|
43
|
+
export declare function historyPressure(history: ReadonlyArray<{
|
|
44
|
+
seq: number;
|
|
45
|
+
instruction: string;
|
|
46
|
+
resultRefs: string[];
|
|
47
|
+
output: JsonValue;
|
|
48
|
+
privacy: PrivacyLabel;
|
|
49
|
+
}>, softTokens: number, hardTokens: number): {
|
|
50
|
+
historyTokens: number;
|
|
51
|
+
softTokens: number;
|
|
52
|
+
hardTokens: number;
|
|
53
|
+
} | undefined;
|
|
54
|
+
export declare function stableSerialize(value: unknown): string;
|
|
55
|
+
export declare function contentHash(value: unknown): string;
|
|
56
|
+
export declare function isUnsafePathSegment(segment: string): boolean;
|
|
57
|
+
export declare function hasUnsafePathSegment(path: readonly string[]): boolean;
|
|
58
|
+
/** Own-property lookup that never walks the prototype chain. */
|
|
59
|
+
export declare function ownChild(container: Record<string, JsonValue>, key: string): JsonValue | undefined;
|
|
60
|
+
export interface RebaseConflict {
|
|
61
|
+
path: string[];
|
|
62
|
+
reason: 'changed_since_base' | 'append_target_changed' | 'history_compaction_requires_review';
|
|
63
|
+
}
|
|
64
|
+
export interface RebaseResult {
|
|
65
|
+
delta?: ContextDelta;
|
|
66
|
+
conflicts: RebaseConflict[];
|
|
67
|
+
}
|
|
68
|
+
export declare function rebaseContextDelta(delta: ContextDelta, baseState: JsonValue, currentState: JsonValue, currentVersion: number): RebaseResult;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { effectivePrivacy, privacyRank, strictestPrivacy } from '../core/types.js';
|
|
3
|
+
function stable(value) {
|
|
4
|
+
if (value === null || typeof value !== 'object')
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return `[${value.map(stable).join(',')}]`;
|
|
8
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
|
|
9
|
+
}
|
|
10
|
+
function hash(value) { return createHash('sha256').update(stable(value)).digest('hex'); }
|
|
11
|
+
export class ContextBuilder {
|
|
12
|
+
state;
|
|
13
|
+
version = '1';
|
|
14
|
+
constructor(state) {
|
|
15
|
+
this.state = state;
|
|
16
|
+
}
|
|
17
|
+
build(input) {
|
|
18
|
+
if (input.instruction.length > 2048)
|
|
19
|
+
throw new Error('INSTRUCTION_TOO_LARGE');
|
|
20
|
+
const global = input.agent.globalVersions.get(input.lane.contextSnapshotVersion);
|
|
21
|
+
if (global === undefined)
|
|
22
|
+
throw new Error('UNKNOWN_CONTEXT_VERSION');
|
|
23
|
+
const resultRefs = input.resultRefs ?? [];
|
|
24
|
+
const results = resultRefs.map((ref) => {
|
|
25
|
+
const result = this.state.results.get(ref);
|
|
26
|
+
if (!result)
|
|
27
|
+
throw new Error(`UNKNOWN_RESULT_REF:${ref}`);
|
|
28
|
+
if (input.lane.visibleResultRefs !== undefined && !input.lane.visibleResultRefs.has(ref))
|
|
29
|
+
throw new Error(`RESULT_NOT_VISIBLE:${ref}`);
|
|
30
|
+
return result;
|
|
31
|
+
});
|
|
32
|
+
const artifactRefs = input.artifactRefs ?? [];
|
|
33
|
+
const artifacts = artifactRefs.map((ref) => {
|
|
34
|
+
const artifact = this.state.artifacts.get(ref);
|
|
35
|
+
if (!artifact)
|
|
36
|
+
throw new Error(`UNKNOWN_ARTIFACT_REF:${ref}`);
|
|
37
|
+
if (artifact.agentId !== undefined && artifact.agentId !== input.agent.id)
|
|
38
|
+
throw new Error(`ARTIFACT_NOT_VISIBLE:${ref}`);
|
|
39
|
+
return artifact;
|
|
40
|
+
});
|
|
41
|
+
const globalMetadata = input.agent.globalPrivacy?.get(input.lane.contextSnapshotVersion) ?? { privacy: 'public' };
|
|
42
|
+
const laneMetadata = { privacy: input.lane.context.privacy ?? 'public', privacyTaints: input.lane.context.privacyTaints };
|
|
43
|
+
const effectiveResults = results.map((result) => ({ result, privacy: effectivePrivacy(result.privacy, result.privacyTaints) }));
|
|
44
|
+
const effectiveArtifacts = artifacts.map((artifact) => ({ artifact, privacy: effectivePrivacy(artifact.privacy, artifact.privacyTaints) }));
|
|
45
|
+
const privacy = strictestPrivacy([globalMetadata.privacy, laneMetadata.privacy, ...effectiveResults.map((item) => item.privacy), ...effectiveArtifacts.map((item) => item.privacy)]);
|
|
46
|
+
const privacyRefs = [
|
|
47
|
+
...(privacyRank(globalMetadata.privacy) > 0 ? [`global:${input.lane.contextSnapshotVersion}`] : []),
|
|
48
|
+
...(privacyRank(laneMetadata.privacy) > 0 ? [`lane:${input.lane.context.version}`] : []),
|
|
49
|
+
...effectiveResults.filter((item) => privacyRank(item.privacy) > 0).map((item) => ({ kind: 'result', ref: item.result.id })),
|
|
50
|
+
...effectiveArtifacts.filter((item) => privacyRank(item.privacy) > 0).map((item) => ({ kind: 'artifact', ref: item.artifact.ref })),
|
|
51
|
+
];
|
|
52
|
+
const privacyTaints = [
|
|
53
|
+
...(globalMetadata.privacyTaints ?? []).map((taint) => ({ path: ['global', ...taint.path], privacy: taint.privacy })),
|
|
54
|
+
...(laneMetadata.privacyTaints ?? []).map((taint) => ({ path: ['lane', ...taint.path], privacy: taint.privacy })),
|
|
55
|
+
...effectiveResults.flatMap((item) => (item.result.privacyTaints ?? []).map((taint) => ({ path: [item.result.id, ...taint.path], privacy: taint.privacy }))),
|
|
56
|
+
...effectiveArtifacts.flatMap((item) => (item.artifact.privacyTaints ?? []).map((taint) => ({ path: [item.artifact.ref, ...taint.path], privacy: taint.privacy }))),
|
|
57
|
+
];
|
|
58
|
+
const contextSpec = { globalSnapshotVersion: input.lane.contextSnapshotVersion, laneSnapshotVersion: input.lane.context.version, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: input.eventIds ?? [], toolSetId: input.toolSetId, instruction: input.instruction, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
|
|
59
|
+
const prefixBlocks = [
|
|
60
|
+
{ kind: 'system', content: input.system ?? '' },
|
|
61
|
+
{ kind: 'policy', content: input.policy ?? {} },
|
|
62
|
+
{ kind: 'tools', content: input.tools ?? {} },
|
|
63
|
+
{ kind: 'global', content: global },
|
|
64
|
+
{ kind: 'history', content: input.lane.context.history.map((record) => ({ seq: record.seq, ...(record.effectId === undefined ? {} : { effectId: record.effectId }), instruction: record.instruction, resultRefs: record.resultRefs, ...(record.resultSelection === undefined ? {} : { resultSelection: record.resultSelection }), ...(record.result === undefined ? {} : { result: record.result }), ...(record.findings === undefined ? {} : { findings: record.findings }), output: record.output, privacy: record.privacy, ...(record.privacyTaints === undefined ? {} : { privacyTaints: record.privacyTaints }) })) },
|
|
65
|
+
];
|
|
66
|
+
const blocks = [...prefixBlocks, { kind: 'lane', content: input.lane.context.state }, { kind: 'events', content: input.eventIds ?? [] }, { kind: 'results', content: results.map((result) => ({ id: result.id, value: result.value ?? null, ...(result.privacyTaints === undefined ? {} : { privacyTaints: result.privacyTaints.map((taint) => ({ path: [...taint.path], privacy: taint.privacy })) }) })) }, { kind: 'artifacts', content: artifacts.map((artifact) => ({ ref: artifact.ref, mediaType: artifact.mediaType, sizeBytes: artifact.sizeBytes, contentHash: artifact.contentHash })) }, { kind: 'instruction', content: input.instruction }];
|
|
67
|
+
return { contextSpec, blocks, prefixHash: hash(prefixBlocks), projectionHash: hash(blocks), builderVersion: this.version, policyVersion: '1', toolSetVersion: input.toolSetId, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function appendHistory(lane, record) {
|
|
71
|
+
const next = structuredClone(lane);
|
|
72
|
+
const seq = next.context.history.length ? next.context.history[next.context.history.length - 1].seq + 1 : 1;
|
|
73
|
+
next.context.history.push({ seq, ...record });
|
|
74
|
+
next.context.version += 1;
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
/** A deterministic, conservative estimate used for admission rather than billing. */
|
|
78
|
+
export function estimateHistoryTokens(history) {
|
|
79
|
+
return Math.ceil(Buffer.byteLength(stableSerialize(history), 'utf8') / 4);
|
|
80
|
+
}
|
|
81
|
+
export function historyPressure(history, softTokens, hardTokens) {
|
|
82
|
+
const historyTokens = estimateHistoryTokens(history);
|
|
83
|
+
return historyTokens > softTokens ? { historyTokens, softTokens, hardTokens } : undefined;
|
|
84
|
+
}
|
|
85
|
+
export function stableSerialize(value) { return stable(value); }
|
|
86
|
+
export function contentHash(value) { return hash(value); }
|
|
87
|
+
/**
|
|
88
|
+
* Path segments that would reach the prototype chain instead of own data. Any
|
|
89
|
+
* ContextDelta, MergeProposal or draft-proxy path containing one of these is
|
|
90
|
+
* rejected before it can touch `Object.prototype`.
|
|
91
|
+
*/
|
|
92
|
+
const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
93
|
+
export function isUnsafePathSegment(segment) { return UNSAFE_PATH_SEGMENTS.has(segment); }
|
|
94
|
+
export function hasUnsafePathSegment(path) { return path.some(isUnsafePathSegment); }
|
|
95
|
+
/** Own-property lookup that never walks the prototype chain. */
|
|
96
|
+
export function ownChild(container, key) { return Object.hasOwn(container, key) ? container[key] : undefined; }
|
|
97
|
+
function atPath(value, path) {
|
|
98
|
+
let current = value;
|
|
99
|
+
for (const part of path) {
|
|
100
|
+
if (!current || typeof current !== 'object' || Array.isArray(current) || isUnsafePathSegment(part))
|
|
101
|
+
return undefined;
|
|
102
|
+
current = ownChild(current, part);
|
|
103
|
+
}
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
function equal(a, b) { return stableSerialize(a) === stableSerialize(b); }
|
|
107
|
+
export function rebaseContextDelta(delta, baseState, currentState, currentVersion) {
|
|
108
|
+
const conflicts = [];
|
|
109
|
+
for (const op of delta.ops) {
|
|
110
|
+
if (op.op === 'compact_history') {
|
|
111
|
+
conflicts.push({ path: ['history'], reason: 'history_compaction_requires_review' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const path = op.path ?? [];
|
|
115
|
+
const baseValue = atPath(baseState, path);
|
|
116
|
+
const currentValue = atPath(currentState, path);
|
|
117
|
+
if (op.op === 'append') {
|
|
118
|
+
if (!Array.isArray(baseValue) || !Array.isArray(currentValue) || currentValue.length < baseValue.length || !baseValue.every((item, index) => equal(item, currentValue[index])))
|
|
119
|
+
conflicts.push({ path, reason: 'append_target_changed' });
|
|
120
|
+
}
|
|
121
|
+
else if (!equal(baseValue, currentValue))
|
|
122
|
+
conflicts.push({ path, reason: 'changed_since_base' });
|
|
123
|
+
}
|
|
124
|
+
if (conflicts.length)
|
|
125
|
+
return { conflicts };
|
|
126
|
+
return { conflicts: [], delta: { ...delta, baseVersion: currentVersion } };
|
|
127
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RebaseConflict } from './builder.js';
|
|
2
|
+
import type { Mutation } from '../core/mutations.js';
|
|
3
|
+
import type { AgentRecord, JsonValue, RuntimeState } from '../core/types.js';
|
|
4
|
+
export interface MergeConflict {
|
|
5
|
+
proposalId: string;
|
|
6
|
+
sourceLaneId: string;
|
|
7
|
+
baseGlobalVersion: number;
|
|
8
|
+
conflicts: RebaseConflict[];
|
|
9
|
+
}
|
|
10
|
+
export interface MergePlan {
|
|
11
|
+
agentId: string;
|
|
12
|
+
version?: number;
|
|
13
|
+
value?: JsonValue;
|
|
14
|
+
appliedProposalIds: string[];
|
|
15
|
+
conflicts: MergeConflict[];
|
|
16
|
+
mutations: Mutation[];
|
|
17
|
+
}
|
|
18
|
+
export declare class ContextMerger {
|
|
19
|
+
private readonly state;
|
|
20
|
+
constructor(state: RuntimeState);
|
|
21
|
+
plan(agentId: string, proposalIds?: string[]): MergePlan;
|
|
22
|
+
commit(agentId: string, proposalIds?: string[]): MergePlan;
|
|
23
|
+
}
|
|
24
|
+
export declare function mergeProposals(state: RuntimeState, agentId: string, proposalIds?: string[]): MergePlan;
|
|
25
|
+
export type { AgentRecord };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { hasUnsafePathSegment, ownChild, rebaseContextDelta, stableSerialize } from './builder.js';
|
|
2
|
+
import { apply } from '../core/mutations.js';
|
|
3
|
+
import { privacyTaintPrivacy, strictestPrivacy } from '../core/types.js';
|
|
4
|
+
function clone(value) { return structuredClone(value); }
|
|
5
|
+
function setPath(root, path, value) {
|
|
6
|
+
let cursor = root;
|
|
7
|
+
for (const part of path.slice(0, -1)) {
|
|
8
|
+
const child = ownChild(cursor, part);
|
|
9
|
+
if (!child || typeof child !== 'object' || Array.isArray(child))
|
|
10
|
+
cursor[part] = {};
|
|
11
|
+
cursor = cursor[part];
|
|
12
|
+
}
|
|
13
|
+
cursor[path[path.length - 1]] = clone(value);
|
|
14
|
+
}
|
|
15
|
+
function removePath(root, path) {
|
|
16
|
+
let cursor = root;
|
|
17
|
+
for (const part of path.slice(0, -1)) {
|
|
18
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
19
|
+
return;
|
|
20
|
+
const next = ownChild(cursor, part);
|
|
21
|
+
if (next === undefined)
|
|
22
|
+
return;
|
|
23
|
+
cursor = next;
|
|
24
|
+
}
|
|
25
|
+
if (cursor && typeof cursor === 'object' && !Array.isArray(cursor))
|
|
26
|
+
delete cursor[path[path.length - 1]];
|
|
27
|
+
}
|
|
28
|
+
function applyDelta(root, delta) {
|
|
29
|
+
const result = clone(root);
|
|
30
|
+
for (const op of delta.ops) {
|
|
31
|
+
if (op.op === 'compact_history')
|
|
32
|
+
throw new Error('GLOBAL_HISTORY_COMPACTION_NOT_ALLOWED');
|
|
33
|
+
const path = op.path ?? [];
|
|
34
|
+
if (path.length === 0 || hasUnsafePathSegment(path))
|
|
35
|
+
throw new Error('INVALID_CONTEXT_PATH');
|
|
36
|
+
if (op.op === 'set')
|
|
37
|
+
setPath(result, path, op.value ?? null);
|
|
38
|
+
else if (op.op === 'remove')
|
|
39
|
+
removePath(result, path);
|
|
40
|
+
else {
|
|
41
|
+
let cursor = result;
|
|
42
|
+
for (const part of path) {
|
|
43
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
44
|
+
throw new Error('APPEND_TARGET_NOT_ARRAY');
|
|
45
|
+
const next = ownChild(cursor, part);
|
|
46
|
+
if (next === undefined)
|
|
47
|
+
throw new Error('APPEND_TARGET_NOT_ARRAY');
|
|
48
|
+
cursor = next;
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(cursor))
|
|
51
|
+
throw new Error('APPEND_TARGET_NOT_ARRAY');
|
|
52
|
+
cursor.push(clone(op.value ?? null));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function proposalOrder(proposals) {
|
|
58
|
+
return [...proposals].sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
|
|
59
|
+
}
|
|
60
|
+
export class ContextMerger {
|
|
61
|
+
state;
|
|
62
|
+
constructor(state) {
|
|
63
|
+
this.state = state;
|
|
64
|
+
}
|
|
65
|
+
plan(agentId, proposalIds) {
|
|
66
|
+
const agent = this.state.agents.get(agentId);
|
|
67
|
+
if (!agent)
|
|
68
|
+
throw new Error(`UNKNOWN_AGENT:${agentId}`);
|
|
69
|
+
const wanted = proposalIds === undefined ? undefined : new Set(proposalIds);
|
|
70
|
+
const proposals = proposalOrder([...this.state.mergeProposals.values()].filter((proposal) => proposal.agentId === agentId && (wanted === undefined || wanted.has(proposal.id))));
|
|
71
|
+
let version = agent.latestGlobalVersion;
|
|
72
|
+
let value = clone(agent.globalVersions.get(version) ?? {});
|
|
73
|
+
let metadata = clone(agent.globalPrivacy?.get(version) ?? { privacy: 'public' });
|
|
74
|
+
const appliedProposalIds = [];
|
|
75
|
+
const conflicts = [];
|
|
76
|
+
const mutations = [];
|
|
77
|
+
const versionMutations = [];
|
|
78
|
+
for (const proposal of proposals) {
|
|
79
|
+
const base = agent.globalVersions.get(proposal.baseGlobalVersion);
|
|
80
|
+
if (base === undefined) {
|
|
81
|
+
conflicts.push({ proposalId: proposal.id, sourceLaneId: proposal.sourceLaneId, baseGlobalVersion: proposal.baseGlobalVersion, conflicts: [{ path: [], reason: 'changed_since_base' }] });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const rebased = rebaseContextDelta(proposal.delta, base, value, version);
|
|
85
|
+
if (!rebased.delta || rebased.conflicts.length) {
|
|
86
|
+
conflicts.push({ proposalId: proposal.id, sourceLaneId: proposal.sourceLaneId, baseGlobalVersion: proposal.baseGlobalVersion, conflicts: rebased.conflicts });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const next = applyDelta(value, rebased.delta);
|
|
91
|
+
const nextMetadata = {
|
|
92
|
+
privacy: strictestPrivacy([metadata.privacy, proposal.delta.privacy ?? 'public', privacyTaintPrivacy(proposal.delta.privacyTaints)]),
|
|
93
|
+
...(metadata.privacyTaints?.length || proposal.delta.privacyTaints?.length ? { privacyTaints: [...(metadata.privacyTaints ?? []), ...(proposal.delta.privacyTaints ?? [])] } : {}),
|
|
94
|
+
};
|
|
95
|
+
if (stableSerialize(next) !== stableSerialize(value) || stableSerialize(nextMetadata) !== stableSerialize(metadata)) {
|
|
96
|
+
value = next;
|
|
97
|
+
version++;
|
|
98
|
+
// Every version number that is handed out must exist in `globalVersions`;
|
|
99
|
+
// otherwise `adopt_context(version)` for an intermediate version fails.
|
|
100
|
+
versionMutations.push({ op: 'setGlobal', agentId, version, value: clone(value), metadata: clone(nextMetadata) });
|
|
101
|
+
}
|
|
102
|
+
metadata = nextMetadata;
|
|
103
|
+
appliedProposalIds.push(proposal.id);
|
|
104
|
+
mutations.push({ op: 'removeMergeProposal', proposalId: proposal.id });
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
conflicts.push({ proposalId: proposal.id, sourceLaneId: proposal.sourceLaneId, baseGlobalVersion: proposal.baseGlobalVersion, conflicts: [{ path: [], reason: 'changed_since_base' }] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (appliedProposalIds.length) {
|
|
111
|
+
mutations.unshift(...versionMutations);
|
|
112
|
+
mutations.push({ op: 'appendEvent', event: { type: 'context.merge_committed', agentId, data: { version, proposalIds: appliedProposalIds } } });
|
|
113
|
+
}
|
|
114
|
+
return { agentId, ...(appliedProposalIds.length ? { version, value: clone(value) } : {}), appliedProposalIds, conflicts, mutations };
|
|
115
|
+
}
|
|
116
|
+
commit(agentId, proposalIds) {
|
|
117
|
+
const plan = this.plan(agentId, proposalIds);
|
|
118
|
+
if (plan.conflicts.length)
|
|
119
|
+
return plan;
|
|
120
|
+
if (plan.mutations.length)
|
|
121
|
+
apply(this.state, plan.mutations);
|
|
122
|
+
return plan;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function mergeProposals(state, agentId, proposalIds) { return new ContextMerger(state).commit(agentId, proposalIds); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { RuntimeAction, SubmitEffectsAction, WaitAction, ForkAction, CancelLaneAction, ProposeCancelAction, CompleteAction, FailAction, AdoptContextAction, EffectSubmission, ForkLaneSpec, SeriesLaneSpec } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RuntimeError } from './types.js';
|
|
2
|
+
export declare class ControlError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
constructor(error: RuntimeError);
|
|
6
|
+
}
|
|
7
|
+
export declare function error(code: string, message: string, details?: unknown): RuntimeError;
|
|
8
|
+
export declare function runtimeErrorFromCause(cause: unknown, fallbackCode?: string): RuntimeError;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class ControlError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(error) {
|
|
5
|
+
super(error.message);
|
|
6
|
+
this.name = 'ControlError';
|
|
7
|
+
this.code = error.code;
|
|
8
|
+
this.details = error.details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function error(code, message, details) {
|
|
12
|
+
return { code, message, ...(details === undefined ? {} : { details: details }) };
|
|
13
|
+
}
|
|
14
|
+
export function runtimeErrorFromCause(cause, fallbackCode = 'EFFECT_FAILED') {
|
|
15
|
+
if (typeof cause === 'object' && cause !== null) {
|
|
16
|
+
const candidate = cause;
|
|
17
|
+
const code = typeof candidate.code === 'string' ? candidate.code : fallbackCode;
|
|
18
|
+
const message = typeof candidate.message === 'string' ? candidate.message : cause instanceof Error ? cause.message : String(cause);
|
|
19
|
+
return {
|
|
20
|
+
code,
|
|
21
|
+
message,
|
|
22
|
+
...(typeof candidate.retryable === 'boolean' ? { retryable: candidate.retryable } : {}),
|
|
23
|
+
...(isJsonValue(candidate.details) ? { details: candidate.details } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { code: fallbackCode, message: cause instanceof Error ? cause.message : String(cause) };
|
|
27
|
+
}
|
|
28
|
+
function isJsonValue(value) {
|
|
29
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
|
|
30
|
+
return true;
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return value.every(isJsonValue);
|
|
33
|
+
if (typeof value !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
return Object.values(value).every(isJsonValue);
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuntimeEvent, RuntimeEventInput, RuntimeState } from './types.js';
|
|
2
|
+
export type { RuntimeEvent, RuntimeEventInput, ResumeInput, WaitResolution, Outcome } from './types.js';
|
|
3
|
+
export declare function normalizeRuntimeEvent(input: RuntimeEventInput, seq: number, defaults?: {
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
timestamp?: number;
|
|
6
|
+
}): RuntimeEvent;
|
|
7
|
+
export declare function appendRuntimeEvent(state: RuntimeState, input: RuntimeEventInput, defaults?: {
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
timestamp?: number;
|
|
10
|
+
}): RuntimeEvent;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function normalizeRuntimeEvent(input, seq, defaults = {}) {
|
|
2
|
+
const payload = input.payload ?? input.data ?? null;
|
|
3
|
+
return {
|
|
4
|
+
id: input.id ?? `event-${seq}`,
|
|
5
|
+
schemaVersion: input.schemaVersion ?? 1,
|
|
6
|
+
sessionId: input.sessionId ?? defaults.sessionId ?? 'session-unknown',
|
|
7
|
+
seq,
|
|
8
|
+
timestamp: input.timestamp ?? defaults.timestamp ?? 0,
|
|
9
|
+
type: input.type,
|
|
10
|
+
...(input.txId === undefined ? {} : { txId: input.txId }),
|
|
11
|
+
...(input.agentId === undefined ? {} : { agentId: input.agentId }),
|
|
12
|
+
...(input.laneId === undefined ? {} : { laneId: input.laneId }),
|
|
13
|
+
...(input.effectId === undefined ? {} : { effectId: input.effectId }),
|
|
14
|
+
...(input.attemptId === undefined ? {} : { attemptId: input.attemptId }),
|
|
15
|
+
...(input.causationId === undefined ? {} : { causationId: input.causationId }),
|
|
16
|
+
payload,
|
|
17
|
+
...(input.data === undefined ? { data: payload } : { data: input.data }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function appendRuntimeEvent(state, input, defaults = {}) {
|
|
21
|
+
const event = normalizeRuntimeEvent(input, state.nextIds.event++, { ...(defaults.sessionId === undefined ? {} : { sessionId: defaults.sessionId }), timestamp: defaults.timestamp ?? state.now });
|
|
22
|
+
state.events.push(event);
|
|
23
|
+
return event;
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentRecord, LaneRecord, ResumePoint, RuntimeState, JsonValue } from './types.js';
|
|
2
|
+
export interface AgentBuildResult {
|
|
3
|
+
agent: AgentRecord;
|
|
4
|
+
root: LaneRecord;
|
|
5
|
+
nextIds: RuntimeState['nextIds'];
|
|
6
|
+
}
|
|
7
|
+
export declare function buildAgent(state: RuntimeState, goal: string, resume: ResumePoint, options?: {
|
|
8
|
+
agentId?: string;
|
|
9
|
+
maxActiveLanes?: number;
|
|
10
|
+
priority?: number;
|
|
11
|
+
initialGlobal?: JsonValue;
|
|
12
|
+
initialGlobalPrivacy?: import('./types.js').PrivacyMetadata;
|
|
13
|
+
parentAgentId?: string;
|
|
14
|
+
depth?: number;
|
|
15
|
+
inheritedFloor?: number;
|
|
16
|
+
policyId?: string;
|
|
17
|
+
limitsId?: string;
|
|
18
|
+
deadlineAt?: number;
|
|
19
|
+
}): AgentBuildResult;
|
|
20
|
+
export declare function createAgent(state: RuntimeState, goal: string, resume: ResumePoint, options?: {
|
|
21
|
+
agentId?: string;
|
|
22
|
+
maxActiveLanes?: number;
|
|
23
|
+
priority?: number;
|
|
24
|
+
initialGlobal?: JsonValue;
|
|
25
|
+
initialGlobalPrivacy?: import('./types.js').PrivacyMetadata;
|
|
26
|
+
parentAgentId?: string;
|
|
27
|
+
depth?: number;
|
|
28
|
+
inheritedFloor?: number;
|
|
29
|
+
policyId?: string;
|
|
30
|
+
limitsId?: string;
|
|
31
|
+
deadlineAt?: number;
|
|
32
|
+
}): {
|
|
33
|
+
agent: AgentRecord;
|
|
34
|
+
root: LaneRecord;
|
|
35
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function buildAgent(state, goal, resume, options = {}) {
|
|
2
|
+
let agentSequence = state.nextIds.agent;
|
|
3
|
+
let agentId = options.agentId ?? `agent-${agentSequence}`;
|
|
4
|
+
if (options.agentId === undefined)
|
|
5
|
+
while (state.agents.has(agentId)) {
|
|
6
|
+
agentSequence++;
|
|
7
|
+
agentId = `agent-${agentSequence}`;
|
|
8
|
+
}
|
|
9
|
+
let laneSequence = state.nextIds.lane;
|
|
10
|
+
let rootId = `lane-${laneSequence}`;
|
|
11
|
+
while (state.lanes.has(rootId)) {
|
|
12
|
+
laneSequence++;
|
|
13
|
+
rootId = `lane-${laneSequence}`;
|
|
14
|
+
}
|
|
15
|
+
const initialPrivacy = structuredClone(options.initialGlobalPrivacy ?? { privacy: 'public' });
|
|
16
|
+
const agent = { id: agentId, rootLaneId: rootId, goal, state: 'created', globalVersions: new Map([[0, structuredClone(options.initialGlobal ?? {})]]), globalPrivacy: new Map([[0, initialPrivacy]]), latestGlobalVersion: 0, maxActiveLanes: options.maxActiveLanes ?? 64, ...(options.policyId === undefined ? {} : { policyId: options.policyId }), ...(options.limitsId === undefined ? {} : { limitsId: options.limitsId }), ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }), ...(options.parentAgentId === undefined ? {} : { parentAgentId: options.parentAgentId }), ...(options.depth === undefined ? {} : { depth: options.depth }) };
|
|
17
|
+
const root = { id: rootId, agentId, status: 'ready', version: 0, goal, resume, contextSnapshotVersion: 0, context: { version: 0, history: [], state: {}, privacy: 'public' }, visibleResultRefs: new Set(), children: new Set(), priority: options.priority ?? 0, ...(options.inheritedFloor === undefined ? {} : { inheritedFloor: options.inheritedFloor }), enqueueSeq: 0, readySince: state.now, ownedEffectIds: new Set() };
|
|
18
|
+
return { agent, root, nextIds: { ...state.nextIds, agent: Math.max(state.nextIds.agent + 1, agentSequence + 1), lane: laneSequence + 1 } };
|
|
19
|
+
}
|
|
20
|
+
export function createAgent(state, goal, resume, options = {}) {
|
|
21
|
+
const built = buildAgent(state, goal, resume, options);
|
|
22
|
+
state.nextIds = built.nextIds;
|
|
23
|
+
const { agent, root } = built;
|
|
24
|
+
state.agents.set(agent.id, agent);
|
|
25
|
+
state.lanes.set(root.id, root);
|
|
26
|
+
return { agent, root };
|
|
27
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { JsonValue } from './types.js';
|
|
2
|
+
export interface FactEnvelope<T extends JsonValue = JsonValue> {
|
|
3
|
+
eventId: string;
|
|
4
|
+
receivedSeq: number;
|
|
5
|
+
fact: T;
|
|
6
|
+
}
|
|
7
|
+
export interface FactInboxDedupeEntry {
|
|
8
|
+
eventId: string;
|
|
9
|
+
receivedSeq: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A durable, exact membership view for event ids already archived by a
|
|
13
|
+
* checkpoint. `contains(eventId)` is used on the hot enqueue path, while the
|
|
14
|
+
* sequence argument lets compaction verify that the archive contains the
|
|
15
|
+
* exact ledger entry rather than only an unrelated event with the same shape.
|
|
16
|
+
*
|
|
17
|
+
* Implementations must make the view durable before acknowledging an archive
|
|
18
|
+
* batch to FactInbox. The interface is synchronous on purpose: enqueue() is a
|
|
19
|
+
* synchronous single-writer boundary and cannot safely await a storage query.
|
|
20
|
+
*/
|
|
21
|
+
export interface FactInboxDedupeArchive {
|
|
22
|
+
readonly archiveId: string;
|
|
23
|
+
/** Highest contiguous receivedSeq durably present in this archive. */
|
|
24
|
+
readonly watermark: number;
|
|
25
|
+
contains(eventId: string, receivedSeq?: number): boolean;
|
|
26
|
+
/** Digest of the exact archived ledger prefix through `watermark`. */
|
|
27
|
+
digestThrough(through: number): string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A durable archive writer used by checkpoint implementations. Membership
|
|
31
|
+
* reads stay synchronous for the FactInbox hot path; checkpoint writes are
|
|
32
|
+
* explicitly asynchronous and must be durable before compaction is acked.
|
|
33
|
+
*/
|
|
34
|
+
export interface FactInboxDedupeArchiveWriter extends FactInboxDedupeArchive {
|
|
35
|
+
append(batch: FactInboxDedupeArchiveBatch): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface FactInboxDedupeArchiveBatch {
|
|
38
|
+
schemaVersion: 1;
|
|
39
|
+
archiveId: string;
|
|
40
|
+
through: number;
|
|
41
|
+
ledgerDigest: string;
|
|
42
|
+
entries: FactInboxDedupeEntry[];
|
|
43
|
+
}
|
|
44
|
+
export interface FactInboxDedupeLedgerSnapshot {
|
|
45
|
+
schemaVersion: 1;
|
|
46
|
+
archivedThrough: number;
|
|
47
|
+
entries: FactInboxDedupeEntry[];
|
|
48
|
+
/** Event ids restored from the pre-ledger v1 snapshot format. */
|
|
49
|
+
legacyEventIds?: string[];
|
|
50
|
+
archiveId?: string;
|
|
51
|
+
archiveDigest?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function factInboxDedupeDigestFrom(previousDigest: string, entries: readonly FactInboxDedupeEntry[]): string;
|
|
54
|
+
export declare function factInboxDedupeDigest(entries: readonly FactInboxDedupeEntry[]): string;
|
|
55
|
+
export interface FactInboxSnapshot<T extends JsonValue = JsonValue> {
|
|
56
|
+
schemaVersion: 1 | 2;
|
|
57
|
+
nextSeq: number;
|
|
58
|
+
seen: string[];
|
|
59
|
+
queue: FactEnvelope<T>[];
|
|
60
|
+
dedupeLedger?: FactInboxDedupeLedgerSnapshot;
|
|
61
|
+
}
|
|
62
|
+
export declare class FactInbox<T extends JsonValue = JsonValue> {
|
|
63
|
+
private readonly queue;
|
|
64
|
+
private readonly seen;
|
|
65
|
+
private nextSeq;
|
|
66
|
+
private archivedThrough;
|
|
67
|
+
private dedupeArchive;
|
|
68
|
+
constructor(options?: {
|
|
69
|
+
dedupeArchive?: FactInboxDedupeArchive;
|
|
70
|
+
});
|
|
71
|
+
/** Attach a backend-provided archive before the first compaction. */
|
|
72
|
+
attachDedupeArchive(archive: FactInboxDedupeArchive): void;
|
|
73
|
+
enqueue(fact: T, eventId: string): FactEnvelope<T> | undefined;
|
|
74
|
+
drain(limit?: number): FactEnvelope<T>[];
|
|
75
|
+
get size(): number;
|
|
76
|
+
get dedupeWatermark(): number;
|
|
77
|
+
has(eventId: string): boolean;
|
|
78
|
+
snapshot(): FactInboxSnapshot<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Build the exact ledger batch that a durable checkpoint/archive must
|
|
81
|
+
* acknowledge before local dedupe entries can be compacted.
|
|
82
|
+
*/
|
|
83
|
+
createDedupeArchiveBatch(through?: number): FactInboxDedupeArchiveBatch;
|
|
84
|
+
/**
|
|
85
|
+
* Acknowledge a batch only after its exact entries are durably archived.
|
|
86
|
+
* This is the sole operation that advances the watermark or removes local
|
|
87
|
+
* dedupe entries; clear() intentionally does not affect deduplication.
|
|
88
|
+
*/
|
|
89
|
+
compactDedupeThrough(batch: FactInboxDedupeArchiveBatch): number;
|
|
90
|
+
restore(snapshot: FactInboxSnapshot<T> | JsonValue): void;
|
|
91
|
+
static fromSnapshot<T extends JsonValue = JsonValue>(snapshot: FactInboxSnapshot<T> | JsonValue, options?: {
|
|
92
|
+
dedupeArchive?: FactInboxDedupeArchive;
|
|
93
|
+
}): FactInbox<T>;
|
|
94
|
+
clear(): void;
|
|
95
|
+
}
|
|
96
|
+
export interface ObservationEnvelope {
|
|
97
|
+
seq: number;
|
|
98
|
+
agentId: string;
|
|
99
|
+
laneId?: string;
|
|
100
|
+
type: 'progress' | 'chunk' | 'trace' | 'warning' | 'diagnostic';
|
|
101
|
+
data: JsonValue;
|
|
102
|
+
timestamp: number;
|
|
103
|
+
}
|
|
104
|
+
export declare class ObservationInbox {
|
|
105
|
+
readonly maxEntries: number;
|
|
106
|
+
readonly maxBytes: number;
|
|
107
|
+
private readonly queue;
|
|
108
|
+
private readonly droppedThroughByAgent;
|
|
109
|
+
private bytes;
|
|
110
|
+
private nextSeq;
|
|
111
|
+
constructor(maxEntries?: number, maxBytes?: number);
|
|
112
|
+
enqueue(input: Omit<ObservationEnvelope, 'seq'>): ObservationEnvelope;
|
|
113
|
+
drain(agentId?: string): ObservationEnvelope[];
|
|
114
|
+
get size(): number;
|
|
115
|
+
get sizeBytes(): number;
|
|
116
|
+
droppedThrough(agentId: string): number;
|
|
117
|
+
snapshot(): ObservationEnvelope[];
|
|
118
|
+
private eventBytes;
|
|
119
|
+
}
|