@arnilo/prism 0.0.1 → 0.0.3

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 (121) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +17 -7
  3. package/dist/agent-definitions.d.ts +12 -0
  4. package/dist/agent-definitions.js +131 -0
  5. package/dist/agent-loops.d.ts +14 -0
  6. package/dist/agent-loops.js +161 -0
  7. package/dist/agents.js +263 -76
  8. package/dist/cache-helpers.d.ts +28 -0
  9. package/dist/cache-helpers.js +73 -0
  10. package/dist/cli-runner.d.ts +38 -2
  11. package/dist/cli-runner.js +167 -5
  12. package/dist/compaction.js +2 -0
  13. package/dist/config.js +47 -12
  14. package/dist/contracts.d.ts +581 -6
  15. package/dist/contracts.js +41 -1
  16. package/dist/contribution-parsing.d.ts +19 -0
  17. package/dist/contribution-parsing.js +124 -0
  18. package/dist/contributions.d.ts +13 -3
  19. package/dist/contributions.js +96 -20
  20. package/dist/extensions.js +3 -0
  21. package/dist/index.d.ts +19 -9
  22. package/dist/index.js +10 -4
  23. package/dist/input.d.ts +7 -1
  24. package/dist/input.js +52 -11
  25. package/dist/instruction-injection.d.ts +28 -0
  26. package/dist/instruction-injection.js +55 -0
  27. package/dist/manifests.d.ts +1 -1
  28. package/dist/manifests.js +3 -3
  29. package/dist/models.d.ts +4 -1
  30. package/dist/models.js +5 -2
  31. package/dist/node/agent-definitions.d.ts +98 -0
  32. package/dist/node/agent-definitions.js +389 -0
  33. package/dist/node/contribution-discovery.d.ts +17 -0
  34. package/dist/node/contribution-discovery.js +163 -0
  35. package/dist/node/instruction-injectors.d.ts +32 -0
  36. package/dist/node/instruction-injectors.js +72 -0
  37. package/dist/node/session-store-jsonl.d.ts +1 -1
  38. package/dist/node/session-store-jsonl.js +42 -4
  39. package/dist/node/system-project-prompts.d.ts +30 -0
  40. package/dist/node/system-project-prompts.js +53 -0
  41. package/dist/provider-events.d.ts +3 -1
  42. package/dist/provider-events.js +34 -0
  43. package/dist/provider-request-policy.js +15 -1
  44. package/dist/providers/openai-compatible.js +1 -1
  45. package/dist/providers.d.ts +6 -2
  46. package/dist/providers.js +15 -1
  47. package/dist/redaction.d.ts +2 -1
  48. package/dist/redaction.js +3 -0
  49. package/dist/registry-options.d.ts +5 -0
  50. package/dist/registry-options.js +5 -0
  51. package/dist/rpc.d.ts +6 -2
  52. package/dist/rpc.js +71 -13
  53. package/dist/session-stores.d.ts +3 -1
  54. package/dist/session-stores.js +67 -6
  55. package/dist/skills.d.ts +4 -1
  56. package/dist/skills.js +3 -1
  57. package/dist/system-prompts.js +6 -2
  58. package/dist/testing/compaction-conformance.d.ts +17 -0
  59. package/dist/testing/compaction-conformance.js +61 -0
  60. package/dist/testing/extension-conformance.d.ts +26 -0
  61. package/dist/testing/extension-conformance.js +55 -0
  62. package/dist/testing/provider-conformance.d.ts +7 -0
  63. package/dist/testing/provider-conformance.js +18 -31
  64. package/dist/testing/session-store-conformance.d.ts +20 -0
  65. package/dist/testing/session-store-conformance.js +92 -0
  66. package/dist/testing/tool-conformance.d.ts +39 -0
  67. package/dist/testing/tool-conformance.js +79 -0
  68. package/dist/tools.d.ts +7 -2
  69. package/dist/tools.js +50 -13
  70. package/docs/agent-definitions.md +251 -0
  71. package/docs/agent-events.md +199 -0
  72. package/docs/agent-loops.md +217 -0
  73. package/docs/agent-session-runtime.md +20 -8
  74. package/docs/cli-rpc.md +39 -4
  75. package/docs/coding-agent-tools.md +208 -0
  76. package/docs/compaction-and-retry.md +2 -2
  77. package/docs/compaction-conformance.md +76 -0
  78. package/docs/compaction-llm.md +6 -3
  79. package/docs/compaction-observational-memory.md +4 -4
  80. package/docs/configuration-and-manifests.md +6 -1
  81. package/docs/context-and-skills.md +79 -6
  82. package/docs/contribution-discovery.md +149 -0
  83. package/docs/contribution-registries.md +9 -6
  84. package/docs/credentials-and-redaction.md +2 -0
  85. package/docs/customization.md +191 -0
  86. package/docs/database-persistence.md +407 -0
  87. package/docs/extension-authoring.md +193 -0
  88. package/docs/extension-conformance.md +80 -0
  89. package/docs/extensions.md +6 -0
  90. package/docs/host-security.md +141 -0
  91. package/docs/index.md +41 -19
  92. package/docs/input-and-prompt-assembly.md +19 -3
  93. package/docs/instruction-injection.md +183 -0
  94. package/docs/migration.md +201 -0
  95. package/docs/model-registry.md +122 -0
  96. package/docs/node-jsonl-session-store.md +5 -4
  97. package/docs/performance.md +127 -0
  98. package/docs/provider-caching.md +206 -0
  99. package/docs/provider-conformance.md +32 -5
  100. package/docs/provider-layer.md +51 -11
  101. package/docs/provider-packages.md +65 -5
  102. package/docs/provider-request-policies.md +113 -0
  103. package/docs/providers/kimi.md +22 -0
  104. package/docs/providers/neuralwatt.md +388 -0
  105. package/docs/providers/openai-compatible.md +1 -0
  106. package/docs/providers/openai.md +21 -0
  107. package/docs/providers/opencode-go.md +31 -3
  108. package/docs/providers/openrouter.md +29 -0
  109. package/docs/providers/zai.md +17 -0
  110. package/docs/public-contracts.md +87 -12
  111. package/docs/release-and-install.md +79 -27
  112. package/docs/runs-and-usage.md +236 -0
  113. package/docs/session-store-conformance.md +78 -0
  114. package/docs/session-stores-and-branching.md +10 -6
  115. package/docs/session-stores.md +126 -0
  116. package/docs/settings-auth-trust-security.md +18 -4
  117. package/docs/structured-output.md +247 -0
  118. package/docs/system-prompts.md +104 -2
  119. package/docs/tool-conformance.md +87 -0
  120. package/docs/tools.md +65 -8
  121. package/package.json +36 -2
@@ -0,0 +1,199 @@
1
+ # Agent events
2
+
3
+ ## What it does
4
+
5
+ `AgentEvent` is the single observable stream every `AgentSession` run emits. Subscribers receive normalized, redacted, in-order events covering agent lifecycle, assistant message streaming, tool execution, queue updates, subscriber overflow, compaction, retry, artifact validation/refinement, and terminal errors. The stream is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`; there is no durable queue, no background work, and no extra dependency.
6
+
7
+ Events are emitted by the runtime and by loops through `LoopContext.emit`, both of which route through `redactAgentEvent(event, activeRedactor)` so every payload is secret-redacted before subscribers observe it.
8
+
9
+ ## When to use it
10
+
11
+ Subscribe via `session.subscribe()` whenever a host needs to observe run progress: render streamed assistant text in a UI, react to tool execution, drive observability/telemetry, or audit artifact validation outcomes. Do not parse provider stream events directly for these — `AgentEvent` is the stable, normalized surface across providers and loops.
12
+
13
+ Do not use `AgentEvent` for durable replay (use a `SessionStore`) or for cross-session coordination (the broadcaster is per-session and live-only).
14
+
15
+ ## Durable event ledger
16
+
17
+ When `AgentConfig.runLedger` or `RunOptions.runLedger` is configured, every emitted `AgentEvent` is also persisted as an `AgentEventRecord` through the host adapter. The runtime calls `redactAgentEvent(event, activeRedactor)` before creating the record, sets `AgentEventRecord.redacted` to `true` when a redactor is active, and writes the record with the same `sessionId`, `runId`, and `timestamp`.
18
+
19
+ Event records preserve emission order within a run because the runtime drains pending event appends before writing the final `RunRecord`. Subscribers still see the live, in-memory stream; the ledger is the durable copy.
20
+
21
+ ## Inputs / request
22
+
23
+ ```ts
24
+ import type { AgentEvent } from "@arnilo/prism";
25
+
26
+ const subscription = session.subscribe({ maxQueuedEvents: 256, overflow: "close" });
27
+ for await (const event of subscription) {
28
+ switch (event.type) {
29
+ case "message_delta": // append event.content
30
+ case "tool_execution_started": // …
31
+ case "artifact_failed": // budget exhausted
32
+ break;
33
+ }
34
+ }
35
+ ```
36
+
37
+ The `AgentEvent` union (grouped by concern):
38
+
39
+ | Group | Variants |
40
+ | --- | --- |
41
+ | Agent lifecycle | `agent_started`, `agent_finished` |
42
+ | Turns | `turn_started`, `turn_finished` |
43
+ | Assistant messages | `message_started`, `message_delta`, `message_finished` |
44
+ | Tool execution | `tool_execution_started`, `tool_execution_progress`, `tool_execution_finished`, `tool_execution_error`, `tool_execution_blocked` |
45
+ | Queue/subscribers | `queue_updated`, `event_subscriber_overflow` |
46
+ | Compaction | `compaction_started`, `compaction_finished` |
47
+ | Retry | `retry_scheduled` |
48
+ | Artifacts | `artifact_validation_started`, `artifact_validation_finished`, `artifact_revision_started`, `artifact_finished`, `artifact_failed` |
49
+ | Errors | `error` |
50
+
51
+ ## Outputs / response / events
52
+
53
+ `AgentEvent` is a discriminated union on `type`. Common fields are `sessionId` and `runId` (both required on streaming/turn/artifact/tool events; `sessionId` is absent on pre-session `error`, `runId` is optional on compaction events).
54
+
55
+ Agent / turn / message events:
56
+
57
+ | Variant | Fields |
58
+ | --- | --- |
59
+ | `agent_started` | `sessionId`, `runId` |
60
+ | `agent_finished` | `sessionId`, `runId`, `usage?: Usage` |
61
+ | `turn_started` / `turn_finished` | `sessionId`, `runId`, `turn: number` |
62
+ | `message_started` / `message_finished` | `sessionId`, `runId`, `message: Message` |
63
+ | `message_delta` | `sessionId`, `runId`, `content: ContentBlock` (`tool_call_delta` fragments may appear here for live UI streaming; stored messages use final `tool_call` blocks) |
64
+
65
+ `message_delta.content.type === "tool_call_delta"` carries `{ index, id?, name?, argumentsText? }`. Treat it as a streaming fragment. The runtime reconstructs and persists a final `tool_call` before executing tools.
66
+
67
+ Tool execution events:
68
+
69
+ | Variant | Fields |
70
+ | --- | --- |
71
+ | `tool_execution_started` | `sessionId`, `runId`, `call: ToolCallContent` |
72
+ | `tool_execution_progress` | `sessionId`, `runId`, `toolCallId`, `name`, `progress?`, `metadata?` |
73
+ | `tool_execution_finished` | `sessionId`, `runId`, `result: ToolResult` |
74
+ | `tool_execution_error` | `sessionId`, `runId`, `call: ToolCallContent`, `error: ErrorInfo` |
75
+ | `tool_execution_blocked` | `sessionId`, `runId`, `toolCallId`, `name`, `reason: string`, `error: ErrorInfo` |
76
+
77
+ Queue / subscriber / compaction / retry events:
78
+
79
+ | Variant | Fields |
80
+ | --- | --- |
81
+ | `queue_updated` | `sessionId`, `runId`, `size: number` |
82
+ | `event_subscriber_overflow` | `sessionId`, `droppedEvents: number`, `maxQueuedEvents: number`, `overflow: "close" \| "drop_oldest" \| "drop_newest"` |
83
+ | `compaction_started` | `sessionId`, `runId?` |
84
+ | `compaction_finished` | `sessionId`, `runId?`, `summary: string` |
85
+ | `retry_scheduled` | `sessionId`, `runId`, `attempt: number`, `delayMs: number`, `error: ErrorInfo` |
86
+
87
+ Artifact validation/refinement events (emitted only by `generateValidateReviseLoop`; `singleShotLoop` emits zero artifact events):
88
+
89
+ | Variant | Fields |
90
+ | --- | --- |
91
+ | `artifact_validation_started` | `sessionId`, `runId`, `turn: number`, `attempt: number` |
92
+ | `artifact_validation_finished` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` |
93
+ | `artifact_revision_started` | `sessionId`, `runId`, `turn`, `attempt`, `failure: ArtifactValidation` |
94
+ | `artifact_finished` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` (loop ended successfully) |
95
+ | `artifact_failed` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` (budget exhausted) |
96
+
97
+ ### Artifact event ordering
98
+
99
+ A `generateValidateReviseLoop` run emits normal turn/message events for every provider turn, then a strictly ordered artifact sequence, correlated by `runId` / `turn` / `attempt`:
100
+
101
+ ```
102
+ turn_started
103
+ → message_started
104
+ → message_delta*
105
+ → message_finished
106
+ → turn_finished
107
+ → artifact_validation_started
108
+ → artifact_validation_finished
109
+ → artifact_revision_started # when a revision will run next
110
+ | artifact_finished # loop ended successfully
111
+ | artifact_failed # budget exhausted (maxRevisions+1 attempts)
112
+ ```
113
+
114
+ - `attempt` is 1-indexed per validation attempt and equals the provider `turn` within `generateValidateReviseLoop`; it mirrors `retry_scheduled.attempt` and the `tool_execution_*` block/finish pairing.
115
+ - Single-shot runs emit zero artifact events.
116
+ - **Validation failure triggering a revision is recoverable and never an `error`.** Only terminal budget exhaustion emits `artifact_failed`. The `error` channel is reserved for real failures (provider failures not caught by retry, aborts, etc.), matching the existing convention used by `tool_execution_blocked`.
117
+
118
+ ## Request/response example
119
+
120
+ ```json
121
+ {
122
+ "type": "event_subscriber_overflow",
123
+ "sessionId": "sess_01J...",
124
+ "droppedEvents": 257,
125
+ "maxQueuedEvents": 256,
126
+ "overflow": "close"
127
+ }
128
+ ```
129
+
130
+ ```json
131
+ {
132
+ "type": "artifact_revision_started",
133
+ "sessionId": "sess_01J...",
134
+ "runId": "run_01J...",
135
+ "turn": 1,
136
+ "attempt": 1,
137
+ "failure": {
138
+ "ok": false,
139
+ "errors": [{ "path": "title", "message": "missing field" }]
140
+ }
141
+ }
142
+ ```
143
+
144
+ ```json
145
+ {
146
+ "type": "artifact_failed",
147
+ "sessionId": "sess_01J...",
148
+ "runId": "run_01J...",
149
+ "turn": 4,
150
+ "attempt": 4,
151
+ "result": { "ok": false, "errors": [{ "message": "still invalid" }] }
152
+ }
153
+ ```
154
+
155
+ ## Implementation example
156
+
157
+ ```ts
158
+ import { createAgent, createMockProvider, providerTextDelta, providerDone, type AgentEvent, type ArtifactValidator } from "@arnilo/prism";
159
+
160
+ const validator: ArtifactValidator<unknown> = (v) =>
161
+ typeof v === "string" && v.length > 0 ? { ok: true } : { ok: false, errors: [{ message: "empty" }] };
162
+
163
+ const session = createAgent({
164
+ model: { provider: "mock", model: "demo" },
165
+ provider: createMockProvider([providerTextDelta("ok"), providerDone()]),
166
+ }).createSession();
167
+
168
+ for await (const event of session.subscribe()) {
169
+ if (event.type === "artifact_finished") console.log("artifact ok", event.attempt);
170
+ if (event.type === "artifact_failed") console.log("artifact exhausted", event.attempt, event.result.errors);
171
+ }
172
+
173
+ await session.run("draft", { loop: { strategy: "generate-validate-revise", validator, maxRevisions: 3 } });
174
+ ```
175
+
176
+ ## Extension and configuration notes
177
+
178
+ - All events flow through `redactAgentEvent(event, activeRedactor)` before subscribers observe them. Configure `AgentConfig.redactor` / `RunOptions.redactor` via `createSecretRedactor([...knownSecretStrings])` so secret values are redacted in `message` content, `errors[].message`, `metadata`, and artifact `result`/`failure` payloads.
179
+ - The artifact variants are emitted only by `generateValidateReviseLoop`. `singleShotLoop` (the default when no `AgentConfig.loop` / `RunOptions.loop` is set) emits zero artifact events. See [Agent loops](agent-loops.md).
180
+ - Subscribers are in-process; the broadcaster is in-memory and live-only. Multiple `subscribe()` calls receive the same stream.
181
+ - `session.subscribe(options)` accepts `maxQueuedEvents` (default `1024`, minimum `1`) and `overflow` (default `"close"`). The `close` policy clears queued payload events, queues one `event_subscriber_overflow` notice for that subscriber, then closes it. `drop_oldest` keeps the newest queued events; `drop_newest` ignores new events while full.
182
+ - The union is additive: new variants are appended without renumbering; subscribers should handle unknown `event.type` gracefully.
183
+
184
+ ## Security and performance notes
185
+
186
+ - The broadcaster is in-memory and live-only. No dependency, no timer, no filesystem/network discovery, no worker, no durable queue.
187
+ - Slow consumers are bounded by `SubscribeOptions`. Use `RunLedger` or host storage for durable replay; do not rely on a live subscriber as a queue.
188
+ - Redaction is exact-string-match only and opt-in via `createSecretRedactor`; values not passed as known secrets are not redacted.
189
+ - `ArtifactValidation.errors[].message` and `metadata` may echo model text; `redactAgentEvent` walks arbitrary nesting and replaces cyclic references with `"[Circular]"` (WeakSet cycle guard), so secret values in `result`/`failure` are redacted without crashing.
190
+ - `artifact_*` events are bounded by `maxRevisions + 1` validation attempts; an always-failing validator cannot loop forever and emits exactly one terminal `artifact_failed`.
191
+ - Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, tool results, or artifact validation payloads.
192
+
193
+ ## Related APIs
194
+ - [Agent/session runtime](agent-session-runtime.md): `session.subscribe()` and the live event broadcaster.
195
+ - [Agent loops](agent-loops.md): `singleShotLoop` and `generateValidateReviseLoop` emit the artifact events.
196
+ - [Structured output](structured-output.md): `ArtifactValidation` shape threaded through parser/validator/repairer.
197
+ - [Public contracts](public-contracts.md): full `AgentEvent` union and `ArtifactValidation` contract.
198
+ - [Tools](tools.md): `tool_execution_*` variants.
199
+ - [Compaction and retry policies](compaction-and-retry.md): `compaction_*` and `retry_scheduled` variants.
@@ -0,0 +1,217 @@
1
+ # Agent loops
2
+
3
+ ## What it does
4
+
5
+ Agent loops make the agent's per-run turn-control flow a replaceable strategy without forking the runtime. The runtime owns provider calls, retry, abort, store appends, redaction, and event emission; a loop only orchestrates those shared primitives through a `LoopContext`. The default `singleShotLoop` is the former inline turn loop extracted verbatim — assemble → generate → append assistant message → optional tool dispatch → next turn. `generateValidateReviseLoop` is the first alternative loop: generate → parse → validate → revise up to a budget.
6
+
7
+ Loops are opt-in. When no `loop` is configured, the runtime runs `singleShotLoop` and behavior is bit-for-bit with the pre-loop runtime.
8
+
9
+ - `singleShotLoop` — default; one-or-more provider turns with bounded tool rounds.
10
+ - `generateValidateReviseLoop(opts)` — factory returning a generate→validate→revise loop parameterized by host callbacks (`validator`, optional `parser`/`repairer`, `maxRevisions`).
11
+ - `resolveLoop(options, config)` — resolves `RunOptions.loop` (wins) over `AgentConfig.loop`, mapping `AgentLoopOptions` to a built-in strategy and passing through a custom `AgentLoopStrategy` instance.
12
+
13
+ The `Artifact*` contracts (`ArtifactValidation`, `ArtifactContext`, `ArtifactParseResult<T>`, `ArtifactParser<T>`, `ArtifactValidator<T>`, `ArtifactRepairer<T>`) are generic over a host-defined type `T`. Prism threads `T` through parser→validator→repairer; it never instantiates `T`. No domain control-flow vocabulary (`workflow`/`node`/`step`) appears in these contracts — the seam stays generic.
14
+
15
+ ## When to use it
16
+
17
+ Use the default `singleShotLoop` implicitly whenever you call `session.run()` — no configuration needed. Opt into `generateValidateReviseLoop` when a run should produce an artifact that must satisfy a host-supplied schema before it is considered complete (e.g. structured output, a validated JSON document, a generated file passing lint) and the host wants Prism to drive the revision turns.
18
+
19
+ Do not use a loop to re-implement provider calls, retry, abort, store, or event emission — those stay runtime-owned and are exposed to the loop only through `LoopContext`. A loop that needs tools in revision turns is out of scope for `generateValidateReviseLoop`; use `singleShotLoop` or supply a custom `AgentLoopStrategy`.
20
+
21
+ ## Inputs / request
22
+
23
+ ```ts
24
+ import {
25
+ createAgent,
26
+ generateValidateReviseLoop,
27
+ singleShotLoop,
28
+ resolveLoop,
29
+ type AgentLoopStrategy,
30
+ type AgentLoopOptions,
31
+ type LoopContext,
32
+ type ArtifactValidator,
33
+ type ArtifactParser,
34
+ type ArtifactRepairer,
35
+ type ArtifactValidation,
36
+ type ArtifactContext,
37
+ type ArtifactParseResult,
38
+ } from "@arnilo/prism";
39
+ ```
40
+
41
+ Per-run and per-agent loop selection (RunOptions wins):
42
+
43
+ ```ts
44
+ // AgentConfig.loop pins a loop for the agent/session.
45
+ const agent = createAgent({
46
+ model,
47
+ provider,
48
+ // optional default loop for this agent:
49
+ loop: { strategy: "single-shot" },
50
+ });
51
+
52
+ // RunOptions.loop overrides per request.
53
+ await session.run(input, {
54
+ loop: {
55
+ strategy: "generate-validate-revise",
56
+ validator: hostValidator,
57
+ parser: hostParser, // optional; default treats assistant text as the value
58
+ repairer: hostRepairer, // optional; default stringifies validation.errors[].message
59
+ maxRevisions: 3, // optional; default 3
60
+ },
61
+ });
62
+
63
+ // Custom loop escape hatch (a host-provided AgentLoopStrategy instance):
64
+ await session.run(input, { loop: myCustomLoop });
65
+ ```
66
+
67
+ `AgentLoopOptions` is the discriminated union:
68
+
69
+ ```ts
70
+ type AgentLoopOptions =
71
+ | { readonly strategy: "single-shot" }
72
+ | {
73
+ readonly strategy: "generate-validate-revise";
74
+ readonly validator: ArtifactValidator<unknown>;
75
+ readonly parser?: ArtifactParser<unknown>;
76
+ readonly repairer?: ArtifactRepairer<unknown>;
77
+ readonly maxRevisions?: number;
78
+ };
79
+ ```
80
+
81
+ Host callback contracts (all generic over host `T`):
82
+
83
+ | Contract | Shape |
84
+ | --- | --- |
85
+ | `ArtifactParser<T>` | `(text: string, ctx: ArtifactContext) => ArtifactParseResult<T> \| Promise<...>` — parse assistant text to a typed value. |
86
+ | `ArtifactValidator<T>` | `(value: T, ctx: ArtifactContext) => ArtifactValidation \| Promise<...>` — return `{ ok: true }` or `{ ok: false, errors }`. |
87
+ | `ArtifactRepairer<T>` | `(value: T \| undefined, failure: ArtifactValidation, ctx: ArtifactContext) => AgentInput \| Promise<...>` — build the revision follow-up input. |
88
+ | `ArtifactValidation` | `{ ok: boolean; errors?: readonly { path?: string; message: string }[]; metadata?: ... }`. |
89
+ | `ArtifactContext` | `{ sessionId, runId, turn, signal, metadata }` — passed to every callback. |
90
+ | `ArtifactParseResult<T>` | `{ ok: boolean; value?: T; error?: string }`. |
91
+
92
+ `LoopContext` (what the runtime builds for the loop each run):
93
+
94
+ | Field | Purpose |
95
+ | --- | --- |
96
+ | `sessionId`, `runId`, `metadata`, `signal` | Run identity and abort. |
97
+ | `history: Message[]` | Live mutable history — the loop pushes assistant and repair messages directly. |
98
+ | `input`, `inputMessages`, `maxToolRounds` | First-turn input, the redacted input messages, and the tool-round budget (single-shot parity hooks). |
99
+ | `assemble(nextInput, toolResults?)` | Wraps `assembleProviderInput()` with resolved skills/tools/context/system prompt/provider options. |
100
+ | `generate(request)` | Wraps provider request policies + `provider_request` middleware + `generateWithRetry()`; returns `ProviderTurnResult`. |
101
+ | `dispatchToolCall(call)` | Wraps `dispatchToolCall()` with resolved registry/middleware/permission/redactor/validate. |
102
+ | `appendMessage(message)` | Appends to the store under the run (redacted). |
103
+ | `emit(event)` | Emits a redacted `AgentEvent`. |
104
+
105
+ ## Outputs / response / events
106
+
107
+ `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` — the last provider usage, handed back to the runtime which emits `agent_finished` with it.
108
+
109
+ Events during a loop run are the existing `AgentEvent`s (`turn_started`, `message_started`, `message_delta`, `message_finished`, `turn_finished`, tool-execution events when the loop dispatches tools, `error` on real failures). Both built-in loops emit `turn_started` before each provider turn, `message_finished` for every assistant draft, and `turn_finished` after the assistant draft is appended. First-turn input is appended to live history once, matching the already-persisted user message.
110
+
111
+ Validation-failure-triggering-a-revision is **not** an `error` event — it is recoverable, like `tool_execution_blocked`. `generateValidateReviseLoop` emits normal turn/message events around each provider turn, then the artifact event sequence `artifact_validation_started` → `artifact_validation_finished` → (`artifact_revision_started`)* → `artifact_finished` (success) | `artifact_failed` (budget exhausted), correlated by `runId`/`turn`/`attempt`; see [Agent events § Artifact event ordering](agent-events.md#artifact-event-ordering). `singleShotLoop` emits zero artifact events. Real failures stay on the `error` channel.
112
+
113
+ A loop has no path to credentials, provider objects, or unredacted secrets. `LoopContext.generate` receives the already-policy-applied, middleware-run, redacted request; `LoopContext.emit` runs through `redactAgentEvent` with the active `SecretRedactor`.
114
+
115
+ ## Request/response example
116
+
117
+ ```ts
118
+ // Default single-shot run (no loop configured).
119
+ await session.run("Summarize the schema.");
120
+ ```
121
+
122
+ ```ts
123
+ // Generate-validate-revise with a host schema validator.
124
+ import { createAgent, type ArtifactValidator } from "@arnilo/prism";
125
+
126
+ const validator: ArtifactValidator<unknown> = (value, _ctx) =>
127
+ typeof value === "string" && value.length > 0
128
+ ? { ok: true }
129
+ : { ok: false, errors: [{ message: "empty artifact" }] };
130
+
131
+ await session.run("Write a one-line release note.", {
132
+ loop: { strategy: "generate-validate-revise", validator, maxRevisions: 3 },
133
+ });
134
+ ```
135
+
136
+ ## Implementation example
137
+
138
+ ```ts
139
+ import { createAgent, createMockProvider, providerTextDelta, providerDone, type ArtifactValidator, type ArtifactParser, type ArtifactRepairer } from "@arnilo/prism";
140
+
141
+ // Host owns the schema shape T. Prism never instantiates it.
142
+ interface JsonDoc { readonly title: string; readonly body: string }
143
+
144
+ const parser: ArtifactParser<JsonDoc> = (text) => {
145
+ try {
146
+ const value = JSON.parse(text) as JsonDoc;
147
+ return { ok: true, value };
148
+ } catch (error) {
149
+ return { ok: false, error: error instanceof Error ? error.message : "parse failed" };
150
+ }
151
+ };
152
+
153
+ const validator: ArtifactValidator<JsonDoc> = (value) =>
154
+ value.title && value.body
155
+ ? { ok: true }
156
+ : { ok: false, errors: [{ path: value.title ? "body" : "title", message: "missing field" }] };
157
+
158
+ const repairer: ArtifactRepairer<JsonDoc> = (_value, failure) => ({
159
+ role: "user",
160
+ content: [{ type: "text", text: `Fix these: ${failure.errors?.map((e) => e.message).join("; ")}` }],
161
+ });
162
+
163
+ const agent = createAgent({
164
+ model: { provider: "mock", model: "demo" },
165
+ provider: createMockProvider([
166
+ providerTextDelta(JSON.stringify({ title: "ok", body: "rev1" })),
167
+ providerDone(),
168
+ ]),
169
+ });
170
+
171
+ await agent.createSession().run("Produce the JSON doc.", {
172
+ loop: { strategy: "generate-validate-revise", validator, parser, repairer, maxRevisions: 3 },
173
+ });
174
+ ```
175
+
176
+ A custom loop is a plain object; pass it directly as `AgentLoopStrategy`:
177
+
178
+ ```ts
179
+ import { type AgentLoopStrategy } from "@arnilo/prism";
180
+
181
+ const twoShotLoop: AgentLoopStrategy = {
182
+ name: "two-shot",
183
+ async run(ctx) {
184
+ await ctx.generate(await ctx.assemble(ctx.input));
185
+ // ...orchestrate further turns via ctx primitives only...
186
+ return undefined;
187
+ },
188
+ };
189
+ await session.run(input, { loop: twoShotLoop });
190
+ ```
191
+
192
+ ## Extension and configuration notes
193
+
194
+ - `RunOptions.loop` wins over `AgentConfig.loop`; when neither is set the runtime uses `singleShotLoop`. This mirrors the other `RunOptions` overrides (`redactor`, `validate`, `activeSkills`).
195
+ - `{ strategy: "single-shot" }` resolves to the exported `singleShotLoop`; `{ strategy: "generate-validate-revise", ... }` is mapped by `resolveLoop()` to `generateValidateReviseLoop(opts)`. An unknown `strategy` throws before the first turn. Passing an `AgentLoopStrategy` instance bypasses the options form entirely (custom-loop escape hatch).
196
+ - The loop is resolved once per run inside `RuntimeAgentSession.run()`, after the usual setup (provider/skills/tools resolution, history rebuild, model-change entry, input append, auto-compaction). The runtime's outer try/catch/finally, run-exclusivity, abort bridging, and subscriber close remain in place around `loop.run(ctx)`.
197
+ - `LoopContext.assemble(nextInput, toolResults?)` accepts an optional tool-result accumulator so `singleShotLoop` can pass its loop-local `toolResults`; `generateValidateReviseLoop` omits it (no tools in revision turns).
198
+ - `maxToolRounds` bounds `singleShotLoop` tool rounds; `maxRevisions` (default 3) bounds `generateValidateReviseLoop` revision turns. Budget exhaustion ends the loop and returns the last usage; it does not throw.
199
+ - A revision cycle appends one assistant draft and one repair user message per revision to the session store, so store entries reflect every attempted draft. The original user input is stored once by the runtime and pushed into loop history once on the first turn.
200
+
201
+ ## Security and performance notes
202
+
203
+ - Loops have no path to credentials, provider objects, or unredacted secrets. `LoopContext.generate` consumes an already-redacted request; `LoopContext.emit` runs through `redactAgentEvent` with the active `SecretRedactor`; `LoopContext.appendMessage` appends a redacted entry.
204
+ - `ArtifactValidation.errors[].message` may echo model text — `artifact_*` event payloads flow through the same `redactAgentEvent` path as other `AgentEvent`s (see [Agent events](agent-events.md)).
205
+ - `generateValidateReviseLoop` makes at most `maxRevisions + 1` provider turns; it cannot loop forever on an always-failing validator. Each revision costs one provider turn plus one store append.
206
+ - The loop is a plain object/factory; no class hierarchy, no background work, no extra dependencies. `LoopContext` is a single object literal of bound arrows built once per run.
207
+ - The Synapta-free boundary is guarded by tests: `src/` imports no `synapta*` package, and the `Artifact*`/`AgentLoop*`/`LoopContext` contracts contain no `workflow`/`node`/`step` field names. Hosts supply their own schema; no host domain type is imported by `src/`.
208
+
209
+ ## Related APIs
210
+ - [Agent/session runtime](agent-session-runtime.md): `RuntimeAgentSession.run()` builds the `LoopContext` and delegates to the resolved loop.
211
+ - [Agent events](agent-events.md): the `artifact_*` event variants and ordering emitted by `generateValidateReviseLoop`.
212
+ - [Structured output](structured-output.md): the `ArtifactParser<T>`/`ArtifactValidator<T>`/`ArtifactRepairer<T>` seam (host-defined `T`, Prism never instantiates it) and a Synapta-style schema→`ArtifactValidation` mapping example.
213
+ - [Public contracts](public-contracts.md): `AgentLoopStrategy`, `AgentLoopOptions`, `LoopContext`, `ProviderTurnResult`, and the `Artifact*` contracts.
214
+ - [Input and prompt assembly](input-and-prompt-assembly.md): `assembleProviderInput()`, the primitive behind `LoopContext.assemble`.
215
+ - [Tools](tools.md): `dispatchToolCall()`, the primitive behind `LoopContext.dispatchToolCall`.
216
+ - [Compaction and retry policies](compaction-and-retry.md): `generateWithRetry()` and retry/compaction primitives the loop never re-implements.
217
+ - [Context and skills](context-and-skills.md): per-run skill/tool resolution feeding `LoopContext.assemble`.
@@ -10,7 +10,7 @@ The agent/session runtime adds the minimal shared SDK surface for running provid
10
10
  - `session.run(input, options)`
11
11
  - `session.prompt(input, options)`
12
12
  - `session.compact(options?)`
13
- - `session.subscribe()`
13
+ - `session.subscribe(options?)`
14
14
  - `session.abort()`
15
15
  - `session.entries()`
16
16
  - `session.checkout(leafId?)`
@@ -32,7 +32,7 @@ createAgent(config: AgentConfig): Agent
32
32
  createAgentSession(config: AgentSessionConfig & { agent: Agent }): AgentSession
33
33
  ```
34
34
 
35
- `AgentConfig.provider` must contain the host-selected provider. Prism does not resolve providers from hidden globals.
35
+ `AgentConfig.provider` must contain the host-selected provider. Prism does not resolve providers from hidden globals. Alternatively, set `AgentConfig.providerSource: ProviderResolver` (or override per run with `RunOptions.providerSource`, which wins) to resolve the provider from `model.provider` each run; when `AgentConfig.provider` is set it takes first precedence and the resolver is bypassed. See [Provider layer § Provider resolver](provider-layer.md#provider-resolver).
36
36
 
37
37
  `session.run(input, options)` accepts the existing Prism input shape:
38
38
 
@@ -42,11 +42,11 @@ string | Message | readonly Message[]
42
42
 
43
43
  `AgentSessionConfig.store` overrides `AgentConfig.store`; otherwise the session gets a private memory store. `AgentSessionConfig.leafId` selects the branch leaf to resume from.
44
44
 
45
- `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. `RunOptions.maxToolRounds` bounds repeated tool turns and defaults to `1`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
45
+ `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"legacy"` by default, or opt-in `"cache_aware"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. `RunOptions.maxToolRounds` bounds repeated tool turns and defaults to `1`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
46
46
 
47
47
  ## Outputs / response / events
48
48
 
49
- `session.subscribe()` returns a live `AsyncIterable<AgentEvent>`. Subscribe before `run()` to observe that run's events.
49
+ `session.subscribe(options?)` returns a live `AsyncIterable<AgentEvent>`. Subscribe before `run()` to observe that run's events. The consumer loop and `session.run()` must run concurrently (e.g. start the `for await` consumer, then `await Promise.all([consumer, session.run("Hi")])`): events are only emitted during a live run, so awaiting the subscribe loop before calling `run()` deadlocks. `SubscribeOptions.maxQueuedEvents` defaults to `1024` (minimum `1`) and caps events queued while the consumer is not awaiting `next()`. `SubscribeOptions.overflow` defaults to `"close"`; it clears queued payload events, delivers one `event_subscriber_overflow` notice to that subscriber, then closes it. `"drop_oldest"` keeps newest events; `"drop_newest"` ignores new events while full.
50
50
 
51
51
  For a text-only provider turn, the runtime emits:
52
52
 
@@ -58,7 +58,9 @@ For a text-only provider turn, the runtime emits:
58
58
  6. `turn_finished`
59
59
  7. `agent_finished`
60
60
 
61
- For complete tool calls, the runtime emits the assistant `tool_call` as `message_delta`, dispatches sequentially through `dispatchToolCall()`, emits tool execution events, appends a tool-result session entry, adds returned `ToolResult` values to the next provider turn, and stops when the provider returns no tool calls or `maxToolRounds` is reached. The next provider turn therefore receives the assistant `tool_call` followed by the matching role `tool` `tool_result` before any final assistant content.
61
+ For tool calls, the runtime streams provider `tool_call_delta` fragments as `message_delta` events for UI consumers, reconstructs the final `tool_call` with the same rules as provider conformance helpers, dispatches each complete call sequentially through `dispatchToolCall()`, emits tool execution events, appends an assistant tool-call session entry and a tool-result session entry, adds returned `ToolResult` values to the next provider turn, and stops when the provider returns no tool calls or `maxToolRounds` is reached. Deltas are live events only; persisted transcripts contain final `tool_call` blocks. The next provider turn therefore receives the assistant `tool_call` followed by the matching role `tool` `tool_result` before any final assistant content.
62
+
63
+ Provider `thinking`/`reasoning` content emitted during a turn is preserved as `thinking` content blocks on the assistant message in session history. On the next turn, provider packages decide how to carry prior reasoning forward. For example, the NeuralWatt provider serializes prior `thinking` blocks under a `reasoning_content` field for reasoning-capable models (gated on `capabilities.reasoning` / `compat.preserve_thinking`, droppable via `compat.clear_thinking`); see [NeuralWatt provider](providers/neuralwatt.md). Non-reasoning providers/models receive no reasoning field, so prior thinking does not leak into providers that do not support it.
62
64
 
63
65
  `session.compact(options?)` runs the selected compaction strategy, appends one `kind: "compaction"` entry under the current leaf, updates the leaf, emits `compaction_started` and `compaction_finished`, and returns the appended `CompactionResult`. If `AgentConfig.compaction` or `RunOptions.compaction` includes `thresholdEntries`, auto-compaction checks once after input/model-change entries are appended and before provider input assembly; `RunOptions.compaction: false` skips that run's auto-compaction.
64
66
 
@@ -113,7 +115,15 @@ await reader;
113
115
 
114
116
  ## Extension and configuration notes
115
117
 
116
- The runtime calls `assembleProviderInput()` on every turn and uses only values supplied on `AgentConfig`: `instructions`, `systemPrompt`, `inputBuilder`, `promptBuilder`, `context`, selected `skills`, active `tools`, `middleware`, `resourceLoader`, metadata, `compaction`, `retry`, and `RunOptions.model`/`systemPrompt`/`compaction`/`retry`. Contributions remain inert until a host passes selected values into the agent config.
118
+ The runtime calls `assembleProviderInput()` on every turn and uses only runtime-consumed values supplied on `AgentConfig`: `instructions`, `systemPrompt`, `inputBuilder`, `promptBuilder`, `inputLayout`, `context`, selected `skills`, active `tools`, `middleware`, `resourceLoader`, metadata, `compaction`, `retry`, and `RunOptions.model`/`systemPrompt`/`inputLayout`/`compaction`/`retry`. Contributions remain inert until a host passes selected values into the agent config.
119
+
120
+ `AgentConfig` fields that are host-owned metadata, not runtime work:
121
+
122
+ | Field | Runtime behavior |
123
+ | --- | --- |
124
+ | `extensions` | Preserved on `agent.config` only. `createAgent()` / `session.run()` do not call `setup()`, load packages, or auto-register contributions. Load extensions with `createExtensionKernel()` before building config. |
125
+ | `settings` | Preserved on `agent.config` only. The runtime does not call `settings.get()`; hosts or provider packages read settings before passing concrete runtime options. |
126
+ | `credentials` | Preserved on `agent.config` only. The runtime does not call `credentials.resolve()`; provider adapters/request policies resolve credentials at the provider edge and pass exact secret values to redaction when needed. |
117
127
 
118
128
  The runtime calls `middleware.run("compaction", { context, result })` after a compaction strategy returns and before appending the standard compaction entry. Middleware can adjust the result summary/data, but the runtime still owns store append ordering and branch parent ids.
119
129
 
@@ -121,7 +131,7 @@ Provider request policy application is one ordered in-memory pass per provider t
121
131
 
122
132
  The runtime calls `middleware.run("retry", { context, decision })` after the retry policy decision and before emitting `retry_scheduled`. Middleware can stop retrying or adjust the delay. Retry wraps only the current provider turn, reuses the same assembled request, and never retries after assistant output has been emitted.
123
133
 
124
- `createAgent()` is a thin wrapper over explicit config. It does not load `AgentConfig.extensions`, scan packages, resolve credentials, read settings, or consult hidden registries. External `AgentDefinition` implementations can call it from their own `create()` method:
134
+ `createAgent()` is a thin wrapper over explicit config. It does not load `AgentConfig.extensions`, scan packages, resolve credentials, read settings, call `Extension.setup()`, or consult hidden registries. External `AgentDefinition` implementations can call it from their own `create()` method:
125
135
 
126
136
  ```ts
127
137
  import { createAgent, createContributionRegistries } from "@arnilo/prism";
@@ -150,7 +160,7 @@ await agent.createSession().run("Hi", { model: overrideModel });
150
160
  - Compaction context contains branch entries and explicit compaction options only; it does not include provider objects, provider requests, credential resolvers, resolved credentials, settings, or hidden metadata.
151
161
  - Store entries contain explicit session data only; Prism does not store provider objects, credential resolvers, resolved credentials, full provider requests, settings, or hidden metadata.
152
162
  - Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, or docs examples.
153
- - The event broadcaster is in-memory and live-only. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue.
163
+ - The event broadcaster is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue.
154
164
 
155
165
  ## Related APIs
156
166
 
@@ -164,4 +174,6 @@ await agent.createSession().run("Hi", { model: overrideModel });
164
174
  - [Middleware hooks](middleware-hooks.md): hooks that configured assembly/runtime can run.
165
175
  - [CLI/RPC](cli-rpc.md): terminal and JSONL adapters over this runtime.
166
176
 
177
+ `AgentConfig.loop` and `RunOptions.loop` select a replaceable per-run control loop (`singleShotLoop` default, or `generate-validate-revise` with host callbacks); see [Agent loops](agent-loops.md). `RunOptions.loop` wins over `AgentConfig.loop`. Built-in loops emit the same normal turn/message envelope around provider turns, and both add the first run input to live history once after the first provider turn so later turns see the same transcript shape.
178
+
167
179
  `AgentConfig.redactor` and `RunOptions.redactor` redact exact known secret strings from provider requests, emitted events, and stored session entries. Redaction is opt-in and exact-match only.
package/docs/cli-rpc.md CHANGED
@@ -35,6 +35,16 @@ CLI flags:
35
35
  | `--context <text>` | Context text reserved for host adapters. |
36
36
  | `--compact <entries>` | Auto-compaction threshold for the run. |
37
37
  | `--max-tool-rounds <n>` | Maximum runtime tool rounds. |
38
+ | `--discover` | Opt-in workspace contribution discovery (`SKILL.md`/`manifest.json`). Never auto-activates or imports. |
39
+ | `--discover-kinds <csv>` | Kinds to scan; defaults to `skill`. Accepts `skill,tool,context,instructions`. |
40
+ | `--no-discovery` | Hard-disable discovery even if `--discover` is set. |
41
+ | `--agents-config <path>` | App config root holding `agents/<name>/AGENT.md` bundles (opt-in). Envelopes only; the host resolves them via `resolveAgentBundle`. The CLI never defaults to the user's home directory. |
42
+ | `--instruction <name>` | Select a registered/discovered instruction injector (repeatable). `--instruction false` disables injectors for the run. Names resolve fail-closed. |
43
+ | `--injector-file <path>` | Load a markdown file as a static `every_turn` injector (repeatable). |
44
+ | `--no-agents-md` | Skip auto-loading `<workspaceRoot>/AGENTS.md` (Phase 31). |
45
+ | `--no-system-md` | Skip auto-loading the global `SYSTEM.md` layer (Phase 31). The CLI does not default to the user's home directory; pass `globalRoot` from a host adapter or use `--system-md-file` to opt in. |
46
+ | `--agents-md-file <path>` | Read AGENTS.md from `<path>` instead — still `source: "app"`, still trust-gated (Phase 31). |
47
+ | `--system-md-file <path>` | Read SYSTEM.md from `<path>` instead — user-owned, `source: "user"` (Phase 31). |
38
48
  | `--help` | Print usage. |
39
49
 
40
50
  RPC request envelope:
@@ -43,7 +53,9 @@ RPC request envelope:
43
53
  { id: string | number; command: string; params?: Record<string, unknown> }
44
54
  ```
45
55
 
46
- Supported command names: `prompt`, `steer`, `followUp`, `abort`, `state`, `messages`, `setModel`, `compact`, `switchSession`, `forkSession`, `cloneSession`, and `command`.
56
+ Supported command names: `prompt`, `steer`, `followUp`, `abort`, `state`, `messages`, `setModel`, `compact`, `switchSession`, `forkSession`, `cloneSession`, `checkout`, and `command`.
57
+
58
+ The `prompt` and `followUp` params accept an optional `instructionInjectors?: readonly string[]` field (Phase 30). Names resolve against the `instructionInjectors` registry passed to `runRpcServer({ instructionInjectors })`; an unknown name fails closed (error response correlated to the request `id`, no provider call).
47
59
 
48
60
  ## Outputs / response / events
49
61
 
@@ -63,6 +75,19 @@ RPC writes responses and async events:
63
75
  { type: "event"; id: string | number; sessionId?: string; runId?: string; event: AgentEvent }
64
76
  ```
65
77
 
78
+ Branch-aware session commands return live handle details:
79
+
80
+ ```ts
81
+ {
82
+ sessionId: string;
83
+ leafId?: string;
84
+ handleId: string;
85
+ handles?: readonly { handleId: string; sessionId: string; leafId?: string }[]; // state only
86
+ }
87
+ ```
88
+
89
+ `sessionId` identifies the durable session. `leafId` is the selected branch tip. `handleId` is the RPC map key used by `switchSession`; forks that share the same `sessionId` get stable ids like `session-1#2` so the parent handle is not overwritten.
90
+
66
91
  Invalid CLI flags return exit code `2`. Invalid JSON, missing ids, unknown RPC commands, unsupported `steer`, unknown command contributions, and runtime failures return `ok: false` response envelopes without executing unknown tools or commands.
67
92
 
68
93
  ## Request/response example
@@ -71,6 +96,10 @@ Invalid CLI flags return exit code `2`. Invalid JSON, missing ids, unknown RPC c
71
96
  {"id":"1","command":"prompt","params":{"input":"Hi"}}
72
97
  {"type":"event","id":"1","sessionId":"s1","runId":"run_1","event":{"type":"message_delta"}}
73
98
  {"id":"1","ok":true,"result":{"sessionId":"s1"}}
99
+ {"id":"2","command":"forkSession","params":{"leafId":"entry_1"}}
100
+ {"id":"2","ok":true,"result":{"sessionId":"s1","leafId":"entry_1","handleId":"s1#2"}}
101
+ {"id":"3","command":"checkout","params":{"leafId":"entry_5"}}
102
+ {"id":"3","ok":true,"result":{"sessionId":"s1","leafId":"entry_5","handleId":"s1#2"}}
74
103
  ```
75
104
 
76
105
  ## Active run behavior
@@ -78,7 +107,7 @@ Invalid CLI flags return exit code `2`. Invalid JSON, missing ids, unknown RPC c
78
107
  `prompt` and `followUp` start an asynchronous run and write their final response only when the run finishes. While a run is active, the RPC loop continues to read and respond to other requests:
79
108
 
80
109
  - `abort` cancels the active run for the current session and responds immediately.
81
- - `state`, `messages`, `setModel`, `switchSession`, `forkSession`, `cloneSession`, and registered `command` requests are processed immediately.
110
+ - `state`, `messages`, `setModel`, `switchSession`, `forkSession`, `cloneSession`, `checkout`, and registered `command` requests are processed immediately.
82
111
  - `compact` is fail-closed: if the current session has an active run, it returns `ok: false` because the session rejects compaction during a run.
83
112
  - A second `prompt` or `followUp` for the same session while it already has an active run returns `ok: false` immediately instead of blocking the input loop.
84
113
 
@@ -116,7 +145,9 @@ await agent.createSession({ id: "s1" }).run("Hi");
116
145
 
117
146
  CLI/RPC are adapters over `AgentSession`. They do not scan packages, import extensions, read config files, fetch resources, resolve credentials, or register tools unless a host adapter explicitly wires those primitives in.
118
147
 
119
- RPC `command` executes only explicitly registered `CommandDefinition` values. `setModel` stores a model override for later prompt/follow-up calls. `compact`, `switchSession`, `forkSession`, and `cloneSession` call the existing session APIs.
148
+ RPC `command` executes only explicitly registered `CommandDefinition` values. `setModel` stores a model override for later prompt/follow-up calls. `compact`, `switchSession`, `forkSession`, `cloneSession`, and `checkout` call the existing session APIs.
149
+
150
+ `forkSession` creates another handle for the same `sessionId` and selected `leafId`; it no longer overwrites the parent handle in the RPC map. Keep the returned `handleId` when a UI needs to switch among sibling branches. `switchSession` accepts `handleId` (preferred), `sessionId`, or `id`; with multiple branch handles, use `handleId` to avoid ambiguity. `checkout` requires `params.leafId`, calls `AgentSession.checkout(leafId)`, and keeps the active handle id unchanged while moving that handle to the existing leaf. `messages` returns entries for the active branch path.
120
151
 
121
152
  ## Security and performance notes
122
153
 
@@ -125,6 +156,7 @@ RPC `command` executes only explicitly registered `CommandDefinition` values. `s
125
156
  - No full TUI or sandbox is provided or implied.
126
157
  - JSONL is processed line by line with Node stdlib; no parser dependency, worker, watcher, or queue is added.
127
158
  - Unknown or malformed CLI/RPC input fails closed.
159
+ - Branch handles (`handleId`, `sessionId`, `leafId`) are identifiers only; do not encode credentials, tokens, provider objects, or secrets into them.
128
160
  - Do not put resolved credential values, tokens, headers, or secrets in prompts, CLI flags, config, events, or docs examples.
129
161
 
130
162
  ## Related APIs
@@ -132,9 +164,12 @@ RPC `command` executes only explicitly registered `CommandDefinition` values. `s
132
164
  - [Agent/session runtime](agent-session-runtime.md): runtime API used by CLI/RPC.
133
165
  - [Contribution registries](contribution-registries.md): command contributions are inert until explicitly wired.
134
166
  - [Configuration and manifests](configuration-and-manifests.md): config data stays separate from CLI/RPC execution.
167
+ - [Contribution discovery (workspace)](contribution-discovery.md): the `--discover` / `--discover-kinds` / `--no-discovery` flags fill host registries; no `import()`, no auto-activate.
135
168
  - [Node filesystem config loader](node-filesystem-config.md): optional explicit config file loading for Node hosts.
136
169
  - [Resource loading](resource-loading.md): explicit resource loading primitives.
137
170
  - [Credentials and redaction](credentials-and-redaction.md): secret redaction helpers and credential boundaries.
138
171
  - [Observational memory compaction package](compaction-observational-memory.md): optional `om:status` and `om:view` command factories for explicitly wired hosts.
139
172
 
140
- The CLI records flags but does not auto-load project-local resources, extensions, tools, or config. Hosts must make explicit trust and permission decisions before wiring any future local loading.
173
+ The CLI records flags but does not auto-load project-local resources, extensions, tools, or config. The two system/project prompt files are the exception: in print/json modes the CLI auto-loads `<workspaceRoot>/AGENTS.md` (trust-gated) and an app-supplied `SYSTEM.md` layer as `AgentConfig.systemPrompt` layers composed with `--system` (base); `--no-agents-md` / `--no-system-md` skip them and `--agents-md-file` / `--system-md-file` override the paths. The CLI does not default `globalRoot` to the user's home directory — pass it from a host adapter or use `--agents-config <path>` for the app-config bundle layout. RPC mode does not auto-read these files (the host owns the session factory). Hosts must make explicit trust and permission decisions before wiring any other local loading.
174
+
175
+ For app-controlled agent bundles under `<configRoot>/agents/<name>/AGENT.md` (including the three-layer `SYSTEM.md` → `AGENT.md` body → repo `AGENTS.md` prompt append and the union skill/tool scopes), see [Agent definitions](agent-definitions.md).