@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.
Files changed (93) hide show
  1. package/dist/context/builder.d.ts +68 -0
  2. package/dist/context/builder.js +127 -0
  3. package/dist/context/index.d.ts +2 -0
  4. package/dist/context/index.js +2 -0
  5. package/dist/context/merger.d.ts +25 -0
  6. package/dist/context/merger.js +125 -0
  7. package/dist/core/actions.d.ts +1 -0
  8. package/dist/core/actions.js +1 -0
  9. package/dist/core/errors.d.ts +8 -0
  10. package/dist/core/errors.js +36 -0
  11. package/dist/core/events.d.ts +10 -0
  12. package/dist/core/events.js +24 -0
  13. package/dist/core/factory.d.ts +35 -0
  14. package/dist/core/factory.js +27 -0
  15. package/dist/core/inbox.d.ts +119 -0
  16. package/dist/core/inbox.js +217 -0
  17. package/dist/core/mutations.d.ts +80 -0
  18. package/dist/core/mutations.js +127 -0
  19. package/dist/core/records.d.ts +1 -0
  20. package/dist/core/records.js +1 -0
  21. package/dist/core/types.d.ts +615 -0
  22. package/dist/core/types.js +109 -0
  23. package/dist/dependencies/graph.d.ts +25 -0
  24. package/dist/dependencies/graph.js +92 -0
  25. package/dist/dependencies/index.d.ts +1 -0
  26. package/dist/dependencies/index.js +1 -0
  27. package/dist/dsl/context-proxy.d.ts +20 -0
  28. package/dist/dsl/context-proxy.js +64 -0
  29. package/dist/dsl/index.d.ts +4 -0
  30. package/dist/dsl/index.js +4 -0
  31. package/dist/dsl/program.d.ts +314 -0
  32. package/dist/dsl/program.js +756 -0
  33. package/dist/dsl/session.d.ts +45 -0
  34. package/dist/dsl/session.js +93 -0
  35. package/dist/dsl/templates-index.d.ts +1 -0
  36. package/dist/dsl/templates-index.js +1 -0
  37. package/dist/dsl/templates.d.ts +85 -0
  38. package/dist/dsl/templates.js +110 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.js +15 -0
  41. package/dist/lifecycle/index.d.ts +2 -0
  42. package/dist/lifecycle/index.js +2 -0
  43. package/dist/lifecycle/scopes.d.ts +38 -0
  44. package/dist/lifecycle/scopes.js +50 -0
  45. package/dist/lifecycle/watchdog.d.ts +16 -0
  46. package/dist/lifecycle/watchdog.js +66 -0
  47. package/dist/models/actions.d.ts +10 -0
  48. package/dist/models/actions.js +68 -0
  49. package/dist/models/index.d.ts +2 -0
  50. package/dist/models/index.js +2 -0
  51. package/dist/models/router.d.ts +187 -0
  52. package/dist/models/router.js +353 -0
  53. package/dist/scheduler/clock.d.ts +45 -0
  54. package/dist/scheduler/clock.js +92 -0
  55. package/dist/scheduler/decision.d.ts +72 -0
  56. package/dist/scheduler/decision.js +63 -0
  57. package/dist/scheduler/index.d.ts +6 -0
  58. package/dist/scheduler/index.js +6 -0
  59. package/dist/scheduler/locks.d.ts +18 -0
  60. package/dist/scheduler/locks.js +106 -0
  61. package/dist/scheduler/ready-queue.d.ts +32 -0
  62. package/dist/scheduler/ready-queue.js +40 -0
  63. package/dist/scheduler/runtime.d.ts +486 -0
  64. package/dist/scheduler/runtime.js +3445 -0
  65. package/dist/scheduler/telemetry.d.ts +111 -0
  66. package/dist/scheduler/telemetry.js +177 -0
  67. package/dist/scheduler/worker.d.ts +158 -0
  68. package/dist/scheduler/worker.js +744 -0
  69. package/dist/storage/artifacts.d.ts +17 -0
  70. package/dist/storage/artifacts.js +90 -0
  71. package/dist/storage/findings.d.ts +12 -0
  72. package/dist/storage/findings.js +70 -0
  73. package/dist/storage/index.d.ts +8 -0
  74. package/dist/storage/index.js +8 -0
  75. package/dist/storage/memory.d.ts +11 -0
  76. package/dist/storage/memory.js +21 -0
  77. package/dist/storage/mutation-log.d.ts +41 -0
  78. package/dist/storage/mutation-log.js +140 -0
  79. package/dist/storage/outbox.d.ts +30 -0
  80. package/dist/storage/outbox.js +59 -0
  81. package/dist/storage/persistence.d.ts +183 -0
  82. package/dist/storage/persistence.js +999 -0
  83. package/dist/storage/policy.d.ts +80 -0
  84. package/dist/storage/policy.js +268 -0
  85. package/dist/storage/session.d.ts +140 -0
  86. package/dist/storage/session.js +447 -0
  87. package/dist/tools/registry.d.ts +125 -0
  88. package/dist/tools/registry.js +308 -0
  89. package/dist/transitions/index.d.ts +2 -0
  90. package/dist/transitions/index.js +1 -0
  91. package/dist/transitions/validate.d.ts +4 -0
  92. package/dist/transitions/validate.js +1118 -0
  93. package/package.json +21 -0
@@ -0,0 +1,447 @@
1
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { mkdir, open } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname } from 'node:path';
5
+ import { privacyRank } from '../core/types.js';
6
+ import { createRuntimeState } from '../core/types.js';
7
+ import { normalizeRuntimeEvent } from '../core/events.js';
8
+ export class InMemoryRuntimeSessionStore {
9
+ snapshots = new Map();
10
+ get(sessionId) { return this.getWithRevision(sessionId)?.snapshot; }
11
+ put(snapshot) { this.putIfRevision(snapshot); }
12
+ getWithRevision(sessionId) {
13
+ const entry = this.snapshots.get(sessionId);
14
+ return entry === undefined ? undefined : { revision: entry.revision, snapshot: structuredClone(entry.snapshot) };
15
+ }
16
+ putIfRevision(snapshot, expectedRevision) {
17
+ validateWarmStartSnapshot(snapshot);
18
+ const current = this.snapshots.get(snapshot.sessionId);
19
+ if (expectedRevision !== undefined && current?.revision !== expectedRevision)
20
+ throw new Error('RUNTIME_SESSION_STORE_CONFLICT');
21
+ const revision = (current?.revision ?? 0) + 1;
22
+ this.snapshots.set(snapshot.sessionId, { revision, snapshot: structuredClone(snapshot) });
23
+ return revision;
24
+ }
25
+ }
26
+ function validProvenance(value) {
27
+ if (typeof value === 'string')
28
+ return value.length > 0;
29
+ if (!value || typeof value !== 'object' || Array.isArray(value))
30
+ return false;
31
+ const ref = value;
32
+ return (ref.kind === 'result' || ref.kind === 'artifact') && typeof ref.ref === 'string' && ref.ref.length > 0;
33
+ }
34
+ function validPrivacyMetadata(value) {
35
+ if (!value || typeof value !== 'object' || Array.isArray(value))
36
+ return false;
37
+ const metadata = value;
38
+ if (!['public', 'cloud_allowed', 'local_only'].includes(String(metadata.privacy)))
39
+ return false;
40
+ const taints = metadata.privacyTaints;
41
+ return taints === undefined || (Array.isArray(taints) && taints.every((taint) => taint && typeof taint === 'object' && Array.isArray(taint.path) && taint.path.length > 0 && taint.path.every((part) => typeof part === 'string' && part.length > 0) && ['public', 'cloud_allowed', 'local_only'].includes(String(taint.privacy))));
42
+ }
43
+ function validateWarmStartSnapshot(snapshot) {
44
+ if (!snapshot || snapshot.schemaVersion !== 1 || typeof snapshot.sessionId !== 'string' || snapshot.sessionId.length === 0 || !snapshot.agent || typeof snapshot.agent.rootLaneId !== 'string' || snapshot.agent.rootLaneId.length === 0 || !Number.isInteger(snapshot.agent.latestGlobalVersion) || snapshot.agent.latestGlobalVersion < 0 || !Array.isArray(snapshot.agent.globalVersions) || !Array.isArray(snapshot.visibleResultRefs) || !Array.isArray(snapshot.results))
45
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
46
+ const versions = new Set();
47
+ for (const entry of snapshot.agent.globalVersions) {
48
+ if (!Array.isArray(entry) || entry.length !== 2 || !Number.isInteger(entry[0]) || entry[0] < 0 || versions.has(entry[0]) || entry[1] === undefined)
49
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
50
+ versions.add(entry[0]);
51
+ }
52
+ if (!versions.has(snapshot.agent.latestGlobalVersion))
53
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
54
+ if (snapshot.agent.globalPrivacy !== undefined) {
55
+ const privacyVersions = new Set();
56
+ for (const entry of snapshot.agent.globalPrivacy) {
57
+ if (!Array.isArray(entry) || entry.length !== 2 || !Number.isInteger(entry[0]) || entry[0] < 0 || privacyVersions.has(entry[0]) || !versions.has(entry[0]) || !validPrivacyMetadata(entry[1]))
58
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
59
+ privacyVersions.add(entry[0]);
60
+ }
61
+ }
62
+ if (new Set(snapshot.visibleResultRefs).size !== snapshot.visibleResultRefs.length || snapshot.visibleResultRefs.some((ref) => typeof ref !== 'string' || ref.length === 0))
63
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
64
+ const resultRefs = new Set();
65
+ for (const entry of snapshot.results) {
66
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || entry[0].length === 0 || resultRefs.has(entry[0]))
67
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
68
+ const result = entry[1];
69
+ if (!result || result.id !== entry[0] || !validPrivacyMetadata(result) || !Array.isArray(result.derivedFrom) || result.derivedFrom.some((ref) => !validProvenance(ref)) || (result.pinCount !== undefined && (!Number.isInteger(result.pinCount) || result.pinCount < 0)) || (result.sizeBytes !== undefined && (!Number.isInteger(result.sizeBytes) || result.sizeBytes < 0)) || (result.storageState !== undefined && !['memory', 'persisted'].includes(result.storageState)))
70
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
71
+ resultRefs.add(entry[0]);
72
+ }
73
+ if (snapshot.visibleResultRefs.some((ref) => !resultRefs.has(ref)))
74
+ throw new Error('INVALID_RUNTIME_SESSION_SNAPSHOT');
75
+ }
76
+ function emptyRuntimeSessionEnvelope() { return { schemaVersion: 1, sessions: [] }; }
77
+ /** Durable synchronous Session Store for hosts that need createAgent() to remain synchronous. */
78
+ export class FileRuntimeSessionStore {
79
+ filePath;
80
+ constructor(filePath) {
81
+ this.filePath = filePath;
82
+ }
83
+ get(sessionId) { return this.getWithRevision(sessionId)?.snapshot; }
84
+ getWithRevision(sessionId) {
85
+ const entry = this.readEnvelope().sessions.find((candidate) => candidate.sessionId === sessionId);
86
+ return entry === undefined ? undefined : { revision: entry.revision, snapshot: structuredClone(entry.snapshot) };
87
+ }
88
+ put(snapshot) { this.putIfRevision(snapshot); }
89
+ putIfRevision(snapshot, expectedRevision) {
90
+ validateWarmStartSnapshot(snapshot);
91
+ return this.withLock(() => {
92
+ const envelope = this.readEnvelope();
93
+ const current = envelope.sessions.find((candidate) => candidate.sessionId === snapshot.sessionId);
94
+ if (expectedRevision !== undefined && current?.revision !== expectedRevision)
95
+ throw new Error('RUNTIME_SESSION_STORE_CONFLICT');
96
+ const revision = (current?.revision ?? 0) + 1;
97
+ const next = { sessionId: snapshot.sessionId, revision, snapshot: structuredClone(snapshot) };
98
+ envelope.sessions = current === undefined ? [...envelope.sessions, next] : envelope.sessions.map((candidate) => candidate.sessionId === snapshot.sessionId ? next : candidate);
99
+ this.writeEnvelope(envelope);
100
+ return revision;
101
+ });
102
+ }
103
+ readEnvelope() {
104
+ try {
105
+ const parsed = JSON.parse(readFileSync(this.filePath, 'utf8'));
106
+ if (!parsed || parsed.schemaVersion !== 1 || !Array.isArray(parsed.sessions))
107
+ throw new Error('INVALID_RUNTIME_SESSION_STORE');
108
+ for (const entry of parsed.sessions) {
109
+ if (!entry || typeof entry.sessionId !== 'string' || !Number.isInteger(entry.revision) || entry.revision < 1)
110
+ throw new Error('INVALID_RUNTIME_SESSION_STORE');
111
+ validateWarmStartSnapshot(entry.snapshot);
112
+ }
113
+ return parsed;
114
+ }
115
+ catch (cause) {
116
+ if (cause.code === 'ENOENT')
117
+ return emptyRuntimeSessionEnvelope();
118
+ if (cause instanceof Error && (cause.message === 'INVALID_RUNTIME_SESSION_STORE' || cause.message === 'INVALID_RUNTIME_SESSION_SNAPSHOT'))
119
+ throw cause;
120
+ throw new Error('INVALID_RUNTIME_SESSION_STORE');
121
+ }
122
+ }
123
+ writeEnvelope(envelope) {
124
+ mkdirSync(dirname(this.filePath), { recursive: true });
125
+ const temporaryPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}-${process.hrtime.bigint().toString()}`;
126
+ let handle;
127
+ try {
128
+ handle = openSync(temporaryPath, 'wx', 0o600);
129
+ writeFileSync(handle, JSON.stringify(envelope), 'utf8');
130
+ fsyncSync(handle);
131
+ closeSync(handle);
132
+ handle = undefined;
133
+ renameSync(temporaryPath, this.filePath);
134
+ }
135
+ finally {
136
+ if (handle !== undefined)
137
+ closeSync(handle);
138
+ rmSync(temporaryPath, { force: true });
139
+ }
140
+ }
141
+ withLock(work) {
142
+ mkdirSync(dirname(this.filePath), { recursive: true });
143
+ const lockPath = `${this.filePath}.lock`;
144
+ const deadline = Date.now() + 30_000;
145
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
146
+ let lock;
147
+ while (lock === undefined) {
148
+ try {
149
+ lock = openSync(lockPath, 'wx', 0o600);
150
+ }
151
+ catch (cause) {
152
+ if (cause.code !== 'EEXIST')
153
+ throw cause;
154
+ const lockStat = statSync(lockPath, { throwIfNoEntry: false });
155
+ if (lockStat && Date.now() - lockStat.mtimeMs > 30_000) {
156
+ rmSync(lockPath, { force: true });
157
+ continue;
158
+ }
159
+ if (Date.now() >= deadline)
160
+ throw new Error('RUNTIME_SESSION_STORE_LOCK_TIMEOUT');
161
+ Atomics.wait(sleeper, 0, 0, 5);
162
+ }
163
+ }
164
+ try {
165
+ return work();
166
+ }
167
+ finally {
168
+ closeSync(lock);
169
+ rmSync(lockPath, { force: true });
170
+ }
171
+ }
172
+ }
173
+ /** Durable SQLite Session Store with transaction-scoped revision CAS. */
174
+ export class SqliteRuntimeSessionStore {
175
+ filePath;
176
+ database;
177
+ constructor(filePath) {
178
+ this.filePath = filePath;
179
+ }
180
+ get(sessionId) { return this.getWithRevision(sessionId)?.snapshot; }
181
+ getWithRevision(sessionId) {
182
+ const row = this.open().prepare('SELECT revision, snapshot FROM runtime_sessions WHERE session_id = ?').get(sessionId);
183
+ if (!row || typeof row.revision !== 'number' || typeof row.snapshot !== 'string')
184
+ return undefined;
185
+ const snapshot = JSON.parse(row.snapshot);
186
+ validateWarmStartSnapshot(snapshot);
187
+ return { revision: row.revision, snapshot: structuredClone(snapshot) };
188
+ }
189
+ put(snapshot) { this.putIfRevision(snapshot); }
190
+ putIfRevision(snapshot, expectedRevision) {
191
+ validateWarmStartSnapshot(snapshot);
192
+ const database = this.open();
193
+ database.exec('BEGIN IMMEDIATE');
194
+ try {
195
+ const current = database.prepare('SELECT revision FROM runtime_sessions WHERE session_id = ?').get(snapshot.sessionId);
196
+ const currentRevision = current && typeof current.revision === 'number' ? current.revision : undefined;
197
+ if (expectedRevision !== undefined && currentRevision !== expectedRevision)
198
+ throw new Error('RUNTIME_SESSION_STORE_CONFLICT');
199
+ const revision = (currentRevision ?? 0) + 1;
200
+ database.prepare('INSERT INTO runtime_sessions (session_id, revision, snapshot) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET revision = excluded.revision, snapshot = excluded.snapshot').run(snapshot.sessionId, revision, JSON.stringify(snapshot));
201
+ database.exec('COMMIT');
202
+ return revision;
203
+ }
204
+ catch (cause) {
205
+ try {
206
+ database.exec('ROLLBACK');
207
+ }
208
+ catch { /* transaction already closed */ }
209
+ throw cause;
210
+ }
211
+ }
212
+ close() { this.database?.close(); this.database = undefined; }
213
+ open() {
214
+ if (this.database)
215
+ return this.database;
216
+ mkdirSync(dirname(this.filePath), { recursive: true });
217
+ const require = createRequire(import.meta.url);
218
+ const { DatabaseSync } = require('node:sqlite');
219
+ this.database = new DatabaseSync(this.filePath);
220
+ this.database.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA busy_timeout = 30000; CREATE TABLE IF NOT EXISTS runtime_sessions (session_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, snapshot TEXT NOT NULL)');
221
+ return this.database;
222
+ }
223
+ }
224
+ /** A durable JSONL audit sink. Each successfully returned append is fsync'd. */
225
+ export class FileRuntimeLogSink {
226
+ filePath;
227
+ pending = Promise.resolve();
228
+ constructor(filePath) {
229
+ this.filePath = filePath;
230
+ }
231
+ async append(log) {
232
+ const operation = this.pending.then(async () => {
233
+ await mkdir(dirname(this.filePath), { recursive: true });
234
+ const handle = await open(this.filePath, 'a', 0o600);
235
+ try {
236
+ await handle.writeFile(`${JSON.stringify(log)}\n`, 'utf8');
237
+ await handle.sync();
238
+ }
239
+ finally {
240
+ await handle.close();
241
+ }
242
+ });
243
+ this.pending = operation.catch(() => undefined);
244
+ await operation;
245
+ }
246
+ }
247
+ /** Sends privacy-filtered audit exports to a host-owned collector. */
248
+ export class HttpRuntimeLogSink {
249
+ endpoint;
250
+ headers;
251
+ timeoutMs;
252
+ fetcher;
253
+ constructor(options) {
254
+ if (!options.endpoint)
255
+ throw new Error('RUNTIME_LOG_ENDPOINT_REQUIRED');
256
+ this.endpoint = options.endpoint;
257
+ this.headers = { 'content-type': 'application/json', ...(options.headers ?? {}) };
258
+ this.timeoutMs = options.timeoutMs ?? 10_000;
259
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0)
260
+ throw new Error('INVALID_RUNTIME_LOG_TIMEOUT');
261
+ this.fetcher = options.fetch ?? globalThis.fetch;
262
+ }
263
+ async append(log) {
264
+ const controller = new AbortController();
265
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
266
+ try {
267
+ const response = await this.fetcher(this.endpoint, { method: 'POST', headers: this.headers, body: JSON.stringify(log), signal: controller.signal });
268
+ if (!response.ok)
269
+ throw new Error(`RUNTIME_LOG_HTTP_${response.status}`);
270
+ }
271
+ catch (cause) {
272
+ if (controller.signal.aborted)
273
+ throw new Error('RUNTIME_LOG_HTTP_TIMEOUT');
274
+ throw cause;
275
+ }
276
+ finally {
277
+ clearTimeout(timer);
278
+ }
279
+ }
280
+ }
281
+ function encodeNumber(value) { return Number.isFinite(value) ? value : 'Infinity'; }
282
+ function decodeNumber(value) {
283
+ if (value === 'Infinity')
284
+ return Number.POSITIVE_INFINITY;
285
+ if (typeof value !== 'number' || Number.isNaN(value))
286
+ throw new Error('INVALID_SESSION_SNAPSHOT');
287
+ return value;
288
+ }
289
+ function validateStateConfiguration(value) {
290
+ const nonNegativeInteger = (candidate) => Number.isInteger(candidate) && candidate >= 0;
291
+ if (typeof value.now !== 'number' || !Number.isFinite(value.now) || !nonNegativeInteger(value.maxTotalLanes) || !nonNegativeInteger(value.maxQueuedEffects) || !nonNegativeInteger(value.historySoftTokens ?? 0) || !nonNegativeInteger(value.historyHardTokens ?? 0) || !nonNegativeInteger(value.maxResultSummaryBytes ?? 0))
292
+ throw new Error('INVALID_SESSION_SNAPSHOT');
293
+ if (value.historyHardTokens !== undefined && value.historySoftTokens !== undefined && value.historyHardTokens < value.historySoftTokens)
294
+ throw new Error('INVALID_SESSION_SNAPSHOT');
295
+ if (value.forkAffinity !== undefined && !['off', 'advise', 'coalesce'].includes(value.forkAffinity))
296
+ throw new Error('INVALID_SESSION_SNAPSHOT');
297
+ for (const key of ['llm', 'tool', 'agent'])
298
+ if (!nonNegativeInteger(decodeNumber(value.maxRunning?.[key])))
299
+ throw new Error('INVALID_SESSION_SNAPSHOT');
300
+ if (value.maxRunning?.none !== 'Infinity' && !nonNegativeInteger(decodeNumber(value.maxRunning?.none)))
301
+ throw new Error('INVALID_SESSION_SNAPSHOT');
302
+ const ids = ['agent', 'lane', 'effect', 'wait', 'result', 'artifact', 'proposal', 'event'];
303
+ if (!value.nextIds || ids.some((key) => !Number.isInteger(value.nextIds[key]) || value.nextIds[key] < 1))
304
+ throw new Error('INVALID_SESSION_SNAPSHOT');
305
+ if (value.trustedSanitizerIds !== undefined && (!Array.isArray(value.trustedSanitizerIds) || new Set(value.trustedSanitizerIds).size !== value.trustedSanitizerIds.length || value.trustedSanitizerIds.some((id) => typeof id !== 'string' || id.length === 0)))
306
+ throw new Error('INVALID_SESSION_SNAPSHOT');
307
+ }
308
+ export function exportRuntimeState(state) {
309
+ return {
310
+ schemaVersion: 1,
311
+ state: {
312
+ now: state.now,
313
+ agents: [...state.agents.entries()].map(([id, agent]) => [id, { ...agent, globalVersions: [...agent.globalVersions.entries()].map(([version, value]) => [version, structuredClone(value)]), ...(agent.globalPrivacy === undefined ? {} : { globalPrivacy: [...agent.globalPrivacy.entries()].map(([version, metadata]) => [version, structuredClone(metadata)]) }) }]),
314
+ lanes: [...state.lanes.entries()].map(([id, lane]) => [id, { ...structuredClone(lane), children: [...lane.children], ownedEffectIds: [...lane.ownedEffectIds], ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: [...lane.visibleResultRefs] }) }]),
315
+ effects: [...state.effects.entries()].map(([id, effect]) => [id, structuredClone(effect)]),
316
+ waits: [...state.waits.entries()].map(([id, wait]) => [id, structuredClone(wait)]),
317
+ results: [...state.results.entries()].map(([id, result]) => [id, structuredClone(result)]),
318
+ artifacts: [...state.artifacts.entries()].map(([ref, artifact]) => [ref, structuredClone(artifact)]),
319
+ toolCallCorrelations: [...state.toolCallCorrelations.entries()].map(([id, correlation]) => [id, structuredClone(correlation)]),
320
+ mergeProposals: [...state.mergeProposals.entries()].map(([id, proposal]) => [id, structuredClone(proposal)]),
321
+ events: state.events.map((event) => normalizeRuntimeEvent(event, event.seq, { sessionId: event.sessionId, timestamp: event.timestamp })),
322
+ ...(state.eventsCompactedThrough === undefined ? {} : { eventsCompactedThrough: state.eventsCompactedThrough }),
323
+ nextIds: { ...state.nextIds },
324
+ maxTotalLanes: state.maxTotalLanes,
325
+ maxQueuedEffects: state.maxQueuedEffects,
326
+ maxRunning: { llm: encodeNumber(state.maxRunning.llm), tool: encodeNumber(state.maxRunning.tool), agent: encodeNumber(state.maxRunning.agent), none: encodeNumber(state.maxRunning.none) },
327
+ forkAffinity: state.forkAffinity,
328
+ historySoftTokens: state.historySoftTokens,
329
+ historyHardTokens: state.historyHardTokens,
330
+ maxResultSummaryBytes: state.maxResultSummaryBytes,
331
+ trustedSanitizerIds: [...state.trustedSanitizerIds].sort(),
332
+ },
333
+ };
334
+ }
335
+ export function exportWarmStartSession(state, sessionId) {
336
+ const agent = state.agents.get(sessionId);
337
+ if (!agent)
338
+ throw new Error(`WARM_START_SOURCE_NOT_FOUND:${sessionId}`);
339
+ const root = state.lanes.get(agent.rootLaneId);
340
+ const laneIds = new Set([...state.lanes.values()].filter((lane) => lane.agentId === sessionId).map((lane) => lane.id));
341
+ const effectIds = new Set([...state.effects.values()].filter((effect) => effect.agentId === sessionId).map((effect) => effect.id));
342
+ const results = [...state.results.entries()].filter(([ref, result]) => (root?.visibleResultRefs?.has(ref) ?? false) || (result.effectId !== undefined && effectIds.has(result.effectId)) || (result.producer?.kind === 'lane' && laneIds.has(result.producer.id)) || (result.producer?.kind === 'effect' && effectIds.has(result.producer.id))).map(([ref, result]) => [ref, structuredClone(result)]);
343
+ return {
344
+ schemaVersion: 1,
345
+ sessionId,
346
+ agent: {
347
+ rootLaneId: agent.rootLaneId,
348
+ latestGlobalVersion: agent.latestGlobalVersion,
349
+ globalVersions: [...agent.globalVersions.entries()].map(([version, value]) => [version, structuredClone(value)]),
350
+ ...(agent.globalPrivacy === undefined ? {} : { globalPrivacy: [...agent.globalPrivacy.entries()].map(([version, value]) => [version, structuredClone(value)]) }),
351
+ },
352
+ visibleResultRefs: [...(root?.visibleResultRefs ?? [])],
353
+ results,
354
+ };
355
+ }
356
+ function relatedEventPrivacy(state, event) {
357
+ const labels = [];
358
+ const effect = event.effectId === undefined ? undefined : state.effects.get(event.effectId);
359
+ if (effect?.input && typeof effect.input === 'object' && !Array.isArray(effect.input)) {
360
+ const inputPrivacy = effect.input.privacy;
361
+ if (inputPrivacy === 'public' || inputPrivacy === 'cloud_allowed' || inputPrivacy === 'local_only')
362
+ labels.push(inputPrivacy);
363
+ }
364
+ const resultRefs = [];
365
+ if (effect?.outcome?.resultRef !== undefined)
366
+ resultRefs.push(effect.outcome.resultRef);
367
+ if (event.type === 'lane.succeeded' && typeof event.data === 'string')
368
+ resultRefs.push(event.data);
369
+ if (event.type === 'privacy.downgraded' && event.data && typeof event.data === 'object' && !Array.isArray(event.data)) {
370
+ const outputRef = event.data.outputRef;
371
+ if (typeof outputRef === 'string')
372
+ resultRefs.push(outputRef);
373
+ }
374
+ for (const ref of resultRefs) {
375
+ const result = state.results.get(ref);
376
+ if (result)
377
+ labels.push(result.privacy);
378
+ }
379
+ return labels.length === 0 ? 'local_only' : labels.reduce((current, next) => privacyRank(next) > privacyRank(current) ? next : current, 'public');
380
+ }
381
+ function redactEvent(event, privacy) {
382
+ return { ...structuredClone(event), payload: { redacted: true, privacy }, data: { redacted: true, privacy } };
383
+ }
384
+ /** Export audit/log data with an explicit privacy ceiling; this is not a recovery snapshot. */
385
+ export function exportRuntimeLog(state, options = {}) {
386
+ const maxPrivacy = options.maxPrivacy ?? 'public';
387
+ const allowed = (privacy) => privacyRank(privacy) <= privacyRank(maxPrivacy);
388
+ const results = [...state.results.values()].map((result) => {
389
+ const copy = structuredClone(result);
390
+ if (allowed(copy.privacy))
391
+ return copy;
392
+ const { value: _value, summary: _summary, ...metadata } = copy;
393
+ return { ...metadata, redacted: true };
394
+ });
395
+ const artifacts = [...state.artifacts.values()].map((artifact) => {
396
+ const copy = structuredClone(artifact);
397
+ if (allowed(copy.privacy))
398
+ return copy;
399
+ const { contentBase64: _content, ...metadata } = copy;
400
+ return { ...metadata, redacted: true };
401
+ });
402
+ const events = state.events.map((event) => {
403
+ const privacy = relatedEventPrivacy(state, event);
404
+ return allowed(privacy) ? structuredClone(event) : redactEvent(event, privacy);
405
+ });
406
+ return { schemaVersion: 1, maxPrivacy, events, results, artifacts };
407
+ }
408
+ /** Applies the privacy ceiling before handing an audit export to its sink. */
409
+ export async function exportRuntimeLogTo(state, sink, options = {}) {
410
+ const log = exportRuntimeLog(state, options);
411
+ await sink.append(log);
412
+ return log;
413
+ }
414
+ export function serializeRuntimeState(state) { return exportRuntimeState(state); }
415
+ export function importRuntimeState(snapshot) {
416
+ const value = snapshot;
417
+ if (!value || value.schemaVersion !== 1 || !value.state || !Array.isArray(value.state.agents) || !Array.isArray(value.state.lanes) || !Array.isArray(value.state.effects) || !Array.isArray(value.state.waits) || !Array.isArray(value.state.results) || !Array.isArray(value.state.events) || (value.state.eventsCompactedThrough !== undefined && (!Number.isInteger(value.state.eventsCompactedThrough) || value.state.eventsCompactedThrough < 0)))
418
+ throw new Error('INVALID_SESSION_SNAPSHOT');
419
+ validateStateConfiguration(value.state);
420
+ const state = createRuntimeState(value.state.maxTotalLanes, { maxQueuedEffects: value.state.maxQueuedEffects, maxRunning: { llm: decodeNumber(value.state.maxRunning.llm), tool: decodeNumber(value.state.maxRunning.tool), agent: decodeNumber(value.state.maxRunning.agent), none: decodeNumber(value.state.maxRunning.none) }, forkAffinity: value.state.forkAffinity ?? 'advise', ...(value.state.historySoftTokens === undefined ? {} : { historySoftTokens: value.state.historySoftTokens }), ...(value.state.historyHardTokens === undefined ? {} : { historyHardTokens: value.state.historyHardTokens }), ...(value.state.maxResultSummaryBytes === undefined ? {} : { maxResultSummaryBytes: value.state.maxResultSummaryBytes }), ...(value.state.trustedSanitizerIds === undefined ? {} : { trustedSanitizerIds: value.state.trustedSanitizerIds }) });
421
+ state.now = value.state.now;
422
+ state.nextIds = { ...value.state.nextIds, artifact: value.state.nextIds.artifact ?? 1, proposal: value.state.nextIds.proposal ?? 1 };
423
+ for (const [id, agent] of value.state.agents) {
424
+ const { globalVersions, globalPrivacy, ...agentValue } = agent;
425
+ state.agents.set(id, { ...agentValue, globalVersions: new Map(globalVersions.map(([version, context]) => [version, structuredClone(context)])), ...(globalPrivacy === undefined ? {} : { globalPrivacy: new Map(globalPrivacy.map(([version, metadata]) => [version, structuredClone(metadata)])) }) });
426
+ }
427
+ for (const [id, lane] of value.state.lanes) {
428
+ const { visibleResultRefs, ...laneValue } = lane;
429
+ state.lanes.set(id, { ...laneValue, children: new Set(lane.children), ownedEffectIds: new Set(lane.ownedEffectIds), ...(visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(visibleResultRefs) }) });
430
+ }
431
+ for (const [id, effect] of value.state.effects)
432
+ state.effects.set(id, structuredClone(effect));
433
+ for (const [id, wait] of value.state.waits)
434
+ state.waits.set(id, structuredClone(wait));
435
+ for (const [id, result] of value.state.results)
436
+ state.results.set(id, { ...structuredClone(result), storageState: result.storageState ?? 'memory', pinCount: result.pinCount ?? 0 });
437
+ for (const [ref, artifact] of value.state.artifacts ?? [])
438
+ state.artifacts.set(ref, structuredClone(artifact));
439
+ for (const [id, correlation] of value.state.toolCallCorrelations ?? [])
440
+ state.toolCallCorrelations.set(id, structuredClone(correlation));
441
+ for (const [id, proposal] of value.state.mergeProposals ?? [])
442
+ state.mergeProposals.set(id, structuredClone(proposal));
443
+ state.events = value.state.events.map((event) => normalizeRuntimeEvent(event, event.seq, { sessionId: event.sessionId, timestamp: event.timestamp }));
444
+ if (value.state.eventsCompactedThrough !== undefined)
445
+ state.eventsCompactedThrough = value.state.eventsCompactedThrough;
446
+ return state;
447
+ }
@@ -0,0 +1,125 @@
1
+ import type { JsonValue, ResourceLockSpec, SideEffectPolicy } from '../core/types.js';
2
+ export interface RuntimeToolPermissions {
3
+ workspaceRoots?: string[];
4
+ networkHosts?: string[];
5
+ }
6
+ export interface RuntimeToolManifest {
7
+ name: string;
8
+ version: string;
9
+ description: string;
10
+ tags?: string[];
11
+ inputSchema: Record<string, unknown>;
12
+ outputSchema: Record<string, unknown>;
13
+ concurrencyClass: 'llm' | 'tool' | 'agent' | 'none';
14
+ locks: ResourceLockSpec[];
15
+ resources?: ResourceLockSpec[];
16
+ supportsAbortSignal: boolean;
17
+ sideEffectPolicy: SideEffectPolicy;
18
+ retrySafety: 'read_only' | 'idempotent' | 'unsafe';
19
+ defaultTimeoutMs: number;
20
+ maxResultSummaryBytes?: number;
21
+ permissions?: RuntimeToolPermissions;
22
+ }
23
+ export interface RuntimeToolContext {
24
+ toolCallId: string;
25
+ effectId: string;
26
+ attemptId: string;
27
+ idempotencyKey?: string;
28
+ agentId: string;
29
+ laneId: string;
30
+ signal: AbortSignal;
31
+ emit(event: {
32
+ type: 'progress' | 'warning' | 'diagnostic';
33
+ data: JsonValue;
34
+ }): void;
35
+ }
36
+ export interface RuntimeReconcileContext {
37
+ toolCallId: string;
38
+ effectId: string;
39
+ attemptId: string;
40
+ agentId: string;
41
+ laneId: string;
42
+ signal: AbortSignal;
43
+ }
44
+ export interface RuntimeReconcileResult {
45
+ status: 'succeeded' | 'failed' | 'cancelled' | 'unknown';
46
+ output?: unknown;
47
+ error?: {
48
+ code: string;
49
+ message: string;
50
+ retryable?: boolean;
51
+ details?: JsonValue;
52
+ };
53
+ }
54
+ export interface RuntimeToolDefinition {
55
+ manifest: RuntimeToolManifest;
56
+ resourceAdmissionMode?: 'explicit' | 'default';
57
+ validateInput?(input: unknown): unknown;
58
+ execute(input: unknown, context: RuntimeToolContext): Promise<unknown> | unknown;
59
+ executionRef?(input: unknown, context: RuntimeToolContext): JsonValue;
60
+ resolveResources?(input: unknown): ResourceLockSpec[];
61
+ reconcile?(executionRef: JsonValue, context: RuntimeReconcileContext): Promise<RuntimeReconcileResult>;
62
+ normalize?(output: unknown): JsonValue;
63
+ summarize?(output: unknown): JsonValue;
64
+ }
65
+ export interface RuntimeToolDiscoveryQuery {
66
+ text?: string;
67
+ tags?: string[];
68
+ sideEffectPolicy?: RuntimeToolManifest['sideEffectPolicy'];
69
+ concurrencyClass?: RuntimeToolManifest['concurrencyClass'];
70
+ limit?: number;
71
+ }
72
+ export interface RuntimeToolDiscoveryResult {
73
+ manifest: RuntimeToolManifest;
74
+ score: number;
75
+ }
76
+ export interface RuntimeToolSetSnapshot {
77
+ id: string;
78
+ version: string;
79
+ tools: RuntimeToolManifest[];
80
+ }
81
+ export interface RuntimeToolRegistryPolicy {
82
+ allow?: string[];
83
+ deny?: string[];
84
+ workspaceRoots?: string[];
85
+ networkHosts?: string[];
86
+ allowNetwork?: boolean;
87
+ }
88
+ export interface RuntimeToolAdmission {
89
+ locks: ResourceLockSpec[];
90
+ sideEffectPolicy: RuntimeToolManifest['sideEffectPolicy'];
91
+ defaultTimeoutMs: number;
92
+ retrySafety: RuntimeToolManifest['retrySafety'];
93
+ version: string;
94
+ }
95
+ /**
96
+ * Core tool catalog used by PulseRuntime. Tool SDK definitions are structurally
97
+ * compatible, so an application may register them directly and still choose a
98
+ * separate executor or adapter.
99
+ */
100
+ export declare class RuntimeToolRegistry {
101
+ private readonly definitions;
102
+ private readonly policy;
103
+ constructor(policy?: RuntimeToolRegistryPolicy);
104
+ register(definition: RuntimeToolDefinition | unknown): void;
105
+ get(name: string): RuntimeToolDefinition | undefined;
106
+ isAllowed(name: string): boolean;
107
+ permissionReasons(name: string): string[];
108
+ list(): RuntimeToolManifest[];
109
+ validateInput(name: string, input: unknown): unknown;
110
+ discover(query?: RuntimeToolDiscoveryQuery): RuntimeToolDiscoveryResult[];
111
+ compileToolSet(id: string, query?: RuntimeToolDiscoveryQuery, version?: string): RuntimeToolSetSnapshot;
112
+ execute(name: string, input: unknown, context: RuntimeToolContext | AbortSignal): Promise<unknown>;
113
+ executeDetailed(name: string, input: unknown, context: RuntimeToolContext | AbortSignal): Promise<{
114
+ output: unknown;
115
+ normalized?: JsonValue;
116
+ summary?: JsonValue;
117
+ manifest: RuntimeToolManifest;
118
+ }>;
119
+ reconcileDetailed(name: string, executionRef: JsonValue, context: RuntimeReconcileContext): Promise<RuntimeReconcileResult>;
120
+ executionRef(name: string, input: unknown, context: RuntimeToolContext): JsonValue | undefined;
121
+ resolveResources(name: string, input: unknown): ResourceLockSpec[];
122
+ admission(name: string, input: unknown): RuntimeToolAdmission;
123
+ private require;
124
+ private permissionsAllowed;
125
+ }