@arnilo/prism 0.7.0 → 0.9.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 (182) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +15 -2
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +91 -10
  10. package/dist/agent-run-state.d.ts +34 -2
  11. package/dist/agent-run-state.js +68 -6
  12. package/dist/agent-session/helpers.js +20 -1
  13. package/dist/agent-session/session/assemble.js +250 -27
  14. package/dist/agent-session/session/persist.d.ts +27 -0
  15. package/dist/agent-session/session/persist.js +94 -12
  16. package/dist/agent-session/session/provider-round.d.ts +14 -4
  17. package/dist/agent-session/session/provider-round.js +197 -25
  18. package/dist/agent-session/session/tool-round.js +24 -2
  19. package/dist/agent-session/session/types.d.ts +36 -2
  20. package/dist/agent-session/session.d.ts +40 -4
  21. package/dist/agent-session/session.js +78 -5
  22. package/dist/attention-compiler.d.ts +51 -2
  23. package/dist/attention-compiler.js +282 -21
  24. package/dist/cache-helpers.d.ts +4 -2
  25. package/dist/cache-helpers.js +8 -6
  26. package/dist/checkpoint-restore.d.ts +45 -0
  27. package/dist/checkpoint-restore.js +54 -0
  28. package/dist/checkpoints.js +7 -11
  29. package/dist/context-budget.d.ts +2 -1
  30. package/dist/context-budget.js +24 -2
  31. package/dist/contracts-core/agent.d.ts +30 -0
  32. package/dist/contracts-core/attention.d.ts +95 -0
  33. package/dist/contracts-core/content.d.ts +15 -0
  34. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  35. package/dist/contracts-core/guardrail-packs.js +2 -0
  36. package/dist/contracts-core/loop.d.ts +42 -0
  37. package/dist/contracts-core/provider.d.ts +25 -0
  38. package/dist/contracts-core/run-limits.d.ts +21 -0
  39. package/dist/contracts-core/session.d.ts +23 -5
  40. package/dist/contracts-core/session.js +21 -2
  41. package/dist/contracts-core/usage.d.ts +40 -0
  42. package/dist/contracts-core/usage.js +8 -0
  43. package/dist/contracts-core.d.ts +2 -0
  44. package/dist/contracts-core.js +2 -0
  45. package/dist/contracts-protocol.d.ts +90 -4
  46. package/dist/contracts-run-state.d.ts +82 -6
  47. package/dist/evidence-grounding.d.ts +29 -0
  48. package/dist/evidence-grounding.js +162 -0
  49. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  50. package/dist/guardrail-packs/coding-standard.js +63 -0
  51. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  52. package/dist/guardrail-packs/destructive-commands.js +46 -0
  53. package/dist/guardrail-packs/errors.d.ts +7 -0
  54. package/dist/guardrail-packs/errors.js +9 -0
  55. package/dist/guardrail-packs/index.d.ts +4 -0
  56. package/dist/guardrail-packs/index.js +15 -0
  57. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  58. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  59. package/dist/guardrail-packs/types.d.ts +16 -0
  60. package/dist/guardrail-packs/types.js +2 -0
  61. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  62. package/dist/guardrail-packs/validation-respect.js +53 -0
  63. package/dist/guardrails.d.ts +20 -1
  64. package/dist/guardrails.js +268 -0
  65. package/dist/host-composition.d.ts +13 -0
  66. package/dist/host-composition.js +33 -2
  67. package/dist/index.d.ts +19 -10
  68. package/dist/index.js +11 -6
  69. package/dist/input.d.ts +8 -1
  70. package/dist/input.js +68 -6
  71. package/dist/middleware.d.ts +37 -2
  72. package/dist/middleware.js +41 -0
  73. package/dist/node/session-store-jsonl.js +18 -3
  74. package/dist/observability.js +6 -0
  75. package/dist/provider-events.d.ts +11 -3
  76. package/dist/provider-events.js +62 -4
  77. package/dist/providers/openai-compatible.js +6 -3
  78. package/dist/providers/transport.d.ts +3 -1
  79. package/dist/providers/transport.js +36 -0
  80. package/dist/redaction.js +18 -2
  81. package/dist/run-bundle.d.ts +89 -0
  82. package/dist/run-bundle.js +150 -0
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +46 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  89. package/dist/testing/prefix-stability-conformance.js +104 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/testing/state-concurrency-conformance.js +5 -12
  93. package/dist/tools.d.ts +5 -0
  94. package/dist/tools.js +11 -3
  95. package/dist/usage-estimation.d.ts +29 -0
  96. package/dist/usage-estimation.js +79 -0
  97. package/docs/ag-ui.md +5 -0
  98. package/docs/agent-events.md +68 -1
  99. package/docs/agent-loops.md +33 -0
  100. package/docs/agent-session-runtime.md +5 -3
  101. package/docs/attention-compiler.md +89 -8
  102. package/docs/coding-agent-tools.md +1 -1
  103. package/docs/coding-security.md +1 -0
  104. package/docs/coding-tools.md +0 -1
  105. package/docs/compaction-and-retry.md +1 -1
  106. package/docs/compaction-observational-memory.md +34 -7
  107. package/docs/connected-apps.md +116 -0
  108. package/docs/context-and-skills.md +13 -0
  109. package/docs/core.md +1 -1
  110. package/docs/diagrams.md +6 -6
  111. package/docs/document-reader.md +9 -9
  112. package/docs/documents.md +32 -11
  113. package/docs/durable-runs.md +129 -0
  114. package/docs/embeddings.md +5 -0
  115. package/docs/enterprise-postgres-state.md +4 -0
  116. package/docs/evaluations.md +5 -0
  117. package/docs/execution-timeline.md +84 -1
  118. package/docs/guardrails.md +71 -2
  119. package/docs/history/079-messaging-primitive-review.md +391 -0
  120. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  121. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  122. package/docs/history/083-prism-work-primitive-review.md +84 -0
  123. package/docs/history/084-primitive-review.md +96 -0
  124. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  125. package/docs/history/README.md +5 -0
  126. package/docs/history/release-handoffs.md +38 -0
  127. package/docs/host-compositions.md +8 -6
  128. package/docs/host-security.md +2 -2
  129. package/docs/index.md +66 -29
  130. package/docs/input-and-prompt-assembly.md +3 -3
  131. package/docs/knowledge-sync.md +4 -0
  132. package/docs/live-testing.md +5 -3
  133. package/docs/mcp-tools.md +1 -0
  134. package/docs/messaging-channel-operations.md +166 -0
  135. package/docs/messaging-channels.md +150 -0
  136. package/docs/middleware-hooks.md +38 -2
  137. package/docs/migrate-to-0.8.md +124 -0
  138. package/docs/migrate-to-0.9.md +210 -0
  139. package/docs/migration.md +43 -0
  140. package/docs/model-registry.md +12 -2
  141. package/docs/multi-agent-patterns.md +25 -2
  142. package/docs/node-jsonl-session-store.md +7 -1
  143. package/docs/observability.md +7 -3
  144. package/docs/openapi-tools.md +1 -1
  145. package/docs/operations.md +1 -3
  146. package/docs/options-index.md +36 -3
  147. package/docs/peer-dependencies.md +6 -6
  148. package/docs/policy-and-audit.md +13 -1
  149. package/docs/postgres-persistence.md +1 -1
  150. package/docs/prefix-stability-conformance.md +93 -0
  151. package/docs/provider-caching.md +4 -4
  152. package/docs/provider-conformance.md +16 -0
  153. package/docs/provider-layer.md +2 -2
  154. package/docs/provider-packages.md +20 -20
  155. package/docs/providers/neuralwatt.md +5 -1
  156. package/docs/public-contracts.md +2 -2
  157. package/docs/rag.md +102 -4
  158. package/docs/release-and-install.md +55 -47
  159. package/docs/run-bundle.md +92 -0
  160. package/docs/runs-and-usage.md +57 -6
  161. package/docs/scoped-agent-memory.md +262 -0
  162. package/docs/server.md +2 -0
  163. package/docs/session-store-conformance.md +1 -2
  164. package/docs/session-stores.md +17 -17
  165. package/docs/sheets.md +9 -9
  166. package/docs/signal-channel.md +112 -0
  167. package/docs/speech.md +5 -1
  168. package/docs/sqlite-persistence.md +1 -1
  169. package/docs/supervisors.md +32 -12
  170. package/docs/telegram-channel.md +157 -0
  171. package/docs/testing.md +2 -2
  172. package/docs/tools.md +17 -0
  173. package/docs/wiki.md +1 -1
  174. package/docs/work-artifacts-and-review.md +1 -1
  175. package/docs/work-connectors.md +9 -9
  176. package/docs/work-sandbox.md +115 -0
  177. package/docs/work-tools.md +38 -16
  178. package/docs/workflows.md +5 -0
  179. package/package.json +9 -3
  180. package/templates/business-worker/manifest.json +2 -1
  181. package/templates/business-worker/src/agent.ts.tmpl +1 -1
  182. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
@@ -48,16 +48,17 @@ await session.run("cheap run", { attentionCompiler: { triggerRatio: 0.95, compac
48
48
  The agent setting is resolved with the run's model at run start, before any provider turn, so a malformed setting or a widening overlay fails the run immediately instead of on the turn that crosses the ratio:
49
49
 
50
50
  - **Allowed in the overlay:** `triggerRatio` / `compactRatio` at or above the agent setting, `keepLast` / `thinkingKeepTurns` at or below it, and extra `excludeTools` (unioned with the agent list, never removed).
51
- - **Rejected:** a lower gate ratio, more protected rows, and `maxInputTokens` / `reserveTokens` — cap inputs are agent-config only, because moving the cap moves the gate itself. Raising `triggerRatio` at or above the agent's `compactRatio` needs `compactRatio` raised in the same overlay.
51
+ - **Rejected:** a lower gate ratio, more protected rows, and `maxInputTokens` / `reserveTokens` / `trigger` — cap inputs and fold axes are agent-config only, because moving either moves the gate itself. Raising `triggerRatio` at or above the agent's `compactRatio` needs `compactRatio` raised in the same overlay.
52
52
  - **Enabling from a run is rejected:** a run may disable or relax the compiler, never switch it on where the agent config left it off.
53
53
 
54
- The **sticky frontier is session-owned and created lazily** the first time an enabled run assembles a request: one `{ thinking, toolCallIds }` set pair per session, shared across runs, provider rounds, and branches, so a stub or strip made once stays applied even on a later under-ratio turn. It lives in memory only — a resumed process simply re-decides from the ratio it sees.
54
+ The **sticky frontier is session-owned and created lazily** the first time an enabled run assembles a request: one `{ thinking, toolCallIds }` set pair per session, shared across runs, provider rounds, and branches, so a stub or strip made once stays applied even on a later under-ratio turn. A durable run with `persistSessionState: true` writes its bounded snapshot into the checkpoint and restores it on resume, so a resumed process keeps its stubs instead of re-deciding its first turn from the ratio.
55
55
 
56
56
  `AttentionCompilerOptions` (all optional):
57
57
 
58
58
  | Field | Type | Default | Meaning |
59
59
  | --- | --- | --- | --- |
60
- | `triggerRatio` | `number` | `0.75` | Fraction of `inputCap` that enables mutation; must be in `(0, 1)` (exclusive). |
60
+ | `triggerRatio` | `number` | `0.75` | Fraction of `inputCap` that enables mutation; must be in `(0, 1)` (exclusive). The reference ratio the report carries and `compactRatio` is checked against. |
61
+ | `trigger` | `AttentionTriggerInput` | — | Fold axes (plan 086 T2). **Replaces** the `triggerRatio` axis when set; omitted keeps it alone, so behavior is unchanged. See [Trigger axes](#trigger-axes). |
61
62
  | `compactRatio` | `number` | `0.9` | Where compaction should fire relative to the compiler; must exceed `triggerRatio`. |
62
63
  | `thinkingKeepTurns` | `number` | `1` | Newest thinking-bearing assistant turns kept intact. |
63
64
  | `keepLast` | `number` | `3` | Newest tool results kept full. |
@@ -71,8 +72,9 @@ The **sticky frontier is session-owned and created lazily** the first time an en
71
72
  | --- | --- | --- |
72
73
  | `model` | `{ limits?: ModelLimits }` | Source of `contextWindow` / `maxOutputTokens` when `maxInputTokens` is absent. |
73
74
  | `compactionTrigger` | `CompactionTrigger` | Optional: validated here so an unknown trigger `type` fails at create time, not on the first turn. An `input_ratio` trigger must exceed `triggerRatio`. |
75
+ | `runInputBudget` | `number \| null` | Cumulative run input budget the `run_input_ratio` axis folds against — pass the resolved `RunLimits.maxInputTokens`. `null` or omitted means the run declares no budget, so that axis falls back to the input cap. Distinct from `maxInputTokens`, which caps a single request. |
74
76
 
75
- **Public surface.** `createAttentionCompiler(options?: AttentionCompilerOptions, context?)` is the factory; `AttentionCompilerOptions` carries the gate ratios, sticky-stage tuning (`thinkingKeepTurns`, `keepLast`), `excludeTools`, and `reserveTokens`. `resolveInputCap(options?: AttentionInputCapOptions, model?)` is the cap resolver, `compileAttention(options: AttentionCompileOptions)` is the per-turn call `assembleProviderInput` makes (`AttentionCompileOptions` also carries `fold`, `frontier`, `redactor`, `signal`, and the `turn`/`sessionId`/`runId` telemetry ids), and `createAttentionTruncationTrigger(options?: AttentionTruncationTriggerOptions)` builds the host-programmable compaction trigger.
77
+ **Public surface.** `createAttentionCompiler(options?: AttentionCompilerOptions, context?)` is the factory; `AttentionCompilerOptions` carries the gate ratios, the optional `trigger` axes, sticky-stage tuning (`thinkingKeepTurns`, `keepLast`), `excludeTools`, and `reserveTokens`. `resolveInputCap(options?: AttentionInputCapOptions, model?)` is the cap resolver, `compileAttention(options: AttentionCompileOptions)` is the per-turn call `assembleProviderInput` makes (`AttentionCompileOptions` also carries `fold`, `frontier`, `redactor`, `signal`, `runInputTokens`, and the `turn`/`sessionId`/`runId` telemetry ids), and `createAttentionTruncationTrigger(options?: AttentionTruncationTriggerOptions)` builds the host-programmable compaction trigger. The frozen handle carries the normalized `trigger` axes, the resolved `runInputBudget`, and `durable`, so a caller can never resolve one and evaluate against another.
76
78
 
77
79
  Input cap resolution: `maxInputTokens` when set, otherwise `contextWindow - (maxOutputTokens ?? 0) - reserveTokens`. Both `resolveInputCap(options?, model?)` and the compiler fail closed with a `TypeError` when neither source is present, when a declared limit is malformed, or when the computed cap is not positive.
78
80
 
@@ -82,10 +84,56 @@ Turn options, passed to `assembleProviderInput`:
82
84
  | --- | --- | --- |
83
85
  | `attentionCompiler` | `AttentionCompilerOptions \| AttentionCompiler` | Raw options are validated for that call; a resolved handle reuses one validation. The session passes the run's resolved handle so a tuning typo fails before the first provider turn. |
84
86
  | `attentionSticky` | `AttentionStickyFrontier` | `{ thinking, toolCallIds }` sets from `createAttentionStickyFrontier()`. The session supplies its own; a direct `assembleProviderInput` caller owns it, and omitting it makes each call mutate for its turn only. |
87
+ | `runInputTokens` | `number` | Run input tokens already charged by provider usage this run (default 0), so the cumulative `run_input_ratio` axis can project this turn onto the spend. The session passes the run limit counter. |
85
88
  | `onAttentionReport` | `(report: AttentionReport) => void` | Called once per **mutated** turn, before `input_assembly` middleware; silent under the ratio. The session uses it to emit `attention_compiled`. |
86
89
 
87
90
  `attentionCompiler` and `contextBudget` are **mutually exclusive** — a compiler-on turn that is still over throws `AttentionBudgetError` rather than evicting through the budget, so passing both fails closed with a `TypeError`.
88
91
 
92
+ ### Trigger axes
93
+
94
+ `trigger` replaces `triggerRatio` as the gate (plan 086 T2). It takes one axis, one predicate function, or an array of them; an array is **any-of**, and the first axis that fires is the one attributed on the report.
95
+
96
+ | Kind | Fires when | Folds to | Fails closed? |
97
+ | --- | --- | --- | --- |
98
+ | `{ kind: "input_ratio", ratio }` | the assembled request reaches `ratio × inputCap` — the legacy `triggerRatio` axis | `ratio × inputCap` | yes |
99
+ | `{ kind: "run_input_ratio", ratio }` | `runInputTokens + estimatedInputTokens` reaches `ratio × runInputBudget`, so a run capped below the model window folds before the cap kills it | every eligible row (a cumulative gate has no per-request target) | **no** — the spend is already booked; folding only slows the counter, and the run limit owns the cap |
100
+ | `{ kind: "token_floor", tokens }` | the assembled request reaches `tokens` tokens, whatever the cap | `tokens` | yes |
101
+ | `{ kind: "predicate", shouldFold }` (or a bare function) | `shouldFold(state)` returns `true` | every eligible row | yes |
102
+
103
+ ```ts
104
+ // The synapta Plan 118 shape: a 1M-window model under a 500k run input cap, where the legacy
105
+ // 0.75 × window gate (745k) could never open before the run died at its cap.
106
+ const compiler = createAttentionCompiler(
107
+ { trigger: { kind: "run_input_ratio", ratio: 0.75 }, keepLast: 3 },
108
+ { model, runInputBudget: 500_000 },
109
+ );
110
+
111
+ // Any-of, attributed in order: the floor is reported when both would fire.
112
+ createAttentionCompiler({ trigger: [{ kind: "token_floor", tokens: 120_000 }, { kind: "input_ratio", ratio: 0.9 }] }, { model });
113
+
114
+ // Host predicate: synchronous, evaluated at most twice per turn (once to decide, once to
115
+ // confirm the stages settled it) with a frozen `AttentionTriggerState`.
116
+ createAttentionCompiler(
117
+ { trigger: (state) => state.estimatedInputTokens > 150_000 && state.turn > 5 },
118
+ { model },
119
+ );
120
+ ```
121
+
122
+ `AttentionTriggerState` is frozen and carries estimates only: `estimatedInputTokens` (this turn's assembled request), `inputCapTokens`, `runInputBudgetTokens` (absent when the run declares none), `runInputTokens` (charged spend so far), and `turn`. A predicate that returns a non-boolean — including a `Promise` from an `async` function — fails closed with a `TypeError` naming the option, rather than silently never firing.
123
+
124
+ Predicate axes run **host-supplied code**, under the same trust as `CompactionTrigger.custom`: the compiler passes host data in and takes a boolean out, never credentials or payloads. Keep them synchronous and side-effect free; they see token estimates and ids, never message text.
125
+
126
+ Rules that hold for every axis:
127
+
128
+ - **Config-time validation.** Unknown kinds, a ratio outside `(0, 1)`, a non-positive `token_floor.tokens`, an empty array, and a predicate that is not a function all throw a `TypeError` naming the option (`attentionCompiler.trigger`, or `attentionCompiler.trigger[1]` inside an array).
129
+ - **Gate is agent-config only.** A run overlay may not set `trigger`, `maxInputTokens`, or `reserveTokens` — the gate and the cap move together, so moving either belongs in the agent config.
130
+ - **One evaluation per turn.** The axes are evaluated at turn start against the measured request and once more after the stages. The per-row loop then compares numbers, so a predicate costs two calls a turn no matter how many rows are eligible.
131
+ - **Sticky and monotonic as ever.** An axis only decides *whether* to fold; the stages still stop as soon as a target is reached, and mutations stay applied on later under-gate turns.
132
+ - **`run_input_ratio` needs its budget.** With `compactAfterTokens`-style run limits declared (`RunLimits.maxInputTokens`, which defaults to `40_000` and kills the run cumulatively), pass the resolved value as `runInputBudget`. Without one the axis is the per-request `input_ratio` comparison.
133
+ - **`triggerRatio` stays the reference.** Omitted alongside `trigger`, it takes the first `input_ratio` axis's ratio so `compactRatio` and the report still describe the real fold point; with no `input_ratio` axis it keeps the default `0.75` as the compaction reference.
134
+
135
+ Runnable end to end: [`examples/attention-budget-axes.ts`](../examples/attention-budget-axes.ts) runs the scenario both axes exist for — a 1M window, a 500k run budget, 24 provider turns. The window axis would need 743k tokens and never gets near (`maxUsed` ≈ 7k, so it never fires); cumulative spend crosses 1 % of the budget on turn 6, the gate opens there, and the run finishes having spent 36k of its 500k with the newest 2 rows raw and every older body a stub. `src/__tests__/attention-compiler-budget.test.ts` asserts the same numbers, including that the first fold could only be explained by carried-over spend.
136
+
89
137
  ## Outputs / response / events
90
138
 
91
139
  `createAttentionCompiler` returns an `AttentionCompiler`: `inputCap`, `reserveTokens`, `triggerRatio`, `compactRatio`, `thinkingKeepTurns`, `keepLast`, and a frozen, de-duplicated `excludeTools`. It performs no I/O and calls no provider.
@@ -97,10 +145,12 @@ Turn options, passed to `assembleProviderInput`:
97
145
  | `used` | `number` | Estimated tokens measured before this turn's mutation. |
98
146
  | `usedAfter` | `number` | Estimated tokens of the same request after the mutation, so `used` → `usedAfter` is the per-turn cost curve. |
99
147
  | `inputCap` | `number` | Resolved cap the ratio was compared against. |
100
- | `triggerRatio` | `number` | Configured ratio. |
148
+ | `triggerRatio` | `number` | Configured ratio — the reference axis, whether or not a `trigger` replaced the gate. |
149
+ | `firedAxis` | `AttentionTriggerKind?` | Axis that opened the gate on this turn, in configured order; absent on an under-gate turn. Plan 087 attribution reads this. |
101
150
  | `droppedThinkingTurns` | `number` | Thinking turns absent from this request — rows re-applied from the sticky frontier count again. |
102
151
  | `stubbedToolResults` | `number` | Tool results stubbed in this request — re-applied rows count again. |
103
152
  | `stubbedBytes` | `number` | Payload bytes those stubs took out of the request (message bytes minus the stub header). |
153
+ | `newFoldedBodies` | `number` | Folded bodies this turn added to the ledger: the `summarize` calls the cache saved, and the durable-fold checkpoint signal. `0` on a turn that only re-applied stored bodies. |
104
154
  | `truncated` | `boolean` | `true` when the gate stopped with eligible rows left, so the sticky frontier is partial. |
105
155
  | `runId` / `sessionId` | `string?` | Owning run/session when known. |
106
156
 
@@ -119,7 +169,7 @@ Tool result read_file [call_1]: omitted 41_982 bytes (sha256 3f9a1c2b4d5e6f70a1b
119
169
 
120
170
  Never stubbed: rows named in `excludeTools`, tool **errors**, results stamped as a decision/approval payload (`approval`, `approvalId`, `prismApproval`, `decision`, `decisions`, `pendingDecisions`, `elicitation` metadata), rows the host fold's own age/byte gates exclude, and any row whose stub would cost more than the payload it replaces. When `toolResultFold.summarize` is configured, that function produces the stub body for the rows the compiler picked (capped by its `maxSummaryBytes`); otherwise the deterministic digest above is used.
121
171
 
122
- `compileAttention({ compiler, groups, context?, skills?, tools?, fold?, frontier?, redactor?, signal?, turn?, sessionId?, runId? })` is what `assembleProviderInput` calls; it returns `{ groups, mutated, report }`. Under the ratio it returns the **same groups object** it was given; when it mutates it returns new `history` / `toolResults` arrays and never writes into the caller's arrays.
172
+ `compileAttention({ compiler, groups, context?, skills?, tools?, fold?, frontier?, attentionFold?, redactor?, signal?, turn?, runInputTokens?, sessionId?, runId? })` is what `assembleProviderInput` calls; it returns `{ groups, mutated, report }`. Under the ratio it returns the **same groups object** it was given; when it mutates it returns new `history` / `toolResults` arrays and never writes into the caller's arrays.
123
173
 
124
174
  Errors:
125
175
 
@@ -137,6 +187,7 @@ Errors:
137
187
  "keepLast": 3,
138
188
  "excludeTools": ["submit_payment"],
139
189
  "reserveTokens": 1024,
190
+ "durable": false,
140
191
  "compaction": {
141
192
  "trigger": {
142
193
  "type": "custom",
@@ -244,11 +295,40 @@ See [Compaction and retry policies](compaction-and-retry.md) for the trigger uni
244
295
  - `excludeTools` is fail closed: entries are validated as non-empty bounded strings, de-duplicated, and frozen; a named tool is never stubbed even when the request stays over the ratio.
245
296
  - The compiler never orchestrates other levers: `toolResultFold.summarize` still wins for fold-eligible rows when a host supplies it, `applyContextBudget` keeps working unchanged for compiler-off agents, and compaction stays a task-boundary operation (`session.compact()` still throws while a run is in flight).
246
297
  - Sticky means sticky: a stripped thinking turn is never restored and a stubbed call id is never un-stubbed, even on a later under-ratio turn — restoring either would rewrite the cached prefix. Pass no `attentionSticky` for one-shot assemblies.
247
- - The frontier is bounded (256 thinking keys, 256 tool-call ids, newest kept) and lives on the session, so it survives turns and runs. A durable run with `persistSessionState: true` also writes it into the checkpoint (`sessionState.attentionSticky`) and restores it on resume, so a resumed run keeps its stubs instead of re-deciding its first turn from the ratio; a malformed or hand-edited frontier is dropped entry by entry, never fatal to a resume.
298
+ - The frontier is bounded (256 thinking keys, 256 tool-call ids, newest kept) and lives on the session, so it survives turns and runs. A durable run with `persistSessionState: true`, or any run with `durable: true`, also writes it into the checkpoint (`sessionState.attentionSticky`) and restores it on resume, so a resumed run keeps its stubs instead of re-deciding its first turn from the ratio; a malformed or hand-edited frontier is dropped entry by entry, never fatal to a resume.
248
299
  - A compiler-on turn assembles from the default message groups (instructions, summaries, history, input, attachments, tool results) exactly like a `contextBudget` turn, so a custom `inputBuilder` is not consulted while the compiler is on.
249
300
  - Compaction timing is programmable per agent through `CompactionOptions.trigger` (`threshold_entries` | `input_ratio` | `custom`); omitting it keeps today's `thresholdEntries` gate. `assertCompactionTrigger(trigger)` validates a trigger independently of the compiler.
250
301
  - The gate is opt-in per agent/run; omit the option for current assembly bytes.
251
302
 
303
+ ### Durable folding
304
+
305
+ `durable: true` puts the fold state on disk (plan 086 T3), so a run that dies mid-investigation resumes
306
+ with the rows it had already folded instead of re-deciding them from the ratio.
307
+
308
+ ```ts
309
+ const agent = createAgent({
310
+ // ...
311
+ attentionCompiler: {
312
+ trigger: { kind: "run_input_ratio", ratio: 0.75 },
313
+ keepLast: 2,
314
+ durable: true,
315
+ },
316
+ runState: { checkpoints, definitionRevision: "1" }, // the write target; `persistSessionState` not required
317
+ toolResultFold: { summarize: hostSummarize }, // optional: bodies become durable too
318
+ });
319
+
320
+ // After a crash the worker resumes where the fold left off:
321
+ await resumeAgentRun(agent, { runId, sessionId }, { decision: "continue", expectedVersion }, { checkpoints, definitionRevision: "1" });
322
+ ```
323
+
324
+ - **One write per fold, never per turn.** The checkpoint is written after the turn's request is assembled and before the provider sees it, only on turns that added folded bodies. A turn that re-applies what the ledger already holds writes nothing.
325
+ - **The fold ledger.** Each folded body is stored once, keyed by tool call id (newest 64, 4 KiB each), and re-applied on every later turn: the host `summarize` runs once per row instead of once per turn, and a sticky row stays byte-identical for the provider cache. Bodies are already redacted and capped by the fold that produced them.
326
+ - **Independent of `persistSessionState`.** That option governs skill and tool-activation state. `durable` is its own opt-in for the fold ledger plus its sticky frontier (`sessionState.attentionFold` / `attentionSticky`), because a resumed run needs both: the frontier decides *what* stays folded, the ledger decides *what body* it was folded to.
327
+ - **Restore is fault-tolerant.** A malformed ledger shape starts from an empty ledger, and a malformed entry is dropped one by one — the row simply re-folds on the next over-gate turn. A hand-edited checkpoint never blocks a resume.
328
+ - **Sizing.** Off by default. On, it costs one checkpoint write per fold turn plus `bodies × (body ≤ maxSummaryBytes)` bytes in the run state (default cap: 64 bodies), and it makes the fold the run's first crash-recovery point when `checkpointPolicy` is `"decision"`.
329
+ - **Requires a durable run.** `durable: true` without `runState` (a checkpoint store) throws `AgentRunStateError` at run start, before the first provider turn. A run overlay may not set `durable`.
330
+ - **Still projection-only.** Durability changes *where the projection is remembered*, not what the store holds: the session store, observational-memory ledger, and semantic stores keep every original payload for recall, branching, and audit.
331
+
252
332
  ## Security and performance notes
253
333
 
254
334
  - Validation is synchronous with no provider I/O, and the returned handle plus `excludeTools` are frozen.
@@ -264,9 +344,10 @@ See [Compaction and retry policies](compaction-and-retry.md) for the trigger uni
264
344
 
265
345
  - [`assembleProviderInput`](input-and-prompt-assembly.md): the compose path the compiler pre-passes when enabled.
266
346
  - [`toolResultFold`](input-and-prompt-assembly.md): host summarizer that wins over the deterministic stub for eligible rows.
267
- - [`CompactionOptions`](compaction-and-retry.md): `trigger` is the host compact-when seam; `thresholdEntries` remains the default gate.
347
+ - [`CompactionOptions`](compaction-and-retry.md): `trigger` is the host compact-when seam; `thresholdEntries` remains the default gate. For fold state that outlives a crash, see [Durable folding](#durable-folding).
268
348
  - [`observational-memory`](compaction-observational-memory.md): host `shouldCompact` / trigger overrides `compactAfterTokens` for post-run compaction.
269
349
  - [`provider caching`](provider-caching.md): why mutations are monotonic and in-place.
270
350
  - [`AttentionReport` measurements](_evidence/phase74-attention-measurements.md): the hermetic fixture behind the savings, cache, resume, and truncation numbers.
351
+ - Example: [`examples/attention-budget-axes.ts`](../examples/attention-budget-axes.ts) — budget-capped long run where only the cumulative axis can open the gate.
271
352
  - [Memory fabric](memory-fabric.md): a context source whose blocks are measured like any other (`working-memory` / `semantic-memory` tags, no layer id).
272
353
  - [`thinking and reasoning`](thinking-and-reasoning.md): the `thinking` blocks the first stage strips.
@@ -610,7 +610,7 @@ Every configurable value is a positive safe integer (context may be zero); Prism
610
610
  - [Language intelligence](language-intelligence.md): optional host-activated LSP contract (`createLanguageIntelligence`) — symbols/definitions/references/diagnostics/hover/rename.
611
611
  - [Process sessions](process-sessions.md): optional managed long-running processes (`createProcessSessions`) — start/output/input/wait/signal/kill/release.
612
612
  - [Forge integration](forge-integration.md): optional GitHub adapter (`createGitHubForge`) — issue context, authenticated push, PR create/update, review comments, checks, bounded handoff reconcile; effect-store idempotency, no duplicate PRs/comments on retry, tokens never in argv/logs/events.
613
- - [Tools](tools.md): the host-owned tool harness — `createToolRegistry`, `dispatchToolCall`, filtering, and the `ToolDefinition` contract these factories satisfy.
613
+ - [Tools](tools.md): the host-owned tool harness — `createToolRegistry`, `dispatchToolCall`, filtering, `toolNarrowing` per-turn menus, and the `ToolDefinition` contract these factories satisfy.
614
614
  - [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
615
615
  - [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
616
616
  - [Tool conformance](tool-conformance.md): assertions for the tool-dispatch blocked-reason matrix these tools participate in.
@@ -219,6 +219,7 @@ A native Windows backend (Job objects / AppContainer) is tracked, not scheduled.
219
219
 
220
220
  ## Related APIs
221
221
 
222
+ - [Work sandbox](work-sandbox.md): work image + `createWorkComposition`; host injects `createDockerSandbox` (prism-work does not fork it)
222
223
  - [Coding agent tools](coding-agent-tools.md): durable plan/todo Markdown helpers and `state.coding` checkpoint metadata for restart/resume without a second runtime
223
224
  - [Hosted sandboxes](hosted-sandboxes.md): E2B pause/resume adapter, filesystem-only snapshots, reconnect by sandbox id
224
225
  - [Workflows](workflows.md): `runWorkflow` / `resumeWorkflow` / `startWorkflowBackground` composition for coding tasks
@@ -24,7 +24,6 @@ npm install @dietrichgebert/ponytail
24
24
  |---|---|---|
25
25
  | `@arnilo/prism-coding-tools/agent` | Core coding tools (read, write, edit, search, bash, git, diagnostics, check, ast-grep, lsp) | — |
26
26
  | `@arnilo/prism-coding-tools/security` | Sandbox execution adapters (Docker/OCI, native disposable sandbox, approval policies, egress proxy) | — |
27
- | `@arnilo/prism-coding-tools/document-reader` | Bounded PDF/DOCX literal-text extraction; optional host-selected Mistral OCR (native fetch, no SDK peer) | `pdf-parse`, `mammoth` |
28
27
  | `@arnilo/prism-coding-tools/openapi` | OpenAPI 3.x tool generator and executor with SSRF protection and parameter validation | — |
29
28
  | `@arnilo/prism-coding-tools/computer-use-linux` | Linux desktop observation and targeting tool bridge | — |
30
29
  | `@arnilo/prism-coding-tools/dev` | Loopback-only developer inspector, event timeline visualizer, and local replay server | — |
@@ -203,7 +203,7 @@ The default strategy does not call a provider. Hosts that need model-generated s
203
203
  - [Session stores and branching](session-stores-and-branching.md): branch entries, compaction entries, and `rebuildSessionContext()` behavior.
204
204
  - [Input and prompt assembly](input-and-prompt-assembly.md): compacted summaries become default summary messages for provider input.
205
205
  - [Agent/session runtime](agent-session-runtime.md): `session.compact()`, opt-in auto-compaction, `RunOptions.retry`, and `retry_scheduled` runtime behavior.
206
- - [Attention compiler](attention-compiler.md): resolves the same input cap and shrinks an over-ratio request before compaction is considered.
206
+ - [Attention compiler](attention-compiler.md): resolves the same input cap and shrinks an over-ratio request before compaction is considered; with `attention.compiler.durable` the fold ledger and frontier are checkpointed per fold, so a run that dies mid-investigation resumes already folded instead of replaying the pre-fold tail.
207
207
  - Example: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — task-boundary compact after each iteration.
208
208
  - [Middleware hooks](middleware-hooks.md): `compaction` and `retry` middleware payload timing.
209
209
  - [Contribution registries](contribution-registries.md): compaction strategy and retry policy contributions.
@@ -67,7 +67,7 @@ Worker limits are finite positive safe integers:
67
67
  | Recent-message window | — | 512 KiB | `renderRecentMessageWindow()` hard cap |
68
68
  | Recall page size | 20 | 100 | `retrieval.pageLimit` / recall tool `limit` |
69
69
 
70
- Direct `runObserver()` / `runReflector()` / `runDropper()` calls retain required `maxTurns` and accept the corresponding shorter worker fields (`maxToolCalls`, `maxResultBytes`, etc.). Named default/hard constants and `resolveMemoryWorkerLimits()` are exported.
70
+ Direct `runObserver()` / `runReflector()` / `runDropper()` calls retain required `maxTurns` and accept the corresponding shorter worker fields (`maxToolCalls`, `maxResultBytes`, etc.). Workers are tool-only: text, thinking, and done events are ignored; a turn with no `tool_call` succeeds as a no-op and records nothing. Named default/hard constants and `resolveMemoryWorkerLimits()` are exported.
71
71
 
72
72
  ## Outputs / response / events
73
73
 
@@ -87,6 +87,7 @@ Key exports:
87
87
  | `createFoldedMemoryDetails()` | Create JSON details for compaction `data.memory`. |
88
88
  | `renderObservationalMemory()` | Render reflections and observations into a prepared memory summary. |
89
89
  | `recallObservationalMemory()` | Recover source evidence for a known observation/reflection id from supplied current-branch entries. `invalidatedIds` withholds content (`reason: "revoked"`) without injecting derived text. |
90
+ | `listInvalidatedIds()` (`@arnilo/prism-memory`) | Read the ids one exact scope currently withholds (`corrected` stays) and pass them as `invalidatedIds`, so blocks that rest on a source revoked mid-turn go stale on the next build. Empty for stores without lineage invalidation. |
90
91
  | `recallObservationalMemoryBranchPage()` | Page eligible user/assistant/tool messages around a cursor entry id (`forward`/`backward`, optional `detail: summary|full`). |
91
92
  | `createMemoryId()` / `isMemoryId()` | Create/check 12-character ids. |
92
93
  | `resolveObservationalMemorySettings()` | Merge `observational-memory` settings with defaults and overrides. |
@@ -94,7 +95,7 @@ Key exports:
94
95
  | `createObservationalMemoryRuntime()` | Low-level explicit flush for advanced hosts or tests. |
95
96
  | `createObservationalMemoryCompactionStrategy()` | Render existing folded memory as a standard Prism compaction summary with `data.memory`. |
96
97
  | `createObservationalMemoryExtension()` | Inert extension helper that registers the strategy contribution unless disabled. |
97
- | `createRecallMemoryTool()` | Optional `recall` tool factory: exact id lookup or current-branch message paging via host-supplied entries. |
98
+ | `createRecallMemoryTool()` | Optional `recall` tool factory: exact id lookup (optionally merged with granted shared scopes) or current-branch message paging via host-supplied entries. |
98
99
  | `createMemoryStatusCommand()` / `createMemoryViewCommand()` | Optional `om:status` and `om:view` command factories. |
99
100
  | `createObservationalMemoryCommands()` | Convenience factory returning status and view commands. |
100
101
 
@@ -102,14 +103,40 @@ Pure utilities create no events, workers, tools, commands, credentials, or provi
102
103
 
103
104
  ### Work-scope index (opt-in)
104
105
 
105
- `WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior.
106
+ `WorkScope` is a host-named, append-only index over one observational-memory ledger. Without `om.scope.*` entries, the map has only its implicit `session` root, context renders the existing active pool, and the dropper keeps its existing behavior. Shared work scopes extend that index across sessions under explicit grants — see "Shared work scopes (opt-in)" below.
106
107
 
107
- Use `createWorkScopeController({ session, appendEntry, secrets? })` to `open`, `close`, `enter`, `leave`, `bind`, or `unbind` scopes. Scope ids are host-defined (`[A-Za-z0-9._:/-]{1,128}`, no `..`); there are caps of 256 scopes, depth/stack 8, 4,096 binds per scope, and 512 characters for labels or kinds. Invalid ids, missing/closed parents, duplicate scopes, unknown record ids, and ownership mismatch fail closed. Labels and kinds receive the same secret redaction as observational-memory text.
108
+ Use `createWorkScopeController({ session, appendEntry, secrets? })` to `open`, `close`, `enter`, `leave`, `bind`, `unbind`, `grant`, or `revoke` scopes. Scope ids are host-defined (`[A-Za-z0-9._:/-]{1,128}`, no `..`); there are caps of 256 scopes, depth/stack 8, 4,096 binds and 1,024 principals per scope, and 512 characters for labels or kinds. Invalid ids, missing/closed parents, duplicate scopes, unknown record ids, reserved/closed grant targets, and ownership mismatch fail closed. Labels and kinds receive the same secret redaction as observational-memory text.
108
109
 
109
110
  `projectWorkMemory(ledger, map, { from, include, closed?, kinds? })` returns a filtered observation/reflection view plus outline. `include` is `self`, `self+ancestors`, `self+descendants`, or `lineage`; `closed: "hide"` is the default, except closed ancestors of `from` remain available. Default attached context uses the current leaf with `self+ancestors`, rendering Scope Outline, Reflections, then Observations. The compaction summary — the layer the next run's pack starts from — renders the same projection, so the full ledger never rides into the prefix; the folded payload keeps every observation, so entering another scope can still surface what that summary hid. `recallObservationalMemory()` still reads the complete current branch by exact id.
110
111
 
111
112
  After a flush records new observations or reflections, it binds those ids once to the current leaf scope only. A host promotes relevant memory explicitly by binding it to an ancestor; a reflection whose bind sits on a **closed** scope can also graduate into durable semantic memory through the fabric's `remember({ kind: "fact" | "procedure", reflectionId })`. While any host scope exists, the runtime skips the observation dropper; the folded-payload byte cap remains a storage safety cap, not working-set garbage collection. `withWorkScope(controller, spec, fn)` opens `spec` if needed, enters it, runs `fn`, and leaves in `finally`; it never closes a scope. This index does not provide resource-scoped observational memory or budget-based dropping as a working-set mechanism.
112
113
 
114
+ ### Shared work scopes (opt-in)
115
+
116
+ A shared work scope lets several sessions contribute to and read one scope under explicit owner grants. Declare it per participant in `attach()`; see `examples/shared-work-scope.ts` for a runnable grant → contribute → recall → revoke demo:
117
+
118
+ ```ts
119
+ const attached = om.attach(session, {
120
+ appendEntry: (entry, options) => store.append(entry, options),
121
+ sharedScopes: { "build-42": { ownerSessionId: "session-...", entries: (sessionId) => store.list(sessionId) } },
122
+ onScopeAccess: (event) => audit.info("om.scope.access", event),
123
+ });
124
+ ```
125
+
126
+ Requirements, all fail-closed:
127
+
128
+ - The participant opens the scope in its own branch (`open`/`enter`) and binds its own observations/reflections to it. Only ids bound to that exact scope id are shared; memory bound to an ancestor, descendant, or other scope stays private.
129
+ - The owner branch carries `om.scope.granted` / `om.scope.revoked` records (`controller.grant(scopeId, principalIds)` / `revoke`) and is the only grant authority; grants in any other branch are inert. A grant is symmetric read+write — use separate scopes for asymmetric visibility.
130
+ - The host `entries(sessionId)` callback is the store/tenant boundary: the package checks grants, it cannot verify another branch's tenant. Keep the callback inside one `OwnershipScope`.
131
+ - Absent, unknown, revoked, unreachable, or not-opened-locally scope state denies the read and reports `onScopeAccess({ granted: false, reason })`. `onScopeAccess` fires for every decision, granted or denied.
132
+ - Revocation lands on the next read: each resolve re-reads the owner branch and re-folds the grant map. The local folded payload never contains foreign observations, so revocation also holds across local compaction.
133
+
134
+ `resolveSharedScopes({ scopes, principalId, map, onAccess? })` reads the owner branch for grants, then the owner and every granted branch, folds each branch separately, and unions the id-keyed results (`mergeObservationalMemoryLedgers`). Raw entry lists are never concatenated across branches — coverage cursors and projection boundaries are positional per branch. Bound memory is merged into the context blocks, `recallObservationalMemory`, the `recall` tool (exact-id only; branch paging stays current-branch), and `om:view`; `om:status` counts stay session-local.
135
+
136
+ Rendering still follows the work-scope projection: the reader needs the shared scope in its current leaf lineage (`enter`, or a host that keeps it entered) for the context block to include it. Recall by exact id does not depend on the leaf. The compaction strategy and its folded payload stay local, so a shared observation re-enters context from the provider rather than from the summary.
137
+
138
+ Cost: one branch read and fold per participating branch per context resolve (and per `recall` call that resolves shared scopes) — not per observation. Cache per flush only if profiling demands it.
139
+
113
140
  ### Compact-when override
114
141
 
115
142
  `createObservationalMemory()` accepts a compact-when gate beside the settings: `trigger` (the same union `CompactionOptions.trigger` uses) or the `shouldCompact(context)` shorthand. When either is set it **replaces** `context.compactAfterTokens`; omitted, the token gate is unchanged.
@@ -220,7 +247,7 @@ The runtime requires host-supplied `session`, an `appendEntry` callback bound to
220
247
 
221
248
  ## Cross-session / delegation-tree recall (opt-in pattern)
222
249
 
223
- Default is per-session: `attach()` + `appendEntry` bind one store/branch, and `recallObservationalMemory(entries, id)` / `createRecallMemoryTool({ getEntries })` see only the entries the host passes for that session. Supervisor children therefore produce observations the parent cannot recall. That is acceptable for v1 — the parent transcript already contains `delegate()` results, so parent OM covers milestones. There is no package primitive for a shared workspace scope (a namespaced multi-tenant store key is out of scope).
250
+ Default is per-session: `attach()` + `appendEntry` bind one store/branch, and `recallObservationalMemory(entries, id)` / `createRecallMemoryTool({ getEntries })` see only the entries the host passes for that session. Supervisor children therefore produce observations the parent cannot recall. That is acceptable for v1 — the parent transcript already contains `delegate()` results, so parent OM covers milestones. When the host can read the participating branches, use a shared work scope instead (above); the funnel below remains the option when it cannot (a namespaced multi-tenant store key is still out of scope).
224
251
 
225
252
  Hosts that need parent recall of child *source* work compose it themselves: wrap the shared `SessionStore.append` so eligible child messages (`isEligibleObservationSourceEntry`) are copied onto a workspace (or parent) session with a **new entry id** and that session's `sessionId`/`parentId`. Parent OM then observes those copies and mints **new** observation ids. Child OM, if attached, stays on the child session with its own ids.
226
253
 
@@ -254,7 +281,7 @@ Wire the wrapped store into both the parent session and each supervisor child fa
254
281
 
255
282
  Rules that keep exact-id recall unambiguous:
256
283
 
257
- - Recall always takes **one** branch (`session.entries()` / `getEntries(sessionId)`). Never concatenate parent + child lists into one `recallObservationalMemory()` call.
284
+ - Recall always takes **one** branch (`session.entries()` / `getEntries(sessionId)`). Never concatenate parent + child lists into one `recallObservationalMemory()` call. Shared work scopes are the supported exception: they union per-branch folded ledgers (id-keyed), never raw entry lists.
258
285
  - Copies mint a new `entry.id`. `createMemorySessionStore` rejects duplicate ids globally; JSONL/DB adapters do too.
259
286
  - Do **not** rewrite the child's OM `appendEntry` onto the workspace session. After each memory append the runtime checks the entry is visible at the **child** leaf and fails closed on a session/store mismatch. Funnel messages; let parent OM observe them.
260
287
  - Do **not** copy `om.*` custom entries across. Their `sourceEntryIds` point at the origin session and would dangle on the workspace branch.
@@ -262,7 +289,7 @@ Rules that keep exact-id recall unambiguous:
262
289
 
263
290
  Cost: the workspace branch grows with every funneled child message; parent `compactAfterTokens` / observation-pool caps still apply but fire sooner. Keep the per-session default unless parent recall of child sources is required.
264
291
 
265
- Ownership: funnel only within the `OwnershipScope` already on the parent agent/store. Child factories receive that ownership from the supervisor; do not share a store across tenants or identities. Observations never leave the store the host scoped.
292
+ Ownership: funnel only within the `OwnershipScope` already on the parent agent/store. Child factories receive that ownership from the supervisor; do not share a store across tenants or identities. Observations never leave the store the host scoped — the same rule applies to shared work-scope grants.
266
293
 
267
294
  ## Security and performance notes
268
295
 
@@ -0,0 +1,116 @@
1
+ # Connected apps
2
+
3
+ ## What it does
4
+
5
+ `createConnectedAppSession()` groups host-selected MCP bridges under one verified identity. It exposes only prefixed `ToolDefinition`s selected by the host and leaves transport construction, OAuth, credentials, and remote effect classification with that host.
6
+
7
+ ## When to use it
8
+
9
+ Use connected apps when one agent needs a small, identity-scoped set of SaaS or internal MCP servers. Use typed [work tools](work-tools.md) instead for high-trust Google Workspace or Microsoft 365 actions that require Prism's draft/approve lifecycle.
10
+
11
+ ## Inputs / request
12
+
13
+ | Input | Required | Contract |
14
+ | --- | --- | --- |
15
+ | `identity` | yes | Active, verified `AgentIdentity` bound to every connection. A `bind()` identity, if supplied, must match tenant, account, user, and principal. |
16
+ | `select` | yes | Host admission callback. `false` denies before any MCP connection. |
17
+ | `effect` | no | Shared `McpToolEffectPolicy`. Omit it to retain MCP's `external_mutation` / `unsupported` default. |
18
+ | `connect` | no | Test seam. Production uses `connectMcpTools`. |
19
+ | `maxApps` | no | Maximum bindings; defaults to 8 and has a hard cap of 32. |
20
+ | `bind({ appId, serverId, transport, allowTools, effect })` | yes | `appId` and `serverId` are unique session identifiers. `transport` is already host-built. `allowTools` is an exact remote-name allowlist; per-binding `effect` overrides the shared policy. |
21
+
22
+ ## Outputs / response / events
23
+
24
+ `bind()` connects one admitted bridge. `tools()` returns its prefixed tools without re-listing. `refresh()` re-lists every bound bridge. `list()` returns only `appId`, `serverId`, and visible prefixed tool names. `unbind()` and `close()` close their bridges.
25
+
26
+ ## Host composition inspection
27
+
28
+ Pass only `apps.list()` identifiers into `inspectHostComposition()`; inspection never connects, refreshes, or receives a transport. A business host with connected apps must provide a verified identity.
29
+
30
+ ```ts
31
+ const bindings = apps.list();
32
+ const report = inspectHostComposition({
33
+ profile: "business",
34
+ agent,
35
+ connectedApps: {
36
+ appIds: bindings.map(({ appId }) => appId),
37
+ serverIds: bindings.map(({ serverId }) => serverId),
38
+ },
39
+ });
40
+ ```
41
+
42
+ ## Request/response example
43
+
44
+ ```json
45
+ {
46
+ "binding": {
47
+ "appId": "slack",
48
+ "serverId": "slack",
49
+ "transport": { "type": "stdio", "command": "/usr/bin/slack-mcp", "args": ["mcp"] },
50
+ "allowTools": ["list_channels", "post_message"]
51
+ },
52
+ "list": [{ "appId": "slack", "serverId": "slack", "tools": ["mcp:slack:list_channels", "mcp:slack:post_message"] }]
53
+ }
54
+ ```
55
+
56
+ ## Implementation example
57
+
58
+ ```ts
59
+ import { createToolRegistry, type AgentIdentity } from "@arnilo/prism";
60
+ import { createConnectedAppSession } from "@arnilo/prism-mcp";
61
+
62
+ const identity: AgentIdentity = {
63
+ tenantId: "tenant-a",
64
+ userId: "user-a",
65
+ principal: { kind: "user", id: "user-a" },
66
+ scopes: ["tools:execute"],
67
+ issuedAt: new Date().toISOString(),
68
+ verified: true,
69
+ };
70
+ const apps = createConnectedAppSession({
71
+ identity,
72
+ select: ({ transport }) => transport.type === "stdio" && transport.command === "/usr/bin/slack-mcp",
73
+ effect: ({ remoteName }) =>
74
+ remoteName.startsWith("list_") ? { kind: "none", idempotency: "none" } : undefined,
75
+ });
76
+ await apps.bind({
77
+ appId: "slack",
78
+ serverId: "slack",
79
+ transport: { type: "stdio", command: "/usr/bin/slack-mcp", args: ["mcp"] },
80
+ allowTools: ["list_channels", "post_message"],
81
+ });
82
+
83
+ const registry = createToolRegistry({ duplicate: "error" });
84
+ for (const tool of apps.tools()) registry.register(tool);
85
+ ```
86
+
87
+ ## Slack MCP wrap example
88
+
89
+ [`examples/connected-slack-mcp.ts`](../examples/connected-slack-mcp.ts) is a network-free template: a mock bridge exposes read and write Slack names, an exact `allowTools` list exposes only the intended tools, and the host policy marks `list_*`, `get_*`, and `search_*` as observations. `post_*`, `update_*`, and `delete_*` remain the MCP external-mutation default.
90
+
91
+ For a real Slack server, the host `select` callback must admit its exact stdio command or HTTP origin. Build credentials into stdio `env` or MCP OAuth before `bind()`; never place tokens in model context.
92
+
93
+ ## Open Connector sidecar (example only)
94
+
95
+ [`examples/open-connector-sidecar/`](../examples/open-connector-sidecar/README.md) shows the opposite shape: Prism as the host for a sibling connector gateway over loopback MCP. The recipe pins an immutable `ghcr.io/oomol-lab/open-connector` release tag (never `main`/`tip`/`latest`), binds port `3000` on `127.0.0.1`, and keeps runtime tokens host-supplied.
96
+
97
+ Admission stays with the host: `select` must admit the exact loopback origin with `allowLoopbackHttp: true`, and an exact `allowTools` list (`search_actions`, `get_action_guide`, `execute_action`) keeps the provider catalog and connection listings off the model. Reads stay observations; `execute_action` is intentionally left unclassified and therefore remains `external_mutation` / `unsupported`. MCP `execute_action` accepts no `Idempotency-Key` — use HTTP `POST /v1/actions/:actionId` for retry-safe writes, or keep writes in [work tools](work-tools.md).
98
+
99
+ Identity mapping is host glue: Open Connector has no Prism identity, so the host resolves `connectionName`/`x-oo-connector-alias` from the verified `AgentIdentity` and issues one runtime token per identity. OC provider egress (including `skipDnsValidation` executors and `OOMOL_CONNECT_ALLOWED_PROXIES`) stays inside Open Connector; Prism's `pinnedFetch` policy cannot be layered over it.
100
+
101
+ No workspace package depends on Open Connector, no OC source is vendored, and `examples/open-connector-sidecar.ts` proves the admission and effect behavior network-free.
102
+
103
+ ## Extension and configuration notes
104
+
105
+ Build stdio `env` and Streamable HTTP `requestInit.headers` in host code before `bind()`. Use `createMcpOAuthTransport()` when the host chooses MCP OAuth. The session does not discover catalogs, construct commands, resolve credentials, or add a second OAuth implementation.
106
+
107
+ ## Security and performance notes
108
+
109
+ `select` is mandatory and deny-by-default. The session never infers an effect from remote descriptions or annotations; unclassified tools stay external mutations with unsupported idempotency. `allowTools` is an allowlist. `list()` excludes transports, headers, environment, and tokens. Binding performs one MCP connect; `tools()` uses cached bridge definitions until explicit `refresh()`.
110
+
111
+ ## Related APIs
112
+
113
+ - [MCP client bridge and server exposure](mcp-tools.md): underlying MCP transports, bridge limits, OAuth, and tool mapping.
114
+ - [Agent identity](agent-identity.md): verified identity lifecycle and delegation boundaries.
115
+ - [Recoverable tool effects](tool-effects.md): effect declarations and mutation recovery semantics.
116
+ - [Work tools](work-tools.md): typed high-trust M365 and Google Workspace actions.
@@ -211,6 +211,19 @@ Mode slices and skill bodies are independent: the injector can add `PONYTAIL MOD
211
211
 
212
212
  Pure validation without the tool: `resolveSkillLoad({ registry, name, tools, loaded, activeSkillNames })`.
213
213
 
214
+ ### Bundled work skills (`docx`, `xlsx`, `powerpoint`, `pdf`)
215
+
216
+ `@arnilo/prism-work/skills` vendors four MIT Hermes productivity skills. `loadWorkSkills()` reads the committed `SKILL.md` files (64 KiB cap) and overlays Prism `toolNames` in TypeScript — vendored markdown is never edited.
217
+
218
+ ```ts
219
+ import { createSkillRegistry } from "@arnilo/prism";
220
+ import { loadWorkSkills } from "@arnilo/prism-work/skills";
221
+
222
+ const registry = createSkillRegistry(loadWorkSkills(), { duplicate: "error" });
223
+ ```
224
+
225
+ Scripts run only via work-sandbox `execFile` (argv, no shell). See [Work sandbox](work-sandbox.md).
226
+
214
227
  ### Context budget priority and skill demotion
215
228
 
216
229
  When `assembleProviderInput` runs with `contextBudget`, `applyContextBudget` evicts droppable sections in layout order. Within `context` blocks and skills, victims sort by ascending `ContextBlock.priority` (missing = **0**), then LIFO within the same priority.
package/docs/core.md CHANGED
@@ -41,7 +41,6 @@ Every peer below is optional and fails closed at first use; the [optional peer d
41
41
  | `@arnilo/prism-core/governance/observability` | OpenTelemetry instrumentation and event tracing | `@opentelemetry/api` |
42
42
  | `@arnilo/prism-core/credentials/node` | Keyring-backed encrypted credential store, scrypt envelope encryption, OAuth2 PKCE providers, and OIDC identity verification | `@napi-rs/keyring` (bundled) |
43
43
  | `@arnilo/prism-core/enterprise/postgres` | Unified multi-tenant enterprise PostgreSQL state (approvals, evaluations, model-router, policy, tool effects, work idempotency) | `pg` |
44
- | `@arnilo/prism-core/integrations/work` | Microsoft 365 and Google Workspace CLI tool adapters with approval gates and idempotency | — |
45
44
  | `@arnilo/prism-core/validation/json-schema` | Ajv-backed JSON Schema tool argument validation | `ajv` (bundled) |
46
45
 
47
46
  ## Usage Examples
@@ -85,3 +84,4 @@ const validator = createJsonSchemaToolArgumentValidator();
85
84
  - Subpaths never load database drivers (`pg`, `better-sqlite3`) unless the specific database subpath is imported.
86
85
  - All database and network drivers fail closed with clear actionable error messages when peers are omitted.
87
86
  - Root `@arnilo/prism` remains dependency-free contracts and CLI runner.
87
+ - Messaging channels are `@arnilo/prism-channels` (`/telegram`, `/signal`), not a `@arnilo/prism-core` subpath.
package/docs/diagrams.md CHANGED
@@ -1,8 +1,8 @@
1
- # Diagramming, draw.io embed client, and mxGraph XML validation (`@arnilo/prism-office/diagrams`)
1
+ # Diagramming, draw.io embed client, and mxGraph XML validation (`@arnilo/prism-work/diagrams`)
2
2
 
3
3
  ## What it does
4
4
 
5
- The `@arnilo/prism-office/diagrams` package provides an origin-enforced draw.io / diagrams.net iframe embed client, XXE-safe mxGraph XML validation, and byte-stable deterministic XML canonicalization for content hashing and visual artifact workflows in Prism applications and agent runtimes.
5
+ The `@arnilo/prism-work/diagrams` package provides an origin-enforced draw.io / diagrams.net iframe embed client, XXE-safe mxGraph XML validation, and byte-stable deterministic XML canonicalization for content hashing and visual artifact workflows in Prism applications and agent runtimes.
6
6
 
7
7
  ### Core Capabilities
8
8
 
@@ -17,7 +17,7 @@ The `@arnilo/prism-office/diagrams` package provides an origin-enforced draw.io
17
17
 
18
18
  ## When to use it
19
19
 
20
- Use `@arnilo/prism-office/diagrams` when applications, host workspaces, or autonomous agents need to:
20
+ Use `@arnilo/prism-work/diagrams` when applications, host workspaces, or autonomous agents need to:
21
21
  1. Embed an interactive, self-hosted draw.io / diagrams.net editor inside a web or Electron iframe with strictly enforced cross-origin security.
22
22
  2. Coordinate diagram editing lifecycles (`init` handshake, `load`, `save`, `autosave`, `merge`, and `export`).
23
23
  3. Execute save-with-preview workflows generating SVG (`xmlsvg`) or PNG (`xmlpng`) visual snapshots from the active editor session.
@@ -148,7 +148,7 @@ Outbound host-to-editor action postMessage:
148
148
  ## Implementation example
149
149
 
150
150
  ```ts
151
- import { createDrawioEmbed, validateDrawioXml, canonicalizeDrawioXml } from "@arnilo/prism-office/diagrams";
151
+ import { createDrawioEmbed, validateDrawioXml, canonicalizeDrawioXml } from "@arnilo/prism-work/diagrams";
152
152
 
153
153
  // 1. Initialize embed client with strict origin binding
154
154
  const embed = createDrawioEmbed({
@@ -240,8 +240,8 @@ const summary = validateDrawioXml(xml, {
240
240
 
241
241
  ## Related APIs
242
242
 
243
- - [`@arnilo/prism-office/documents`](./documents.md): Specification-compliant OpenXML document generation and preview rendering for DOCX, XLSX, and PPTX.
244
- - [`@arnilo/prism-office/sheets`](./sheets.md): Spreadsheet and CSV parsing engine with strict financial decimal safety guarantees.
243
+ - [`@arnilo/prism-work/documents`](./documents.md): Specification-compliant OpenXML document generation and preview rendering for DOCX, XLSX, and PPTX.
244
+ - [`@arnilo/prism-work/sheets`](./sheets.md): Spreadsheet and CSV parsing engine with strict financial decimal safety guarantees.
245
245
  - [`@arnilo/prism-web-tools/browser`](./browser-automation.md): Browser automation tools and quarantine lifecycle.
246
246
  - [`@arnilo/prism-ag-ui`](./ag-ui.md): Agent-User Interface projection and timeline components.
247
247
  - [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
@@ -1,14 +1,14 @@
1
- # Document reader (`@arnilo/prism-coding-tools/document-reader`)
1
+ # Document reader (`@arnilo/prism-work/document-reader`)
2
2
 
3
3
  > **Optional peer install:** `pdf-parse` and/or `mammoth` — see [Optional peer dependencies](peer-dependencies.md). OCR uses **native fetch**, not an SDK peer.
4
4
 
5
5
  ## What it does
6
6
 
7
- Optional bounded literal-text extraction for PDF and DOCX files, consumed by the coding `read` tool (plan 018 closeout `doc-reader`, 0.1.6). `createDocumentReader()` returns a `DocumentReader` that the host wires into `createReadTool(cwd, { documentReader })`; the read tool then extracts text from supported documents instead of falling back to the raw text page. Scanned PDFs/images need a **host-selected** `createMistralOcrParser({ apiKey })` passed in `parsers` — default wiring never calls an external OCR service.
7
+ Optional bounded literal-text extraction for PDF, DOCX, XLSX, and PPTX files, consumed by the coding `read` tool. `createDocumentReader()` returns a `DocumentReader` that the host wires into `createReadTool(cwd, { documentReader })`; the read tool then extracts text from supported documents instead of falling back to the raw text page. XLSX renders as TSV and PPTX as a slide title/bullet outline through the in-package OOXML parser. Scanned PDFs/images need a **host-selected** `createMistralOcrParser({ apiKey })` passed in `parsers` — default wiring never calls an external OCR service.
8
8
 
9
9
  ## When to use it
10
10
 
11
- Use when coding agents must read PDF/Office files (specs, requirements docs, reports) as literal text. Do **not** use it when embedded content execution, macro evaluation, or external resource fetching is required — this adapter never does any of those by construction, and the optional peer parsers (`pdf-parse`, `mammoth`) are the only parsing code involved. Docker-less hosts that need document reads pair this with the network-free native sandbox backend (`@arnilo/prism-coding-tools/security` `createNativeSandbox`) for the surrounding tool execution.
11
+ Use when coding agents must read PDF/Office files (specs, requirements docs, reports) as literal text. Do **not** use it when embedded content execution, macro evaluation, or external resource fetching is required — this adapter never does any of those by construction. PDF/DOCX use optional peer parsers (`pdf-parse`, `mammoth`); XLSX/PPTX use the bounded Prism OOXML parser. Docker-less hosts that need document reads pair this with the network-free native sandbox backend (`@arnilo/prism-coding-tools/security` `createNativeSandbox`) for the surrounding tool execution.
12
12
 
13
13
  Activation is explicit: no file-extension sniffing anywhere enables parsing. Absent `documentReader` option = exactly the 0.1.5 read behavior.
14
14
 
@@ -21,10 +21,10 @@ Activation is explicit: no file-extension sniffing anywhere enables parsing. Abs
21
21
  | `maxBytes` | Hard input size cap; oversize files refuse before loading | 32 MiB | 512 MiB |
22
22
  | `maxPages` | Page cap for formats that report pages; over-page documents refuse | 1000 | 10 000 |
23
23
  | `maxTextBytes` | Extracted-literal-text cap; over-cap results truncate (`truncatedBy: "bytes"`) | 2 MiB | 64 MiB |
24
- | `parsers` | Host-selected `DocumentParser[]`; default wiring loads the optional peers | `[pdf, docx]` | — |
24
+ | `parsers` | Host-selected `DocumentParser[]`; default wiring includes peer-backed PDF/DOCX plus OOXML XLSX/PPTX | `[pdf, docx, xlsx, pptx]` | — |
25
25
  | `redactor` | Optional `SecretRedactor` applied to extracted text at the adapter boundary | none | — |
26
26
 
27
- Format gating is magic-byte based: PDF header (`%PDF-`); DOCX zip container + `word/document.xml` part marker. Unsupported buffers return `null` and the read falls through to its text path.
27
+ Format gating is magic-byte based: PDF header (`%PDF-`); DOCX/XLSX/PPTX zip container plus `word/document.xml`, `xl/workbook.xml`, or `ppt/presentation.xml` part marker. Unsupported buffers return `null` and the read falls through to its text path.
28
28
 
29
29
  ## Outputs / response / events
30
30
 
@@ -36,7 +36,7 @@ Errors: `DocumentReaderError` with code `ERR_PRISM_DOCUMENT_READER` for missing
36
36
 
37
37
  ```ts
38
38
  import { createReadTool } from "@arnilo/prism-coding-tools/agent";
39
- import { createDocumentReader } from "@arnilo/prism-coding-tools/document-reader";
39
+ import { createDocumentReader } from "@arnilo/prism-work/document-reader";
40
40
 
41
41
  const documentReader = await createDocumentReader({
42
42
  maxBytes: 32 * 1024 * 1024,
@@ -51,7 +51,7 @@ A `read` of `spec.pdf` yields text content extracted from the PDF (up to 2 MiB o
51
51
  ## Implementation example
52
52
 
53
53
  ```ts
54
- import { createDocumentReader, createPdfParser, type DocumentParser } from "@arnilo/prism-coding-tools/document-reader";
54
+ import { createDocumentReader, createPdfParser, type DocumentParser } from "@arnilo/prism-work/document-reader";
55
55
 
56
56
  // Host-selected parser wiring: swap in a different PDF backend without touching bounds.
57
57
  const myPdfParser: DocumentParser = {
@@ -64,7 +64,7 @@ const myPdfParser: DocumentParser = {
64
64
  };
65
65
  const reader = await createDocumentReader({ parsers: [myPdfParser, await createPdfParser()] });
66
66
 
67
- import { createMistralOcrParser } from "@arnilo/prism-coding-tools/document-reader";
67
+ import { createMistralOcrParser } from "@arnilo/prism-work/document-reader";
68
68
  const ocr = createMistralOcrParser({
69
69
  apiKey: hostKey, // never read from process.env
70
70
  recordUsage: (u) => router.recordUsage({ /* Task 7 */ tokens: 0, costUsd: hostPrice(u) }),
@@ -74,7 +74,7 @@ const scanned = await createDocumentReader({ parsers: [ocr] }); // not in the de
74
74
 
75
75
  ## Extension and configuration notes
76
76
 
77
- - Default parser wiring uses the optional peer dependencies `pdf-parse` (PDF) and `mammoth` (DOCX raw text). Both are declared optional (`peerDependenciesMeta`); `createDocumentReader` fails closed with a documented error at creation when a selected format's peer is absent — never at read time. Hosts pin parser versions (their CVE surface is the host's responsibility; parser advisory is reviewed at ship time).
77
+ - Default parser wiring uses optional peer dependencies `pdf-parse` (PDF) and `mammoth` (DOCX raw text), plus in-package XLSX/PPTX parsing. Missing selected peers fail closed at creation — never at read time. XLSX sheet count and PPTX slide count share `maxPages`; text remains capped at `maxTextBytes`.
78
78
  - `createMistralOcrParser` is **not** a default parser. It POSTs `https://api.mistral.ai/v1/ocr` (`mistral-ocr-latest`) with inline `data:` URLs (`include_image_base64: false`). No Files API upload, so no remote cleanup. Host `documentUrl` values pass `assertSsrfAllowedUrl`. Extracted markdown is untrusted. Caps: 8 MiB / 32 pages / 60 s / 1 in-flight by default (hard 50 MiB / 10 000 pages / 180 s / 4). Pass `recordUsage` to admit cost through Task 7 accounting. `baseUrl` selects residency.
79
79
  - DOCX has no page concept in raw text: `pages` is always `1` and the page cap applies to PDF only; the text cap governs DOCX output.
80
80
  - The read tool re-checks `maxTextBytes` on results (parity with its text-page bounds check) and refuses reader output beyond it.