@arnilo/prism 0.0.23 → 0.0.25

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 (49) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/dist/agent-event-source.d.ts +11 -0
  3. package/dist/agent-event-source.js +512 -0
  4. package/dist/agent-loops.js +37 -5
  5. package/dist/agent-run-state.d.ts +27 -1
  6. package/dist/agent-run-state.js +86 -5
  7. package/dist/agents.js +890 -78
  8. package/dist/contracts.d.ts +338 -4
  9. package/dist/contracts.js +53 -0
  10. package/dist/index.d.ts +9 -4
  11. package/dist/index.js +5 -2
  12. package/dist/testing/agent-event-source-conformance.d.ts +4 -0
  13. package/dist/testing/agent-event-source-conformance.js +54 -0
  14. package/dist/testing/persistence-schema.d.ts +2 -2
  15. package/dist/testing/persistence-schema.js +58 -21
  16. package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
  17. package/dist/testing/tool-effect-store-conformance.js +85 -0
  18. package/dist/tool-effects.d.ts +15 -0
  19. package/dist/tool-effects.js +338 -0
  20. package/dist/tools.d.ts +4 -1
  21. package/dist/tools.js +219 -9
  22. package/docs/0.1.0-readiness.md +10 -9
  23. package/docs/a2a.md +6 -2
  24. package/docs/ag-ui-adoption.md +77 -0
  25. package/docs/ag-ui.md +77 -42
  26. package/docs/agent-events.md +5 -1
  27. package/docs/agent-loops.md +9 -1
  28. package/docs/agent-session-runtime.md +9 -2
  29. package/docs/browser-automation.md +2 -0
  30. package/docs/coding-agent-tools.md +2 -0
  31. package/docs/coding-security.md +1 -1
  32. package/docs/database-persistence.md +2 -0
  33. package/docs/enterprise-postgres-state.md +5 -1
  34. package/docs/host-security.md +8 -1
  35. package/docs/index.md +13 -11
  36. package/docs/mcp-tools.md +19 -2
  37. package/docs/migration.md +46 -0
  38. package/docs/performance.md +25 -0
  39. package/docs/postgres-persistence.md +5 -2
  40. package/docs/public-contracts.md +2 -0
  41. package/docs/release-and-install.md +70 -690
  42. package/docs/server.md +10 -6
  43. package/docs/sqlite-persistence.md +10 -2
  44. package/docs/supervisors.md +6 -0
  45. package/docs/tool-effects.md +95 -0
  46. package/docs/tools.md +4 -0
  47. package/docs/work-tools.md +4 -0
  48. package/docs/workflows.md +1 -1
  49. package/package.json +11 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.25] - 2026-08-06
4
+
5
+ ### Added
6
+ - Durable custom `AgentLoopStrategy` hooks: optional `revision` / `snapshot` / `restore`; `AgentLoopStateError` fail-closed codes; fingerprint includes loop `{name,revision}`.
7
+ - Shared pending-decision model: parallel approvals, batch CAS `decisions`, sticky allow/reject for run, modified arguments, elicitation; nested supervisor attribution.
8
+ - Protocol mappings: AG-UI/ACP/server batch resume, MCP elicitation helpers, coding `ask_user_decision` elicitation hook.
9
+ - Opt-in A2UI painting middleware + standard AG-UI projectors (`messages`/`state`/`activity`).
10
+ - Network-free Phase 8 conformance + `benchmark-0.0.25.json` evidence; examples `durable-loops-and-approvals.ts`, `ag-ui-a2ui.ts`.
11
+
12
+ ### Changed
13
+ - Publishable graph remains **47** manifests at **0.0.25**.
14
+ - Fingerprint loop entry shape `string` → `{name,revision}` (0.0.24 persisted durable runs fail closed on resume).
15
+
16
+ ### Breaking (minor, pre-1.0)
17
+ - Custom loops on durable runs need snapshot/restore hooks or `ERR_PRISM_LOOP_NOT_DURABLE`.
18
+ - Resume prefers `decisions: RunDecision[]`; legacy binary `decision` remains but is exclusive with the batch path.
19
+ - ACP permission offers four outcomes; `reject_once` is blocked-continue (cancelled stays terminal deny).
20
+
21
+ See [docs/migration.md](docs/migration.md) for the 0.0.24 → 0.0.25 guide.
22
+
23
+ ## [0.0.24] - 2026-08-04
24
+
25
+ ### Added
26
+ - Durable `AgentEventSource` with append/page/subscribe/resume and PostgreSQL LISTEN/NOTIFY wakeups (schema v6 streams + v7 retention index).
27
+ - Recoverable `ToolEffectStore` claim/CAS lifecycle; enterprise PostgreSQL `toolEffects`; coding/browser/work/MCP/supervisor effect classification.
28
+ - AG-UI 0.0.57 full input/event/interrupt compatibility plus MCP Apps and remote A2A adapters.
29
+ - Protected Phase 7 process conformance and `benchmark-0.0.24.json` evidence.
30
+ - Example `examples/distributed-events-and-tool-effects.ts`; docs `docs/tool-effects.md`.
31
+
32
+ ### Changed
33
+ - Work mutations require core-derived idempotency keys; ambiguous outcomes stay `unknown` (not exactly-once).
34
+ - Publishable graph remains **47** manifests at **0.0.24**.
35
+
36
+ ### Breaking (minor, pre-1.0)
37
+ - Hosts using approved work mutations must supply `effectStore` / core `idempotencyKey` (model keys ignored).
38
+ - Durable event reconnect uses `AgentEventSource` cursors / `Last-Event-ID`; sticky sessions are optional only.
39
+
40
+ See [docs/migration.md](docs/migration.md) for the 0.0.23 → 0.0.24 guide.
41
+
3
42
  ## [0.0.23] - 2026-08-03
4
43
 
5
44
  ### Added
@@ -17,7 +56,6 @@
17
56
 
18
57
  See [docs/migration.md](docs/migration.md) for the 0.0.22 → 0.0.23 guide.
19
58
 
20
-
21
59
  ## [0.0.22] - 2026-07-31
22
60
 
23
61
  ### Added
@@ -45,7 +83,6 @@ See [docs/migration.md](docs/migration.md) for the full 0.0.21 → 0.0.22 notes.
45
83
 
46
84
  See [docs/migration.md](docs/migration.md) for the full 0.0.20 → 0.0.21 notes.
47
85
 
48
-
49
86
  ## [0.0.20] - 2026-07-31
50
87
 
51
88
  ### Added
@@ -169,7 +206,6 @@ All notable changes to this project will be documented in this file.
169
206
  - Versioned all 35 first-party manifests and exact internal ranges to `0.0.12`; `@arnilo/prism-all` includes AG-UI while `@arnilo/prism-code` and `@arnilo/prism-sdk` remain free of UI protocol dependencies.
170
207
  - Added network-free interoperability/compaction evidence: `scripts/benchmark-0.0.12.mjs`.
171
208
 
172
-
173
209
  ## [0.0.11] - 2026-07-22
174
210
 
175
211
  ### Added
@@ -0,0 +1,11 @@
1
+ import type { AgentEventSource, AgentEventSourceOptions } from "./contracts.js";
2
+ export type AgentEventSourceErrorCode = "ERR_PRISM_AGENT_EVENT_SOURCE_INPUT" | "ERR_PRISM_AGENT_EVENT_SOURCE_CURSOR" | "ERR_PRISM_AGENT_EVENT_SOURCE_RETENTION" | "ERR_PRISM_AGENT_EVENT_SOURCE_OVERFLOW" | "ERR_PRISM_AGENT_EVENT_SOURCE_CLOSED";
3
+ export declare class AgentEventSourceError extends Error {
4
+ readonly code: AgentEventSourceErrorCode;
5
+ constructor(code: AgentEventSourceErrorCode, message: string);
6
+ }
7
+ /**
8
+ * In-process, non-production reference implementation. It has no cross-process
9
+ * wakeup or persistence; use a database-backed source for distributed delivery.
10
+ */
11
+ export declare function createMemoryAgentEventSource(options?: AgentEventSourceOptions): AgentEventSource;
@@ -0,0 +1,512 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ export class AgentEventSourceError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "AgentEventSourceError";
8
+ }
9
+ }
10
+ const DEFAULT_MAX_EVENT_BYTES = 64 * 1024;
11
+ const HARD_MAX_EVENT_BYTES = 1024 * 1024;
12
+ const DEFAULT_MAX_PAGE_SIZE = 100;
13
+ const HARD_MAX_PAGE_SIZE = 500;
14
+ const DEFAULT_MAX_CURSOR_BYTES = 4 * 1024;
15
+ const HARD_MAX_CURSOR_BYTES = 16 * 1024;
16
+ const DEFAULT_MAX_QUEUED_EVENTS = 128;
17
+ const HARD_MAX_QUEUED_EVENTS = 4096;
18
+ const DEFAULT_MAX_SUBSCRIBERS = 256;
19
+ const HARD_MAX_SUBSCRIBERS = 4096;
20
+ const DEFAULT_POLL_INTERVAL_MS = 1000;
21
+ const HARD_POLL_INTERVAL_MS = 30_000;
22
+ const DEFAULT_RECONNECT_INITIAL_MS = 100;
23
+ const HARD_RECONNECT_INITIAL_MS = 5000;
24
+ const DEFAULT_RECONNECT_MAX_MS = 5000;
25
+ const HARD_RECONNECT_MAX_MS = 30_000;
26
+ const DEFAULT_MAX_RETAINED_EVENTS_PER_RUN = 10_000;
27
+ const HARD_MAX_RETAINED_EVENTS_PER_RUN = 100_000;
28
+ const DEFAULT_MAX_RETENTION_AGE_MS = 30 * 24 * 60 * 60 * 1000;
29
+ const HARD_MAX_RETENTION_AGE_MS = 365 * 24 * 60 * 60 * 1000;
30
+ const DEFAULT_CLEANUP_BATCH = 100;
31
+ const HARD_CLEANUP_BATCH = 500;
32
+ /**
33
+ * In-process, non-production reference implementation. It has no cross-process
34
+ * wakeup or persistence; use a database-backed source for distributed delivery.
35
+ */
36
+ export function createMemoryAgentEventSource(options = {}) {
37
+ const limits = resolveOptions(options);
38
+ const streams = new Map();
39
+ const byId = new Map();
40
+ const subscribers = new Set();
41
+ const secret = randomBytes(32);
42
+ let closed = false;
43
+ const source = {
44
+ async append(input) {
45
+ assertOpen();
46
+ const record = normalizeRecord(input, limits.maxEventBytes);
47
+ const existing = byId.get(record.id);
48
+ if (existing) {
49
+ if (sameRecord(existing, record))
50
+ return clone(existing);
51
+ throw inputError();
52
+ }
53
+ const stream = getStream(record, true);
54
+ const sequence = record.sequence ?? stream.nextSequence;
55
+ if (!Number.isSafeInteger(sequence) || sequence < 1 || sequence !== stream.nextSequence)
56
+ throw inputError();
57
+ const stored = { ...record, sequence };
58
+ assertCursorFits(stored);
59
+ stream.records.set(stored.sequence, stored);
60
+ stream.nextSequence += 1;
61
+ byId.set(stored.id, stored);
62
+ while (stream.records.size > limits.maxRetainedEventsPerRun)
63
+ drop(stream, stream.records.values().next().value);
64
+ for (const subscriber of subscribers)
65
+ subscriber.notify();
66
+ return clone(stored);
67
+ },
68
+ async page(input) {
69
+ assertOpen();
70
+ throwIfAborted(input.signal);
71
+ const read = normalizeRead(input, limits.maxPageSize);
72
+ const stream = getStream({ ...read.ownership, sessionId: read.sessionId, runId: read.runId }, false);
73
+ const records = orderedRecords(stream);
74
+ const after = read.after === undefined ? undefined : decodeCursor(read.after, read, stream, records);
75
+ const start = after === undefined ? 0 : positionAfter(records, after, stream);
76
+ const selected = records.slice(start, start + read.limit);
77
+ const hasMore = start + selected.length < records.length;
78
+ return {
79
+ items: selected.map((record) => ({ record: clone(record), cursor: encodeCursor(record) })),
80
+ ...(hasMore && selected.length > 0 ? { nextCursor: encodeCursor(selected.at(-1)) } : {}),
81
+ terminal: !hasMore && terminalAt(records, after),
82
+ };
83
+ },
84
+ subscribe(input) {
85
+ return handoffAgentEvents((after) => source.page({ ...input, ...(after === undefined ? {} : { after }) }), () => createWake(input.signal), input.after, limits.maxRetainedEventsPerRun);
86
+ },
87
+ async cleanup(input) {
88
+ assertOpen();
89
+ throwIfAborted(input.signal);
90
+ const ownership = normalizeOwnership(input.ownership);
91
+ const before = parseTimestamp(input.before, "input");
92
+ const limit = bounded(input.limit, DEFAULT_CLEANUP_BATCH, HARD_CLEANUP_BATCH, "cleanup limit");
93
+ const matches = [];
94
+ for (const stream of streams.values()) {
95
+ if (!sameOwnership(ownership, stream.ownership))
96
+ continue;
97
+ for (const record of stream.records.values()) {
98
+ if (Date.parse(record.timestamp) < before)
99
+ matches.push({ stream, record });
100
+ }
101
+ }
102
+ matches.sort((a, b) => a.record.timestamp.localeCompare(b.record.timestamp) ||
103
+ a.record.sequence - b.record.sequence ||
104
+ a.record.id.localeCompare(b.record.id));
105
+ for (const { stream, record } of matches.slice(0, limit))
106
+ drop(stream, record);
107
+ return { deleted: Math.min(matches.length, limit) };
108
+ },
109
+ close() {
110
+ if (closed)
111
+ return;
112
+ closed = true;
113
+ for (const subscriber of subscribers)
114
+ subscriber.fail(closedError());
115
+ subscribers.clear();
116
+ },
117
+ };
118
+ return source;
119
+ function getStream(input, create) {
120
+ const ownership = normalizeOwnership(input);
121
+ const sessionId = requiredText(input.sessionId);
122
+ const runId = requiredText(input.runId);
123
+ const key = streamKey(ownership, sessionId, runId);
124
+ let stream = streams.get(key);
125
+ if (!stream && create) {
126
+ stream = { ownership, sessionId, runId, records: new Map(), nextSequence: 1 };
127
+ streams.set(key, stream);
128
+ }
129
+ return stream;
130
+ }
131
+ function encodeCursor(record) {
132
+ const payload = {
133
+ v: 1,
134
+ tenantId: record.tenantId,
135
+ ...(record.accountId === undefined ? {} : { accountId: record.accountId }),
136
+ ...(record.userId === undefined ? {} : { userId: record.userId }),
137
+ sessionId: record.sessionId,
138
+ runId: record.runId,
139
+ sequence: record.sequence,
140
+ id: record.id,
141
+ };
142
+ const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
143
+ const signature = createHmac("sha256", secret).update(body).digest("base64url");
144
+ const encoded = `${body}.${signature}`;
145
+ if (Buffer.byteLength(encoded, "utf8") > limits.maxCursorBytes)
146
+ throw inputError();
147
+ return encoded;
148
+ }
149
+ function decodeCursor(encoded, input, stream, records) {
150
+ if (typeof encoded !== "string" || encoded.length === 0 || Buffer.byteLength(encoded, "utf8") > limits.maxCursorBytes) {
151
+ throw cursorError();
152
+ }
153
+ const [body, signature, extra] = encoded.split(".");
154
+ if (!body || !signature || extra !== undefined)
155
+ throw cursorError();
156
+ const expected = createHmac("sha256", secret).update(body).digest();
157
+ let actual;
158
+ try {
159
+ actual = Buffer.from(signature, "base64url");
160
+ }
161
+ catch {
162
+ throw cursorError();
163
+ }
164
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
165
+ throw cursorError();
166
+ let value;
167
+ try {
168
+ value = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
169
+ }
170
+ catch {
171
+ throw cursorError();
172
+ }
173
+ if (!validCursor(value))
174
+ throw cursorError();
175
+ const cursor = value;
176
+ if (!sameOwnership(input.ownership, cursor) || cursor.sessionId !== input.sessionId || cursor.runId !== input.runId) {
177
+ throw cursorError();
178
+ }
179
+ if (!stream || positionOf(records, cursor) < 0)
180
+ throw retentionError();
181
+ return cursor;
182
+ }
183
+ function assertCursorFits(record) {
184
+ void encodeCursor(record);
185
+ }
186
+ function createWake(signal) {
187
+ throwIfAborted(signal);
188
+ if (subscribers.size >= limits.maxSubscribers)
189
+ throw overflowError();
190
+ const subscription = new WakeSubscription(limits.maxQueuedEvents, signal, () => subscribers.delete(subscription));
191
+ subscribers.add(subscription);
192
+ return subscription;
193
+ }
194
+ function drop(stream, record) {
195
+ if (stream.records.get(record.sequence) === record)
196
+ stream.records.delete(record.sequence);
197
+ if (byId.get(record.id) === record)
198
+ byId.delete(record.id);
199
+ }
200
+ function assertOpen() {
201
+ if (closed)
202
+ throw closedError();
203
+ }
204
+ }
205
+ /** Shared replay/live loop. Its live iterator is started before every page, closing the query-to-wait race. */
206
+ function handoffAgentEvents(page, createWake, initialAfter, maxSeen) {
207
+ return {
208
+ [Symbol.asyncIterator]() {
209
+ const wake = createWake();
210
+ const iterator = run(wake);
211
+ return {
212
+ next: () => iterator.next(),
213
+ async return() {
214
+ await wake.return?.();
215
+ return (await iterator.return?.(undefined)) ?? { value: undefined, done: true };
216
+ },
217
+ async throw(error) {
218
+ await wake.return?.();
219
+ if (iterator.throw)
220
+ return iterator.throw(error);
221
+ throw error;
222
+ },
223
+ };
224
+ },
225
+ };
226
+ async function* run(wake) {
227
+ const seen = new Set();
228
+ let after = initialAfter;
229
+ let waiting = nextWake(wake); // Register live delivery before replaying durable rows.
230
+ try {
231
+ while (true) {
232
+ const replay = await page(after);
233
+ for (const item of replay.items) {
234
+ after = item.cursor;
235
+ if (seen.has(item.record.id))
236
+ continue;
237
+ seen.add(item.record.id);
238
+ if (seen.size > maxSeen)
239
+ seen.delete(seen.values().next().value);
240
+ yield item;
241
+ }
242
+ if (replay.terminal)
243
+ return;
244
+ const next = await waiting;
245
+ if (next.done)
246
+ return;
247
+ waiting = nextWake(wake);
248
+ }
249
+ }
250
+ finally {
251
+ await wake.return?.();
252
+ }
253
+ }
254
+ }
255
+ function nextWake(wake) {
256
+ const next = wake.next();
257
+ // A slow subscriber can overflow while replay items are yielded. Keep the
258
+ // rejection observed until the generator reaches its next wait.
259
+ void next.catch(() => undefined);
260
+ return next;
261
+ }
262
+ class WakeSubscription {
263
+ maxQueuedEvents;
264
+ onFinish;
265
+ queued = 0;
266
+ waiting;
267
+ failure;
268
+ done = false;
269
+ abort;
270
+ constructor(maxQueuedEvents, signal, onFinish) {
271
+ this.maxQueuedEvents = maxQueuedEvents;
272
+ this.onFinish = onFinish;
273
+ this.abort = () => this.fail(signal?.reason ?? new DOMException("Aborted", "AbortError"));
274
+ if (signal?.aborted)
275
+ this.abort();
276
+ else
277
+ signal?.addEventListener("abort", this.abort, { once: true });
278
+ }
279
+ notify() {
280
+ if (this.done)
281
+ return;
282
+ if (this.waiting) {
283
+ const { resolve } = this.waiting;
284
+ this.waiting = undefined;
285
+ resolve({ value: undefined, done: false });
286
+ return;
287
+ }
288
+ if (this.queued >= this.maxQueuedEvents) {
289
+ this.fail(overflowError());
290
+ return;
291
+ }
292
+ this.queued += 1;
293
+ }
294
+ fail(error) {
295
+ if (this.done)
296
+ return;
297
+ this.failure = error;
298
+ this.finish();
299
+ }
300
+ async next() {
301
+ if (this.failure !== undefined)
302
+ throw this.failure;
303
+ if (this.done)
304
+ return { value: undefined, done: true };
305
+ if (this.queued > 0) {
306
+ this.queued -= 1;
307
+ return { value: undefined, done: false };
308
+ }
309
+ return new Promise((resolve, reject) => {
310
+ this.waiting = { resolve, reject };
311
+ });
312
+ }
313
+ async return() {
314
+ this.finish();
315
+ return { value: undefined, done: true };
316
+ }
317
+ finish() {
318
+ if (this.done)
319
+ return;
320
+ this.done = true;
321
+ this.onFinish();
322
+ const waiting = this.waiting;
323
+ this.waiting = undefined;
324
+ if (this.failure !== undefined)
325
+ waiting?.reject(this.failure);
326
+ else
327
+ waiting?.resolve({ value: undefined, done: true });
328
+ }
329
+ }
330
+ function resolveOptions(options) {
331
+ const resolved = {
332
+ maxEventBytes: bounded(options.maxEventBytes, DEFAULT_MAX_EVENT_BYTES, HARD_MAX_EVENT_BYTES, "maxEventBytes"),
333
+ maxPageSize: bounded(options.maxPageSize, DEFAULT_MAX_PAGE_SIZE, HARD_MAX_PAGE_SIZE, "maxPageSize"),
334
+ maxCursorBytes: bounded(options.maxCursorBytes, DEFAULT_MAX_CURSOR_BYTES, HARD_MAX_CURSOR_BYTES, "maxCursorBytes"),
335
+ maxQueuedEvents: bounded(options.maxQueuedEvents, DEFAULT_MAX_QUEUED_EVENTS, HARD_MAX_QUEUED_EVENTS, "maxQueuedEvents"),
336
+ maxSubscribers: bounded(options.maxSubscribers, DEFAULT_MAX_SUBSCRIBERS, HARD_MAX_SUBSCRIBERS, "maxSubscribers"),
337
+ pollIntervalMs: bounded(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS, HARD_POLL_INTERVAL_MS, "pollIntervalMs"),
338
+ reconnectInitialMs: bounded(options.reconnectInitialMs, DEFAULT_RECONNECT_INITIAL_MS, HARD_RECONNECT_INITIAL_MS, "reconnectInitialMs"),
339
+ reconnectMaxMs: bounded(options.reconnectMaxMs, DEFAULT_RECONNECT_MAX_MS, HARD_RECONNECT_MAX_MS, "reconnectMaxMs"),
340
+ maxRetainedEventsPerRun: bounded(options.maxRetainedEventsPerRun, DEFAULT_MAX_RETAINED_EVENTS_PER_RUN, HARD_MAX_RETAINED_EVENTS_PER_RUN, "maxRetainedEventsPerRun"),
341
+ maxRetentionAgeMs: bounded(options.maxRetentionAgeMs, DEFAULT_MAX_RETENTION_AGE_MS, HARD_MAX_RETENTION_AGE_MS, "maxRetentionAgeMs"),
342
+ };
343
+ if (resolved.reconnectInitialMs > resolved.reconnectMaxMs) {
344
+ throw new RangeError("reconnectInitialMs must not exceed reconnectMaxMs");
345
+ }
346
+ return resolved;
347
+ }
348
+ function normalizeRecord(input, maxEventBytes) {
349
+ if (!input || typeof input !== "object")
350
+ throw inputError();
351
+ const record = clone(input);
352
+ const ownership = normalizeOwnership(record);
353
+ const sessionId = requiredText(record.sessionId);
354
+ const runId = requiredText(record.runId);
355
+ const id = requiredText(record.id);
356
+ if (record.redacted !== true ||
357
+ record.type !== record.event?.type ||
358
+ record.event.sessionId !== sessionId ||
359
+ record.event.runId !== runId) {
360
+ throw inputError();
361
+ }
362
+ parseTimestamp(record.timestamp, "input");
363
+ if (record.sequence !== undefined && (!Number.isSafeInteger(record.sequence) || record.sequence < 1))
364
+ throw inputError();
365
+ if (Buffer.byteLength(JSON.stringify(record), "utf8") > maxEventBytes)
366
+ throw inputError();
367
+ return {
368
+ ...record,
369
+ ...ownership,
370
+ id,
371
+ sessionId,
372
+ runId,
373
+ ...(record.sequence === undefined ? {} : { sequence: record.sequence }),
374
+ };
375
+ }
376
+ function normalizeRead(input, maxPageSize) {
377
+ if (!input || typeof input !== "object")
378
+ throw inputError();
379
+ return {
380
+ ...input,
381
+ ownership: normalizeOwnership(input.ownership),
382
+ sessionId: requiredText(input.sessionId),
383
+ runId: requiredText(input.runId),
384
+ limit: bounded(input.limit, maxPageSize, maxPageSize, "page limit"),
385
+ };
386
+ }
387
+ function normalizeOwnership(input) {
388
+ if (!input || typeof input.tenantId !== "string" || input.tenantId.length === 0)
389
+ throw inputError();
390
+ if (input.accountId !== undefined && (typeof input.accountId !== "string" || input.accountId.length === 0))
391
+ throw inputError();
392
+ if (input.userId !== undefined && (typeof input.userId !== "string" || input.userId.length === 0))
393
+ throw inputError();
394
+ return {
395
+ tenantId: input.tenantId,
396
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
397
+ ...(input.userId === undefined ? {} : { userId: input.userId }),
398
+ };
399
+ }
400
+ function requiredText(value) {
401
+ if (typeof value !== "string" || value.length === 0)
402
+ throw inputError();
403
+ return value;
404
+ }
405
+ function parseTimestamp(value, kind) {
406
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) {
407
+ throw kind === "cursor" ? cursorError() : inputError();
408
+ }
409
+ const parsed = Date.parse(value);
410
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value)
411
+ throw kind === "cursor" ? cursorError() : inputError();
412
+ return parsed;
413
+ }
414
+ function bounded(value, fallback, hard, name) {
415
+ const resolved = value ?? fallback;
416
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > hard)
417
+ throw inputError(`${name} is outside its allowed range`);
418
+ return resolved;
419
+ }
420
+ function sameOwnership(a, b) {
421
+ return a.tenantId === b.tenantId && a.accountId === b.accountId && a.userId === b.userId;
422
+ }
423
+ function streamKey(ownership, sessionId, runId) {
424
+ return JSON.stringify([ownership.tenantId, ownership.accountId, ownership.userId, sessionId, runId]);
425
+ }
426
+ function orderedRecords(stream) {
427
+ return stream ? [...stream.records.values()] : [];
428
+ }
429
+ function positionOf(records, cursor) {
430
+ return records.findIndex((record) => record.sequence === cursor.sequence && record.id === cursor.id);
431
+ }
432
+ function positionAfter(records, cursor, stream) {
433
+ const position = positionOf(records, cursor);
434
+ if (position >= 0)
435
+ return position + 1;
436
+ if (!stream || cursor.sequence < stream.nextSequence)
437
+ throw retentionError();
438
+ throw cursorError();
439
+ }
440
+ function terminalAt(records, after) {
441
+ const last = records.at(-1);
442
+ if (!last || !isTerminal(last))
443
+ return false;
444
+ return after === undefined || after.sequence <= last.sequence;
445
+ }
446
+ function isTerminal(record) {
447
+ return (record.type === "agent_finished" || record.type === "agent_denied" || record.type === "run_limit_exceeded" || record.type === "error");
448
+ }
449
+ function sameRecord(existing, input) {
450
+ const { sequence: _existingSequence, ...existingWithoutSequence } = existing;
451
+ const { sequence: _inputSequence, ...inputWithoutSequence } = input;
452
+ return stableJson(existingWithoutSequence) === stableJson(inputWithoutSequence);
453
+ }
454
+ function stableJson(value) {
455
+ if (value === null || typeof value !== "object")
456
+ return JSON.stringify(value);
457
+ if (Array.isArray(value))
458
+ return `[${value.map(stableJson).join(",")}]`;
459
+ return `{${Object.entries(value)
460
+ .sort(([a], [b]) => a.localeCompare(b))
461
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
462
+ .join(",")}}`;
463
+ }
464
+ function clone(value) {
465
+ try {
466
+ const encoded = JSON.stringify(value);
467
+ if (encoded === undefined)
468
+ throw new Error("not JSON serializable");
469
+ return JSON.parse(encoded);
470
+ }
471
+ catch {
472
+ throw inputError();
473
+ }
474
+ }
475
+ function validCursor(value) {
476
+ if (!value || typeof value !== "object" || Array.isArray(value))
477
+ return false;
478
+ const cursor = value;
479
+ return (cursor.v === 1 &&
480
+ typeof cursor.tenantId === "string" &&
481
+ cursor.tenantId.length > 0 &&
482
+ (cursor.accountId === undefined || (typeof cursor.accountId === "string" && cursor.accountId.length > 0)) &&
483
+ (cursor.userId === undefined || (typeof cursor.userId === "string" && cursor.userId.length > 0)) &&
484
+ typeof cursor.sessionId === "string" &&
485
+ cursor.sessionId.length > 0 &&
486
+ typeof cursor.runId === "string" &&
487
+ cursor.runId.length > 0 &&
488
+ Number.isSafeInteger(cursor.sequence) &&
489
+ cursor.sequence > 0 &&
490
+ typeof cursor.id === "string" &&
491
+ cursor.id.length > 0);
492
+ }
493
+ function throwIfAborted(signal) {
494
+ if (signal?.aborted)
495
+ throw signal.reason ?? new DOMException("Aborted", "AbortError");
496
+ }
497
+ function inputError(message = "Invalid agent event source input") {
498
+ return new AgentEventSourceError("ERR_PRISM_AGENT_EVENT_SOURCE_INPUT", message);
499
+ }
500
+ function cursorError() {
501
+ return new AgentEventSourceError("ERR_PRISM_AGENT_EVENT_SOURCE_CURSOR", "Invalid agent event cursor");
502
+ }
503
+ function retentionError() {
504
+ return new AgentEventSourceError("ERR_PRISM_AGENT_EVENT_SOURCE_RETENTION", "Agent event history is unavailable");
505
+ }
506
+ function overflowError() {
507
+ return new AgentEventSourceError("ERR_PRISM_AGENT_EVENT_SOURCE_OVERFLOW", "Agent event subscription overflow");
508
+ }
509
+ function closedError() {
510
+ return new AgentEventSourceError("ERR_PRISM_AGENT_EVENT_SOURCE_CLOSED", "Agent event source is closed");
511
+ }
512
+ //# sourceMappingURL=agent-event-source.js.map
@@ -1,3 +1,4 @@
1
+ import { AgentLoopStateError } from "./contracts.js";
1
2
  import { createId } from "./ids.js";
2
3
  import { inputMessages } from "./input.js";
3
4
  import { artifactStructuredOutputRequest, withoutStructuredOutput } from "./structured-output.js";
@@ -21,6 +22,8 @@ function toolResultMessage(result) {
21
22
  // fires artifact_* events here as a noop seam — single-shot emits zero.
22
23
  export const singleShotLoop = {
23
24
  name: "single-shot",
25
+ // Durable via the runtime's pending-call mechanism; no loop-local state to snapshot.
26
+ revision: "1",
24
27
  async run(ctx) {
25
28
  let usage;
26
29
  let toolRounds = 0;
@@ -74,16 +77,39 @@ export function generateValidateReviseLoop(opts) {
74
77
  const max = opts.maxRevisions ?? 3;
75
78
  const repairer = opts.repairer ?? defaultRepairer();
76
79
  const finalOnly = opts.structuredOutputTiming === "final-turn-only" && opts.toolCalls === "bounded";
80
+ // ponytail: per-run state hoisted to factory scope so snapshot/restore can capture it;
81
+ // resolveLoop invokes this factory once per run, so there is no cross-run leakage.
82
+ let attempts = 0;
83
+ let artifactPhase = !finalOnly;
84
+ let savedSchema;
85
+ let pendingHistory = [];
77
86
  return {
78
87
  name: "generate-validate-revise",
88
+ revision: "1",
89
+ snapshot() {
90
+ return {
91
+ attempts,
92
+ artifactPhase,
93
+ savedSchema: savedSchema ?? null,
94
+ pendingHistory,
95
+ };
96
+ },
97
+ restore(snapshot) {
98
+ const state = snapshot;
99
+ if (typeof state.attempts !== "number" || !Number.isInteger(state.attempts) || typeof state.artifactPhase !== "boolean") {
100
+ throw new AgentLoopStateError("ERR_PRISM_LOOP_SNAPSHOT", "generate-validate-revise snapshot drift");
101
+ }
102
+ attempts = state.attempts;
103
+ artifactPhase = state.artifactPhase;
104
+ savedSchema = (state.savedSchema ?? undefined);
105
+ // Repair messages were appended to the session before suspension, so the rebuilt
106
+ // history already carries them; re-applying pendingHistory would duplicate them.
107
+ pendingHistory = [];
108
+ },
79
109
  async run(ctx) {
80
110
  let usage;
81
111
  let nextInput = ctx.input;
82
- let pendingHistory = [];
83
112
  let toolRounds = 0;
84
- let attempts = 0;
85
- let artifactPhase = !finalOnly;
86
- let savedSchema;
87
113
  for (let turn = 1; attempts <= max; turn += 1) {
88
114
  throwIfAborted(ctx.signal);
89
115
  await ctx.applyPendingSteers?.();
@@ -219,7 +245,7 @@ export function dispatchableToolCalls(calls) {
219
245
  export async function dispatchToolCallsInOrder(calls, ctx) {
220
246
  if (calls.length === 0)
221
247
  return;
222
- ctx.chargeToolRound?.(calls);
248
+ await ctx.chargeToolRound?.(calls);
223
249
  const concurrency = calls.some((call) => ctx.isToolCallExclusive?.(call)) ? 1 : Math.max(1, Math.min(ctx.toolConcurrency, calls.length));
224
250
  if (concurrency === 1) {
225
251
  for (const call of calls) {
@@ -243,10 +269,16 @@ export async function dispatchToolCallsInOrder(calls, ctx) {
243
269
  await Promise.all(workers);
244
270
  for (const result of results) {
245
271
  throwIfAborted(ctx.signal);
272
+ if (!result)
273
+ continue;
246
274
  await appendToolResultMessage(result, ctx);
247
275
  }
248
276
  }
249
277
  async function appendToolResultMessage(result, ctx) {
278
+ // Approval-gated calls return a marker instead of a real result; the transcript must not
279
+ // record a phantom tool_result for a call that never dispatched.
280
+ if (result.metadata?.approvalPending === true)
281
+ return;
250
282
  const message = toolResultMessage(result);
251
283
  ctx.history.push(message);
252
284
  await ctx.appendMessage(message);