@arnilo/prism 0.6.0 → 0.8.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 (178) hide show
  1. package/CHANGELOG.md +79 -5
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +4 -0
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-definitions.js +1 -0
  6. package/dist/agent-run-lifecycle.js +39 -4
  7. package/dist/agent-run-state.d.ts +18 -0
  8. package/dist/agent-run-state.js +39 -9
  9. package/dist/agent-session/helpers.js +6 -1
  10. package/dist/agent-session/session/assemble.js +159 -7
  11. package/dist/agent-session/session/persist.d.ts +16 -0
  12. package/dist/agent-session/session/persist.js +64 -4
  13. package/dist/agent-session/session/provider-round.d.ts +3 -3
  14. package/dist/agent-session/session/provider-round.js +12 -6
  15. package/dist/agent-session/session/tool-round.js +5 -1
  16. package/dist/agent-session/session/types.d.ts +22 -1
  17. package/dist/agent-session/session.d.ts +16 -0
  18. package/dist/agent-session/session.js +42 -3
  19. package/dist/artifacts.d.ts +39 -1
  20. package/dist/artifacts.js +73 -0
  21. package/dist/attention-compiler.d.ts +121 -0
  22. package/dist/attention-compiler.js +479 -0
  23. package/dist/checkpoints.js +7 -11
  24. package/dist/cli-init.js +20 -6
  25. package/dist/context-budget.d.ts +20 -1
  26. package/dist/context-budget.js +10 -1
  27. package/dist/contracts-core/agent.d.ts +7 -0
  28. package/dist/contracts-core/attention.d.ts +66 -0
  29. package/dist/contracts-core/attention.js +2 -0
  30. package/dist/contracts-core/compaction.d.ts +59 -0
  31. package/dist/contracts-core/compaction.js +77 -1
  32. package/dist/contracts-core/content.d.ts +5 -0
  33. package/dist/contracts-core/loop.d.ts +42 -0
  34. package/dist/contracts-core/provider.d.ts +4 -0
  35. package/dist/contracts-core/run-limits.d.ts +2 -0
  36. package/dist/contracts-core.d.ts +1 -0
  37. package/dist/contracts-core.js +1 -0
  38. package/dist/contracts-protocol.d.ts +44 -3
  39. package/dist/contracts-run-state.d.ts +32 -5
  40. package/dist/evidence-grounding.d.ts +29 -0
  41. package/dist/evidence-grounding.js +162 -0
  42. package/dist/host-composition.d.ts +91 -0
  43. package/dist/host-composition.js +279 -0
  44. package/dist/index.d.ts +13 -6
  45. package/dist/index.js +7 -4
  46. package/dist/input.d.ts +13 -1
  47. package/dist/input.js +40 -1
  48. package/dist/provider-events.d.ts +3 -1
  49. package/dist/provider-events.js +2 -2
  50. package/dist/providers/transport.d.ts +3 -1
  51. package/dist/providers/transport.js +36 -0
  52. package/dist/redaction.js +18 -2
  53. package/dist/run-bundle.d.ts +89 -0
  54. package/dist/run-bundle.js +149 -0
  55. package/dist/secure-agent.d.ts +2 -0
  56. package/dist/secure-agent.js +6 -1
  57. package/dist/testing/state-concurrency-conformance.js +5 -12
  58. package/dist/tool-result-fold.d.ts +12 -0
  59. package/dist/tool-result-fold.js +13 -6
  60. package/dist/tools.d.ts +10 -0
  61. package/dist/tools.js +41 -0
  62. package/docs/acp-agent.md +42 -11
  63. package/docs/acp.md +2 -1
  64. package/docs/ag-ui.md +10 -3
  65. package/docs/agent-definitions.md +9 -1
  66. package/docs/agent-events.md +4 -1
  67. package/docs/agent-loops.md +33 -0
  68. package/docs/agent-session-runtime.md +8 -7
  69. package/docs/attention-compiler.md +272 -0
  70. package/docs/cli-rpc.md +4 -2
  71. package/docs/coding-agent-tools.md +1 -1
  72. package/docs/coding-security.md +6 -3
  73. package/docs/coding-tools.md +0 -1
  74. package/docs/coding-workspaces.md +22 -0
  75. package/docs/compaction-and-retry.md +36 -4
  76. package/docs/compaction-observational-memory.md +63 -10
  77. package/docs/connected-apps.md +116 -0
  78. package/docs/context-and-skills.md +17 -2
  79. package/docs/conversations.md +1 -1
  80. package/docs/core.md +1 -1
  81. package/docs/dev-inspector.md +4 -0
  82. package/docs/device-adapters.md +1 -0
  83. package/docs/diagrams.md +6 -6
  84. package/docs/document-reader.md +18 -10
  85. package/docs/documents.md +40 -11
  86. package/docs/durable-runs.md +87 -0
  87. package/docs/enterprise-postgres-state.md +6 -2
  88. package/docs/evaluations.md +168 -4
  89. package/docs/execution-timeline.md +186 -0
  90. package/docs/guardrails.md +33 -0
  91. package/docs/history/0.7.0-primitive-review.md +254 -0
  92. package/docs/history/079-messaging-primitive-review.md +391 -0
  93. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  94. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  95. package/docs/history/083-prism-work-primitive-review.md +84 -0
  96. package/docs/history/084-primitive-review.md +96 -0
  97. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  98. package/docs/history/README.md +5 -0
  99. package/docs/history/migration-0.0.md +2 -2
  100. package/docs/history/release-handoffs.md +75 -1
  101. package/docs/host-compositions.md +149 -0
  102. package/docs/host-security.md +2 -2
  103. package/docs/hosted-sandboxes.md +94 -0
  104. package/docs/index.md +82 -45
  105. package/docs/input-and-prompt-assembly.md +1 -0
  106. package/docs/knowledge-sync.md +84 -0
  107. package/docs/language-intelligence.md +1 -1
  108. package/docs/live-testing.md +8 -3
  109. package/docs/mcp-tools.md +3 -1
  110. package/docs/memory-fabric.md +416 -0
  111. package/docs/messaging-channel-operations.md +166 -0
  112. package/docs/messaging-channels.md +150 -0
  113. package/docs/migrate-to-0.5.md +1 -1
  114. package/docs/migrate-to-0.6.md +1 -0
  115. package/docs/migrate-to-0.7.md +345 -0
  116. package/docs/migrate-to-0.8.md +124 -0
  117. package/docs/migration.md +43 -1
  118. package/docs/model-registry.md +12 -2
  119. package/docs/model-routing.md +79 -4
  120. package/docs/multi-agent-patterns.md +20 -6
  121. package/docs/observability.md +52 -1
  122. package/docs/openapi-tools.md +1 -1
  123. package/docs/operations.md +14 -4
  124. package/docs/options-index.md +47 -3
  125. package/docs/peer-dependencies.md +12 -10
  126. package/docs/postgres-persistence.md +1 -1
  127. package/docs/process-sessions.md +3 -1
  128. package/docs/prompt-registry.md +1 -1
  129. package/docs/provider-caching.md +4 -2
  130. package/docs/provider-conformance.md +1 -1
  131. package/docs/provider-layer.md +2 -2
  132. package/docs/provider-packages.md +22 -22
  133. package/docs/providers/bedrock.md +71 -7
  134. package/docs/providers/neuralwatt.md +5 -1
  135. package/docs/providers/openai.md +1 -1
  136. package/docs/rag.md +24 -8
  137. package/docs/realtime-voice.md +87 -0
  138. package/docs/release-and-install.md +53 -45
  139. package/docs/run-bundle.md +92 -0
  140. package/docs/runs-and-usage.md +17 -2
  141. package/docs/server.md +7 -3
  142. package/docs/sheets.md +9 -9
  143. package/docs/signal-channel.md +112 -0
  144. package/docs/speech.md +7 -1
  145. package/docs/sqlite-persistence.md +1 -1
  146. package/docs/supervisors.md +33 -5
  147. package/docs/telegram-channel.md +157 -0
  148. package/docs/testing.md +2 -2
  149. package/docs/thinking-and-reasoning.md +3 -1
  150. package/docs/tools.md +6 -5
  151. package/docs/web-tools.md +2 -1
  152. package/docs/wiki.md +1 -1
  153. package/docs/work-artifacts-and-review.md +14 -4
  154. package/docs/work-connectors.md +12 -10
  155. package/docs/work-sandbox.md +115 -0
  156. package/docs/work-tools.md +50 -18
  157. package/docs/workflows.md +69 -1
  158. package/docs/working-and-semantic-memory.md +25 -14
  159. package/package.json +5 -3
  160. package/templates/README.md +2 -0
  161. package/templates/business-worker/README.md.tmpl +19 -0
  162. package/templates/business-worker/env.example.tmpl +1 -0
  163. package/templates/business-worker/gitignore.tmpl +11 -0
  164. package/templates/business-worker/manifest.json +12 -0
  165. package/templates/business-worker/package.json.tmpl +23 -0
  166. package/templates/business-worker/src/agent.ts.tmpl +92 -0
  167. package/templates/business-worker/src/index.ts.tmpl +13 -0
  168. package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
  169. package/templates/business-worker/tsconfig.json.tmpl +15 -0
  170. package/templates/personal-assistant/README.md.tmpl +18 -0
  171. package/templates/personal-assistant/env.example.tmpl +1 -0
  172. package/templates/personal-assistant/gitignore.tmpl +11 -0
  173. package/templates/personal-assistant/manifest.json +11 -0
  174. package/templates/personal-assistant/package.json.tmpl +23 -0
  175. package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
  176. package/templates/personal-assistant/src/index.ts.tmpl +13 -0
  177. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
  178. package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
@@ -0,0 +1,112 @@
1
+ # Signal channel (experimental)
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-channels/signal` is a Node-only, experimental adapter for an **externally supervised** [signal-cli v0.14.8](https://github.com/AsamK/signal-cli/tree/v0.14.8) Unix-socket daemon. It is not an official Signal bot API, does not make a bot official, and must be enabled only after an operator records the acceptable-use and GPL distribution decisions required for its deployment.
6
+
7
+ Importing or constructing the adapter does nothing: it never downloads, spawns, registers, links, relinks, configures, or health-checks `signal-cli`; opens no network connection; and has no environment or credential lookup. Host owns that lifecycle, account, state directory, encryption, backup/retention and process supervisor.
8
+
9
+ ## When to use it
10
+
11
+ Use only after an operator records acceptable-use and GPL distribution decisions for an already-operated Signal account. Not an official bot API; not for groups, topics, attachments, identity linking, bulk messaging (including `MessagingRuntime.notify`, which is unicast to one existing DM binding only), automated account creation, or streaming previews (Signal has no Bot API draft equivalent, so this adapter ignores `MessagingRuntimeOptions.onAssistantDelta`).
12
+
13
+ ```bash
14
+ npm install @arnilo/prism @arnilo/prism-channels
15
+ ```
16
+
17
+ ## Inputs / request
18
+
19
+ `SignalAdapterOptions`: host `connectionId`, absolute private `socketPath`, daemon `account`, `signalCliVersion` (must equal `SIGNAL_CLI_VERSION`), `policy` attestation, and service-owned `checkpoints`/`leases`/`cursorOwnership`. `signal-cli` is a host-operated binary, not an npm peer.
20
+
21
+ ## Host setup
22
+
23
+ Use only a private, host-selected Unix socket on the same Node/Linux host. Do not expose a TCP or HTTP bridge. Protect socket parent/state directories with service-only permissions (for example `0700`) and protect backups. Signal transport decrypts at `signal-cli`; Prism stores and model providers receive plaintext according to host policy.
24
+
25
+ The supported daemon shape is manual receive mode, selected by the host for the pinned CLI version:
26
+
27
+ ```sh
28
+ signal-cli --data-dir /private/prism-signal -a +15550001111 \
29
+ daemon --socket=/private/run/prism-signal.sock --receive-mode=manual
30
+ ```
31
+
32
+ `signal-cli` is GPLv3 and its upstream compatibility can change after Signal service changes. It requires the pinned release's documented JRE/native dependencies (v0.14.8 documents JRE 25). Account registration/relinking may disrupt existing clients; never automate it from Prism or chat input. Consult current Signal terms and operator policy before enabling automated replies; terms/policy can prohibit an intended deployment.
33
+
34
+ ## Request/response example
35
+
36
+ ```ts
37
+ import { createMessagingRuntime } from "@arnilo/prism-channels";
38
+ import { createSignalAdapter, SIGNAL_CLI_VERSION } from "@arnilo/prism-channels/signal";
39
+
40
+ const adapter = createSignalAdapter({
41
+ connectionId: "support-signal",
42
+ socketPath: "/private/run/prism-signal.sock",
43
+ account: "+15550001111", // host daemon account, never taken from a message
44
+ signalCliVersion: SIGNAL_CLI_VERSION,
45
+ policy: {
46
+ acceptableUse: "operator_approved",
47
+ gplDistribution: "operator_approved",
48
+ termsVersion: "2026-09-review", // host's recorded policy review, not a Signal approval
49
+ },
50
+ checkpoints: serviceCheckpoints,
51
+ leases: serviceLeases,
52
+ cursorOwnership: { tenantId: "signal-service" },
53
+ // multiAccount: true, // only for a daemon where account is supplied on RPC requests
54
+ });
55
+
56
+ const runtime = createMessagingRuntime({
57
+ authorize: hostAuthorizeSignalSender, // verify observed Signal UUID -> Prism identity/grant
58
+ resolveAgent: hostResolveAgent,
59
+ deliver: (reply) => adapter.send(reply),
60
+ checkpoints: userCheckpoints,
61
+ leases: userLeases,
62
+ });
63
+
64
+ await adapter.start((event) => runtime.admit(event));
65
+ // Shutdown: await adapter.stop(); await runtime.stop();
66
+ ```
67
+
68
+ ## Implementation example
69
+
70
+ [`examples/signal-agent.ts`](../examples/signal-agent.ts) contains the same host composition helper.
71
+
72
+ `start()` acquires `prism.channels.v1.signal.receiver`, probes its checkpoint writer, connects the persistent Unix socket, then calls only `subscribeReceive`. It renews its receiver lease; loss fails closed by pausing intake. `stop()` unsubscribes when possible, closes its socket, aborts reconnection and releases only its lease. A second receiver under the same service ownership cannot subscribe.
73
+
74
+ The adapter accepts manual `params.result.envelope` notifications only when they name the selected account and contain a direct text `dataMessage` from a valid Signal UUID. It ignores receipts, sync echoes, reactions, stories, edits, groups (including group topics, which have no supported thread mapping) and attachments before `admit`. UUID (not display name or phone number) becomes the external actor/conversation ID; the runtime must still authorize it. An account mismatch pauses the adapter before admission. `health()` reports independent bounded `bridge`, `subscription`, and `account` states for host monitoring without message text, phone numbers, socket path or key material.
75
+
76
+ ## Outputs / response / events
77
+
78
+ `send()` accepts only replies for its configured `connectionId` and a UUID destination fixed by the runtime's authorized binding. It calls only `send`; chat text cannot choose an RPC method, account, socket, recipient or identity action. Replies are plain text and chunked at 2,000 UTF-16 code units without splitting surrogate pairs.
79
+
80
+ A server-issued approval control is rendered as plain reply text:
81
+
82
+ ```text
83
+ Reply /approve <opaque-token> to allow once.
84
+ Reply /deny <opaque-token> to deny.
85
+ ```
86
+
87
+ Only the shared runtime validates and consumes that opaque token. Free-form text remains untrusted input; no Signal reaction, attachment, identity-trust or linking action is exposed.
88
+
89
+ Known daemon errors map to a bounded reason (`signal_rate_limited`, `signal_captcha_required`, `signal_relink_required`, `signal_identity_changed`, or `signal_rpc_error`). A changed/untrusted identity stops normal sends until an operator verifies it and current authorization is re-established. Timeout/socket loss after a write — or a failure after an earlier chunk — throws an ambiguous-delivery error; the durable reply journal records `delivery_unknown` and must not automatically resend it.
90
+
91
+ ## Extension and configuration notes
92
+
93
+ | Setting | Default | Range |
94
+ | --- | ---: | ---: |
95
+ | JSON-RPC line/frame | 128 KiB | 1 KiB–512 KiB |
96
+ | Pending RPC requests | 16 | 1–64 |
97
+ | Pending admitted notifications | 32 | 1–128 |
98
+ | RPC timeout | 10 s | 100 ms–60 s |
99
+ | Reconnect delay | 1 s | 1 ms–60 s (exponential, 30 s default max) |
100
+ | Receiver lease | 90 s | 5 s–5 min |
101
+ | Outbound chunk | 2,000 UTF-16 units | fixed |
102
+
103
+ The adapter pauses/unsubscribes on journal admission capacity/storage failure, malformed/oversized frames, account mismatch, or lease loss. It reconnects with bounded backoff after an established bridge loss and rechecks writer readiness before subscribing. It has **no documented application acknowledgment or replay log**: a daemon notification received before a crash and before durable commit can be lost. This adapter does not claim lossless intake, exactly-once execution, message read status, or end-to-end encryption through a model/provider.
104
+
105
+ ## Security and performance notes
106
+
107
+ Construction is inert: no spawn, TCP/HTTP, environment lookup, or credential read. Chat text cannot choose RPC method, account, socket, recipient, or identity action. UUID (not phone or display name) is the only observed actor id. A changed identity stops sends until an operator verifies it. Receive-to-commit can lose a notification; treat that as a documented loss window, not a retry.
108
+
109
+ ## Related APIs
110
+
111
+ - [Messaging channels](messaging-channels.md): authorization and durable replies.
112
+ - [Messaging channel operations](messaging-channel-operations.md): recovery and operator review.
package/docs/speech.md CHANGED
@@ -14,7 +14,11 @@ and `runTranscriptionConformance` from `@arnilo/prism/testing/provider-conforman
14
14
  ## When to use it
15
15
 
16
16
  Use it for one-shot voice output and batch/stream transcription where the host
17
- owns playback, capture, and audio storage. Do not use it for interactive
17
+ owns playback, capture, and audio storage. Messaging hosts wrap these providers
18
+ and hand the wrappers to a channel adapter — Telegram `transcribe` turns an
19
+ inbound voice note into turn text and `synthesize` adds a voice note next to a
20
+ final reply ([telegram-channel.md](telegram-channel.md)); `@arnilo/prism-channels`
21
+ never imports this package and never stores audio. Do not use it for interactive
18
22
  bidirectional voice — that is the Realtime session contract
19
23
  ([`RealtimeSession`](public-contracts.md)), which keeps its own
20
24
  `audio_delta`/`transcript_delta` events. Streaming here is one-directional:
@@ -120,6 +124,8 @@ await runSpeechConformance({
120
124
  - [Realtime sessions](public-contracts.md): `RealtimeSession` for interactive
121
125
  bidirectional voice; `RealtimeEvent.transcript_delta` is this contract's
122
126
  naming anchor.
127
+ - [Realtime voice](realtime-voice.md): governed bridge from a Realtime session
128
+ into host tool dispatch, barge-in, and transcript privacy.
123
129
  - [Provider conformance](provider-conformance.md): `runSpeechConformance` /
124
130
  `runTranscriptionConformance` and the offline conformance matrix.
125
131
  - [Provider packages](provider-packages.md): subpath import rules for
@@ -54,7 +54,7 @@ import { createSqlitePersistence } from "@arnilo/prism-core/sessions/sqlite";
54
54
  | `SessionStore.readBranchPath` | Recursive ancestor query from `leafId` (or latest leaf) in root→leaf order. |
55
55
  | `RunLedger.append*` | Inserts run/event/tool/usage rows; events receive monotonic per-run `sequence` values. |
56
56
  | `ProductionPersistenceStore.query*` | Parameterized cursor pagination on indexed columns. |
57
- | `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. |
57
+ | `checkpoints` | Generic versioned `CheckpointStore` backed by `prism_checkpoints`; ownership, CAS/fencing checks, bounded pagination, and workflow suspended/denied/schedule/state/replay values without a schema migration. A load or delete under a non-matching ownership scope reads as absent and a cross-scope write fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — no ownership-shaped existence oracle. |
58
58
  | `leases` | Atomic `LeaseStore` backed by `prism_leases`; database-clock expiry, opaque renew/release token, monotonic takeover fence. |
59
59
  | `close()` | Closes the underlying database when the adapter opened it. |
60
60
 
@@ -10,22 +10,29 @@ Use a supervisor when a host or agent must choose a child dynamically. Use `@arn
10
10
 
11
11
  ## Inputs / request
12
12
 
13
+ **Option surfaces** — `CreateSupervisorOptions` (ownership, child catalog, hooks, `childEvents`, limits), `SupervisorLimits` / `ResolvedSupervisorLimits` (depth, active children, child events, bytes), `DelegationWaitOptions` (`timeoutMs`, `signal`), `CreateSpawnAgentToolOptions` / `CreateDelegationControlToolOptions` (supervisor, tool name, sync/async mode), `WorktreeChildFactoryOptions` (workspace lifecycle, repository, roots), and `ObserveSupervisorLifecycleOptions` (supervisor, emit, redactor, steps).
14
+
13
15
  | API/field | Meaning |
14
16
  | --- | --- |
15
17
  | `createSupervisor({ ownership, children })` | Creates one ownership-scoped supervisor. |
16
18
  | `SupervisorChild.createAgent(context)` | Child-owned factory; receives derived resource/thread IDs, narrowed permission, abort signal, and nested `delegate`. |
17
19
  | `delegate({ childId, input, threadId?, limits?, signal? })` | Invokes one allow-listed child. Input is text and byte-bounded. |
20
+ | `delegateAsync({ childId, input, threadId?, limits?, signal? })` | Starts one local child and returns `{ delegationId, status: "running" }` without waiting for its result. |
21
+ | `wait(delegationId)` / `cancel(delegationId)` | Joins one local async child (capped at supervisor timeout) or aborts it. Unknown and foreign IDs share one denial. |
22
+ | `createSpawnAgentTool({ supervisor, name? })` | Returns non-exclusive `spawn_agent` tool for a parent model. Its closed schema exposes only host child IDs, input, optional thread ID, and `mode`. |
23
+ | `createWaitAgentTool` / `createCancelAgentTool` | Return `wait_agent` / `cancel_agent` tools for host-owned async handles. |
24
+ | `Supervisor.childIds` | Frozen advertised child-id list the spawn tool's schema enum is built from; model arguments cannot extend it. |
18
25
  | `hooks.before` | May reject, modify redacted input, or narrow limits/policy. |
19
26
  | `hooks.after` | Observes redacted terminal summary; failures cannot alter settled result. |
20
27
  | `limits` | Depth 4/16, active children 4/32, input 64 KiB/1 MiB, steps 8/64, tools 32/256, tokens 20k/1m, timeout 60s/30m, event queue 128/4096, child events/delegation 256/4096, child-event bytes 32 KiB/256 KiB default/hard. Over-cap `delegate()` throws `SupervisorLimitError` before incrementing `activeChildren`. Hook rejection and timeout decrement the count exactly once (no leaked timers). |
21
28
 
22
29
  ## Outputs / response / events
23
30
 
24
- `delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. `subscribe()` emits bounded `delegation_started`, `delegation_finished`, `delegation_rejected`, and `delegation_error` metadata events. Graceful close drains already-queued terminal events before the iterator completes (same core multiplexer contract). Hosts may project those events through observability `handleDelegation()` using the parent Prism run ID; no OpenTelemetry dependency enters this package.
31
+ `delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. `delegateAsync()` returns a local running handle; `wait()` returns its result (or `{ status: "cancelled" }` after `cancel()`), and stays idempotent while its terminal record is retained (bounded by `limits.maxQueuedEvents`; an evicted or foreign id returns the same non-enumerating error). `subscribe()` emits bounded `delegation_started`, `delegation_finished`, `delegation_rejected`, and `delegation_error` metadata events. Graceful close drains already-queued terminal events before the iterator completes (same core multiplexer contract). Hosts may project those events through observability `handleDelegation()` using the parent Prism run ID; no OpenTelemetry dependency enters this package.
25
32
 
26
33
  ### Child event passthrough (opt-in)
27
34
 
28
- `createSupervisor({ childEvents: true })` projects a redacted, size-capped **milestone** subset of child `AgentEvent`s onto the same stream as `delegation_child_event` (tagged `childId`, `delegationId`, `depth`). v1 covers run start/finish/`suspended`/`denied` and tool-execution started/finished/error/blocked — not per-token `message_delta`. Default off: the stream is byte-identical to today (no subscribe, no allocation). Caps: `limits.maxChildEventsPerDelegation` (256/4096) and `limits.maxChildEventBytes` (32 KiB/256 KiB); exceeding either drops further child events and emits one `delegation_child_events_capped` marker (never throws). Events pass through the supervisor `redactor` before emission. Children never receive supervisor internals or store/subscription access. Resume-path rebuilds (`resumeNestedRun`) do not currently project child events — live passthrough is the initial `delegate()` session only.
35
+ `createSupervisor({ childEvents: true })` projects a redacted, size-capped **milestone** subset of child `AgentEvent`s onto the same stream as `delegation_child_event` (tagged `childId`, `delegationId`, `depth`). v1 covers run start/finish/`suspended`/`denied` and tool-execution started/finished/error/blocked — not per-token `message_delta`. Default off: the stream is byte-identical to today (no subscribe, no allocation). Caps: `limits.maxChildEventsPerDelegation` (256/4096) and `limits.maxChildEventBytes` (32 KiB/256 KiB); exceeding either drops further child events and emits one `delegation_child_events_capped` marker (never throws). Events pass through the supervisor `redactor` before emission. Children never receive supervisor internals or store/subscription access. Resume-path rebuilds (`resumeNestedRun`) attach the same pump to the rebuilt child session, so a delegation that suspended for approval keeps projecting milestones after the root run resumes; counters restart per pump, so each attempt gets the full cap.
29
36
 
30
37
  ## Request/response example
31
38
 
@@ -54,17 +61,36 @@ const supervisor = createSupervisor({
54
61
  const result = await supervisor.delegate({ childId: "research", input: "Check sources" });
55
62
  ```
56
63
 
64
+ ## Model-facing spawn tool
65
+
66
+ `createSpawnAgentTool({ supervisor })` turns the same host-owned child allow-list into non-exclusive `spawn_agent` tool calls, so independent calls use the parent session's `toolConcurrency`. The schema has only `childId`, `input`, optional `threadId`, and `mode: "sync" | "async"` (default `sync`); unknown children fail closed as standard tool errors before delegation. Model arguments cannot supply child tools, identity, scopes, or higher limits. Async returns only a local `{ delegationId, status: "running" }` handle. Install `wait_agent` once per handle for wait-all, or `cancel_agent` to abort it; cancellation is terminally reported by `wait_agent`. Parent-run abort propagates to running children. Handles are in-process, ownership-scoped, and bounded — they do not survive host restart.
67
+
68
+ ```ts
69
+ import { createAgent } from "@arnilo/prism";
70
+ import { createCancelAgentTool, createSpawnAgentTool, createWaitAgentTool } from "@arnilo/prism-core/runtime/supervisor";
71
+
72
+ const parent = createAgent({
73
+ /* parent model/provider */
74
+ tools: [createSpawnAgentTool({ supervisor }), createWaitAgentTool({ supervisor }), createCancelAgentTool({ supervisor })],
75
+ });
76
+ await parent.createSession().run("Research auth and billing", {
77
+ loop: { strategy: "single-shot", toolConcurrency: 2 },
78
+ });
79
+ ```
80
+
57
81
  > **Contract — child factories return `Agent`.** `createAgent` must return an `Agent`, not an `AgentSession` (or a plain object). Wrong type throws `SupervisorError: child "<id>" factory must return an Agent, got <type>` on both initial `delegate()` and nested resume. Nested approvals also need a **stable config** plus a **durable (or rebuild-stable) store** — calling `createSession()` inside the factory and returning that session loses the child's checkpointed leaf. Live demo: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) (`childAgent` returns `createAgent(...)`).
58
82
 
59
83
  ## Durable child approvals
60
84
 
61
- With `checkpoints` + `definitionRevision`, every child run is durable with `interruptBeforeTool: true`. A child that suspends on pending decisions throws `AgentDelegationSuspendedError` out of `delegate()`; when the delegation runs inside a root agent's tool, core converts it into a root suspension whose `interruption.pendingDecisions` carry hashed root-visible approval ids (`sub_<sha256(runId:childApprovalId)>`) and `attribution.path` (redacted child ids, root first, at most 8 deep). Root decisions route back through the same CAS rules: pass `supervisor.resumeNestedRun` as `resumeNestedRun` in the root run's `runState` and in every `resumeAgentRun` options object. The supervisor rebuilds the child from a bounded delegation mapping stored in the same checkpoint store (child id, delegation/thread ids, redacted input, version), re-runs the `before` hook so its narrowing applies to the resumed run (hooks must be idempotent), and re-attributes re-suspensions recursively, so grandchild decisions surface with the full path. A delegating child's own `interruptBeforeTool` also gates its delegate tool, so hosts approve delegation and the child's own side effects as separate stages. Root `*_for_run` stickies record the attribution path and only match the same delegation path; child stickies live on the child run and expire with it. A root approval never widens the child: the child's narrowed permission re-runs at dispatch. Unknown or foreign nested run ids fail closed with one non-enumerating error. Child factories must return stable configs and a durable (or rebuild-stable) session store for resume to work.
85
+ With `checkpoints` + `definitionRevision`, every child run is durable with `interruptBeforeTool: true`. A child that suspends on pending decisions throws `AgentDelegationSuspendedError` out of `delegate()`; when the delegation runs inside a root agent's tool, core converts it into a root suspension whose `interruption.pendingDecisions` carry hashed root-visible approval ids (`sub_<sha256(runId:childApprovalId)>`) and `attribution.path` (redacted child ids, root first, at most 8 deep). Root decisions route back through the same CAS rules: pass `supervisor.resumeNestedRun` as `resumeNestedRun` in the root run's `runState` and in every `resumeAgentRun` options object. The supervisor rebuilds the child from a bounded delegation mapping stored in the same checkpoint store (child id, delegation/thread ids, redacted input, version), re-runs the `before` hook so its narrowing applies to the resumed run (hooks must be idempotent), and re-attributes re-suspensions recursively, so grandchild decisions surface with the full path. A delegating child's own `interruptBeforeTool` also gates its delegate tool, so hosts approve delegation and the child's own side effects as separate stages. Root `*_for_run` stickies record the attribution path and only match the same delegation path; child stickies live on the child run and expire with it. A root approval never widens the child: the child's narrowed permission re-runs at dispatch. Unknown or foreign nested run ids fail closed with one non-enumerating error. A resumed attempt is terminal-symmetric with live `delegate()`: it publishes `delegation_finished` (`delegation_rejected` when the re-run `before` hook denies) and runs `hooks.after` once with the original `childId`/`delegationId`, which is what lets an isolated child's worktree be cleaned up. A suspended child stays non-terminal — no finish event, no `after` — and a rebuild that throws before the run starts (stale version, fingerprint drift) publishes nothing and runs no terminal hook, so a duplicate resume attempt can never clean up a live suspended child. Child factories must return stable configs and a durable (or rebuild-stable) session store for resume to work.
62
86
 
63
87
  ## Extension and configuration notes
64
88
 
89
+ Parallel isolated children: wrap one catalog factory with `createWorktreeChildFactory` from `@arnilo/prism-coding-tools/agent` and pass its `after` as the supervisor's terminal hook — the supervisor stays git-agnostic, and the child context gains `cwd` pointing at its own linked worktree. See [Coding workspaces](coding-workspaces.md#spawn-isolation-supervisor-children).
90
+
65
91
  Child factories resolve their own providers/credentials and construct context/memory using the supplied IDs. Parent, child, returned-agent, budget, and hook permission policies are AND-composed. Child/request/hook limits can only lower inherited limits. A nested factory can call the supplied `delegate()`; immutable path state rejects cycles and depth overflow.
66
92
 
67
- Supervisors propagate parent `identity` and `effectStore` to every child agent/run so delegated tool effects stay under the same ownership scope.
93
+ Supervisors propagate parent `identity` and `effectStore` to every child agent/run so delegated tool effects stay under the same ownership scope. Set host-authored `SupervisorChild.scopes` to derive a child identity with `narrowIdentity`; `assertIdentityPropagation` rejects scope widening before its factory runs.
68
94
 
69
95
  ## Security and performance notes
70
96
 
@@ -82,7 +108,9 @@ Supervisors propagate parent `identity` and `effectStore` to every child agent/r
82
108
  - [Agent identity](agent-identity.md): host-verified identity and narrow delegation.
83
109
  - [A2A interoperability](a2a.md): separate remote protocol boundary. `A2ATaskLifecycle` adapts host durable agent/workflow state directly; it does not route A2A execution through local supervisor child planning.
84
110
  - [Workflows](workflows.md): preferred deterministic orchestration.
85
- - Example: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — per-child models, factory returns `Agent`.
111
+ - [Coding workspaces](coding-workspaces.md): opt-in per-child worktree isolation via `createWorktreeChildFactory`.
112
+ - [Coding agent tools](coding-agent-tools.md): opt-in `observeSupervisorLifecycle` bridges supervisor `delegation_*` events to coding `subagent_started` / `subagent_stopped` for host timelines.
113
+ - Examples: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — per-child models, factory returns `Agent`; [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) — two model-requested explore children in one tool turn.
86
114
  - [Working and semantic memory](working-and-semantic-memory.md): child scope construction.
87
115
  - [Host security](host-security.md): permission and credential boundaries.
88
116
  - [Obscura browser engine](obscura.md): optional binary-backed generic tools for child agents.
@@ -0,0 +1,157 @@
1
+ # Telegram channel
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-channels/telegram` is framework-free Telegram Bot API transport for text, with bounded attachment handling. It uses native `fetch`; importing it or creating a factory starts no listener, poller, credential lookup, environment lookup, or network request.
6
+
7
+ ## When to use it
8
+
9
+ Use for official Telegram Bot API text and bounded media. Private DMs are the default; group/supergroup text (including forum topics) is opt-in per adapter with `allowGroups: true` and stays subject to host `authorize`. Optional ephemeral streaming previews (`sendDrafts`) are private-chat only. Opt-in host hooks add voice transcription, document extraction and speech synthesis; images reach a model only when it declares image input. Not for Secret Chats, unbounded or stored media, or model-selected destinations.
10
+
11
+ Install peer and channel package:
12
+
13
+ ```bash
14
+ npm install @arnilo/prism @arnilo/prism-channels
15
+ ```
16
+
17
+ ## Inputs / request
18
+
19
+ `TelegramAdapterOptions` (polling): host `connectionId`, `botToken` (`CredentialValueSource`), service-owned `checkpoints`/`leases`/`cursorOwnership`, optional `allowGroups`, `sendDrafts`, `maxAttachmentBytes`, `transcribe`, `synthesize` and `extractDocumentText`. `TelegramWebhookHandlerOptions` add independent `webhookSecret` and the `admit` callback (intake only — outbound sends, drafts and `fetchAttachment` use an adapter instance, which works without `start()`). Importing or constructing starts no network.
20
+
21
+ ## Request/response example
22
+
23
+ Polling is explicit and restart-safe only with a service-owned `CheckpointStore` cursor and `LeaseStore` receiver lease. `start()` verifies the token with `getMe`, refuses to poll if `getWebhookInfo` reports an existing webhook, then runs 30-second `getUpdates` requests. It accepts private `message.text`/`caption` updates from non-bot senders, plus `group`/`supergroup` text and media when `allowGroups: true`. `photo`, `voice` and `document` become bounded attachment *refs* — see [Attachments and voice](#attachments-and-voice). Service, edit and bot-echo updates are acknowledged without agent execution, as are channel/anonymous-admin `sender_chat` messages and forum topics that fail the host grant.
24
+
25
+ A parsed group message keeps the admission keys transport-exact: `externalConversationId` is the chat id (negative for groups), `externalActorId` is `from.id`, and `message_thread_id` becomes `threadId`. Only `claims` carry untrusted context (`chatType`, `isForum`, `forwarded`), and `authorize` receives them so a host can refuse forwarded messages or non-forum groups. Threads and senders are separate sessions and separate grants: a grant for `(chat A, topic 7, user X)` admits neither user Y, chat B, nor topic 8.
26
+
27
+ ```ts
28
+ import { createMessagingRuntime } from "@arnilo/prism-channels";
29
+ import { createTelegramAdapter } from "@arnilo/prism-channels/telegram";
30
+
31
+ const adapter = createTelegramAdapter({
32
+ connectionId: "support-telegram",
33
+ botToken: hostCredentials.telegramBotToken, // string, function, or CredentialValueSource
34
+ checkpoints: serviceCheckpoints,
35
+ leases: serviceLeases,
36
+ cursorOwnership: { tenantId: "service" }, // service scope, not a Telegram claim
37
+ allowGroups: true, // opt-in groups/topics; still deny-by-default at authorize()
38
+ maxAttachmentBytes: 1024 * 1024, // per `getFile` download; hard ceiling 4 MiB
39
+ transcribe: (audio, format) => transcription.transcribe({ model: "whisper-1", audio, format }), // voice -> turn text
40
+ synthesize: (text) => speech.synthesize({ model: "tts-1", input: text }), // text -> voice note, final replies only
41
+ extractDocumentText: (bytes, mimeType) => hostExtractor(bytes, mimeType), // documents are inert without this
42
+ });
43
+
44
+ const runtime = createMessagingRuntime({
45
+ // Exact observed tuple: chat id, optional topic id, sender id. Display names are never keys.
46
+ authorize: ({ externalConversationId, threadId, externalActorId, claims }) =>
47
+ claims?.forwarded === true
48
+ ? false
49
+ : (hostGrants.get(externalConversationId, threadId, externalActorId) ?? false),
50
+ resolveAgent: hostResolveAgent,
51
+ deliver: (reply) => adapter.send(reply),
52
+ onAssistantDelta: (delta) => adapter.sendDraft(delta), // opt-in previews; needs sendDrafts: true
53
+ fetchAttachment: (ref) => adapter.fetchAttachment(ref), // lets a model that declares image input receive the image
54
+ checkpoints: userCheckpoints,
55
+ leases: userLeases,
56
+ });
57
+
58
+ await adapter.start((event) => runtime.admit(event));
59
+ // shutdown: await adapter.stop(); await runtime.stop();
60
+ ```
61
+
62
+ [`examples/telegram-agent.ts`](../examples/telegram-agent.ts) exposes same composition as a host-supplied helper.
63
+
64
+ The adapter awaits `runtime.admit()` before advancing `offset`. Return the admission result from the callback: `capacity`, `unavailable`, and `stopped` keep Telegram's update unacknowledged; all other settled dispositions advance the durable cursor. The operation journal then deduplicates any replay before model or tool work.
65
+
66
+ A connection receiver lease (`prism.channels.v1.telegram.receiver`) is held for the poll loop. A second poller or a webhook worker using the same `connectionId`, service ownership and `LeaseStore` receives no work. This fences stored intake state; it cannot retract an already accepted platform request from a paused process.
67
+
68
+ ## Implementation example
69
+
70
+ Mount `createTelegramWebhookHandler` on a host HTTPS route. It is a Web-standard `(Request) => Promise<Response>` handler; Prism does not create a listener, configure DNS/TLS, or call `setWebhook` for you.
71
+
72
+ ```ts
73
+ import { createTelegramWebhookHandler } from "@arnilo/prism-channels/telegram";
74
+
75
+ const telegramWebhook = createTelegramWebhookHandler({
76
+ connectionId: "support-telegram",
77
+ botToken: hostCredentials.telegramBotToken,
78
+ webhookSecret: hostSecrets.telegramWebhookSecret, // 1–256 URL-safe characters
79
+ admit: (event) => runtime.admit(event),
80
+ leases: serviceLeases,
81
+ cursorOwnership: { tenantId: "service" },
82
+ allowGroups: true, // must match the poller you are replacing
83
+ });
84
+
85
+ // Example host adapter: route only this fixed HTTPS path to telegramWebhook(request).
86
+ ```
87
+
88
+ Configure Telegram deliberately after the route, TLS and independent secret exist. Do not set `drop_pending_updates` and do not remove an active webhook merely to switch modes.
89
+
90
+ ```sh
91
+ curl --fail-with-body \
92
+ -H 'content-type: application/json' \
93
+ -d '{"url":"https://bot.example/telegram","secret_token":"<independent-secret>","allowed_updates":["message","callback_query"]}' \
94
+ "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook"
95
+ ```
96
+
97
+ The handler requires `POST` + JSON, verifies `X-Telegram-Bot-Api-Secret-Token` with a fixed-length hash comparison before body parsing, bounds body bytes, acquires the same receiver lease, and calls `admit` before returning `204`. Bad secret/body returns `401`/`400`/`413`; lease, capacity, stopped runtime, or journal unavailability returns `503` so Telegram retries. Untrusted but final inputs (bot/media/unknown sender/anonymous admin, or group updates when `allowGroups` is off) return `204` without an agent call. `allowGroups` is parsed identically in both modes; set it the same on the webhook worker and the poller you replaced.
98
+
99
+ `callback_query` is promptly acknowledged with `answerCallbackQuery` and only accepts the runtime's `p:a:<opaque-token>` / `p:d:<opaque-token>` callback shape from a non-bot sender in an admitted private, group, or topic conversation (a topic callback keeps its `message_thread_id` as `threadId`). The adapter maps it to an `approval` event; callback data never becomes model input. The runtime validates its short-lived server-side binding before core resume.
100
+
101
+ ## Outputs / response / events
102
+
103
+ `send()` accepts only replies addressed to its own `connectionId`; the runtime fixes `externalConversationId` and the reply `threadId` from the authorized binding, so a topic answer posts to that topic's `message_thread_id` and the model cannot choose either. It sends plain text, chunks at 3,500 UTF-16 code units without splitting surrogate pairs, and retries documented `retry_after` delays up to five attempts. A bounded server-issued control set renders as an inline keyboard on the final chunk; arbitrary model text cannot create buttons. There is no Markdown parse mode.
104
+
105
+ Known API failures return `{ delivered: false, reason }`. A network/timeout outcome, or a later failure after a prior chunk was accepted, rejects with a bounded ambiguous-delivery error. The common durable reply journal records thrown delivery as `delivery_unknown`; do not automatically resend it. Bot token URLs are never included in adapter errors and redirects are forbidden.
106
+
107
+ Telegram's `getUpdates` and webhooks are mutually exclusive. Updates remain available for at most 24 hours; the cursor is an ordering/acknowledgment record, not an exactly-once agent execution guarantee. Bots are not Secret Chats.
108
+
109
+ ### Streaming previews (drafts)
110
+
111
+ With `sendDrafts: true` and `MessagingRuntimeOptions.onAssistantDelta: (delta) => adapter.sendDraft(delta)`, the adapter renders the answer while it is produced through Bot API `sendMessageDraft`: same chat and `message_thread_id` as the bound turn, `draft_id` fixed at one preview slot per chat, text capped to the last 3,500 UTF-16 code units. Previews are coalesced — one request in flight, the latest partial wins — so a token stream never floods the API, and failures are ignored because a draft is only a preview. Cancel, `stop()` and run end stop further previews. Drafts are private-chat only: group and supergroup chats are skipped even when `allowGroups` and `sendDrafts` are both on. No draft is ever a `ChannelReply`: nothing is journaled, no controls or attachments can ride along, and the terminal `sendMessage` remains the only message the user keeps.
112
+
113
+ ### Attachments and voice
114
+
115
+ A `photo`, `voice` or `document` message is parsed into at most eight `ChannelAttachmentRef` values on the event: `kind`, `transportFileId` (Telegram `file_id`), optional `mimeType`, `byteLength` and `fileName`. A photo collapses to its largest size. The event never carries bytes, a URL, a caption-as-identity, or a model-chosen id, and nothing here can be addressed by model output.
116
+
117
+ Before `admit`, the adapter runs the one bounded fetch the host opted into (Bot API `getFile` plus a capped download):
118
+
119
+ - `voice` + host `transcribe` → the transcript becomes ordinary turn text (`caption`, then transcript). The audio is discarded after the call.
120
+ - `document` + host `extractDocumentText` → the extracted text becomes turn text. Without the hook the document stays inert: the message admits with an attachment ref and no text, and the runtime answers with a bounded "cannot be processed" notice instead of a model call.
121
+ - `photo` is never fetched by the adapter. The runtime fetches it (through `fetchAttachment`) only when the resolved agent's model declares `image` input, then sends it as one image content block next to the caption; otherwise the turn is refused with the same notice and no model call. `image` is the only attachment kind that becomes model input — audio/documents are text-only because Prism receives their host-produced text.
122
+
123
+ Oversize fails closed twice: the runtime denies an event whose *declared* sizes exceed `maxAttachmentBytes` (`denied: oversized`, no fetch, no model call), and the adapter refuses to buffer a body whose `Content-Length` or streamed size exceeds the same cap. Failures (network, credential, API, malformed `file_path`) yield no bytes, so the turn is refused rather than run without the attachment.
124
+
125
+ Outbound, `synthesize` adds a Bot API `sendVoice` next to the text for `final` replies: same bound chat and `message_thread_id`, OGG/OPUS, MP3 or M4A formats only, best-effort — a synthesis or upload failure never fails or re-sends the delivered text reply. Notices stay text-only. Nothing hangs a voice note on model-selected destinations.
126
+
127
+ ## Extension and configuration notes
128
+
129
+ | Setting | Default | Range |
130
+ | --- | ---: | ---: |
131
+ | Poll timeout | 30 s | 1–50 s |
132
+ | Poll batch | 100 | 1–100 |
133
+ | Receiver lease | 90 s | poll timeout + 10 s–5 min |
134
+ | Webhook/API body | 128 KiB | 1 KiB–1 MiB |
135
+ | Outbound chunk | 3,500 UTF-16 units | fixed (under Telegram's 4,096 limit) |
136
+ | Known-send attempts | 5 | fixed |
137
+ | Group/topic text (`allowGroups`) | off | boolean |
138
+ | Streaming previews (`sendDrafts`) | off | boolean (private chats only) |
139
+ | Attachment bytes (`maxAttachmentBytes`) | 1 MiB | 1 B–4 MiB |
140
+ | Voice transcription (`transcribe`) | off | host `TranscriptionProvider` wrapper |
141
+ | Speech synthesis (`synthesize`) | off | host `SpeechProvider` wrapper (final replies) |
142
+ | Document text (`extractDocumentText`) | off | host extractor |
143
+
144
+ ## Security and performance notes
145
+
146
+ The webhook secret is independent of the bot token and compared with a fixed-length hash before body parsing. Redirects are forbidden; bot-token URLs never appear in adapter errors. Polling and webhooks are mutually exclusive. Model text cannot create inline buttons.
147
+
148
+ Groups and topics are off by default and never self-authorize: the adapter only parses an observed `(chat id, topic id?, user id)` and the host grant decides, so a group membership claim is not an identity. Chat titles, usernames and display names are never actor ids. Bots, `sender_chat` (channel posts and anonymous admins), and non-text updates are dropped before admission; forwarded messages are admitted but flagged in `claims` so the grant can refuse them. In groups Telegram privacy mode limits what a bot receives (commands and replies to it) unless an operator disables it in BotFather; Prism does not work around that platform behavior. Signal stays DM-only.
149
+
150
+ Drafts are display-only and cannot become input: they carry no controls or attachments, the destination chat/thread always comes from the bound event (never from model output or from the delta), text is redacted with the same redactor as finals, and the 3,500-unit cap keeps the request inside Bot API limits. Since `sendDraft` is fire-and-forget and failures are swallowed, a draft can never fail a turn, mask a delivery failure, or replace the journaled final reply.
151
+
152
+ Media is bounded on every axis. Only the current event's refs are ever fetched (`fetchAttachment` takes a ref, and the runtime only passes refs of the event it is running), the cap is enforced twice (declared size and streamed body), and downloads use the same fixed origin with redirects forbidden. Bytes live only for the fetch call: they are never journaled (`ChannelOperationRecord` and `ChannelReplyRecord` store text only), never logged, and never written to disk. A `file_id` is not a capability — the bot proves nothing to Telegram by holding one, and a caller-supplied or model-invented id yields at most one bounded request inside the cap. Hosts wire `transcribe`/`synthesize`/`extractDocumentText` to their own provider wrappers; `@arnilo/prism-channels` never depends on `@arnilo/prism-providers`.
153
+
154
+ ## Related APIs
155
+
156
+ - [Messaging channels](messaging-channels.md): authorization, durable operations and reply recovery.
157
+ - [Web-standard server handler](server.md): mounting a `Request`/`Response` handler in a Node or framework host.
package/docs/testing.md CHANGED
@@ -21,14 +21,14 @@ Documents how the hermetic suite runs, which stage a new suite belongs to, and t
21
21
  | build race | `scripts/phase23-build-race.test.mjs` |
22
22
  | workspace suites | `npm run test --workspaces --if-present` |
23
23
 
24
- Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain.
24
+ Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). A successful `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` first removes stale evidence, then writes gitignored `scripts/postgres-evidence.json` with only current `gitHead`, capture time, and TAP counts; release evidence accepts it only at the same `HEAD`. Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
25
25
 
26
26
  ## Isolation rules
27
27
 
28
28
  - **Scratch roots come from the OS.** A suite that writes anything creates its root with `mkdtempSync(join(tmpdir(), "prism-…"))` and removes it in `after()`. Never rely on `process.cwd()` for write targets: the same suite runs with different working directories (workspace stage vs. root stage), so a cwd-relative root silently writes into the repository.
29
29
  - **Pass explicit roots.** Wiki, memory, and store helpers default `workspaceRoot` to `process.cwd()`; suites pass their scratch root (and a `wikiRoot` relative to it) instead of accepting the default.
30
30
  - **Tracked fixtures stay byte-identical.** `packages/memory/.wiki/` is a tracked wiki fixture and `docs/` is a tracked corpus. `scripts/wiki-scratch-isolation.test.mjs` runs the wiki suites from the package and from the repository root and fails if the tracked fixture hashes change, if a new file appears inside the fixture, if `<repo>/.wiki/` is scaffolded, or if the old cwd-relative scratch directories reappear.
31
- - **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count.
31
+ - **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count. The wiki gate additionally uses `--test-isolation=none`: all nested files run in its one runner process, avoiding process-worker IPC deserialization without retrying failures.
32
32
  - **Wait by polling, not by sleeping.** Async browser state (download quarantine, idle reaping) is not awaitable from the outside — `manager.ts` settles it on a fire-and-forget listener promise — so a fixed sleep is a race that loses under CPU load and fails the assertion for a reason unrelated to the behavior under test. Suites poll observable state through `waitFor(read, ok, label, { timeoutMs, intervalMs })` in `packages/web-tools/src/browser/__tests__/wait-for.ts`, which returns as soon as the state appears and otherwise throws naming the label and the last observed value. Fixed sleeps remain only where real elapsed time is the subject of the test (idle TTLs).
33
33
 
34
34
  ## Related APIs
@@ -93,7 +93,8 @@ Core maps only shapes shared by ≥2 packages (or an explicit no-op). Unique kno
93
93
  | `@arnilo/prism-providers/commandcode` / `@arnilo/prism-providers/opencode-go` | Gateway level tables (`claude-*` → `output_config_effort`, `gpt-5.6*` → `openai_reasoning`, K3/DeepSeek/GLM → `reasoning_effort`, K2.x/MiniMax/Qwen → `thinking_type`) |
94
94
  | `@arnilo/prism-providers/alibaba` | `thinking_type` mapped onto Qwen `enable_thinking` (toggle, no effort levels) |
95
95
  | `@arnilo/prism-providers/ollama` | `reasoning_effort`; `gpt-oss*` declares `low/medium/high`; native `think` field never mixed in |
96
- | `@arnilo/prism-providers/azure` / `.../vertex` / `.../bedrock` | OpenAI-compat sanitized forwarder (`reasoning_effort` / `reasoning` object), snapped to declared levels |
96
+ | `@arnilo/prism-providers/azure` / `.../vertex` / `.../bedrock` (compatible route) | OpenAI-compat sanitized forwarder (`reasoning_effort` / `reasoning` object), snapped to declared levels |
97
+ | `@arnilo/prism-providers/bedrock` (native `converse` route) | Anthropic family: `additionalModelRequestFields.thinking` (`enabled`/`disabled`/`adaptive`, default budget injected); OpenAI family: `reasoning_effort` snapped to declared levels |
97
98
  | `@arnilo/prism-providers/ai-sdk` | `noop` — host `LanguageModelV4` owns reasoning settings |
98
99
 
99
100
  ## Declared levels and snapping
@@ -133,6 +134,7 @@ OpenRouter and Hyper derive their sets from each provider's models API (`support
133
134
  - [Provider request policies](provider-request-policies.md) — `mergeProviderRequestOptions`
134
135
  - [Use-case model selection](use-case-model-selection.md) — session vs worker/summary model binding (workers take `thinkingLevel`)
135
136
  - [Agent/session runtime](agent-session-runtime.md) — prior-reasoning preservation across turns
137
+ - [Attention compiler](attention-compiler.md) — opt-in strip of thinking turns older than `thinkingKeepTurns` once the request crosses a ratio of the input cap
136
138
  - [Provider packages](provider-packages.md) — package boundaries and discovery
137
139
  - Per-provider pages under [docs/providers](providers/) — declared levels, wire field, and snapping per provider
138
140
  - [Thinking coverage evidence matrix](_evidence/thinking-coverage-2026-09-05.md) — per-model legality, source, and test pins
package/docs/tools.md CHANGED
@@ -150,14 +150,14 @@ Configuration can carry allow/deny names, but Prism does not define a policy cla
150
150
 
151
151
  ### Per-run tool scoping
152
152
 
153
- `session.run()` intentionally has no `RunOptions.tools` or `RunOptions.toolFilter`. Scope tools by building the active `ToolRegistry` for the agent/session, by resolving declarative `AgentDefinition.tools`, or by using `PermissionPolicy` / `ToolValidator` to fail closed at dispatch time. Skills do not grant tool access; `toolNames` only validates that host-active tools exist.
153
+ There is still no `RunOptions.tools` or `RunOptions.toolFilter` — those would replace or mutate the registry. `RunOptions.toolNames` is an optional **allow-list of already-registered names**. Omitted → every registered tool (legacy). Empty → no tools this run. Unknown names fail closed. The run snapshots the matching `ToolDefinition`s once; provider schemas, `search_tools`, skill `toolNames` checks, and dispatch all use that snapshot. Resume stores the grant and intersects it with current authority — it cannot widen, even if the live registry grew. Middleware, skills, and nested calls cannot add names outside the grant. Skills do not grant tool access; skill `toolNames` only validates that host-active tools exist.
154
154
 
155
155
  ```ts
156
- const activeTools = createToolRegistry([searchTool]);
157
- const agent = createAgent({ model, provider, tools: activeTools, permission, validator });
156
+ const agent = createAgent({ model, provider, tools: registry, permission, validator });
157
+ await session.run(input, { toolNames: ["web_search"] });
158
158
  ```
159
159
 
160
- Need different tools for one request? Build a short-lived agent/session with a narrower registry, or block extra calls with `PermissionPolicy` / `RunOptions.validate`. No extra per-run tool API exists yet; add one only when host apps need it.
160
+ Scope the active `ToolRegistry` (or declarative `AgentDefinition.tools`) at agent construction. `PermissionPolicy` / `RunOptions.validate` still fail closed at dispatch; `toolNames` only intersects that host-active set.
161
161
 
162
162
  ### Artifact-loop tools
163
163
 
@@ -274,7 +274,7 @@ Limits (mirroring the skill-disclosure DEFAULT/HARD cap pattern):
274
274
 
275
275
  - `search_tools({ query, k? })` returns inert `name: short description [matched: …]` lines — no schemas or tool bodies — and marks returned tools active for the session. Activation is names-only in run persistence (`sessionState.activatedToolNames`, capped at 128 names) and inert for tools absent from the current registry; a host can reset it with `session.clearActivatedTools()`.
276
276
  - Fail closed: any index or scoring error discloses the full input list — never zero tools, never wider than the input list. Exhausting the frozen 1024-tool index cap is surfaced the same way.
277
- - Disclosure never grants access: dispatch re-checks registry membership and allow/deny (`unknown_tool` / `tool_denied`) on every call regardless of what was described. Search results are intersected with the disclosed list structurally — searched tools are only ever selected from that list, never widened.
277
+ - Disclosure never grants access: dispatch re-checks registry membership and allow/deny (`unknown_tool` / `tool_denied`) on every call regardless of what was described. Search results are intersected with the disclosed list structurally — searched tools are only ever selected from that list, never widened. When `RunOptions.toolNames` is set, the search index is built from that snapshot only.
278
278
  - Scoring is BM25-lite lexical (name tokens weigh ×3, IDF from the registry): bounded, dependency-free, deterministic tie-breaks. ponytail ceiling: embedder-backed scoring via `@arnilo/prism-memory/rag` if accuracy fixtures fall short.
279
279
  - Cross-link: skills apply the same discipline to prompt text — see [Context and skills](context-and-skills.md).
280
280
 
@@ -293,6 +293,7 @@ Limits (mirroring the skill-disclosure DEFAULT/HARD cap pattern):
293
293
  - [Middleware hooks](middleware-hooks.md): `tool_call` and `tool_result` middleware used during dispatch.
294
294
  - [Credentials and redaction](credentials-and-redaction.md): redaction helpers used for tool execution errors.
295
295
  - [Observational memory compaction package](compaction-observational-memory.md): optional exact-id recall tool factory.
296
+ - [Memory fabric](memory-fabric.md): optional governed note tools (`memory.view`/`read`/`insert`/`recall`/`forget`) jailed to a host directory.
296
297
  - [Tool execution primitives](tool-execution-primitives.md): JSON Schema adapter, parallelism, MCP bridge, and execution-policy designs.
297
298
  - [MCP client bridge](mcp-tools.md): optional remote tool mapping plus separate bounded resource/prompt facades; non-tool MCP capabilities never bypass tool dispatch by masquerading as `ToolDefinition`.
298
299
  - [Recoverable tool effects](tool-effects.md): optional `tool.effect` + `effectStore` claim/CAS recovery around dispatch.
package/docs/web-tools.md CHANGED
@@ -67,7 +67,7 @@ Default/hard limits: query 4/16 KiB; results 10/20; URLs 5/20; request 256 KiB/1
67
67
 
68
68
  ## Security and performance notes
69
69
 
70
- Provider credentials never enter tool schemas/results, prompts, telemetry, URLs, or errors. Error text excludes remote bodies. Search snippets, Markdown, and extracted JSON are prompt-injection-capable data: never concatenate them into system instructions or use them to modify tools, permissions, credentials, trust, routing, or schemas. Firecrawl fetches target URLs remotely; Prism cannot claim target DNS pinning after handoff. Use controlled host fetch when that guarantee is required.
70
+ Provider credentials never enter tool schemas/results, prompts, telemetry, URLs, or errors. Error text excludes remote bodies. Search snippets, Markdown, and extracted JSON are prompt-injection-capable data: never concatenate them into system instructions or use them to modify tools, permissions, credentials, trust, routing, or schemas. `snapshotWebEvidence({ url, body, provider })` hashes an already-fetched body into the shared `ArtifactCitation` evidence shape; it does not refetch and does not store credentials. Firecrawl fetches target URLs remotely; Prism cannot claim target DNS pinning after handoff. Use controlled host fetch when that guarantee is required.
71
71
 
72
72
  Default tests use injected fake fetch and make no public request. Restricted smoke: `PRISM_LIVE_WEB=1 npm run test:live -w @arnilo/prism-web-tools` plus least-privilege provider environment credential. Prefer the [`browser`](browser-automation.md) subpath over ordinary public retrieval; use browser automation only for interactive/authenticated/JavaScript-heavy work behind a host egress proxy. Arbitrary HTML execution, model-selected providers, automatic OAuth forwarding, and generic web/MCP passthrough are unsupported.
73
73
 
@@ -77,4 +77,5 @@ Default tests use injected fake fetch and make no public request. Restricted smo
77
77
  - [Credential storage](credential-storage.md): explicit resolver composition and environment mapping.
78
78
  - [Host security](host-security.md): SSRF, untrusted-content, and secret boundaries.
79
79
  - [MCP tools](mcp-tools.md): hardened prototype path for official vendor MCP servers.
80
+ - [Work artifacts and review](work-artifacts-and-review.md): `snapshotWebEvidence` produces shared citation evidence from an already-fetched body.
80
81
  - [Performance and resource limits](performance.md): operational ceilings and benchmark evidence.
package/docs/wiki.md CHANGED
@@ -85,7 +85,7 @@ The staging primitive behind `/wiki-ingest`, `wiki_ingest`, and the CLI. Accepts
85
85
  | :--- | :--- |
86
86
  | Text-like files and `text` | Decoded as UTF-8 (RAG text/markdown/html parsers) |
87
87
  | Uncompressed PDF | Parsed by the RAG PDF parser (bounded pages/bytes) |
88
- | Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-coding-tools/document-reader`) |
88
+ | Compressed PDF / DOCX | Throws a named error unless the host supplies `options.extractDocument` (e.g. wire `createDocumentReader()` from `@arnilo/prism-work/document-reader`) |
89
89
  | `url` | `assertSsrfAllowedUrl` runs first (private/link-local hosts rejected before any fetch); then the host `fetchUrl` hook supplies the bytes/text — missing or empty hook output fails closed. Staged filename comes from the hook, the URL extension (`doc.pdf`), or `source.md` |
90
90
  | Images | Staged as-is; stub extract points at the staged `source.*` — no OCR; view the file |
91
91
  | Unknown binary | Fails closed unless `extractDocument` claims it |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-core/runtime/server` ships a durable artifact co-work review service (Phase 9 / 0.0.14): authorized attach of source/output references with MIME/hash/version, producer-run attribution, citations/data sources, and preview metadata; revision comparison; reviewer approve/reject (request-changes) with last-validated recovery; and authorized, expiring delivery links. Core (`@arnilo/prism`) exports artifact **types only** (`ArtifactRecord`, `ArtifactRevision`, `ArtifactApproval`, `ArtifactDeliveryToken`, approval state `pending | approved | rejected`). Prism persists bounded metadata, revisions, approvals, and delivery references over the existing versioned checkpoint store — **never file bodies**; hosts own blob storage and rendering.
5
+ `@arnilo/prism-core/runtime/server` ships a durable artifact co-work review service: authorized attach of source/output references with MIME/hash/version, producer-run attribution, citations/data sources, and preview metadata; revision comparison; reviewer approve/reject (request-changes) with last-validated recovery; and authorized, expiring delivery links. Citations may carry shared evidence fields (`sourceId`, `revision`, `contentHash`, `retrievedAt`, `excerpt`, `span`, `tenantId`, `support`). Approve stamps `evidenceDigest` over those identity tuples. Core (`@arnilo/prism`) exports artifact types plus `checkCitationIntegrity` / `citationBindingDigest` / `approvalEvidenceIntact`. Prism persists bounded metadata, revisions, approvals, and delivery references over the existing versioned checkpoint store — **never file bodies**; hosts own blob storage and rendering. Integrity is existence/hash/span/ACL only; `support` is an optional host verdict, not proof.
6
6
 
7
7
  ## When to use it
8
8
 
@@ -34,8 +34,8 @@ Every operation input carries `ownership` (from host `authorize`, never request
34
34
  | `list` | Ownership/thread-scoped `PersistencePage<ArtifactRecord>` |
35
35
  | `get` | `ArtifactRecord` |
36
36
  | `revise` | `ArtifactRecord` with an appended revision (new revision resets state to pending) |
37
- | `compare` | `{ artifactId, from, to, changed: { hash, mime, uri, citations } }` — hash+metadata only |
38
- | `approve` / `reject` | `ArtifactRecord`; approve advances `lastValidatedVersion`, reject never clears it |
37
+ | `compare` | `{ artifactId, from, to, changed: { hash, mime, uri, citations } }` — hash+metadata only; structural Office diffs use `diffDocument` |
38
+ | `approve` / `reject` | `ArtifactRecord`; approve advances `lastValidatedVersion` and stamps `evidenceDigest`; reject never clears last-validated |
39
39
  | `lastValidated` | The last approved `ArtifactRevision` (fails closed before any approval) |
40
40
  | `deliveryLink` | `{ link, token }` — signed expiring `ArtifactDeliveryToken` |
41
41
 
@@ -87,7 +87,7 @@ export const handler = createArtifactHandler({ service: artifacts, authorize: ho
87
87
 
88
88
  - Every operation requires authenticated identity + thread ownership derived from host `authorize`; cross-ownership access fails closed as `not_found` (never leaks existence).
89
89
  - Concurrent reviewer conflicts resolve via checkpoint CAS (`expectedVersion`); the loser gets a retryable `conflict` and no approval is lost or duplicated. A throw before commit persists nothing, so failed updates roll back.
90
- - Local filesystem paths are rejected in `uri`/citations (`file:`, absolute, or drive paths); records are redacted before persist and on response, so paths/secrets/document-private data never enter records, events, or exports.
90
+ - Local filesystem paths are rejected in `uri`/citations (`file:`, absolute, or drive paths); records are redacted before persist and on response, so paths/secrets/document-private data never enter records, events, or exports. Citation evidence is untrusted/inert: hosts pass already-retrieved snapshots into `checkCitationIntegrity` (no URL refetch, no persisted presigned credentials). A live source hash/revision/ACL change fails integrity even when a semantic judge scores the prose 1.0.
91
91
  - Frozen caps (default / hard): artifacts per thread 64/256; revisions per artifact 32/128; record 8/64 KiB; preview 16/64 KiB; citations 32/128 and 2/8 KiB each; MIME 128/512 B; hash 256/1 KiB; compare exactly 2 revisions; delivery TTL 5 min/24 h; delivery token 4/16 KiB. Raising the revision cap may require raising `recordBytes` (aggregate backstop).
92
92
  - Compare is hash+metadata-bounded (hosts render content); no file bodies are persisted or transferred. With a wired body store, bodies live in the host's object store and are streamed through the adapter (bounded by `maxBodyBytes` 64 MiB/512 MiB, concurrent transfers 4/16, presign TTL 10 min/24 h); object-store outages surface typed `ERR_PRISM_S3_*` / `ERR_PRISM_ARTIFACT_BODY_*` errors, never silent success.
93
93
 
@@ -95,6 +95,14 @@ export const handler = createArtifactHandler({ service: artifacts, authorize: ho
95
95
 
96
96
  `@arnilo/prism-coding-tools/agent` composes over this service for the coding patch review workflow: `createCodingPatchReviewManifest` builds a bounded manifest (repository/worktree identity, base/head, patch digest, changed paths, diffstat, check and diagnostic summaries) and returns a structural `ArtifactAttachInput` whose `preview.review` embeds the manifest and whose `hash` is the patch SHA-256; `assertCodingPatchAccepted` derives `pending|accepted|rejected|superseded` from the returned `ArtifactRecord` by binding to the exact artifact revision, digest, and identity — any patch/repository/worktree/base/head change supersedes a prior acceptance (a newer revision attached after approval makes the old acceptance stale and refused). Decisions never apply/commit/push/merge; the manifest never embeds a raw patch body. Full contract: [Coding review and diagnostics](coding-review-and-diagnostics.md).
97
97
 
98
+ ## Business action drafts and editable approvals (0.7.0)
99
+
100
+ Business tools (e.g. mail, calendar, documents in `@arnilo/prism-work/connectors`) record mutations through durable `WorkDraftStore` drafts before execution. Human reviewers can approve, deny, or edit draft payloads directly:
101
+ - AG-UI clients advertise and send `approveWithEdits` with revised arguments (`editedArgs`/`modifiedArguments`).
102
+ - The server resume endpoint accepts `{ decision: "approve", modifiedArguments: { ... } }` under CAS `expectedVersion`.
103
+ - If arguments are modified, a new draft revision is created with bumped revision number and payload digest. The previous revision's approval is invalidated and the mutation requires approval for the revised content.
104
+ - Untyped/malformed edits, recipient escalation, schema violations, or stale CAS versions fail closed.
105
+
98
106
  ## Live probe (plans/064 Task 9)
99
107
 
100
108
  The S3 artifact-body store has an operator-gated live probe against a real S3-compatible endpoint (use a throwaway bucket):
@@ -115,3 +123,5 @@ Probes: put → get (hash + size verified), presigned delivery URL with `X-Amz-S
115
123
  - [Policy and audit](policy-and-audit.md): `onDecision` events bridge here for an auditable review ledger.
116
124
  - [Host security](host-security.md): identity/ownership, redaction, and expiring-link boundaries.
117
125
  - [Frontend interoperability (AG-UI and ACP)](ag-ui.md): projects artifact progress/approval/download-link as redacted co-work events over the durable-resume stream.
126
+ - [Documents, spreadsheets, and presentations](documents.md): `diffDocument` for structural paragraph/table/cell/slide review.
127
+ - [Evaluations](evaluations.md): `createCitationIntegrityScorer` invariant over `environment.citations`.