@arnilo/prism-supervisor 0.0.22 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.24] - 2026-08-04
4
+
5
+ ### Added
6
+ - Durable `AgentEventSource` (memory + PostgreSQL LISTEN/NOTIFY), recoverable `ToolEffectStore`, and AG-UI MCP/MCP Apps/A2A fronting for Phase 7.
7
+
8
+ ### Changed
9
+ - Publishable graph remains **47** manifests at **0.0.24**; peers and lockfile move together.
10
+
11
+ See [migration guide](../../docs/migration.md) for the 0.0.23 → 0.0.24 notes.
12
+
13
+ ## [0.0.23] - 2026-08-03
14
+
15
+ ### Changed
16
+ - Released with exact 0.0.23 graph.
17
+
3
18
  ## [0.0.22] - 2026-07-31
4
19
 
5
20
  ### Changed
@@ -10,14 +25,11 @@
10
25
  ### Changed
11
26
  - Released with exact 0.0.21 graph.
12
27
 
13
-
14
-
15
28
  ## [0.0.20] - 2026-07-31
16
29
 
17
30
  ### Changed
18
31
  - Released with exact 0.0.20 graph.
19
32
 
20
-
21
33
  ## [0.0.19] - 2026-07-30
22
34
 
23
35
  ### Changed
package/README.md CHANGED
@@ -26,6 +26,6 @@ const supervisor = createSupervisor({
26
26
  console.log((await supervisor.delegate({ childId: "research", input: "Check sources" })).text);
27
27
  ```
28
28
 
29
- Also exports bounded A2A 1.0 cards, handler/client, rich one-of parts, host-owned `A2ATaskLifecycle`, reconnect subscriptions, and push-config CRUD. Direct text invocation remains compatible; durable get/list/cancel/subscribe and rich raw/data/URL parts require explicit adapters/policy. URL parts are validated but never fetched. Push persistence/network/credentials and exact-owner checks remain host-owned; explicit `deliverA2APushEvent()` only bounds attempts/time and forwards stable event IDs for host idempotency. Returned configs omit secrets. JSON-RPC/HTTPS is the only binding.
29
+ Also exports bounded A2A 1.0 cards, handler/client, rich one-of parts, `client.streamMessage()` for verified task/message stream records, host-owned `A2ATaskLifecycle`, `createA2AAgentEventSource()` for shared durable run→task subscriptions, reconnect subscriptions, and push-config CRUD. Direct text invocation remains compatible; durable get/list/cancel/subscribe and rich raw/data/URL parts require explicit adapters/policy. URL parts are validated but never fetched. Push persistence/network/credentials and exact-owner checks remain host-owned; explicit `deliverA2APushEvent()` only bounds attempts/time and forwards stable event IDs for host idempotency. Returned configs omit secrets. JSON-RPC/HTTPS is the only binding.
30
30
 
31
31
  See [Supervisors](../../docs/supervisors.md) and [A2A interoperability](../../docs/a2a.md).
@@ -70,17 +70,17 @@ export function createA2AClient(options) {
70
70
  return taskResult(parseTaskResult(rpc.result), options);
71
71
  });
72
72
  }
73
- async function* stream(input, call = {}) {
73
+ async function* streamMessage(message, call = {}) {
74
74
  if (active >= limits.maxConcurrentRequests)
75
75
  throw new A2AError("A2A client concurrency exceeded", 429, "ERR_PRISM_A2A_CONCURRENCY");
76
76
  active += 1;
77
77
  const owned = ownedSignal(call.signal, limits.timeoutMs);
78
78
  let reader;
79
79
  try {
80
- assertInput(input, limits.maxRequestBytes);
80
+ assertMessage(message, limits.maxRequestBytes);
81
81
  await getCardWithin(owned.signal);
82
82
  const id = ++requestId;
83
- const body = JSON.stringify(requestBody(id, "SendStreamingMessage", input));
83
+ const body = JSON.stringify({ jsonrpc: "2.0", id, method: "SendStreamingMessage", params: { message } });
84
84
  if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
85
85
  throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
86
86
  const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
@@ -100,6 +100,7 @@ export function createA2AClient(options) {
100
100
  throw new A2AError("A2A stream request failed", response.status, "ERR_PRISM_A2A_REMOTE");
101
101
  reader = response.body.getReader();
102
102
  let terminal = false;
103
+ let count = 0;
103
104
  for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
104
105
  if (terminal)
105
106
  throw new A2AError("A2A stream continued after terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
@@ -115,19 +116,17 @@ export function createA2AClient(options) {
115
116
  const rpc = parseRpcResponse(parsed, id);
116
117
  if (rpc.error)
117
118
  throw new A2AError(safeRemote(rpc.error.message, options), 502, "ERR_PRISM_A2A_REMOTE");
118
- const task = parseTaskResult(rpc.result);
119
- if (task.status.state === "TASK_STATE_FAILED" ||
120
- task.status.state === "TASK_STATE_CANCELED" ||
121
- task.status.state === "TASK_STATE_REJECTED")
122
- throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
123
- if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED")
124
- throw new A2AError(`Remote A2A task interrupted: ${task.status.state}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
125
- if (task.status.state === "TASK_STATE_COMPLETED")
119
+ const event = parseA2AStreamEvent(rpc.result, `stream-${++count}`);
120
+ const status = eventStatus(event);
121
+ if (status === "TASK_STATE_FAILED" ||
122
+ status === "TASK_STATE_CANCELED" ||
123
+ status === "TASK_STATE_REJECTED" ||
124
+ status === "TASK_STATE_INPUT_REQUIRED" ||
125
+ status === "TASK_STATE_AUTH_REQUIRED")
126
+ terminal = true;
127
+ if (status === "TASK_STATE_COMPLETED")
126
128
  terminal = true;
127
- for (const artifact of task.artifacts ?? [])
128
- for (const part of artifact.parts)
129
- if (typeof part.text === "string")
130
- yield options.redactor?.redact(part.text) ?? part.text;
129
+ yield event;
131
130
  }
132
131
  if (!terminal)
133
132
  throw new A2AError("A2A stream ended before terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
@@ -138,6 +137,18 @@ export function createA2AClient(options) {
138
137
  active -= 1;
139
138
  }
140
139
  }
140
+ async function* stream(input, call = {}) {
141
+ assertInput(input, limits.maxRequestBytes);
142
+ for await (const event of streamMessage({ role: "user", messageId: "stream-input", parts: [{ text: input }] }, call)) {
143
+ const status = eventStatus(event);
144
+ if (status === "TASK_STATE_FAILED" || status === "TASK_STATE_CANCELED" || status === "TASK_STATE_REJECTED")
145
+ throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
146
+ if (status === "TASK_STATE_INPUT_REQUIRED" || status === "TASK_STATE_AUTH_REQUIRED")
147
+ throw new A2AError(`Remote A2A task interrupted: ${status}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
148
+ for (const text of streamEventText(event))
149
+ yield options.redactor?.redact(text) ?? text;
150
+ }
151
+ }
141
152
  async function getCardWithin(signal) {
142
153
  const response = await fetcher(cardUrl, {
143
154
  method: "GET",
@@ -226,15 +237,16 @@ export function createA2AClient(options) {
226
237
  if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
227
238
  throw new A2AError("A2A subscribe request failed", response.status, "ERR_PRISM_A2A_REMOTE");
228
239
  reader = response.body.getReader();
229
- let previous = "", count = 0;
240
+ const seen = new Set();
241
+ let count = 0;
230
242
  for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
231
243
  const rpc = parseRpcResponse(JSON.parse(data), request);
232
244
  if (rpc.error)
233
245
  throw remoteProtocolError(rpc.error.code, rpc.error.message, options);
234
246
  const event = parseTaskEvent(rpc.result);
235
- if (event.eventId === previous)
247
+ if (seen.has(event.eventId))
236
248
  continue;
237
- previous = event.eventId;
249
+ seen.add(event.eventId);
238
250
  if (++count > limits.maxReplayEvents)
239
251
  throw new A2AError("A2A replay exceeds event limit", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
240
252
  yield event;
@@ -269,6 +281,7 @@ export function createA2AClient(options) {
269
281
  send,
270
282
  sendMessage,
271
283
  stream,
284
+ streamMessage,
272
285
  getTask,
273
286
  listTasks,
274
287
  cancelTask,
@@ -433,6 +446,7 @@ function parseTaskResult(value) {
433
446
  status: {
434
447
  state: task.status.state,
435
448
  timestamp: typeof task.status.timestamp === "string" ? task.status.timestamp : new Date(0).toISOString(),
449
+ ...(task.status.message === undefined ? {} : { message: parseRemoteMessage(task.status.message) }),
436
450
  },
437
451
  artifacts,
438
452
  history,
@@ -487,6 +501,31 @@ function parseRemoteMessage(value) {
487
501
  taskId: typeof value.taskId === "string" ? value.taskId : undefined,
488
502
  };
489
503
  }
504
+ function parseA2AStreamEvent(value, fallbackEventId) {
505
+ if (isRecord(value) && isRecord(value.message))
506
+ return { eventId: fallbackEventId, message: parseRemoteMessage(value.message) };
507
+ if (isRecord(value) && !Object.hasOwn(value, "eventId") && (isRecord(value.task) || typeof value.id === "string")) {
508
+ return { eventId: fallbackEventId, task: parseTaskResult(value) };
509
+ }
510
+ return parseTaskEvent(value);
511
+ }
512
+ function eventStatus(event) {
513
+ if ("task" in event)
514
+ return event.task.status.state;
515
+ if ("statusUpdate" in event)
516
+ return event.statusUpdate.status.state;
517
+ return undefined;
518
+ }
519
+ function streamEventText(event) {
520
+ const parts = "message" in event
521
+ ? event.message.parts
522
+ : "task" in event
523
+ ? (event.task.artifacts?.flatMap((artifact) => artifact.parts) ?? [])
524
+ : "artifactUpdate" in event
525
+ ? event.artifactUpdate.artifact.parts
526
+ : [];
527
+ return parts.flatMap((part) => (typeof part.text === "string" ? [part.text] : []));
528
+ }
490
529
  function parseTaskEvent(value) {
491
530
  if (!isRecord(value) || typeof value.eventId !== "string" || !value.eventId)
492
531
  throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
@@ -688,6 +727,19 @@ function assertInput(input, maxBytes) {
688
727
  if (new TextEncoder().encode(input).byteLength > maxBytes)
689
728
  throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
690
729
  }
730
+ function assertMessage(message, maxBytes) {
731
+ if (!message.messageId || !Array.isArray(message.parts) || message.parts.length === 0)
732
+ throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
733
+ let encoded;
734
+ try {
735
+ encoded = JSON.stringify(message);
736
+ }
737
+ catch {
738
+ throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
739
+ }
740
+ if (Buffer.byteLength(encoded, "utf8") > maxBytes)
741
+ throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
742
+ }
691
743
  function headersObject(headers) {
692
744
  return Object.fromEntries(new Headers(headers).entries());
693
745
  }
@@ -0,0 +1,42 @@
1
+ import type { AgentEventSource, AgentRunRef, DurableAgentEventRecord } from "@arnilo/prism";
2
+ import type { A2AAuthorization, A2ATask, A2ATaskEvent } from "./a2a-types.js";
3
+ export type A2ATaskEventPayload = {
4
+ readonly task: A2ATask;
5
+ } | {
6
+ readonly statusUpdate: Extract<A2ATaskEvent, {
7
+ readonly statusUpdate: unknown;
8
+ }>["statusUpdate"];
9
+ } | {
10
+ readonly artifactUpdate: Extract<A2ATaskEvent, {
11
+ readonly artifactUpdate: unknown;
12
+ }>["artifactUpdate"];
13
+ };
14
+ export interface A2AAgentEventTask {
15
+ readonly task: A2ATask;
16
+ readonly run: AgentRunRef;
17
+ }
18
+ export interface A2AAgentEventSourceOptions {
19
+ readonly source: AgentEventSource;
20
+ /** Resolves only host-owned task state and its exact Prism run. */
21
+ readonly resolveTask: (input: {
22
+ readonly id: string;
23
+ readonly authorization: A2AAuthorization;
24
+ readonly signal: AbortSignal;
25
+ }) => A2AAgentEventTask | undefined | Promise<A2AAgentEventTask | undefined>;
26
+ /** Maps at most one durable Prism record to one A2A update. Event IDs stay source-owned cursors. */
27
+ readonly map: (input: {
28
+ readonly record: DurableAgentEventRecord;
29
+ readonly task: A2ATask;
30
+ readonly authorization: A2AAuthorization;
31
+ }) => A2ATaskEventPayload | undefined | Promise<A2ATaskEventPayload | undefined>;
32
+ }
33
+ export interface A2AAgentEventSource {
34
+ subscribe(input: {
35
+ readonly id: string;
36
+ readonly afterEventId?: string;
37
+ readonly authorization: A2AAuthorization;
38
+ readonly signal: AbortSignal;
39
+ }): AsyncIterable<A2ATaskEvent>;
40
+ }
41
+ /** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
42
+ export declare function createA2AAgentEventSource(options: A2AAgentEventSourceOptions): A2AAgentEventSource;
@@ -0,0 +1,40 @@
1
+ import { A2AError } from "./errors.js";
2
+ /** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
3
+ export function createA2AAgentEventSource(options) {
4
+ return {
5
+ subscribe(input) {
6
+ return {
7
+ async *[Symbol.asyncIterator]() {
8
+ input.signal.throwIfAborted();
9
+ const resolved = await options.resolveTask(input);
10
+ if (!resolved?.run.sessionId || resolved.task.id !== input.id) {
11
+ throw new A2AError("Task unavailable", 404, "ERR_PRISM_A2A_TASK");
12
+ }
13
+ let first = true;
14
+ for await (const item of options.source.subscribe({
15
+ ownership: input.authorization.ownership,
16
+ sessionId: resolved.run.sessionId,
17
+ runId: resolved.run.runId,
18
+ after: input.afterEventId,
19
+ signal: input.signal,
20
+ })) {
21
+ if (!item.record.redacted)
22
+ throw new A2AError("Task event unavailable", 500, "ERR_PRISM_A2A_TASK");
23
+ const payload = await options.map({ record: item.record, task: resolved.task, authorization: input.authorization });
24
+ if (!payload)
25
+ continue;
26
+ if (first && input.afterEventId === undefined && !("task" in payload)) {
27
+ throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
28
+ }
29
+ first = false;
30
+ yield { eventId: item.cursor, ...payload };
31
+ }
32
+ if (first && input.afterEventId === undefined) {
33
+ throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
34
+ }
35
+ },
36
+ };
37
+ },
38
+ };
39
+ }
40
+ //# sourceMappingURL=a2a-event-source.js.map
@@ -113,6 +113,11 @@ export type A2ATaskEvent = {
113
113
  readonly lastChunk?: boolean;
114
114
  };
115
115
  };
116
+ /** A streaming response may be a task lifecycle or one direct message. */
117
+ export type A2AStreamEvent = A2ATaskEvent | {
118
+ readonly eventId: string;
119
+ readonly message: A2AMessage;
120
+ };
116
121
  export type A2ARequestId = string | number | null;
117
122
  export interface A2AJsonRpcRequest {
118
123
  readonly jsonrpc: "2.0";
@@ -286,6 +291,10 @@ export interface A2AClient {
286
291
  stream(input: string, options?: {
287
292
  readonly signal?: AbortSignal;
288
293
  }): AsyncIterable<string>;
294
+ /** Rich `SendStreamingMessage` events. Host-supplied messages remain subject to client bounds/card verification. */
295
+ streamMessage(message: A2AMessage, options?: {
296
+ readonly signal?: AbortSignal;
297
+ }): AsyncIterable<A2AStreamEvent>;
289
298
  getTask(id: string, options?: {
290
299
  readonly signal?: AbortSignal;
291
300
  readonly historyLength?: number;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./a2a-card.js";
2
+ export * from "./a2a-event-source.js";
2
3
  export * from "./a2a-client.js";
3
4
  export * from "./a2a-parts.js";
4
5
  export * from "./a2a-push.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./a2a-card.js";
2
+ export * from "./a2a-event-source.js";
2
3
  export * from "./a2a-client.js";
3
4
  export * from "./a2a-parts.js";
4
5
  export * from "./a2a-push.js";
@@ -1,9 +1,13 @@
1
- import { AgentRunError, createAgent, createEventMultiplexer, } from "@arnilo/prism";
1
+ import { AgentRunError, assertIdentityActive, assertIdentityMatchesOwnership, createAgent, createEventMultiplexer, } from "@arnilo/prism";
2
2
  import { SupervisorDeniedError, SupervisorError, SupervisorLimitError, SupervisorValidationError } from "./errors.js";
3
3
  import { narrowSupervisorLimits, resolveSupervisorLimits } from "./limits.js";
4
4
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
5
5
  export function createSupervisor(options) {
6
6
  requireOwnership(options.ownership);
7
+ if (options.identity) {
8
+ assertIdentityActive(options.identity);
9
+ assertIdentityMatchesOwnership(options.identity, options.ownership);
10
+ }
7
11
  const id = options.id ?? "supervisor";
8
12
  if (!ID.test(id))
9
13
  throw new SupervisorValidationError("Supervisor id is invalid");
@@ -80,6 +84,8 @@ export function createSupervisor(options) {
80
84
  depth,
81
85
  path,
82
86
  ownership: options.ownership,
87
+ identity: options.identity,
88
+ effectStore: options.effectStore,
83
89
  resourceId,
84
90
  threadId,
85
91
  permission: preliminaryPermission,
@@ -90,6 +96,8 @@ export function createSupervisor(options) {
90
96
  ...childAgent.config,
91
97
  permission: intersectPolicies(preliminaryPermission, childAgent.config.permission),
92
98
  ownership: options.ownership,
99
+ identity: options.identity ?? childAgent.config.identity,
100
+ effectStore: options.effectStore ?? childAgent.config.effectStore,
93
101
  redactor: options.redactor ?? childAgent.config.redactor,
94
102
  });
95
103
  const session = agent.createSession({
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentRunResult, OwnershipScope, PermissionPolicy, SecretRedactor } from "@arnilo/prism";
1
+ import type { Agent, AgentIdentity, AgentRunResult, OwnershipScope, PermissionPolicy, SecretRedactor, ToolEffectStore } from "@arnilo/prism";
2
2
  import type { ResolvedSupervisorLimits, SupervisorLimits } from "./limits.js";
3
3
  export interface DelegationRequest {
4
4
  readonly childId: string;
@@ -14,6 +14,10 @@ export interface DelegationChildContext {
14
14
  readonly depth: number;
15
15
  readonly path: readonly string[];
16
16
  readonly ownership: OwnershipScope;
17
+ /** Parent-verified identity; child factories cannot widen it. */
18
+ readonly identity?: AgentIdentity;
19
+ /** One shared durable effect store for every child run. */
20
+ readonly effectStore?: ToolEffectStore;
17
21
  readonly resourceId: string;
18
22
  readonly threadId: string;
19
23
  readonly permission: PermissionPolicy;
@@ -86,6 +90,10 @@ export type SupervisorEvent = {
86
90
  export interface CreateSupervisorOptions {
87
91
  readonly id?: string;
88
92
  readonly ownership: OwnershipScope;
93
+ /** Optional parent-verified identity, propagated unchanged to every child. */
94
+ readonly identity?: AgentIdentity;
95
+ /** Optional parent effect store, propagated unchanged to every child. */
96
+ readonly effectStore?: ToolEffectStore;
89
97
  readonly children: Readonly<Record<string, SupervisorChild>>;
90
98
  readonly permission?: PermissionPolicy;
91
99
  readonly limits?: SupervisorLimits;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism-supervisor",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Bounded supervisor delegation and A2A 1.0 durable task, rich-part, reconnect, and push interoperability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.22"
28
+ "@arnilo/prism": "0.0.24"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."