@arnilo/prism 0.0.4 → 0.0.5

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 (70) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +34 -10
  3. package/dist/agents.js +146 -19
  4. package/dist/cli-init.d.ts +41 -0
  5. package/dist/cli-init.js +390 -0
  6. package/dist/cli-runner.d.ts +7 -1
  7. package/dist/cli-runner.js +13 -1
  8. package/dist/content.d.ts +19 -0
  9. package/dist/content.js +197 -69
  10. package/dist/contracts.d.ts +94 -9
  11. package/dist/contracts.js +8 -0
  12. package/dist/feedback.d.ts +48 -0
  13. package/dist/feedback.js +230 -0
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.js +4 -3
  16. package/dist/providers/media.d.ts +3 -1
  17. package/dist/providers/media.js +11 -1
  18. package/dist/testing/feedback.d.ts +6 -0
  19. package/dist/testing/feedback.js +37 -0
  20. package/dist/testing/persistence-schema.d.ts +3 -3
  21. package/dist/testing/persistence-schema.js +32 -2
  22. package/dist/testing/run-ledger-conformance.js +7 -1
  23. package/docs/a2a.md +73 -0
  24. package/docs/agent-events.md +4 -6
  25. package/docs/agent-loops.md +1 -1
  26. package/docs/agent-session-runtime.md +14 -16
  27. package/docs/cli-rpc.md +35 -7
  28. package/docs/coding-agent-tools.md +2 -2
  29. package/docs/coding-security.md +7 -3
  30. package/docs/compaction-observational-memory.md +2 -0
  31. package/docs/context-and-skills.md +1 -0
  32. package/docs/credentials-and-redaction.md +2 -2
  33. package/docs/database-persistence.md +9 -6
  34. package/docs/evaluations.md +122 -0
  35. package/docs/extensions.md +2 -2
  36. package/docs/host-security.md +20 -3
  37. package/docs/index.md +29 -17
  38. package/docs/mcp-tools.md +49 -4
  39. package/docs/migration.md +33 -3
  40. package/docs/multimodal-content.md +14 -6
  41. package/docs/observability.md +14 -6
  42. package/docs/performance.md +209 -0
  43. package/docs/postgres-persistence.md +6 -4
  44. package/docs/provider-conformance.md +1 -0
  45. package/docs/provider-packages.md +2 -0
  46. package/docs/providers/ai-sdk.md +113 -0
  47. package/docs/public-contracts.md +6 -5
  48. package/docs/rag.md +113 -0
  49. package/docs/release-and-install.md +100 -77
  50. package/docs/review-coverage-2026-07-15.md +193 -0
  51. package/docs/runs-and-usage.md +41 -4
  52. package/docs/server.md +139 -0
  53. package/docs/settings-auth-trust-security.md +5 -5
  54. package/docs/sqlite-persistence.md +4 -3
  55. package/docs/supervisors.md +71 -0
  56. package/docs/workflow-orchestration-primitives.md +19 -3
  57. package/docs/workflows.md +97 -23
  58. package/docs/working-and-semantic-memory.md +169 -0
  59. package/package.json +12 -2
  60. package/templates/init/README.md.tmpl +28 -0
  61. package/templates/init/env.example.tmpl +1 -0
  62. package/templates/init/gitignore.tmpl +11 -0
  63. package/templates/init/optional/evals-example.ts.tmpl +17 -0
  64. package/templates/init/optional/workflows-example.ts.tmpl +27 -0
  65. package/templates/init/package.json.tmpl +22 -0
  66. package/templates/init/providers.json +76 -0
  67. package/templates/init/src/agent.ts.tmpl +10 -0
  68. package/templates/init/src/index.ts.tmpl +12 -0
  69. package/templates/init/src/tests/agent.test.ts.tmpl +24 -0
  70. package/templates/init/tsconfig.json.tmpl +15 -0
@@ -8,6 +8,10 @@ Interactive TUI (**C-012**) is **out of scope** for Plan 057 and deferred. Workf
8
8
 
9
9
  **Final Task 7 architecture (2026-07-14):** DAG/coordinator semantics remain package-local. Reusable versioned checkpoints, atomic leases, and bounded async event fan-in live in core as `CheckpointStore`, `LeaseStore`, and `EventMultiplexer`. Workflow adapters are thin domain facades; lease fencing plus checkpoint CAS prevents stale-worker commits.
10
10
 
11
+ **Phase 8 addendum (2026-07-15):** `@arnilo/prism-workflows` extends the same checkpoint JSON/state machine with `suspended` and terminal `denied` statuses, `suspend()`, persisted suspension/resume records, and `workflow_suspended` / `workflow_resumed` events. An approved resume requires the displayed checkpoint `expectedVersion`; the first checkpoint write CAS-claims it before node execution. Suspended runs are absent from coordinator `queued`/`running` polls, so no worker or polling loop remains active. No SQL migration or second approval store is required.
12
+
13
+ **Phase 11 addendum (2026-07-16):** schedules use a separate generic checkpoint namespace plus per-fire `LeaseStore` claims and deterministic queued-run IDs; SQLite/PostgreSQL need no workflow-specific table or migration. Background execution remains `enqueueWorkflow` + the existing coordinator. Nested workflow nodes call the same runner with inherited policy/ownership/checkpoint/event seams. Shared JSON state is validated, redacted, byte/history bounded, and checkpointed by version. Replay creates a new checkpoint with immutable source lineage and copied terminal evidence; approval-bearing prior paths cannot be copied.
14
+
11
15
  ## When to use it
12
16
 
13
17
  - **Workflow package authors** should start here, then follow [Agent/session runtime](agent-session-runtime.md), [Agent loops](agent-loops.md), [Runs and usage ledger](runs-and-usage.md), [CLI/RPC](cli-rpc.md), and [Database persistence](database-persistence.md).
@@ -121,7 +125,7 @@ Static review of `src/agents.ts`, `src/agent-loops.ts`, `src/rpc.ts`, `src/cli-r
121
125
  | Event fan-in | Generic core multiplexer | Package queue duplication | **Core multiplexer + package facade** | One bounded/abort-aware implementation |
122
126
  | Distributed ownership | Process-local active map | Generic leases + package coordinator | **LeaseStore + fenced checkpoint CAS** | Atomic claims and monotonic fences prevent split brain across processes |
123
127
  | Host control (no TUI) | Interactive terminal package | Public APIs + optional RPC/`CommandDefinition` | **Public APIs + optional commands** | Replaces former TUI Tasks for feature completeness |
124
- | Approval prompts | Core `ApprovalHandler` | Host `ExecutionPolicy` / `CodingApprovalFn` | **Host callbacks** | Mirrors coding-security pattern |
128
+ | Approval prompts | Core `ApprovalHandler` | Durable workflow suspension + host `ExecutionPolicy` | **Checkpoint suspension** | Survives restart; approved tool execution still rechecks current host policy |
125
129
  | Interactive TUI | Ship in Plan 057 | Defer C-012 | **Defer** | Explicit product decision after Task 0 |
126
130
 
127
131
  ## Locked package adapter contracts (Task 1)
@@ -435,6 +439,12 @@ runRpcServer({
435
439
  | Workflow `maxCheckpointBytes` | Full checkpoint blob | 1 MiB | Resume metadata only; not full transcripts |
436
440
  | Workflow event buffer | Per run merge queue | 2048 | Coalesce node status; drop with `workflow_event_overflow` |
437
441
  | Workflow list/status page size | Status helper default | 100 | Bounded run listing for hosts |
442
+ | Nested depth | Default / hard | 8 / 32 | Prevent recursive composition exhaustion |
443
+ | Shared state | Default / hard bytes | 64 KiB / 512 KiB | Keep node context/checkpoints bounded |
444
+ | State history | Default / hard snapshots | 32 / 128 | Preserve replay state without unbounded history |
445
+ | Replay lineage | Default / hard depth | 8 / 32 | Prevent replay-chain abuse |
446
+ | Schedule input | Default / hard bytes | 256 KiB / 1 MiB | Bound persisted trigger payload |
447
+ | Schedule due claims | Default / hard per poll | 16 / 256 | Bound one poll; idle waits 1s by default |
438
448
 
439
449
  ## Threat model and design matrix
440
450
 
@@ -444,7 +454,7 @@ runRpcServer({
444
454
  | 2 | Unbounded fan-out (dynamic list) | Workflow package | Cap `maxFanOut`; fail `node_failed` when exceeded |
445
455
  | 3 | Resumed checkpoint tampered (wrong tenant/version) | Workflow adapter | Fail closed; no partial node execution |
446
456
  | 4 | Checkpoint contains secrets | Workflow + redactor | Redact before persist; `redacted: true` metadata |
447
- | 5 | Shell/tool approval during workflow | Host + ExecutionPolicy | Show/include `kind`, `command`, `paths`, `risk`, `workflowId`, `nodeId` |
457
+ | 5 | Shell/tool approval during workflow | Host + workflow | Opt-in tool approval suspends before side effects; resume uses ownership + expected-version CAS, then rechecks current `ExecutionPolicy` with workflow/node metadata |
448
458
  | 6 | Cancel during node execution | Workflow | `signal` abort → in-flight `session.abort()`; checkpoint marks `aborted` |
449
459
  | 7 | Untrusted workflow definition file | Host | Load from trusted path only; schema-validate before `runWorkflow` |
450
460
  | 8 | Node output passed to next node | Workflow | Size-bound; type validate; redact at boundary |
@@ -452,6 +462,12 @@ runRpcServer({
452
462
  | 10 | Cross-tenant list/status query | Workflow adapter | Scope by ownership; never return other tenants' runs |
453
463
  | 11 | RPC workflow cancel races session abort | Workflow commands | Cancel is idempotent; fails closed if run unknown/unauthorized |
454
464
  | 12 | 1000-node workflow checkpoint growth | Workflow | Store ready set + bounded outputs only; no full transcript duplication |
465
+ | 13 | Two reviewers resume one suspension | Workflow adapter | Expected-version CAS claims checkpoint before execution; one succeeds, stale reviewer fails |
466
+ | 14 | Forged/cross-tenant resume | Workflow + host | Ownership and definition hash checked; declared schema requires host validator; payload redacted before persistence |
467
+ | 15 | Duplicate/crashed schedule fire | Workflow schedules | Per-fire lease, deterministic run ID, queued checkpoint idempotency, schedule CAS |
468
+ | 16 | Nested workflow broadens capability | Workflow runner | Child inherits parent tool/agent/policy/ownership/signal seams; bounded inherited depth |
469
+ | 17 | Replay mutates evidence or reuses approval | Workflow replay | New checkpoint + immutable lineage; source untouched; copied approval-bearing path rejected |
470
+ | 18 | State/history resource exhaustion | Workflow runner/checkpoint | Host validation plus state byte/history and aggregate checkpoint ceilings |
455
471
 
456
472
  ## Final primitive decisions (Task 1, superseded where noted by Task 6)
457
473
 
@@ -547,7 +563,7 @@ await session.run("Hi", { signal: AbortSignal.timeout(60_000) });
547
563
  - Workflow agent nodes call public `AgentSession` APIs only; no imports from `src/agents.ts` internals.
548
564
  - Workflow checkpoints adapt `ProductionPersistenceStore.checkpoints` (or any `CheckpointStore`); no raw database handles enter the workflow package.
549
565
  - Multimodal and credential packages from Plan 056 compose unchanged in workflow examples (Task 4).
550
- - `prism-all` umbrella **excludes** `@arnilo/prism-workflows` (optional orchestration; install explicitly). Documented in package README and `docs/release-and-install.md`.
566
+ - `@arnilo/prism-workflows` is available directly and through `prism-sdk`/`prism-all`; installation does not start a worker or workflow.
551
567
  - C-012 interactive TUI remains a future optional package if needed; it is not required for workflow feature completeness.
552
568
 
553
569
  ## Related APIs
package/docs/workflows.md CHANGED
@@ -2,21 +2,22 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-workflows` is an optional package for typed, bounded DAG orchestration over Prism sessions, tools, events, and persistence seams. Hosts define acyclic workflows with agent/function/tool/conditional/fan-out/join nodes; the package runs a Kahn-style scheduler with a bounded worker pool, emits package-local `WorkflowEvent`s, checkpoints progress, and can coordinate queued runs across multiple host processes using durable leases and fencing.
5
+ `@arnilo/prism-workflows` is an optional package for typed, bounded DAG orchestration over Prism sessions, tools, events, and persistence seams. Hosts define acyclic workflows with agent/function/tool/conditional/fan-out/join/nested-workflow nodes; the package runs a Kahn-style scheduler with a bounded worker pool, emits package-local `WorkflowEvent`s, checkpoints progress, and can coordinate queued runs across multiple host processes using durable leases and fencing.
6
6
 
7
7
  Primary exports:
8
8
 
9
9
  | Export | Purpose |
10
10
  | --- | --- |
11
11
  | `defineWorkflow` / `buildGraph` | Validate definitions (acyclicity, edge refs, limits) and build deterministic successor/indegree maps |
12
- | `agentNode`, `functionNode`, `toolNode`, `conditionalNode`, `fanOutNode`, `joinNode` | Typed node factories |
13
- | `runWorkflow` / `resumeWorkflow` | Execute or resume a run with concurrency, abort, redaction, and optional checkpoints |
12
+ | `agentNode`, `functionNode`, `toolNode`, `conditionalNode`, `fanOutNode`, `joinNode`, `workflowNode` | Typed node factories, including composition through the same runner |
13
+ | `runWorkflow` / `resumeWorkflow` / `suspend` / `replayWorkflow` | Execute, durably suspend, exactly-once resume, or create an immutable-lineage replay from a succeeded node |
14
14
  | `createMemoryWorkflowCheckpoints` | In-process `WorkflowCheckpointAdapter` over core `createMemoryCheckpointStore()` |
15
15
  | `createWorkflowCheckpoints` | Adapt core `CheckpointStore` (including SQLite/PostgreSQL persistence capabilities) to workflow checkpoint shapes |
16
16
  | `createWorkflowEventBus` | Bounded pub/sub for `WorkflowEvent` with overflow policy |
17
17
  | `getWorkflowRun` / `listWorkflowRuns` / `cancelWorkflowRun` | Status, paginated list, and cancel helpers |
18
- | `createWorkflowCommands` | Optional `CommandDefinition[]` for `runRpcServer` (`workflow.start` / `status` / `list` / `cancel` / `resume`) |
19
- | `enqueueWorkflow` / `createWorkflowCoordinator` | Persist queued work and atomically claim/renew/execute it across processes using `LeaseStore` |
18
+ | `createWorkflowCommands` | Optional `CommandDefinition[]` for direct/background/replay/status/list/cancel/resume and, when selected, schedule control |
19
+ | `enqueueWorkflow` / `startWorkflowBackground` / `createWorkflowCoordinator` | Persist queued work and atomically claim/renew/execute it across processes using `LeaseStore` |
20
+ | `createWorkflowSchedules` | Explicit ownership-scoped one-time/interval/host-calculated schedules over existing checkpoint/lease stores |
20
21
 
21
22
  Included through `@arnilo/prism-sdk` and `@arnilo/prism-all`; installing either profile does not start workflows. Interactive TUI is out of scope (C-012 deferred).
22
23
 
@@ -40,6 +41,11 @@ Use `createWorkflowCoordinator()` when multiple processes share SQLite/PostgreSQ
40
41
  | `limits.maxConcurrency` | Default 8 |
41
42
  | `limits.maxNodeOutputBytes` | Default 4 MiB |
42
43
  | `limits.maxCheckpointBytes` | Default 1 MiB |
44
+ | `limits.maxNestedDepth` / hard cap | 8 / 32; inherited by child workflows |
45
+ | `limits.maxStateBytes` / hard cap | 64 KiB / 512 KiB |
46
+ | `limits.maxStateHistory` / hard cap | 32 / 128 state snapshots; updates stop before evidence would be discarded |
47
+ | `limits.maxReplayDepth` / hard cap | 8 / 32 lineage generations |
48
+ | `state.initial` / `state.schema` | Initial shared JSON object and optional host-validated schema |
43
49
 
44
50
  `runWorkflow(workflow, input, options?)`:
45
51
 
@@ -56,23 +62,38 @@ Use `createWorkflowCoordinator()` when multiple processes share SQLite/PostgreSQ
56
62
  | `signal` | Cancels the run and in-flight agent sessions |
57
63
  | `onEvent` | Synchronous `WorkflowEvent` sink |
58
64
  | `runId` | Caller-supplied id; otherwise generated (`wfr_…`) |
65
+ | `resume` | For suspended runs: `{ decision: "approve" | "deny", input?, expectedVersion }`; version is mandatory for an exact-once CAS claim |
66
+ | `validateResume` | Host validator for resume input; required when `suspend()` declares `resumeSchema` |
67
+ | `validateState` | Host validator for every initial/restored/updated state; required when workflow declares `state.schema` |
68
+ | `initialState` | Optional host initial state override; nested workflows receive parent state automatically |
59
69
 
60
- `resumeWorkflow(workflow, { runId }, options)` loads the checkpoint, validates schema/version/tenant/`definitionHash`, and continues pending/ready nodes only.
70
+ A function node returns `suspend({ reason, data?, resumeSchema? })` to persist `status: "suspended"`. Its next invocation receives `ctx.resume` only after an approved resume. `resumeWorkflow(workflow, { runId }, options)` validates schema/version/ownership/`definitionHash`, claims the checkpoint before node execution, and continues the suspended node. Denial persists terminal `denied` status without invoking it. Existing failed/aborted checkpoint resume remains available without a human decision.
71
+
72
+ Every node receives bounded `ctx.state`, `ctx.stateVersion`, and async `ctx.updateState(patch, { mode: "merge" | "replace" })`. Updates serialize, validate, redact, and snapshot before checkpoint save. `workflowNode({ workflow })` runs its child with the same ownership, agent/tool registries, execution policy, redactor, signal, checkpoints, and event bus; child state replaces parent state after success.
73
+
74
+ `replayWorkflow(workflow, { sourceRunId, fromNodeId, runId? }, options)` requires a succeeded source/node, creates a new checkpoint, copies terminal evidence outside the selected node's downstream closure, restores selected-node pre-state, and records `{ sourceRunId, fromNodeId, rootRunId, depth }`. Source evidence is untouched. Copying any prior nested/tool approval is rejected; replay from that approval node or earlier so Phase 8 approval executes again.
61
75
 
62
76
  `createWorkflowCoordinator({ coordinatorId, workflows, checkpoints, leases, ... })` polls queued/running checkpoints with bounded pages, atomically claims each run, renews its lease, and aborts/fences work after lease loss. Key controls: `leaseTtlMs` (default 30s), `renewalIntervalMs` (default TTL/3), `pollIntervalMs` (default 1s), `maxConcurrentRuns` (default 4), and `pageSize` (default 100, maximum 500).
63
77
 
78
+ `createWorkflowSchedules({ store, leases, checkpoints, workflows, ownership, ownerId, calculators? })` is inert until its host calls `pollOnce()` or `run({ signal })`. Ownership requires `tenantId` plus `accountId` or `userId`. Methods are `create`, `get`, `list`, `pause`, `resume`, `trigger`, `delete`, `pollOnce`, and `run`. A record has one required `nextRunAt`, optional fixed `intervalMs` or registered `calculatorId` (never both), bounded input/metadata, status, version, and last-fire attribution. Manual trigger requires an idempotency key. Scheduled run IDs derive from schedule ID plus fire timestamp, so retry after enqueue-before-advance finds the same queued checkpoint instead of duplicating it. Defaults: page 100/hard 500, due claims 16/hard 256, input 256 KiB/hard 1 MiB, poll 1s, fire lease 30s.
79
+
64
80
  ## Outputs / response / events
65
81
 
66
82
  `runWorkflow` / `resumeWorkflow` resolve to `WorkflowRunResult`:
67
83
 
68
84
  | Field | Notes |
69
85
  | --- | --- |
70
- | `runId`, `workflowId`, `status` | `succeeded` / `failed` / `aborted` |
86
+ | `runId`, `workflowId`, `status` | `queued` / `running` / `suspended` / `succeeded` / `failed` / `denied` / `aborted` |
71
87
  | `outputs` | Map of succeeded node outputs |
72
- | `error` | First fail-fast error when status is `failed` |
73
- | `version`, `definitionHash`, `createdAt`, `updatedAt` | Checkpoint identity |
88
+ | `state` | Final/current bounded shared JSON state |
89
+ | `lineage` | Replay source/root/node/depth record when this is a replay |
90
+ | `suspension` | Current/persisted `{ nodeId, reason, data?, resumeSchema?, requestedAt }` |
91
+ | `resume` | Attributable resume decision/input/version/time record |
92
+ | `version` | Checkpoint CAS identity shown to reviewers and required on suspended resume |
74
93
 
75
- Package-local `WorkflowEvent` types: `workflow_started`, `workflow_finished`, `node_started`, `node_finished`, `node_failed`, `node_skipped`, `checkpoint_saved`, `agent_event` (wraps a redacted `AgentEvent`), `workflow_event_overflow`. Sequences are monotonic; drain/order is deterministic by `(sequence, nodeId)`.
94
+ Schedule `onEvent` receives bounded-attribution `schedule_fired` or metadata-only `schedule_failed`; schedule input is never copied into these events.
95
+
96
+ Package-local `WorkflowEvent` types: `workflow_started`, `workflow_suspended`, `workflow_resumed`, `workflow_finished`, `node_started`, `node_finished`, `node_failed`, `node_skipped`, `checkpoint_saved`, `agent_event` (wraps a redacted `AgentEvent`), `workflow_event_overflow`. Sequences are monotonic; drain/order is deterministic by `(sequence, nodeId)`.
76
97
 
77
98
  ## Request/response example
78
99
 
@@ -111,6 +132,10 @@ import {
111
132
  cancelWorkflowRun,
112
133
  enqueueWorkflow,
113
134
  createWorkflowCoordinator,
135
+ createWorkflowSchedules,
136
+ replayWorkflow,
137
+ workflowNode,
138
+ suspend,
114
139
  } from "@arnilo/prism-workflows";
115
140
  import { runRpcServer } from "@arnilo/prism";
116
141
  import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
@@ -122,11 +147,20 @@ const research = agentNode({
122
147
  const draft = functionNode({
123
148
  execute: async (ctx) => `Draft from ${String(ctx.upstream.research)}`,
124
149
  });
150
+ const publish = functionNode({
151
+ execute: async (ctx) => ctx.resume
152
+ ? publishDraft(ctx.upstream.draft, ctx.resume.input)
153
+ : suspend({
154
+ reason: "publish",
155
+ data: { draft: ctx.upstream.draft },
156
+ resumeSchema: { type: "object", required: ["reviewer"] },
157
+ }),
158
+ });
125
159
 
126
160
  const workflow = defineWorkflow({
127
161
  id: "research-draft",
128
- nodes: { research, draft },
129
- edges: [["research", "draft"]],
162
+ nodes: { research, draft, publish },
163
+ edges: [["research", "draft"], ["draft", "publish"]],
130
164
  limits: { maxNodes: 256, maxFanOut: 32, maxConcurrency: 4 },
131
165
  });
132
166
 
@@ -157,12 +191,20 @@ const result = await runWorkflow(workflow, { topic: "hooks" }, {
157
191
  onEvent: (event) => sink.push(event),
158
192
  });
159
193
 
160
- // Durable resume after process restart (reopen persistence, then adapt its store):
161
- await resumeWorkflow(workflow, { runId: result.runId }, {
162
- checkpoints,
163
- agentFactory: (name) => agents.resolve(name).createSession(),
164
- ownership: { tenantId: "t1" },
165
- });
194
+ // Durable human resume after process restart. Use result.version shown to reviewer.
195
+ if (result.status === "suspended") {
196
+ await resumeWorkflow(workflow, { runId: result.runId }, {
197
+ checkpoints,
198
+ agentFactory: (name) => agents.resolve(name).createSession(),
199
+ ownership: { tenantId: "t1" },
200
+ resume: {
201
+ decision: "approve",
202
+ input: { reviewer: "Ada" },
203
+ expectedVersion: result.version,
204
+ },
205
+ validateResume: ({ value }) => validateResumePayload(value),
206
+ });
207
+ }
166
208
 
167
209
  await cancelWorkflowRun({
168
210
  workflowId: workflow.id,
@@ -172,11 +214,34 @@ await cancelWorkflowRun({
172
214
  });
173
215
 
174
216
  // Optional host control via existing CLI/RPC CommandDefinition seam:
217
+ const schedules = createWorkflowSchedules({
218
+ store: persistence.checkpoints,
219
+ leases: persistence.leases,
220
+ checkpoints,
221
+ workflows: { [workflow.id]: workflow },
222
+ ownership: { tenantId: "t1", userId: "ops" },
223
+ ownerId: process.env.HOSTNAME ?? "scheduler-1",
224
+ });
225
+ await schedules.create({
226
+ id: "daily-research",
227
+ workflowId: workflow.id,
228
+ nextRunAt: "2026-07-17T00:00:00.000Z",
229
+ intervalMs: 86_400_000,
230
+ input: { topic: "hooks" },
231
+ });
232
+ // Host calls schedules.pollOnce() from an existing timer, or explicitly starts schedules.run({ signal }).
233
+
234
+ const replay = await replayWorkflow(workflow, {
235
+ sourceRunId: result.runId,
236
+ fromNodeId: "draft",
237
+ }, { checkpoints, ownership: { tenantId: "t1" }, agentFactory });
238
+
175
239
  runRpcServer({
176
240
  createSession,
177
241
  commands: createWorkflowCommands({
178
242
  workflows: { [workflow.id]: workflow },
179
243
  checkpoints,
244
+ schedules,
180
245
  runOptions: { ownership: { tenantId: "t1" }, agentFactory },
181
246
  }),
182
247
  });
@@ -187,7 +252,7 @@ runRpcServer({
187
252
  - Workflow semantics stay in this optional package; generic checkpoint persistence and bounded event fan-in live in core.
188
253
  - `ProductionPersistenceStore.checkpoints` and `.leases` are optional generic capabilities. First-party SQLite/PostgreSQL adapters own `prism_checkpoints` / `prism_leases`; workflows only adapt them.
189
254
  - `createWorkflowEventBus()` delegates queueing, source fan-in, overflow, abort, and close behavior to core `createEventMultiplexer()`.
190
- - `createWorkflowCommands()` is optional; hosts that already use `runRpcServer({ commands })` can drive start/status/list/cancel/resume without a TUI.
255
+ - `createWorkflowCommands()` is optional; hosts can drive `workflow.start` / `enqueue` / `replay` / `status` / `list` / `cancel` / `resume`. The six `schedule.*` commands appear only when a scoped `schedules` service is supplied.
191
256
  - Hosts may bridge `WorkflowEvent` into OpenTelemetry or custom sinks; there is no built-in TUI.
192
257
  - Agent exclusivity is per session: one active `run()` at a time, same as core.
193
258
 
@@ -195,19 +260,28 @@ runRpcServer({
195
260
 
196
261
  - Definitions fail closed on cycles, unknown edges, self-edges, and `maxNodes` overflow.
197
262
  - Fan-out length is bounded by `maxFanOut`; concurrency by `maxConcurrency`.
198
- - Node outputs and checkpoints are byte-bounded (`maxNodeOutputBytes`, `maxCheckpointBytes`).
263
+ - Node outputs, shared state/history, schedule input/records, and checkpoints are byte/count/depth bounded. Checkpoint size remains the final aggregate ceiling.
199
264
  - Event buses use a bounded buffer (default 2048) with `close` / `drop_oldest` / `drop_newest` overflow.
200
- - Checkpoints redact via `SecretRedactor` / `secrets` before save; resume rejects tenant, schema, and definition-hash mismatch.
265
+ - Checkpoints redact suspension/resume payloads via `SecretRedactor` / `secrets` before save; resume rejects tenant, schema, definition-hash, and expected-version mismatch.
266
+ - Suspension requires a checkpoint adapter, consumes no worker/polling slot, and is ignored by distributed coordinators until explicit resume.
267
+ - Concurrent resumes race on checkpoint CAS before node execution; one wins and stale/duplicate reviewers fail closed. Approved tool nodes then re-run current `ExecutionPolicy`, so durable approval cannot grant stale permissions.
268
+ - `toolNode({ approval: { reason, data?, resumeSchema? } })` suspends before tool execution. Denial is terminal `denied`; no tool side effect occurs.
201
269
  - `cancelWorkflowRun` aborts local runs immediately and writes a durable cancellation request for a remotely leased run; workers check it during lease renewal.
202
270
  - Tool nodes attach `workflowId` / `nodeId` on `ExecutionAction.metadata` for approval/audit context.
203
- - Scheduler stores O(nodes + active outputs); ready-node work uses indegree maps, not repeated full scans.
271
+ - Nested workflows inherit host registries/policies and cannot inject broader tools, agents, ownership, or credentials. Nested depth is inherited; child suspension bubbles to the parent review cursor.
272
+ - Replay source ownership/hash/status/node eligibility are checked before a new checkpoint is created. Source records are immutable, lineage is bounded, and copied approval-bearing paths are rejected.
273
+ - Schedule services are ownership-scoped and explicitly started. Per-fire leases plus deterministic run IDs/CAS prevent duplicate enqueue across coordinators and crash retry. Host calculator IDs resolve only from the supplied map; no callback or cron expression is persisted.
274
+ - Scheduler stores O(nodes + active outputs + bounded state history); ready-node work uses indegree maps, not repeated full scans.
204
275
  - Lease acquisition is atomic; opaque tokens protect renew/release; monotonically increasing fencing tokens plus checkpoint compare-and-swap prevent expired workers from committing after takeover. Node functions must honor `ctx.signal` for prompt cooperative cancellation.
205
276
 
277
+ Use workflows for known, durable, replayable graphs. Use optional supervisor delegation only when child selection must be dynamic at runtime; do not replace deterministic nodes with model routing without a concrete need.
278
+
206
279
  ## Related APIs
207
280
 
208
281
  - Examples: `examples/workflow-research-and-review.ts`, `examples/workflow-parallel-research.ts`, `examples/workflow-tool-approval.ts`, `examples/workflow-multimodal-document.ts`, `examples/workflow-sqlite-resume.ts`, `examples/workflow-postgres-resume.ts`, `examples/workflow-event-sink.ts`, `examples/workflow-rpc-cancel.ts`, `examples/workflow-distributed-coordinator.ts` — offline runnable demos; PostgreSQL safely skips unless `PRISM_TEST_POSTGRES_URL` is set.
209
282
  - [Workflow orchestration primitives](workflow-orchestration-primitives.md): Task 0–1 inventory and locked adapter contracts
210
- - [Agent/session runtime](agent-session-runtime.md): `AgentSession.run()`, abort, subscribe
283
+ - [Agent/session runtime](agent-session-runtime.md): `AgentSession.run()`/`stream()`, abort, subscribe
284
+ - [Supervisor delegation](supervisors.md): bounded dynamic child selection.
211
285
  - [Agent events](agent-events.md): core `AgentEvent` wrapped by `agent_event`
212
286
  - [Session stores and branching](session-stores-and-branching.md): session `leafId` reuse on resume
213
287
  - [CLI/RPC](cli-rpc.md): host control seam; wire `createWorkflowCommands()` into `runRpcServer`
@@ -0,0 +1,169 @@
1
+ # Working and semantic memory
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-memory` is an optional package for schema/template-backed working memory and embedding-based semantic recall. It owns narrow `Embedder` and `VectorStore` contracts reused by `@arnilo/prism-rag`, plus an in-memory reference path and one PostgreSQL/pgvector production adapter.
6
+
7
+ ## When to use it
8
+
9
+ Use it when a host needs durable per-tenant profile/state (working memory) or top-K semantic retrieval over prior thread entries. Do not use it as a replacement for observational memory compaction: observational memory compresses source-backed observations; semantic memory retrieves embeddings; working memory stores the current structured profile.
10
+
11
+ Ordinary Prism sessions do not require this package or any vector backend.
12
+
13
+ ## Inputs / request
14
+
15
+ `createMemory(options)`:
16
+
17
+ | Field | Required | Meaning |
18
+ | --- | --- | --- |
19
+ | `tenantId` | yes | Tenant isolation key |
20
+ | `resourceId` | yes | Resource/user isolation key |
21
+ | `threadId` | for semantic ops | Thread isolation; optional for resource-scoped working memory |
22
+ | `embedder` | yes | Host-owned or package hash embedder |
23
+ | `vectorStore` / `workingStore` | no | Defaults to in-memory adapters |
24
+ | `schema` / `validateWorkingMemory` | no | Working-memory shape checks (JSON Schema subset or host hook) |
25
+ | `workingMemoryTemplate` | no | `{{path}}` template for context injection |
26
+ | `limits` | no | top-K, adjacent range, batch, payload, injected-token caps |
27
+ | `redactor` / `secrets` | no | Redact text/metadata before persist/inject |
28
+
29
+ Semantic indexing:
30
+
31
+ ```ts
32
+ await memory.remember({ entries: [{ id, text, metadata?, sequence? }] }, { wait?: boolean })
33
+ ```
34
+
35
+ Semantic recall:
36
+
37
+ ```ts
38
+ await memory.recall(query, { topK?, messageRange?, signal? })
39
+ ```
40
+
41
+ ## Outputs / response / events
42
+
43
+ | API | Result |
44
+ | --- | --- |
45
+ | `updateWorking` / `getWorking` | Versioned `WorkingMemoryRecord` |
46
+ | `remember` | `{ accepted, pending, done }` — default `wait: false` indexes asynchronously |
47
+ | `recall` | `{ hits, adjacent }` tenant/thread scoped |
48
+ | `createContextProvider()` | Inert `ContextProvider` blocks for working and/or semantic text |
49
+ | `createWorkingMemoryProcessor({ extract })` | Explicit host-invoked updater; never auto-runs |
50
+
51
+ No package-owned agent events are emitted. Injection uses existing context assembly only.
52
+
53
+ ## Request/response example
54
+
55
+ ```json
56
+ {
57
+ "tenantId": "t1",
58
+ "resourceId": "user-ada",
59
+ "threadId": "thread-1",
60
+ "working": { "name": "Ada", "preferences": { "format": "concise" } },
61
+ "recall": {
62
+ "query": "preferred response format",
63
+ "topK": 5,
64
+ "messageRange": 1
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Implementation example
70
+
71
+ ```ts
72
+ import { createAgent, createMockProvider, providerDone, providerTextDelta } from "@arnilo/prism";
73
+ import { createHashEmbedder, createMemory } from "@arnilo/prism-memory";
74
+
75
+ const memory = createMemory({
76
+ tenantId: "t1",
77
+ resourceId: "user-ada",
78
+ threadId: "thread-1",
79
+ embedder: createHashEmbedder(),
80
+ workingMemoryTemplate: "Name: {{name}}; Format: {{preferences.format}}",
81
+ schema: {
82
+ type: "object",
83
+ properties: {
84
+ name: { type: "string" },
85
+ preferences: {
86
+ type: "object",
87
+ properties: { format: { type: "string" } },
88
+ required: ["format"],
89
+ additionalProperties: false,
90
+ },
91
+ },
92
+ required: ["name"],
93
+ additionalProperties: false,
94
+ },
95
+ });
96
+
97
+ await memory.updateWorking({ name: "Ada", preferences: { format: "concise" } });
98
+ await memory.remember({ entries: [{ id: "m1", text: "Prefers concise answers" }] });
99
+
100
+ const agent = createAgent({
101
+ model: { provider: "mock", model: "demo" },
102
+ provider: createMockProvider([providerTextDelta("Got it."), providerDone()]),
103
+ context: [memory.createContextProvider()],
104
+ });
105
+ ```
106
+
107
+ PostgreSQL/pgvector:
108
+
109
+ ```ts
110
+ import { createPostgresMemoryStores, createMemory, createHashEmbedder } from "@arnilo/prism-memory";
111
+
112
+ const stores = await createPostgresMemoryStores({
113
+ connectionString: process.env.DATABASE_URL!,
114
+ schema: "prism_memory",
115
+ dimensions: 32,
116
+ });
117
+
118
+ const memory = createMemory({
119
+ tenantId: "t1",
120
+ resourceId: "user-ada",
121
+ threadId: "thread-1",
122
+ embedder: createHashEmbedder({ dimensions: 32 }),
123
+ workingStore: stores.workingStore,
124
+ vectorStore: stores.vectorStore,
125
+ });
126
+ ```
127
+
128
+ ## Extension and configuration notes
129
+
130
+ - Hosts wire the context provider into `AgentConfig.context` or `resolveContextProviders()`.
131
+ - The working-memory processor is opt-in and host-invoked; middleware is not required.
132
+ - `createHashEmbedder()` is for tests/demos only; production hosts supply a real `Embedder`.
133
+ - Observational memory (`@arnilo/prism-compaction-observational-memory`) remains unchanged and composable.
134
+ - Profile bundles do not include this package yet.
135
+
136
+ Shared conformance:
137
+
138
+ ```ts
139
+ import { runMemoryConformance, createHashEmbedder, createMemoryVectorStore, createMemoryWorkingStore } from "@arnilo/prism-memory";
140
+
141
+ await runMemoryConformance(() => ({
142
+ embedder: createHashEmbedder(),
143
+ vectorStore: createMemoryVectorStore(),
144
+ workingStore: createMemoryWorkingStore(),
145
+ }));
146
+ ```
147
+
148
+ ## Security and performance notes
149
+
150
+ - Every write/query/delete requires `tenantId` + `resourceId`; semantic paths also require `threadId`.
151
+ - Cross-tenant and cross-thread access is denied.
152
+ - Configure `secrets` / `redactor` so memory text and metadata cannot persist or inject raw canaries.
153
+ - Injected context is inert text — it cannot grant tools or permissions.
154
+ - Hard caps: top-K ≤ 32, messageRange ≤ 4, embed batch ≤ 128, injected tokens ≤ 8000, payload/working-memory byte limits enforced.
155
+ - Default `remember()` does not block agent completion; pass `{ wait: true }` when indexing must finish first.
156
+ - PostgreSQL live suite is gated by `PRISM_TEST_POSTGRES_URL` and requires the `vector` extension.
157
+
158
+ ## Delegated-agent isolation
159
+
160
+ Supervisor child factories receive unique derived `resourceId` and `threadId` values. Construct each child's `createMemory()` facade from those exact values; never reuse parent memory scope or let model-supplied IDs select another resource.
161
+
162
+ ## Related APIs
163
+
164
+ - [Supervisor delegation](supervisors.md): package-derived child resource/thread scope.
165
+ - [Retrieval-augmented generation](rag.md): bounded document chunks reuse this package's embed/vector contracts.
166
+ - [Context and skills](context-and-skills.md): `ContextProvider` injection seam.
167
+ - [Observational memory compaction package](compaction-observational-memory.md): source-backed observation/reflection memory distinction.
168
+ - [PostgreSQL persistence](postgres-persistence.md): session/run persistence; memory vectors live in this optional package instead.
169
+ - [Middleware hooks](middleware-hooks.md): reuse existing `context` hook if hosts transform injected blocks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,6 +54,10 @@
54
54
  "types": "./dist/testing/run-ledger-conformance.d.ts",
55
55
  "default": "./dist/testing/run-ledger-conformance.js"
56
56
  },
57
+ "./testing/feedback": {
58
+ "types": "./dist/testing/feedback.d.ts",
59
+ "default": "./dist/testing/feedback.js"
60
+ },
57
61
  "./node/config": {
58
62
  "types": "./dist/node/config.d.ts",
59
63
  "default": "./dist/node/config.js"
@@ -95,6 +99,7 @@
95
99
  "!dist/__tests__",
96
100
  "!dist/**/*.map",
97
101
  "docs",
102
+ "templates",
98
103
  "CHANGELOG.md"
99
104
  ],
100
105
  "workspaces": [
@@ -108,6 +113,11 @@
108
113
  "packages/coding-agent",
109
114
  "packages/coding-security",
110
115
  "packages/workflows",
116
+ "packages/evals",
117
+ "packages/memory",
118
+ "packages/rag",
119
+ "packages/server",
120
+ "packages/supervisor",
111
121
  "packages/prism-*"
112
122
  ],
113
123
  "scripts": {
@@ -116,7 +126,7 @@
116
126
  "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
117
127
  "test": "npm run build && node --test dist/__tests__/*.test.js && npm run test --workspaces --if-present",
118
128
  "pack:dry-run": "npm pack --dry-run && npm run pack:dry-run --workspaces --if-present",
119
- "test:postgres": "npm run test:postgres --workspace @arnilo/prism-session-store-postgres",
129
+ "test:postgres": "npm run test:postgres --workspace @arnilo/prism-session-store-postgres && npm run test:postgres --workspace @arnilo/prism-memory",
120
130
  "release:dry-run": "npm run sdk:ready",
121
131
  "release:check": "node scripts/release.mjs check",
122
132
  "release:publish": "node scripts/release.mjs publish",
@@ -0,0 +1,28 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Minimal Prism agent scaffold generated by `prism init`.
4
+
5
+ __PROVIDER_README_NOTE__
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ npm install
11
+ npm test
12
+ ```
13
+
14
+ `npm test` always uses the built-in mock provider (no network, no credentials).
15
+
16
+ __NEXT_STEPS_LIVE__
17
+
18
+ ## Layout
19
+
20
+ - `src/agent.ts` — creates the agent with the selected provider.
21
+ - `src/index.ts` — runs one prompt and prints `AgentRunResult.text`.
22
+ - `src/__tests__/agent.test.ts` — offline mock smoke test.
23
+ __OPTIONAL_DOCS__
24
+ ## Notes
25
+
26
+ - Prism does not auto-load credentials, databases, telemetry, or tools.
27
+ - Keep real secrets out of git; `.gitignore` excludes `.env` and local stores.
28
+ - Docs: https://github.com/ashiqrniloy/prism
@@ -0,0 +1 @@
1
+ __ENV_EXAMPLE__
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.db
6
+ *.sqlite
7
+ *.sqlite3
8
+ .prism/
9
+ coverage/
10
+ .DS_Store
11
+ *.log
@@ -0,0 +1,17 @@
1
+ import type { AgentRunResult } from "@arnilo/prism";
2
+ import { defineScorer, scoreRun } from "@arnilo/prism-evals";
3
+
4
+ /**
5
+ * Deterministic scorer example. Not wired into `npm start` — call after a run.
6
+ */
7
+ export const greetsScorer = defineScorer({
8
+ id: "greets",
9
+ score: ({ result }) => ({
10
+ score: /\bhello\b/i.test(result.text) ? 1 : 0,
11
+ reason: "checks for a hello greeting",
12
+ }),
13
+ });
14
+
15
+ export async function scoreGreeting(result: AgentRunResult) {
16
+ return scoreRun({ result, scorers: [greetsScorer] });
17
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ createMemoryWorkflowCheckpoints,
3
+ defineWorkflow,
4
+ functionNode,
5
+ runWorkflow,
6
+ } from "@arnilo/prism-workflows";
7
+
8
+ /**
9
+ * Tiny workflow example. Not wired into `npm start` — import and call from your host.
10
+ */
11
+ export async function runHelloWorkflow(name: string) {
12
+ const workflow = defineWorkflow({
13
+ id: "hello",
14
+ nodes: {
15
+ greet: functionNode({
16
+ execute: async (ctx) => {
17
+ const input = ctx.workflowInput as { name?: string };
18
+ return { message: `Hello, ${input.name ?? name}` };
19
+ },
20
+ }),
21
+ },
22
+ });
23
+
24
+ return runWorkflow(workflow, { name }, {
25
+ checkpoints: createMemoryWorkflowCheckpoints(),
26
+ });
27
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "typecheck": "tsc -p tsconfig.json --noEmit",
9
+ "test": "npm run build && node --test dist/__tests__/agent.test.js",
10
+ "start": "npm run build && node dist/index.js"
11
+ },
12
+ "dependencies": {
13
+ __DEPENDENCIES__
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.0.0",
17
+ "typescript": "^5.7.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ }
22
+ }