@arnilo/prism 0.0.23 → 0.0.24

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 (42) hide show
  1. package/CHANGELOG.md +19 -3
  2. package/dist/agent-event-source.d.ts +11 -0
  3. package/dist/agent-event-source.js +512 -0
  4. package/dist/agent-run-state.js +6 -1
  5. package/dist/agents.js +7 -2
  6. package/dist/contracts.d.ts +157 -0
  7. package/dist/index.d.ts +7 -2
  8. package/dist/index.js +4 -1
  9. package/dist/testing/agent-event-source-conformance.d.ts +4 -0
  10. package/dist/testing/agent-event-source-conformance.js +54 -0
  11. package/dist/testing/persistence-schema.d.ts +2 -2
  12. package/dist/testing/persistence-schema.js +58 -21
  13. package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
  14. package/dist/testing/tool-effect-store-conformance.js +85 -0
  15. package/dist/tool-effects.d.ts +15 -0
  16. package/dist/tool-effects.js +338 -0
  17. package/dist/tools.d.ts +3 -1
  18. package/dist/tools.js +204 -9
  19. package/docs/0.1.0-readiness.md +9 -9
  20. package/docs/a2a.md +6 -2
  21. package/docs/ag-ui-adoption.md +77 -0
  22. package/docs/ag-ui.md +39 -42
  23. package/docs/agent-events.md +5 -1
  24. package/docs/browser-automation.md +2 -0
  25. package/docs/coding-agent-tools.md +2 -0
  26. package/docs/database-persistence.md +2 -0
  27. package/docs/enterprise-postgres-state.md +5 -1
  28. package/docs/host-security.md +8 -1
  29. package/docs/index.md +11 -9
  30. package/docs/mcp-tools.md +17 -2
  31. package/docs/migration.md +22 -0
  32. package/docs/performance.md +24 -0
  33. package/docs/postgres-persistence.md +5 -2
  34. package/docs/public-contracts.md +2 -0
  35. package/docs/release-and-install.md +52 -693
  36. package/docs/server.md +9 -6
  37. package/docs/sqlite-persistence.md +10 -2
  38. package/docs/supervisors.md +2 -0
  39. package/docs/tool-effects.md +95 -0
  40. package/docs/tools.md +4 -0
  41. package/docs/work-tools.md +4 -0
  42. package/package.json +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.24] - 2026-08-04
4
+
5
+ ### Added
6
+ - Durable `AgentEventSource` with append/page/subscribe/resume and PostgreSQL LISTEN/NOTIFY wakeups (schema v6 streams + v7 retention index).
7
+ - Recoverable `ToolEffectStore` claim/CAS lifecycle; enterprise PostgreSQL `toolEffects`; coding/browser/work/MCP/supervisor effect classification.
8
+ - AG-UI 0.0.57 full input/event/interrupt compatibility plus MCP Apps and remote A2A adapters.
9
+ - Protected Phase 7 process conformance and `benchmark-0.0.24.json` evidence.
10
+ - Example `examples/distributed-events-and-tool-effects.ts`; docs `docs/tool-effects.md`.
11
+
12
+ ### Changed
13
+ - Work mutations require core-derived idempotency keys; ambiguous outcomes stay `unknown` (not exactly-once).
14
+ - Publishable graph remains **47** manifests at **0.0.24**.
15
+
16
+ ### Breaking (minor, pre-1.0)
17
+ - Hosts using approved work mutations must supply `effectStore` / core `idempotencyKey` (model keys ignored).
18
+ - Durable event reconnect uses `AgentEventSource` cursors / `Last-Event-ID`; sticky sessions are optional only.
19
+
20
+ See [docs/migration.md](docs/migration.md) for the 0.0.23 → 0.0.24 guide.
21
+
3
22
  ## [0.0.23] - 2026-08-03
4
23
 
5
24
  ### Added
@@ -17,7 +36,6 @@
17
36
 
18
37
  See [docs/migration.md](docs/migration.md) for the 0.0.22 → 0.0.23 guide.
19
38
 
20
-
21
39
  ## [0.0.22] - 2026-07-31
22
40
 
23
41
  ### Added
@@ -45,7 +63,6 @@ See [docs/migration.md](docs/migration.md) for the full 0.0.21 → 0.0.22 notes.
45
63
 
46
64
  See [docs/migration.md](docs/migration.md) for the full 0.0.20 → 0.0.21 notes.
47
65
 
48
-
49
66
  ## [0.0.20] - 2026-07-31
50
67
 
51
68
  ### Added
@@ -169,7 +186,6 @@ All notable changes to this project will be documented in this file.
169
186
  - 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
187
  - Added network-free interoperability/compaction evidence: `scripts/benchmark-0.0.12.mjs`.
171
188
 
172
-
173
189
  ## [0.0.11] - 2026-07-22
174
190
 
175
191
  ### 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
@@ -28,7 +28,12 @@ export function agentFingerprint(agent, revision) {
28
28
  instructions: config.instructions ?? null,
29
29
  systemPrompt,
30
30
  skills: skills.map((skill) => ({ name: skill.name, instructions: skill.instructions, toolNames: skill.toolNames })),
31
- tools: tools.map((tool) => ({ name: tool.name, parameters: tool.parameters, exclusive: tool.exclusive })),
31
+ tools: tools.map((tool) => ({
32
+ name: tool.name,
33
+ parameters: tool.parameters,
34
+ exclusive: tool.exclusive,
35
+ effect: typeof tool.effect === "function" ? "classifier" : tool.effect,
36
+ })),
32
37
  guardrails: guardrails.map((guardrail) => ({ name: guardrail.name, stage: guardrail.stage, revision: guardrail.revision })),
33
38
  loop: typeof config.loop === "object" && config.loop && "strategy" in config.loop
34
39
  ? config.loop.strategy
package/dist/agents.js CHANGED
@@ -160,6 +160,7 @@ class RuntimeAgentSession {
160
160
  activeRedactor;
161
161
  activeProvider;
162
162
  activeLedger;
163
+ activeEffectStore;
163
164
  activeOwnership;
164
165
  activeIdentity;
165
166
  activeIdempotencyKey;
@@ -244,6 +245,7 @@ class RuntimeAgentSession {
244
245
  (options.redactor !== undefined ||
245
246
  options.ownership !== undefined ||
246
247
  options.validate !== undefined ||
248
+ options.effectStore !== undefined ||
247
249
  options.runState !== undefined)) {
248
250
  throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
249
251
  }
@@ -257,8 +259,8 @@ class RuntimeAgentSession {
257
259
  }
258
260
  if (durableOptions) {
259
261
  validateRunStateOptions(durableOptions);
260
- if (options.model || options.guardrails || options.loop)
261
- throw new AgentRunStateError("Durable runs require model, guardrails, and loop on AgentConfig for fingerprinting");
262
+ if (options.model || options.guardrails || options.loop || options.effectStore)
263
+ throw new AgentRunStateError("Durable runs require model, guardrails, loop, and effect store on AgentConfig for fingerprinting");
262
264
  const configuredLoop = this.agent.config.loop;
263
265
  if (configuredLoop && !isBuiltInLoop(configuredLoop))
264
266
  throw new AgentRunStateError("Custom AgentLoopStrategy is not durable");
@@ -277,6 +279,7 @@ class RuntimeAgentSession {
277
279
  this.pendingSoftInterrupt = false;
278
280
  this.activeRedactor = options.redactor ?? this.agent.config.redactor;
279
281
  this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
282
+ this.activeEffectStore = options.effectStore ?? this.agent.config.effectStore;
280
283
  this.activeOwnership = options.ownership ?? this.agent.config.ownership;
281
284
  this.activeIdentity = resolveRunIdentity(options.identity, this.agent.config.identity, this.activeOwnership);
282
285
  if (this.activeIdentity && !this.activeOwnership)
@@ -492,6 +495,7 @@ class RuntimeAgentSession {
492
495
  trust: this.agent.config.trust,
493
496
  redactor: this.activeRedactor,
494
497
  ledger: this.activeLedger,
498
+ effectStore: this.activeEffectStore,
495
499
  ownership: this.activeOwnership,
496
500
  identity: this.activeIdentity,
497
501
  guardrails: this.activeGuardrails,
@@ -644,6 +648,7 @@ class RuntimeAgentSession {
644
648
  }
645
649
  finally {
646
650
  this.activeLedger = undefined;
651
+ this.activeEffectStore = undefined;
647
652
  this.activeOwnership = undefined;
648
653
  this.activeIdentity = undefined;
649
654
  this.activeIdempotencyKey = undefined;