@arnilo/prism 0.0.4 → 0.0.6

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 (97) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +34 -10
  3. package/dist/agent-loops.d.ts +1 -0
  4. package/dist/agent-loops.js +26 -16
  5. package/dist/agents.js +147 -21
  6. package/dist/cli-init.d.ts +41 -0
  7. package/dist/cli-init.js +390 -0
  8. package/dist/cli-runner.d.ts +7 -1
  9. package/dist/cli-runner.js +13 -1
  10. package/dist/content.d.ts +19 -0
  11. package/dist/content.js +197 -69
  12. package/dist/contracts.d.ts +96 -9
  13. package/dist/contracts.js +8 -0
  14. package/dist/feedback.d.ts +48 -0
  15. package/dist/feedback.js +230 -0
  16. package/dist/ids.d.ts +2 -0
  17. package/dist/ids.js +6 -0
  18. package/dist/index.d.ts +10 -4
  19. package/dist/index.js +6 -3
  20. package/dist/providers/media.d.ts +3 -1
  21. package/dist/providers/media.js +11 -1
  22. package/dist/session-stores.js +2 -3
  23. package/dist/testing/feedback.d.ts +6 -0
  24. package/dist/testing/feedback.js +37 -0
  25. package/dist/testing/persistence-schema.d.ts +48 -10
  26. package/dist/testing/persistence-schema.js +166 -22
  27. package/dist/testing/run-ledger-conformance.js +7 -1
  28. package/dist/thinking.d.ts +42 -0
  29. package/dist/thinking.js +92 -0
  30. package/dist/tools.js +2 -3
  31. package/dist/use-case-model.d.ts +63 -0
  32. package/dist/use-case-model.js +52 -0
  33. package/docs/a2a.md +75 -0
  34. package/docs/agent-events.md +14 -21
  35. package/docs/agent-loops.md +12 -9
  36. package/docs/agent-session-runtime.md +14 -16
  37. package/docs/cli-rpc.md +35 -7
  38. package/docs/coding-agent-tools.md +35 -14
  39. package/docs/coding-security.md +7 -3
  40. package/docs/compaction-llm.md +17 -7
  41. package/docs/compaction-observational-memory.md +30 -4
  42. package/docs/context-and-skills.md +1 -0
  43. package/docs/credential-storage.md +58 -9
  44. package/docs/credentials-and-redaction.md +3 -3
  45. package/docs/database-persistence.md +17 -9
  46. package/docs/evaluations.md +122 -0
  47. package/docs/extensions.md +2 -2
  48. package/docs/host-security.md +26 -5
  49. package/docs/index.md +43 -28
  50. package/docs/mcp-tools.md +74 -13
  51. package/docs/migration.md +177 -3
  52. package/docs/multimodal-content.md +14 -6
  53. package/docs/node-filesystem-config.md +1 -0
  54. package/docs/node-jsonl-session-store.md +5 -4
  55. package/docs/observability.md +14 -6
  56. package/docs/performance.md +209 -0
  57. package/docs/postgres-persistence.md +8 -6
  58. package/docs/provider-caching.md +16 -4
  59. package/docs/provider-conformance.md +40 -1
  60. package/docs/provider-packages.md +62 -3
  61. package/docs/providers/ai-sdk.md +149 -0
  62. package/docs/providers/kimi.md +124 -61
  63. package/docs/providers/neuralwatt.md +19 -13
  64. package/docs/providers/openai.md +56 -13
  65. package/docs/providers/opencode-go.md +118 -30
  66. package/docs/providers/openrouter.md +105 -35
  67. package/docs/providers/zai.md +94 -45
  68. package/docs/public-contracts.md +6 -5
  69. package/docs/rag.md +113 -0
  70. package/docs/release-and-install.md +100 -79
  71. package/docs/review-coverage-2026-07-15.md +193 -0
  72. package/docs/review-coverage-2026-07-17-provider-validation.md +192 -0
  73. package/docs/runs-and-usage.md +42 -5
  74. package/docs/server.md +139 -0
  75. package/docs/settings-auth-trust-security.md +5 -5
  76. package/docs/sqlite-persistence.md +6 -5
  77. package/docs/structured-output.md +1 -1
  78. package/docs/supervisors.md +71 -0
  79. package/docs/thinking-and-reasoning.md +98 -0
  80. package/docs/tool-execution-primitives.md +3 -3
  81. package/docs/tools.md +15 -0
  82. package/docs/use-case-model-selection.md +109 -0
  83. package/docs/workflow-orchestration-primitives.md +20 -3
  84. package/docs/workflows.md +114 -33
  85. package/docs/working-and-semantic-memory.md +170 -0
  86. package/package.json +13 -3
  87. package/templates/init/README.md.tmpl +28 -0
  88. package/templates/init/env.example.tmpl +1 -0
  89. package/templates/init/gitignore.tmpl +11 -0
  90. package/templates/init/optional/evals-example.ts.tmpl +17 -0
  91. package/templates/init/optional/workflows-example.ts.tmpl +27 -0
  92. package/templates/init/package.json.tmpl +22 -0
  93. package/templates/init/providers.json +76 -0
  94. package/templates/init/src/agent.ts.tmpl +10 -0
  95. package/templates/init/src/index.ts.tmpl +12 -0
  96. package/templates/init/src/tests/agent.test.ts.tmpl +24 -0
  97. 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)
@@ -383,6 +387,7 @@ const review = functionNode({ execute: async (ctx) => lint(ctx.upstream.draft) }
383
387
 
384
388
  const workflow = defineWorkflow({
385
389
  id: "research-draft-review",
390
+ revision: "2026-07-19.1",
386
391
  nodes: { research, draft, review },
387
392
  edges: [
388
393
  ["research", "draft"],
@@ -435,6 +440,12 @@ runRpcServer({
435
440
  | Workflow `maxCheckpointBytes` | Full checkpoint blob | 1 MiB | Resume metadata only; not full transcripts |
436
441
  | Workflow event buffer | Per run merge queue | 2048 | Coalesce node status; drop with `workflow_event_overflow` |
437
442
  | Workflow list/status page size | Status helper default | 100 | Bounded run listing for hosts |
443
+ | Nested depth | Default / hard | 8 / 32 | Prevent recursive composition exhaustion |
444
+ | Shared state | Default / hard bytes | 64 KiB / 512 KiB | Keep node context/checkpoints bounded |
445
+ | State history | Default / hard snapshots | 32 / 128 | Preserve replay state without unbounded history |
446
+ | Replay lineage | Default / hard depth | 8 / 32 | Prevent replay-chain abuse |
447
+ | Schedule input | Default / hard bytes | 256 KiB / 1 MiB | Bound persisted trigger payload |
448
+ | Schedule due claims | Default / hard per poll | 16 / 256 | Bound one poll; idle waits 1s by default |
438
449
 
439
450
  ## Threat model and design matrix
440
451
 
@@ -444,7 +455,7 @@ runRpcServer({
444
455
  | 2 | Unbounded fan-out (dynamic list) | Workflow package | Cap `maxFanOut`; fail `node_failed` when exceeded |
445
456
  | 3 | Resumed checkpoint tampered (wrong tenant/version) | Workflow adapter | Fail closed; no partial node execution |
446
457
  | 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` |
458
+ | 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
459
  | 6 | Cancel during node execution | Workflow | `signal` abort → in-flight `session.abort()`; checkpoint marks `aborted` |
449
460
  | 7 | Untrusted workflow definition file | Host | Load from trusted path only; schema-validate before `runWorkflow` |
450
461
  | 8 | Node output passed to next node | Workflow | Size-bound; type validate; redact at boundary |
@@ -452,6 +463,12 @@ runRpcServer({
452
463
  | 10 | Cross-tenant list/status query | Workflow adapter | Scope by ownership; never return other tenants' runs |
453
464
  | 11 | RPC workflow cancel races session abort | Workflow commands | Cancel is idempotent; fails closed if run unknown/unauthorized |
454
465
  | 12 | 1000-node workflow checkpoint growth | Workflow | Store ready set + bounded outputs only; no full transcript duplication |
466
+ | 13 | Two reviewers resume one suspension | Workflow adapter | Expected-version CAS claims checkpoint before execution; one succeeds, stale reviewer fails |
467
+ | 14 | Forged/cross-tenant resume | Workflow + host | Ownership and definition hash checked; declared schema requires host validator; payload redacted before persistence |
468
+ | 15 | Duplicate/crashed schedule fire | Workflow schedules | Per-fire lease, deterministic run ID, queued checkpoint idempotency, schedule CAS |
469
+ | 16 | Nested workflow broadens capability | Workflow runner | Child inherits parent tool/agent/policy/ownership/signal seams; bounded inherited depth |
470
+ | 17 | Replay mutates evidence or reuses approval | Workflow replay | New checkpoint + immutable lineage; source untouched; copied approval-bearing path rejected |
471
+ | 18 | State/history resource exhaustion | Workflow runner/checkpoint | Host validation plus state byte/history and aggregate checkpoint ceilings |
455
472
 
456
473
  ## Final primitive decisions (Task 1, superseded where noted by Task 6)
457
474
 
@@ -547,7 +564,7 @@ await session.run("Hi", { signal: AbortSignal.timeout(60_000) });
547
564
  - Workflow agent nodes call public `AgentSession` APIs only; no imports from `src/agents.ts` internals.
548
565
  - Workflow checkpoints adapt `ProductionPersistenceStore.checkpoints` (or any `CheckpointStore`); no raw database handles enter the workflow package.
549
566
  - 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`.
567
+ - `@arnilo/prism-workflows` is available directly and through `prism-sdk`/`prism-all`; installation does not start a worker or workflow.
551
568
  - C-012 interactive TUI remains a future optional package if needed; it is not required for workflow feature completeness.
552
569
 
553
570
  ## 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
 
@@ -28,24 +29,32 @@ Use `createWorkflowCoordinator()` when multiple processes share SQLite/PostgreSQ
28
29
 
29
30
  ## Inputs / request
30
31
 
31
- `defineWorkflow({ id, nodes, edges, limits? })`:
32
+ `defineWorkflow({ id, revision, nodes, edges, limits? })`:
32
33
 
33
34
  | Field | Notes |
34
35
  | --- | --- |
35
36
  | `id` | Stable workflow id (required) |
37
+ | `revision` | Non-empty host-authored definition revision (required); parent and nested revisions enter `definitionHash` |
36
38
  | `nodes` | Record of node definitions (`kind` + typed fields) |
37
39
  | `edges` | `[from, to]` pairs; must be acyclic; unknown ids rejected |
38
- | `limits.maxNodes` | Default 1000 |
39
- | `limits.maxFanOut` | Default 64 |
40
- | `limits.maxConcurrency` | Default 8 |
41
- | `limits.maxNodeOutputBytes` | Default 4 MiB |
42
- | `limits.maxCheckpointBytes` | Default 1 MiB |
40
+ | `limits.maxNodes` | Default 1,000 / hard cap 10,000 |
41
+ | `limits.maxFanOut` | Default 64 / hard cap 1,024 |
42
+ | `limits.maxConcurrency` | Default 8 / hard cap 256 |
43
+ | `limits.maxNodeOutputBytes` | Default 4 MiB / hard cap 16 MiB |
44
+ | `limits.maxCheckpointBytes` | Default 1 MiB / hard cap 8 MiB |
45
+ | `limits.maxNestedDepth` / hard cap | 8 / 32; inherited by child workflows |
46
+ | `limits.maxStateBytes` / hard cap | 64 KiB / 512 KiB |
47
+ | `limits.maxStateHistory` / hard cap | 32 / 128 state snapshots; updates stop before evidence would be discarded |
48
+ | `limits.maxReplayDepth` / hard cap | 8 / 32 lineage generations |
49
+ | `state.initial` / `state.schema` | Initial shared JSON object and optional host-validated schema |
50
+
51
+ All workflow limits and runtime `concurrency` reject non-safe integers, zero, negatives, NaN, `Infinity`, and values above the named hard cap. Node retries allow 0–100; an explicit node timeout allows 1–86,400,000 ms. Omitting `timeoutMs` remains an explicit host choice.
43
52
 
44
53
  `runWorkflow(workflow, input, options?)`:
45
54
 
46
55
  | Option | Notes |
47
56
  | --- | --- |
48
- | `concurrency` | Worker pool size (capped by workflow/global limits) |
57
+ | `concurrency` | Worker pool size; positive safe integer, hard cap 256, and capped by the workflow limit |
49
58
  | `checkpoints` | `WorkflowCheckpointAdapter` for save/load/list |
50
59
  | `agentFactory` | `(agentName) => AgentSession` for agent nodes |
51
60
  | `tools` | Tool registry/lookup for tool nodes |
@@ -56,29 +65,45 @@ Use `createWorkflowCoordinator()` when multiple processes share SQLite/PostgreSQ
56
65
  | `signal` | Cancels the run and in-flight agent sessions |
57
66
  | `onEvent` | Synchronous `WorkflowEvent` sink |
58
67
  | `runId` | Caller-supplied id; otherwise generated (`wfr_…`) |
68
+ | `resume` | For suspended runs: `{ decision: "approve" | "deny", input?, expectedVersion }`; version is mandatory for an exact-once CAS claim |
69
+ | `validateResume` | Host validator for resume input; required when `suspend()` declares `resumeSchema` |
70
+ | `validateState` | Host validator for every initial/restored/updated state; required when workflow declares `state.schema` |
71
+ | `initialState` | Optional host initial state override; nested workflows receive parent state automatically |
59
72
 
60
- `resumeWorkflow(workflow, { runId }, options)` loads the checkpoint, validates schema/version/tenant/`definitionHash`, and continues pending/ready nodes only.
73
+ 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.
74
+
75
+ 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.
76
+
77
+ `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
78
 
62
79
  `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
80
 
81
+ `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.
82
+
64
83
  ## Outputs / response / events
65
84
 
66
85
  `runWorkflow` / `resumeWorkflow` resolve to `WorkflowRunResult`:
67
86
 
68
87
  | Field | Notes |
69
88
  | --- | --- |
70
- | `runId`, `workflowId`, `status` | `succeeded` / `failed` / `aborted` |
89
+ | `runId`, `workflowId`, `status` | `queued` / `running` / `suspended` / `succeeded` / `failed` / `denied` / `aborted` |
71
90
  | `outputs` | Map of succeeded node outputs |
72
- | `error` | First fail-fast error when status is `failed` |
73
- | `version`, `definitionHash`, `createdAt`, `updatedAt` | Checkpoint identity |
91
+ | `state` | Final/current bounded shared JSON state |
92
+ | `lineage` | Replay source/root/node/depth record when this is a replay |
93
+ | `suspension` | Current/persisted `{ nodeId, reason, data?, resumeSchema?, requestedAt }` |
94
+ | `resume` | Attributable resume decision/input/version/time record |
95
+ | `version` | Checkpoint CAS identity shown to reviewers and required on suspended resume |
74
96
 
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)`.
97
+ Schedule `onEvent` receives bounded-attribution `schedule_fired` or metadata-only `schedule_failed`; schedule input is never copied into these events.
98
+
99
+ 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
100
 
77
101
  ## Request/response example
78
102
 
79
103
  ```json
80
104
  {
81
105
  "id": "research-draft",
106
+ "revision": "2026-07-19.1",
82
107
  "nodes": ["research", "draft"],
83
108
  "edges": [["research", "draft"]],
84
109
  "limits": { "maxNodes": 256, "maxFanOut": 32, "maxConcurrency": 4 }
@@ -111,6 +136,10 @@ import {
111
136
  cancelWorkflowRun,
112
137
  enqueueWorkflow,
113
138
  createWorkflowCoordinator,
139
+ createWorkflowSchedules,
140
+ replayWorkflow,
141
+ workflowNode,
142
+ suspend,
114
143
  } from "@arnilo/prism-workflows";
115
144
  import { runRpcServer } from "@arnilo/prism";
116
145
  import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
@@ -122,11 +151,21 @@ const research = agentNode({
122
151
  const draft = functionNode({
123
152
  execute: async (ctx) => `Draft from ${String(ctx.upstream.research)}`,
124
153
  });
154
+ const publish = functionNode({
155
+ execute: async (ctx) => ctx.resume
156
+ ? publishDraft(ctx.upstream.draft, ctx.resume.input)
157
+ : suspend({
158
+ reason: "publish",
159
+ data: { draft: ctx.upstream.draft },
160
+ resumeSchema: { type: "object", required: ["reviewer"] },
161
+ }),
162
+ });
125
163
 
126
164
  const workflow = defineWorkflow({
127
165
  id: "research-draft",
128
- nodes: { research, draft },
129
- edges: [["research", "draft"]],
166
+ revision: "2026-07-19.1",
167
+ nodes: { research, draft, publish },
168
+ edges: [["research", "draft"], ["draft", "publish"]],
130
169
  limits: { maxNodes: 256, maxFanOut: 32, maxConcurrency: 4 },
131
170
  });
132
171
 
@@ -157,26 +196,58 @@ const result = await runWorkflow(workflow, { topic: "hooks" }, {
157
196
  onEvent: (event) => sink.push(event),
158
197
  });
159
198
 
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
- });
199
+ // Durable human resume after process restart. Use result.version shown to reviewer.
200
+ if (result.status === "suspended") {
201
+ await resumeWorkflow(workflow, { runId: result.runId }, {
202
+ checkpoints,
203
+ agentFactory: (name) => agents.resolve(name).createSession(),
204
+ ownership: { tenantId: "t1" },
205
+ resume: {
206
+ decision: "approve",
207
+ input: { reviewer: "Ada" },
208
+ expectedVersion: result.version,
209
+ },
210
+ validateResume: ({ value }) => validateResumePayload(value),
211
+ });
212
+ }
166
213
 
167
214
  await cancelWorkflowRun({
168
215
  workflowId: workflow.id,
169
216
  runId: result.runId,
217
+ workflow,
170
218
  checkpoints,
171
219
  ownership: { tenantId: "t1" },
172
220
  });
173
221
 
174
222
  // Optional host control via existing CLI/RPC CommandDefinition seam:
223
+ const schedules = createWorkflowSchedules({
224
+ store: persistence.checkpoints,
225
+ leases: persistence.leases,
226
+ checkpoints,
227
+ workflows: { [workflow.id]: workflow },
228
+ ownership: { tenantId: "t1", userId: "ops" },
229
+ ownerId: process.env.HOSTNAME ?? "scheduler-1",
230
+ });
231
+ await schedules.create({
232
+ id: "daily-research",
233
+ workflowId: workflow.id,
234
+ nextRunAt: "2026-07-17T00:00:00.000Z",
235
+ intervalMs: 86_400_000,
236
+ input: { topic: "hooks" },
237
+ });
238
+ // Host calls schedules.pollOnce() from an existing timer, or explicitly starts schedules.run({ signal }).
239
+
240
+ const replay = await replayWorkflow(workflow, {
241
+ sourceRunId: result.runId,
242
+ fromNodeId: "draft",
243
+ }, { checkpoints, ownership: { tenantId: "t1" }, agentFactory });
244
+
175
245
  runRpcServer({
176
246
  createSession,
177
247
  commands: createWorkflowCommands({
178
248
  workflows: { [workflow.id]: workflow },
179
249
  checkpoints,
250
+ schedules,
180
251
  runOptions: { ownership: { tenantId: "t1" }, agentFactory },
181
252
  }),
182
253
  });
@@ -187,27 +258,37 @@ runRpcServer({
187
258
  - Workflow semantics stay in this optional package; generic checkpoint persistence and bounded event fan-in live in core.
188
259
  - `ProductionPersistenceStore.checkpoints` and `.leases` are optional generic capabilities. First-party SQLite/PostgreSQL adapters own `prism_checkpoints` / `prism_leases`; workflows only adapt them.
189
260
  - `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.
261
+ - `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
262
  - Hosts may bridge `WorkflowEvent` into OpenTelemetry or custom sinks; there is no built-in TUI.
192
263
  - Agent exclusivity is per session: one active `run()` at a time, same as core.
193
264
 
194
265
  ## Security and performance notes
195
266
 
196
- - Definitions fail closed on cycles, unknown edges, self-edges, and `maxNodes` overflow.
197
- - Fan-out length is bounded by `maxFanOut`; concurrency by `maxConcurrency`.
198
- - Node outputs and checkpoints are byte-bounded (`maxNodeOutputBytes`, `maxCheckpointBytes`).
267
+ - Definitions require a non-empty host-authored `revision` and fail closed on cycles, unknown edges, self-edges, invalid limits, and `maxNodes` overflow. Revision and every nested revision enter the deterministic definition hash; hosts must bump revision when function/tool behavior changes.
268
+ - Fan-out length is bounded by `maxFanOut`; concurrency by `maxConcurrency`; every count/byte/runtime option has a finite hard cap.
269
+ - Node outputs, shared state/history, schedule input/records, and checkpoints are byte/count/depth bounded. Checkpoint size remains the final aggregate ceiling.
199
270
  - 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.
201
- - `cancelWorkflowRun` aborts local runs immediately and writes a durable cancellation request for a remotely leased run; workers check it during lease renewal.
271
+ - Checkpoints redact suspension/resume payloads via `SecretRedactor` / `secrets` before save; resume rejects tenant, schema, definition-hash, and expected-version mismatch.
272
+ - Suspension requires a checkpoint adapter, consumes no worker/polling slot, and is ignored by distributed coordinators until explicit resume.
273
+ - 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.
274
+ - `toolNode({ approval: { reason, data?, resumeSchema? } })` suspends before tool execution. Denial is terminal `denied`; no tool side effect occurs.
275
+ - `cancelWorkflowRun` requires the current workflow definition and exact tenant/account/user ownership. It verifies recursive definition hash before abort/mutation, then aborts local runs or writes a durable cancellation request for remotely leased work. Tenant-only or missing ownership cannot cancel a more-specific owned run.
276
+ - Active registry identity includes workflow ID, run ID, and exact ownership. Exact duplicates fail instead of overwriting; distinct owners remain isolated in lookup/list/cancel/unregister.
202
277
  - 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.
278
+ - 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.
279
+ - 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.
280
+ - 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.
281
+ - Scheduler stores O(nodes + active outputs + bounded state history); ready-node work uses indegree maps, not repeated full scans.
204
282
  - 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
283
 
284
+ 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.
285
+
206
286
  ## Related APIs
207
287
 
208
288
  - 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
289
  - [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
290
+ - [Agent/session runtime](agent-session-runtime.md): `AgentSession.run()`/`stream()`, abort, subscribe
291
+ - [Supervisor delegation](supervisors.md): bounded dynamic child selection.
211
292
  - [Agent events](agent-events.md): core `AgentEvent` wrapped by `agent_event`
212
293
  - [Session stores and branching](session-stores-and-branching.md): session `leafId` reuse on resume
213
294
  - [CLI/RPC](cli-rpc.md): host control seam; wire `createWorkflowCommands()` into `runRpcServer`
@@ -0,0 +1,170 @@
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
+ - Every embedding is a non-empty finite number vector. `embedBatched()`, in-memory `VectorStore` upserts/queries, and PostgreSQL/pgvector parameters reject NaN, ±Infinity, non-numbers, and wrong configured dimensions before similarity scoring or SQL. Custom adapters can call `assertFiniteVector(vector, label, expectedLength?)` at their trust boundary.
156
+ - Default `remember()` does not block agent completion; pass `{ wait: true }` when indexing must finish first.
157
+ - PostgreSQL live suite is gated by `PRISM_TEST_POSTGRES_URL` and requires the `vector` extension.
158
+
159
+ ## Delegated-agent isolation
160
+
161
+ 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.
162
+
163
+ ## Related APIs
164
+
165
+ - [Supervisor delegation](supervisors.md): package-derived child resource/thread scope.
166
+ - [Retrieval-augmented generation](rag.md): bounded document chunks reuse this package's embed/vector contracts.
167
+ - [Context and skills](context-and-skills.md): `ContextProvider` injection seam.
168
+ - [Observational memory compaction package](compaction-observational-memory.md): source-backed observation/reflection memory distinction.
169
+ - [PostgreSQL persistence](postgres-persistence.md): session/run persistence; memory vectors live in this optional package instead.
170
+ - [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.6",
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"
@@ -88,13 +92,14 @@
88
92
  }
89
93
  },
90
94
  "bin": {
91
- "prism": "./dist/cli.js"
95
+ "prism": "dist/cli.js"
92
96
  },
93
97
  "files": [
94
98
  "dist",
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
+ }