@theokit/sdk 4.2.1 → 4.2.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 (90) hide show
  1. package/CHANGELOG.md +103 -86
  2. package/README.md +5 -5
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js.map +1 -1
  5. package/dist/a2a/subagent.d.cts +2 -2
  6. package/dist/a2a/subagent.d.ts +2 -2
  7. package/dist/compaction.cjs.map +1 -1
  8. package/dist/compaction.d.cts +1 -1
  9. package/dist/compaction.d.ts +1 -1
  10. package/dist/compaction.js.map +1 -1
  11. package/dist/{cron-Y_aM4m4-.d.cts → cron-CWQPZTwX.d.cts} +2 -2
  12. package/dist/{cron-pjzn3jxf.d.ts → cron-DVM9f1I_.d.ts} +2 -2
  13. package/dist/cron.cjs.map +1 -1
  14. package/dist/cron.d.cts +2 -2
  15. package/dist/cron.d.ts +2 -2
  16. package/dist/cron.js.map +1 -1
  17. package/dist/{errors-B4QB3-Wu.d.ts → errors-CG2RpeW-.d.ts} +1 -1
  18. package/dist/{errors-B03TBTc_.d.cts → errors-gE8612p9.d.cts} +1 -1
  19. package/dist/errors.d.cts +2 -2
  20. package/dist/eval.cjs.map +1 -1
  21. package/dist/eval.js.map +1 -1
  22. package/dist/filesystem/index.cjs.map +1 -1
  23. package/dist/filesystem/index.js.map +1 -1
  24. package/dist/filesystem/local-filesystem.d.cts +1 -1
  25. package/dist/filesystem/local-filesystem.d.ts +1 -1
  26. package/dist/filesystem/types.d.cts +1 -1
  27. package/dist/filesystem/types.d.ts +1 -1
  28. package/dist/index.cjs +1 -9
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +10 -10
  31. package/dist/index.d.ts +10 -10
  32. package/dist/index.js +1 -9
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/agent-loop/doom-loop-tracker.d.ts +1 -1
  35. package/dist/internal/budget/usage-accumulator.d.ts +1 -1
  36. package/dist/internal/llm/model-identifier.d.ts +1 -1
  37. package/dist/internal/llm/ollama-native.d.ts +2 -2
  38. package/dist/internal/llm/openai.d.ts +1 -1
  39. package/dist/internal/llm/router.d.ts +3 -4
  40. package/dist/internal/llm/vertex-gemini.d.ts +1 -1
  41. package/dist/internal/memory/adapters/ollama-embedding.d.ts +1 -1
  42. package/dist/internal/memory/embedding-adapter.d.ts +3 -3
  43. package/dist/internal/persistence/index.cjs.map +1 -1
  44. package/dist/internal/persistence/index.js.map +1 -1
  45. package/dist/internal/providers/builtin/vertex.d.ts +1 -1
  46. package/dist/internal/resilience/circuit-breaker.d.ts +1 -1
  47. package/dist/internal/runtime/concurrency/abort-utils.d.ts +1 -1
  48. package/dist/internal/runtime/lifecycle/stream-to-completion.d.ts +1 -1
  49. package/dist/internal/runtime/registry/run-registry.d.ts +0 -1
  50. package/dist/internal/runtime/skills/skill-frontmatter.d.ts +0 -1
  51. package/dist/internal/task/registry.d.ts +0 -3
  52. package/dist/internal/telemetry/span-names.d.ts +0 -1
  53. package/dist/internal/workflow/telemetry.d.ts +0 -2
  54. package/dist/messages.cjs.map +1 -1
  55. package/dist/messages.d.cts +1 -1
  56. package/dist/messages.d.ts +1 -1
  57. package/dist/messages.js.map +1 -1
  58. package/dist/models.cjs.map +1 -1
  59. package/dist/models.js.map +1 -1
  60. package/dist/path-safety.d.cts +1 -2
  61. package/dist/path-safety.d.ts +1 -2
  62. package/dist/permission-engine.d.ts +3 -3
  63. package/dist/persistence.cjs.map +1 -1
  64. package/dist/persistence.js.map +1 -1
  65. package/dist/project.cjs.map +1 -1
  66. package/dist/project.js.map +1 -1
  67. package/dist/{run-CqihtbtU.d.cts → run-DFM1H2jW.d.cts} +8 -8
  68. package/dist/{run-CqihtbtU.d.ts → run-DFM1H2jW.d.ts} +8 -8
  69. package/dist/skills.cjs.map +1 -1
  70. package/dist/skills.js.map +1 -1
  71. package/dist/subscription/index.cjs.map +1 -1
  72. package/dist/subscription/index.js.map +1 -1
  73. package/dist/subscription/internal/sse-encoder.d.cts +1 -1
  74. package/dist/subscription/internal/sse-encoder.d.ts +1 -1
  75. package/dist/subscription/internal/sse-parser.d.cts +1 -1
  76. package/dist/subscription/internal/sse-parser.d.ts +1 -1
  77. package/dist/task-store.cjs.map +1 -1
  78. package/dist/task-store.js.map +1 -1
  79. package/dist/types/agent.d.ts +1 -1
  80. package/dist/types/mcp.d.ts +1 -1
  81. package/dist/types/processors.d.ts +1 -1
  82. package/dist/types/run.d.ts +2 -2
  83. package/dist/types/usage.d.ts +1 -1
  84. package/dist/workflow.cjs +1 -9
  85. package/dist/workflow.cjs.map +1 -1
  86. package/dist/workflow.d.cts +1 -1
  87. package/dist/workflow.d.ts +1 -1
  88. package/dist/workflow.js +1 -9
  89. package/dist/workflow.js.map +1 -1
  90. package/package.json +2 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - docs: professionalized the README for OSS publication and made the exported TypeScript types the canonical public contract. Removed the drifted `docs.md` API-contract file (it still documented the old `nextTheo` pagination field while the code returns `nextCursor`); `docs/harness-capability-map.md` + `docs/error-codes.md` are the human-friendly references. Fixed broken links, placeholder prose, a wrong API name in an example, a stale version claim, and capability claims that belong to other packages. No code or public API change.
8
+
9
+ ## 4.2.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Internal cleanup + dead-code hardening (no public-API change; exported names byte-stable).
14
+
15
+ - Removed 21 dead internal symbols the `quality:dead` gate could not see (knip ignores `src/internal/**`), plus dead test-seam helpers, with cascade cleanups.
16
+ - Added a `quality:dead-internal` gate (`tools/check-internal-deadcode.mjs`) that fails the build on any orphaned `**/internal/**` export OR unused private top-level declaration — closing the internal dead-code blind-spot.
17
+ - Tightened `tsconfig.base.json` (`noUnusedLocals` + `noUnusedParameters` + `allowUnreachableCode: false`) and removed the write-only `_truncated` flag in `@theokit/sdk-tools` shell-exec (capping behavior unchanged).
18
+ - Repo/docs hygiene: trimmed `docs/` to 3 files (the code is the documentation), consolidated the knowledge base, and cleaned repository history.
19
+
3
20
  ## 4.2.1
4
21
 
5
22
  ### Patch Changes
@@ -202,9 +219,9 @@
202
219
 
203
220
  - ea3cd14: **SE35 — schedule a workflow on the `Cron` primitive (`workflow` + `inputData`).**
204
221
 
205
- A `Cron` job may now target a committed `Workflow` (SE27–30) instead of an agent — the runtime-legitimate slice of Mastra's Schedules. `Cron.create({ cron, workflow, inputData })` runs `workflow.run(inputData)` on each fire, reusing the shipped in-process scheduler + Task-registry observability. Mutually exclusive with agent targets: exactly one of `agent` | `agentId` | `workflow`; `message` is required for agent targets and forbidden with a workflow (typed `ConfigurationError`s: `cron_ambiguous_target` / `cron_no_target` / `cron_workflow_message` / `cron_missing_message`). `Cron.run(jobId)` returns `Run | WorkflowRun`; the fire handler records the correct terminal status for either shape.
222
+ A `Cron` job may now target a committed `Workflow` (SE27–30) instead of an agent. `Cron.create({ cron, workflow, inputData })` runs `workflow.run(inputData)` on each fire, reusing the shipped in-process scheduler + Task-registry observability. Mutually exclusive with agent targets: exactly one of `agent` | `agentId` | `workflow`; `message` is required for agent targets and forbidden with a workflow (typed `ConfigurationError`s: `cron_ambiguous_target` / `cron_no_target` / `cron_workflow_message` / `cron_missing_message`). `Cron.run(jobId)` returns `Run | WorkflowRun`; the fire handler records the correct terminal status for either shape.
206
223
 
207
- Per ADR 0014, the job holds the `Workflow` **instance** (not a `workflowId` + resolver registry) — the cron store is in-memory, so there is no serialization problem to solve and a registry would be YAGNI; workflow cron jobs are local-runtime only (an instance can't cross the cloud boundary). Fire lifecycle hooks (`prepare`/`onFinish`/`onError`/`onAbort`) are deferred with a named re-eval trigger. Back-compat: agent-target jobs are byte-identical. From the Mastra Schedules comparison (SDK Evolution roadmap SE35).
224
+ Per ADR 0014, the job holds the `Workflow` **instance** (not a `workflowId` + resolver registry) — the cron store is in-memory, so there is no serialization problem to solve and a registry would be YAGNI; workflow cron jobs are local-runtime only (an instance can't cross the cloud boundary). Fire lifecycle hooks (`prepare`/`onFinish`/`onError`/`onAbort`) are deferred with a named re-eval trigger. Back-compat: agent-target jobs are byte-identical. (SDK Evolution roadmap SE35.)
208
225
 
209
226
  ## 2.29.0
210
227
 
@@ -219,16 +236,16 @@
219
236
  - **Standing `goal` config** — `AgentOptions.goal` (`{ judgeModel?, maxRuns?, prompt? }`). Precedence (remembered in the record): per-objective `record.options` → standing `goal` config → built-in default (`maxRuns` 20). The judge is the activation switch: with no judge resolved, the standing objective is inert (no scoring, no budget consumed).
220
237
  - **`runUntil(goal?, options?)`** — `goal` is now OPTIONAL. Existing callers pass `goal` (unchanged transient behavior). Omitting `goal` with `options.threadId` set reads the durable objective, resolves options by precedence, caps per-call `maxTurns` by the remaining durable budget, runs the loop, and writes `runsUsed`/`status` back — `maxTurns` exhaustion leaves the objective `active` so raising `maxRuns` later resumes. Omitting `goal` with no objective (or no `threadId`) yields a single `status_change: paused` and never throws.
221
238
 
222
- Reuses existing seams only (the shipped `runUntil` loop + `ConversationStorage`) — no new loop, no parallel runtime, no in-agentic-loop step (that is SE34). ADR 0012. From the Mastra Goals comparison (SDK Evolution roadmap SE33).
239
+ Reuses existing seams only (the shipped `runUntil` loop + `ConversationStorage`) — no new loop, no parallel runtime, no in-agentic-loop step (that is SE34). ADR 0012. (SDK Evolution roadmap SE33.)
223
240
 
224
- - a8bed75: **SE34 — per-send `isTaskComplete` + `<current-objective>` projection (Mastra Goals parity, non-invasive half).**
241
+ - a8bed75: **SE34 — per-send `isTaskComplete` + `<current-objective>` projection (non-invasive half).**
225
242
 
226
243
  Two opt-in `SendOptions`, both byte-identical to today when unused. The loop-touching in-agentic-loop goal step is DEFERRED with a named re-eval trigger (ADR 0013).
227
244
 
228
245
  - **`completionCheck` (`isTaskComplete`)** — `send(msg, { completionCheck: { criteria, judgeModel?, apiKey? } })`. After the send reaches terminal `finished`, the shipped LLM-as-judge scores the final reply against `criteria`; the verdict surfaces on `RunResult.completionCheck` (`{ complete, reason, parseFailed }`) AND a typed `completion_check` run-event. This is the finer-grained, single-`send()` completion gate (contrast `runUntil`, which judges BETWEEN sends). Implemented as an outermost run wrapper — only judges a `finished` run with text, memoized (the judge fires exactly once), fail-safe (a judge parse failure yields `complete: false`, never a silent "done").
229
246
  - **`objectiveThreadId` (`<current-objective>` projection)** — `send(msg, { objectiveThreadId })` reads the SE33 durable objective for that thread and, when it is `active`, prepends a `<current-objective>…</current-objective>` block to the assembled system prompt for that send, so the model always sees what it is working toward. Minimal + fail-soft (a storage read error never breaks the send) — not a general signal-provider framework.
230
247
 
231
- Both reuse shipped seams (the run-wrapping seam + `judgeCallImpl` for the check; the SE33 objective store + system-prompt assembly for the projection). The agent tool-calling loop is UNTOUCHED. From the Mastra Goals comparison (SDK Evolution roadmap SE34).
248
+ Both reuse shipped seams (the run-wrapping seam + `judgeCallImpl` for the check; the SE33 objective store + system-prompt assembly for the projection). The agent tool-calling loop is UNTOUCHED. (SDK Evolution roadmap SE34.)
232
249
 
233
250
  ## 2.28.0
234
251
 
@@ -238,7 +255,7 @@
238
255
 
239
256
  A pluggable filesystem _storage_ provider, the storage-side twin of `@theokit/sdk/sandbox`. `FilesystemBackend` is an abstract class with four methods (`readFile` / `writeFile` / `stat` / `list`), an `exists()` derived on the base, a boundary `basePath`, a `readOnly` flag, structured `stat().mtimeMs` (the read-before-write oracle for SE32), and typed errors (`FileNotFoundError` / `FilesystemSecurityError` / `FilesystemReadOnlyError` / `StaleFileError`). `LocalFilesystem` is the local-process implementation, boundary-enforced by reusing the core path-guard (traversal + symlink escape → `FilesystemSecurityError`). `FilesystemProvider` + `resolveFilesystem` support a per-request resolver `(ctx) => FilesystemBackend` for multi-tenant roots.
240
257
 
241
- Unlike `SandboxBackend` (whose file ops shell out via `execute`, require command execution, and give no structured `stat`), a `FilesystemBackend` serves a filesystem-only workspace with no sandbox — see ADR 0011 for why file ops are NOT routed through `SandboxBackend`. `@theokit/sdk-tools`' `createWriteFileTool` now accepts an optional `filesystem` backend (writes route through it; omitted ⇒ identical local-`projectRoot` behavior). This is the backend seam, NOT a bundled `Workspace` and NOT a new toolset — bring-your-own-tools stands; `mounts`/FUSE, S3/GCS, and LSP remain out of core. From the Mastra Workspaces comparison (SDK Evolution roadmap SE31).
258
+ Unlike `SandboxBackend` (whose file ops shell out via `execute`, require command execution, and give no structured `stat`), a `FilesystemBackend` serves a filesystem-only workspace with no sandbox — see ADR 0011 for why file ops are NOT routed through `SandboxBackend`. `@theokit/sdk-tools`' `createWriteFileTool` now accepts an optional `filesystem` backend (writes route through it; omitted ⇒ identical local-`projectRoot` behavior). This is the backend seam, NOT a bundled `Workspace` and NOT a new toolset — bring-your-own-tools stands; `mounts`/FUSE, S3/GCS, and LSP remain out of core. (SDK Evolution roadmap SE31.)
242
259
 
243
260
  ## 2.27.0
244
261
 
@@ -248,25 +265,25 @@
248
265
 
249
266
  `Workflow.create({ ..., inputSchema, outputSchema })` (from `@theokit/sdk/workflow`) now validates the workflow's overall input and final output, closing the SE19 debt (a Workflow carried no top-level schema — only per-step `FnStep` schemas). When `inputSchema` is set, `run(input)` validates `input` BEFORE step 1; a mismatch fails fast with `status: "failed"` and a typed `WorkflowInputError` in `run.error` (no step executes, no silent coerce). When `outputSchema` is set, the terminal `completed` output is validated before `WorkflowRun.output` is populated; a mismatch yields `status: "failed"` with a typed `WorkflowOutputError` (only the `completed` path is checked — suspended/failed runs skip output validation).
250
267
 
251
- Both surface as `status: "failed"` (never a throw — consistent with the executor's non-throwing step-error contract). Back-compat: absent schemas ⇒ unchanged. New exports `WorkflowInputError` / `WorkflowOutputError`. `workflowAsTool` (SE19) keeps taking its own `inputSchema` to preserve its structural `{ run }` contract. Mirrors Mastra's `createWorkflow({ inputSchema, outputSchema })`. From the Mastra Workflows comparison (SDK Evolution roadmap SE27).
268
+ Both surface as `status: "failed"` (never a throw — consistent with the executor's non-throwing step-error contract). Back-compat: absent schemas ⇒ unchanged. New exports `WorkflowInputError` / `WorkflowOutputError`. `workflowAsTool` (SE19) keeps taking its own `inputSchema` to preserve its structural `{ run }` contract. (SDK Evolution roadmap SE27.)
252
269
 
253
270
  - 09b89ea: **SE28 — `Workflow.stream()` (step-event stream during execution).**
254
271
 
255
272
  `workflow.stream(input, opts?)` (from `@theokit/sdk/workflow`) runs the workflow and emits step-level events as they happen, instead of only the terminal result. It returns a `WorkflowStream` — an async iterator of `WorkflowEvent`s (`step_started` / `step_completed` (with `output`) / `step_failed` (with `error`) / `workflow_suspended` / `workflow_completed`) plus a `result` promise resolving to the SAME terminal `WorkflowRun` `run()` returns (the authoritative outcome — the stream ends when the run terminates).
256
273
 
257
- Events fire in execution order for top-level steps (nested `parallel`/`branch`/`foreach` emit as their single wrapping step — coarse-grained by design). This is a STEP-event stream, distinct from the token-delta agent stream deferred in SE24. `run()` is unchanged + authoritative. New public types `WorkflowEvent` + `WorkflowStream`. Mirrors Mastra's `run.stream()` / `stream.result`. From the Mastra Workflows comparison (SDK Evolution roadmap SE28).
274
+ Events fire in execution order for top-level steps (nested `parallel`/`branch`/`foreach` emit as their single wrapping step — coarse-grained by design). This is a STEP-event stream, distinct from the token-delta agent stream deferred in SE24. `run()` is unchanged + authoritative. New public types `WorkflowEvent` + `WorkflowStream`. (SDK Evolution roadmap SE28.)
258
275
 
259
276
  - f13d499: **SE29 — workflow shared state (`stateSchema` + `state` / `setState`).**
260
277
 
261
278
  Workflow steps can now share values without threading them through every step's input/output. `Workflow.create({ stateSchema, initialState })` (from `@theokit/sdk/workflow`) seeds a shared state; every step's `StepContext` gains `state` (read the current value) and `setState(next)` (update it for subsequent steps). `setState` validates against `stateSchema` when set — a mismatch throws a typed `WorkflowStateError` that fails the step/run (Rule 8); an invalid `initialState` fails the run fast before step 1.
262
279
 
263
- State is captured in the `WorkflowSnapshot` (bumped to `_schemaVersion: 2`) and restored on `Workflow.resume` — it survives a suspend→resume round-trip. A pre-SE29 (`_schemaVersion: 1`) snapshot has no state and resumes with `initialState`. Back-compat: no `stateSchema`/`initialState` ⇒ `state` is `undefined` and `setState` is unvalidated. New export `WorkflowStateError`. Mirrors Mastra's workflow `state`/`setState`/`stateSchema`. From the Mastra Workflows comparison (SDK Evolution roadmap SE29).
280
+ State is captured in the `WorkflowSnapshot` (bumped to `_schemaVersion: 2`) and restored on `Workflow.resume` — it survives a suspend→resume round-trip. A pre-SE29 (`_schemaVersion: 1`) snapshot has no state and resumes with `initialState`. Back-compat: no `stateSchema`/`initialState` ⇒ `state` is `undefined` and `setState` is unvalidated. New export `WorkflowStateError`. (SDK Evolution roadmap SE29.)
264
281
 
265
282
  - 8ce8441: **SE30 — workflows-as-steps (`workflowStep`) + `cloneWorkflow`.**
266
283
 
267
284
  `workflowStep(child, { id? })` (from `@theokit/sdk/workflow`) uses a committed `Workflow` as a step inside another workflow: `.then(workflowStep(child))`. The child runs in its OWN executor (own runId, single-flight lock, and step-id space — so nested ids never collide with the parent's); its output becomes the step output. `cloneWorkflow(wf, { id })` returns a new independent `Workflow` with the same committed steps under a new name + a fresh workflowId (clones run independently, distinct observability identity).
268
285
 
269
- A non-`completed` child fails the parent step with a typed `WorkflowNestedError`. **Nested suspend/resume is NOT supported in v1** (TheoKit's resume continues AFTER the suspended step, so a nested child would be skipped) — a nested `suspended` fails with a clear message pointing at a top-level suspend; re-running the child on resume (which would re-execute its side effects) is deliberately avoided. ADR 0010. New export `WorkflowNestedError`. Mirrors Mastra's workflows-as-steps + `cloneWorkflow`. From the Mastra Workflows comparison (SDK Evolution roadmap SE30).
286
+ A non-`completed` child fails the parent step with a typed `WorkflowNestedError`. **Nested suspend/resume is NOT supported in v1** (TheoKit's resume continues AFTER the suspended step, so a nested child would be skipped) — a nested `suspended` fails with a clear message pointing at a top-level suspend; re-running the child on resume (which would re-execute its side effects) is deliberately avoided. ADR 0010. New export `WorkflowNestedError`. (SDK Evolution roadmap SE30.)
270
287
 
271
288
  ## 2.26.0
272
289
 
@@ -276,7 +293,7 @@
276
293
 
277
294
  `AgentOptions.inputProcessors` run in order before the LLM (normalize / validate / block / rewrite the user message); `outputProcessors` run on the model's final text before it reaches the caller (redact / block). A `Processor` is `{ id; processInput?; processOutput?; onViolation? }`; each handler receives `ctx` with `abort(reason)` (block → the run stops with `RunResult.tripwire { reason, processorId }` + a `tripwire` run-event via `SendOptions.onRunEvent`) and `warn(message, detail?)` (non-blocking → fires `onViolation`, continues), and returns the (possibly rewritten) payload.
278
295
 
279
- The core ships no `strategy` enum — block/rewrite/redact/warn reduce to `abort` / return-string / `warn` (the built-in SE25 processors expose a `strategy` option over these). An input block never reaches the model (a terminal tripwire run); an output block turns a finished run's result into a tripwire on `wait()`. Streaming output redaction is deferred (v1 processes the buffered `wait()` path). Cloud agents reject processors (function handlers don't serialize). Back-compat: no processors ⇒ unchanged. New public types `Processor` / `ProcessorViolation` / `InputProcessorContext` / `OutputProcessorContext` / `ProcessorControls` / `ProcessorTripwire` / `RunTripwireEvent` + `RunResult.tripwire`. ADR 0008. Mirrors Mastra Guardrails input/output processors. From the Mastra Guardrails comparison (SDK Evolution roadmap SE24).
296
+ The core ships no `strategy` enum — block/rewrite/redact/warn reduce to `abort` / return-string / `warn` (the built-in SE25 processors expose a `strategy` option over these). An input block never reaches the model (a terminal tripwire run); an output block turns a finished run's result into a tripwire on `wait()`. Streaming output redaction is deferred (v1 processes the buffered `wait()` path). Cloud agents reject processors (function handlers don't serialize). Back-compat: no processors ⇒ unchanged. New public types `Processor` / `ProcessorViolation` / `InputProcessorContext` / `OutputProcessorContext` / `ProcessorControls` / `ProcessorTripwire` / `RunTripwireEvent` + `RunResult.tripwire`. ADR 0008. (SDK Evolution roadmap SE24.)
280
297
 
281
298
  - cc8efee: **SE25 — deterministic in-tree guardrail processors (`createUnicodeNormalizer`, `createTokenLimiter`).**
282
299
 
@@ -287,7 +304,7 @@
287
304
 
288
305
  Both are OPT-IN (add to `inputProcessors`/`outputProcessors`); nothing auto-injects them; back-compat preserved.
289
306
 
290
- **`BatchPartsProcessor` is intentionally DEFERRED**, not shipped: TheoKit's `run.stream()` emits full `SDKAssistantMessage`s, not token-granular deltas, so there is no SSE chunk stream to coalesce in the in-process runtime (Mastra's BatchParts cuts HTTP network overhead). It becomes meaningful only alongside a future HTTP/SSE streaming transport (the same milestone as SE24's deferred streaming-output redaction). Mirrors Mastra's deterministic guardrail processors. From the Mastra Guardrails comparison (SDK Evolution roadmap SE25).
307
+ **A batch-parts processor is intentionally DEFERRED**, not shipped: TheoKit's `run.stream()` emits full `SDKAssistantMessage`s, not token-granular deltas, so there is no SSE chunk stream to coalesce in the in-process runtime (batch-coalescing only saves HTTP network overhead). It becomes meaningful only alongside a future HTTP/SSE streaming transport (the same milestone as SE24's deferred streaming-output redaction). (SDK Evolution roadmap SE25.)
291
308
 
292
309
  ### Patch Changes
293
310
 
@@ -295,7 +312,7 @@
295
312
 
296
313
  Records the decision (ADR 0009) to DELEGATE the LLM-classifier guardrail processors — moderation, PII, prompt-injection, language, prompt-scrubber — to specialist libraries / consumer code built ON the SE24 seam, rather than shipping concrete classifiers in `@theokit/sdk` core (mirrors the AUTH-DELEGATION lock: constant churn — provider/model deltas, taxonomies, thresholds, jailbreak patterns — vs a stable seam a single-maintainer core can own). No classifier is added to core.
297
314
 
298
- Ships the paved path: `docs/concepts/guardrails.md` (how to build moderation / PII / injection processors on the seam + recommended external classifiers) and `examples/guardrails/` (a runnable moderation + PII-redaction example over a pluggable classifier). No public API change. From the Mastra Guardrails comparison (SDK Evolution roadmap SE26).
315
+ Ships the paved path: `docs/concepts/guardrails.md` (how to build moderation / PII / injection processors on the seam + recommended external classifiers) and `examples/guardrails/` (a runnable moderation + PII-redaction example over a pluggable classifier). No public API change. (SDK Evolution roadmap SE26.)
299
316
 
300
317
  ## 2.25.0
301
318
 
@@ -305,25 +322,25 @@
305
322
 
306
323
  `agent.skills.list()` already returned skill metadata (name + description only); SE20 adds `agent.skills.get(name)` returning the skill INCLUDING its `instructions` (body) — read from the inline `createSkill` body, or from the filesystem SKILL.md (frontmatter stripped) for discovered skills. Returns `undefined` when no enabled skill matches (malformed skills stay excluded). New public type `SDKAgentSkillDetail`.
307
324
 
308
- `list()` stays lean (the `<skills>` block only ever carries name + description); full bodies come only through `get`. Mirrors Mastra's `agent.getSkill(name)`. Additive + backward-compatible. From the Mastra Agent-skills comparison (SDK Evolution roadmap SE20).
325
+ `list()` stays lean (the `<skills>` block only ever carries name + description); full bodies come only through `get`. Additive + backward-compatible. (SDK Evolution roadmap SE20.)
309
326
 
310
327
  - 09865ee: **SE21 — `references` on `createSkill` (bundle supporting docs on an inline skill).**
311
328
 
312
- `createSkill({ ..., references })` now accepts an optional `references` map (filename → content), mirroring a filesystem skill's `references/` directory. The docs travel on the inline skill object and surface to the app via `agent.skills.get(name)` (new `references` field on `SDKAgentSkillDetail`); they are NOT injected into the model prompt. Omitted when not provided (backward-compatible). Mirrors Mastra Agent-skills `references`.
329
+ `createSkill({ ..., references })` now accepts an optional `references` map (filename → content), mirroring a filesystem skill's `references/` directory. The docs travel on the inline skill object and surface to the app via `agent.skills.get(name)` (new `references` field on `SDKAgentSkillDetail`); they are NOT injected into the model prompt. Omitted when not provided (backward-compatible).
313
330
 
314
- Also closes a latent boundary leak surfaced by this change: `agent.skills.list()` now projects to the public shape (name + description only), so an inline skill's `instructions` / `references` / `source` never leak through `list()` — the body is reachable exclusively through `get()`, matching the documented `SystemPromptSkillRef` contract. From the Mastra Agent-skills comparison (SDK Evolution roadmap SE21).
331
+ Also closes a latent boundary leak surfaced by this change: `agent.skills.list()` now projects to the public shape (name + description only), so an inline skill's `instructions` / `references` / `source` never leak through `list()` — the body is reachable exclusively through `get()`, matching the documented `SystemPromptSkillRef` contract. (SDK Evolution roadmap SE21.)
315
332
 
316
333
  - abfcc5d: **SE22 — dynamic skills resolver (`skills: (ctx) => SkillsSettings`).**
317
334
 
318
335
  `AgentOptions.skills` now accepts a resolver function in addition to the static `SkillsSettings` object. The resolver receives a per-send context (`agentId`, `cwd`, `model`, `userMessage`, `memory` — mirroring the systemPrompt resolver's context, minus the not-yet-resolved `skills`) and returns the `SkillsSettings` for that run. It is evaluated per `send()` before skill assembly, so a cached `getOrCreate` agent re-resolves each run — pick skills from runtime context (e.g. the user's role).
319
336
 
320
- A static object behaves exactly as today. The agent-scoped `agent.skills` handle reflects the static/base config; the resolver drives the per-send `<skills>` block. The SDK imposes no timeout (wrap your own `Promise.race`); a throwing resolver fails the run — no silent fallback (Rule 8). Cloud agents reject a function resolver (it can't run on PaaS — resolve to a static object first), mirroring the systemPrompt-resolver cloud rule. New public types `SkillsResolver` + `SkillsResolverContext`. Mirrors Mastra Agent-skills `skills: ({ requestContext }) => SkillInput[]`. From the Mastra Agent-skills comparison (SDK Evolution roadmap SE22).
337
+ A static object behaves exactly as today. The agent-scoped `agent.skills` handle reflects the static/base config; the resolver drives the per-send `<skills>` block. The SDK imposes no timeout (wrap your own `Promise.race`); a throwing resolver fails the run — no silent fallback (Rule 8). Cloud agents reject a function resolver (it can't run on PaaS — resolve to a static object first), mirroring the systemPrompt-resolver cloud rule. New public types `SkillsResolver` + `SkillsResolverContext`. (SDK Evolution roadmap SE22.)
321
338
 
322
339
  - 0b9c0ac: **SE23 — `defineSkillReadTool` (opt-in model-facing lazy skill read).**
323
340
 
324
341
  `defineSkillReadTool(skills)` returns a `skill_read` `CustomTool` the consumer explicitly adds to `AgentOptions.tools`. When the model calls it with a skill name, the handler returns that skill's `instructions` (+ SE21 `references`); an unknown-but-well-formed name returns a typed "not found" string listing the available skills — NOT a throw that kills the run (Rule 8). Malformed input (missing `name`) fails at the trust boundary via the input schema.
325
342
 
326
- The SDK never auto-injects it — bring-your-own-tools stays intact (sibling of `defineSubAgent` / `workflowAsTool`). This is the LAZY read path that complements the eager `<skills>` block (name + description only): the block discloses which skills exist; `skill_read` loads a body on demand. The consumer controls exposure by choosing which skills to pass. See ADR 0007. Mirrors Mastra's `skill_read` but opt-in, not auto-injected. From the Mastra Agent-skills comparison (SDK Evolution roadmap SE23).
343
+ The SDK never auto-injects it — bring-your-own-tools stays intact (sibling of `defineSubAgent` / `workflowAsTool`). This is the LAZY read path that complements the eager `<skills>` block (name + description only): the block discloses which skills exist; `skill_read` loads a body on demand. The consumer controls exposure by choosing which skills to pass. See ADR 0007. The `skill_read` tool is opt-in, never auto-injected. (SDK Evolution roadmap SE23.)
327
344
 
328
345
  ## 2.24.0
329
346
 
@@ -333,25 +350,25 @@
333
350
 
334
351
  `defineTool` (from `@theokit/sdk`) gains an optional `outputSchema` (a Zod schema). When set, the handler returns the STRUCTURED output inferred from it (`z.infer<outputSchema>`), the value is validated against the schema, and the tool result becomes its serialization — a string stays as-is, an object is JSON-stringified. A validation failure raises `ZodError` (converted to a `tool_result(isError)`), so a malformed tool output fails loudly instead of silently reaching the model.
335
352
 
336
- Additive + fully backward-compatible: with no `outputSchema` the handler returns a plain `string` exactly as before (the handler return type is `string` when `outputSchema` is absent, `z.infer<outputSchema>` when present, via a conditional type). Mirrors Mastra `createTool`'s `outputSchema`. Pairs with SE17 (`toModelOutput`). From the Mastra Tools comparison (SDK Evolution roadmap SE16).
353
+ Additive + fully backward-compatible: with no `outputSchema` the handler returns a plain `string` exactly as before (the handler return type is `string` when `outputSchema` is absent, `z.infer<outputSchema>` when present, via a conditional type). Pairs with SE17 (`toModelOutput`). (SDK Evolution roadmap SE16.)
337
354
 
338
355
  - f621734: **SE17 — `toModelOutput` on `defineTool` (model-facing vs app-facing output split).**
339
356
 
340
357
  `defineTool` (from `@theokit/sdk`) gains an optional `toModelOutput`. The handler returns the FULL result (validated by SE16's `outputSchema`); `toModelOutput(output)` maps it to the compact / multimodal representation the MODEL sees in the `tool_result` — so rich app-facing detail is not forced into model context. It returns a `string` OR SE7 `ToolResultContentBlock[]` (text + image). Absent ⇒ the tool result is the serialized handler output (SE16 / pre-SE17 behavior, unchanged).
341
358
 
342
- Mirrors Mastra's `toModelOutput` and the Vercel AI SDK. Additive + backward-compatible. From the Mastra Tools comparison (SDK Evolution roadmap SE17).
359
+ Additive + backward-compatible. (SDK Evolution roadmap SE17.)
343
360
 
344
361
  - 72435db: **SE18 — `SendOptions.activeTools` (per-send runtime tool subset).**
345
362
 
346
363
  `agent.send(input, { activeTools })` restricts, per send, which of the agent's registered tools the model may actually call. A tool whose canonical name is not in the list is vetoed at dispatch (its handler never runs) — reusing the existing `withToolWhitelist` path that `Agent.fork`'s `allowedTools` uses, NOT `PermissionEngine`. Composes with `toolChoice`: `activeTools` narrows the set, `toolChoice` gates calling within it. Absent ⇒ the full toolset is available (unchanged).
347
364
 
348
- The loop runs inside a `withToolWhitelist(new Set(activeTools))` scope when set. Additive + backward-compatible. Mirrors Mastra `activeTools` + the Vercel AI SDK. From the Mastra Tools comparison (SDK Evolution roadmap SE18).
365
+ The loop runs inside a `withToolWhitelist(new Set(activeTools))` scope when set. Additive + backward-compatible. (SDK Evolution roadmap SE18.)
349
366
 
350
367
  - f92f720: **SE19 — `workflowAsTool` (expose a Workflow as an agent tool).**
351
368
 
352
- `workflowAsTool(workflow, { name, description, inputSchema })` (from `@theokit/sdk/workflow`) turns a `Workflow` into an agent `CustomTool`, completing the Mastra "X as tools" trio (tools; agents-as-tools via `defineSubAgent`; workflows-as-tools). The handler validates the model's args against `spec.inputSchema`, runs the workflow, and returns its output (a string as-is, else JSON). A run that does not reach `status: "completed"` raises a typed `WorkflowToolError` (workflow step errors do NOT throw — they surface via `run.status === "failed"`).
369
+ `workflowAsTool(workflow, { name, description, inputSchema })` (from `@theokit/sdk/workflow`) turns a `Workflow` into an agent `CustomTool`, completing the "X as tools" trio (tools; agents-as-tools via `defineSubAgent`; workflows-as-tools). The handler validates the model's args against `spec.inputSchema`, runs the workflow, and returns its output (a string as-is, else JSON). A run that does not reach `status: "completed"` raises a typed `WorkflowToolError` (workflow step errors do NOT throw — they surface via `run.status === "failed"`).
353
370
 
354
- Because a `Workflow` carries no top-level schema (`WorkflowOptions` is `name`/`persistence`/`workflowId`; schemas are per-step), the caller supplies the tool `inputSchema` in the spec (like `defineTool`). Accepts any `{ run }`-shaped workflow (structural), so it never imports the `Workflow` class. New exports: `workflowAsTool`, `WorkflowToolError`, `WorkflowAsToolSpec`. Additive. From the Mastra Tools comparison (SDK Evolution roadmap SE19).
371
+ Because a `Workflow` carries no top-level schema (`WorkflowOptions` is `name`/`persistence`/`workflowId`; schemas are per-step), the caller supplies the tool `inputSchema` in the spec (like `defineTool`). Accepts any `{ run }`-shaped workflow (structural), so it never imports the `Workflow` class. New exports: `workflowAsTool`, `WorkflowToolError`, `WorkflowAsToolSpec`. Additive. (SDK Evolution roadmap SE19.)
355
372
 
356
373
  ## 2.23.0
357
374
 
@@ -361,21 +378,21 @@
361
378
 
362
379
  `DelegationStartDecision` (from `@theokit/sdk/a2a`) gains `modifiedMaxSteps?: number`. When an `onDelegationStart` hook returns it (and does not reject), `defineSubAgent` forwards it as `SendOptions.maxIterations` to the child `agent.send`, capping how many tool-loop rounds the subagent may run. Composes with SE10 (`signal`) and SE12 (`messageFilter` preamble) onto a single child `send`. Absent ⇒ the child uses its default iteration ceiling (unchanged).
363
380
 
364
- Completes the SE11 `onDelegationStart` decision contract (the deferred `modifiedMaxSteps` — the `SendOptions.maxIterations` plumbing already existed). Additive + backward-compatible. From the Mastra supervisor-agents comparison (SDK Evolution roadmap SE13).
381
+ Completes the SE11 `onDelegationStart` decision contract (the deferred `modifiedMaxSteps` — the `SendOptions.maxIterations` plumbing already existed). Additive + backward-compatible. (SDK Evolution roadmap SE13.)
365
382
 
366
383
  - b51dc6a: **SE14 — subagent result-context control (`SubAgentSpec.includeToolResults`).**
367
384
 
368
- `defineSubAgent()` (from `@theokit/sdk/a2a`) gains an opt-in `includeToolResults`. When `true`, the child's completed tool-call results (name + result) are appended to the delegation payload returned to the supervisor, inside a delimited `<subagent-tool-results>` block; when absent/`false` the delegation returns the child's final text only — **text-only stays the default** (Mastra's scoped posture).
385
+ `defineSubAgent()` (from `@theokit/sdk/a2a`) gains an opt-in `includeToolResults`. When `true`, the child's completed tool-call results (name + result) are appended to the delegation payload returned to the supervisor, inside a delimited `<subagent-tool-results>` block; when absent/`false` the delegation returns the child's final text only — **text-only stays the default** (the scoped-context posture).
369
386
 
370
387
  Implemented as a `run.stream()` replay after `run.wait()` (a proven, safe idiom — the run buffers events and `stream()` replays them) collecting `tool_call` events with `status: "completed"`. **No `RunResult` change** — reads the existing public stream surface; tool _args_ are never surfaced (only completed results). Rationale + the `RunResult`-field alternative are recorded in ADR 0006.
371
388
 
372
- Additive + backward-compatible (default `false` never touches the stream). From the Mastra supervisor-agents comparison (SDK Evolution roadmap SE14).
389
+ Additive + backward-compatible (default `false` never touches the stream). (SDK Evolution roadmap SE14.)
373
390
 
374
391
  - 30e02d9: **SE15 — `iteration` count on the delegation-hook context (reject-after-N).**
375
392
 
376
- `DelegationStartContext` and `DelegationCompleteContext` (from `@theokit/sdk/a2a`) gain `iteration: number` — a 1-based per-`defineSubAgent`-instance invocation counter, incremented before `onDelegationStart` runs (a rejected delegation still counts). This enables the Mastra reject-after-N-iterations pattern: `onDelegationStart: (ctx) => ctx.iteration > 8 ? { proceed: false, rejectionReason } : { proceed: true }`. `onDelegationComplete` sees the same iteration its `onDelegationStart` did.
393
+ `DelegationStartContext` and `DelegationCompleteContext` (from `@theokit/sdk/a2a`) gain `iteration: number` — a 1-based per-`defineSubAgent`-instance invocation counter, incremented before `onDelegationStart` runs (a rejected delegation still counts). This enables a reject-after-N-iterations pattern: `onDelegationStart: (ctx) => ctx.iteration > 8 ? { proceed: false, rejectionReason } : { proceed: true }`. `onDelegationComplete` sees the same iteration its `onDelegationStart` did.
377
394
 
378
- Also fixes a delegation-hook DX regression: `onDelegationStart` / `onDelegationComplete` now accept a **side-effect-only (void-returning) callback** (e.g. `(ctx) => { log(ctx) }`) — the common case, mirroring Mastra's `async ctx => { … }` hooks — via a shared `DelegationHookResult<T>` return type. Additive + backward-compatible. From the Mastra supervisor-agents comparison (SDK Evolution roadmap SE15).
395
+ Also fixes a delegation-hook DX regression: `onDelegationStart` / `onDelegationComplete` now accept a **side-effect-only (void-returning) callback** (e.g. `(ctx) => { log(ctx) }`) — the common case, for `async ctx => { … }` hooks — via a shared `DelegationHookResult<T>` return type. Additive + backward-compatible. (SDK Evolution roadmap SE15.)
379
396
 
380
397
  ## 2.22.0
381
398
 
@@ -388,7 +405,7 @@
388
405
  - Additive + backward-compatible: a handler invoked with no `ctx` (single-arg call sites) behaves exactly as before — no signal, no cancellation.
389
406
  - The child agent is still disposed in `finally`, including on cancel.
390
407
 
391
- Matches the Mastra supervisor-agents "abortSignal forwarded to delegated subagents" behavior (SDK Evolution roadmap SE10).
408
+ The parent run's `AbortSignal` is forwarded to delegated subagents; aborting the parent cancels the in-flight subagent at its next step (SDK Evolution roadmap SE10).
392
409
 
393
410
  - 8e3249d: **SE11 — delegation lifecycle hooks on `defineSubAgent` (`onDelegationStart` / `onDelegationComplete`).**
394
411
 
@@ -399,7 +416,7 @@
399
416
 
400
417
  Additive + backward-compatible: specs without hooks behave exactly as before. New exported types: `DelegationStartContext`, `DelegationStartDecision`, `DelegationCompleteContext`, `DelegationCompleteDecision`.
401
418
 
402
- Matches the Mastra supervisor `onDelegationStart` / `onDelegationComplete` control points (SDK Evolution roadmap SE11).
419
+ Adds `onDelegationStart` / `onDelegationComplete` control points for delegation (SDK Evolution roadmap SE11).
403
420
 
404
421
  - d2d0d16: **SE12 — opt-in parent-context forwarding for subagents (`messageFilter`).**
405
422
 
@@ -408,7 +425,7 @@
408
425
  - New `ctx.messages` on the custom-tool handler `ToolContext`: a **read-only, text-only** projection of the current turn's transcript (`ToolContextMessage[]`), threaded by the agent loop the same way `ctx.signal` (#65) and `ctx.context` (M7) are. Non-text parts (tool calls / results) are dropped — a tool never sees raw wire parts or nested tool args.
409
426
  - `messageFilter({ messages, input, name })` returns the subset to forward; `defineSubAgent` prepends it to the delegated input as a role-tagged context preamble. A filter returning `[]` forwards nothing. A filter that drops sensitive turns (e.g. anything `confidential`) provably keeps them out of the child context.
410
427
 
411
- New exported types: `ToolContextMessage`, `MessageFilterArgs`. Additive + backward-compatible. Rationale + the transcript-exposure trade-off are recorded in ADR 0005. From the Mastra supervisor-agents comparison (SDK Evolution roadmap SE12).
428
+ New exported types: `ToolContextMessage`, `MessageFilterArgs`. Additive + backward-compatible. Rationale + the transcript-exposure trade-off are recorded in ADR 0005. (SDK Evolution roadmap SE12.)
412
429
 
413
430
  ## 2.21.0
414
431
 
@@ -421,15 +438,15 @@
421
438
  - `default` — rules decide; unmatched ⇒ `ask` (fail-closed).
422
439
  - `plan` — read-only: `allow` rules pass, everything else ⇒ `deny`.
423
440
  - `acceptEdits` — auto-approve the UNMATCHED verdict but still honor an explicit `ask` rule (Codex `UnlessTrusted`).
424
- - `bypass` — everything ⇒ `allow` EXCEPT an explicit `deny` rule (OpenCode `dangerously-skip-permissions` / Codex `Never`).
441
+ - `bypass` — everything ⇒ `allow` EXCEPT an explicit `deny` rule (a skip-permissions posture; Codex `Never`).
425
442
 
426
- **Invariant (both OpenCode + Codex):** an explicit `deny` is immune to every auto-approve mode. `bypass`/`acceptEdits` never un-deny.
443
+ **Invariant:** an explicit `deny` is immune to every auto-approve mode. `bypass`/`acceptEdits` never un-deny.
427
444
 
428
445
  `createPermissionPlugin` gains `mode` + an enriched async **`canUseTool(toolName, input, ctx)`** gate (the Anthropic-parity shape) that resolves the `ask` verdict to allow/deny — fail-closed on absent/throwing gate. The old `onAsk(toolName)` is kept as a `@deprecated` back-compat fallback.
429
446
 
430
447
  New exports: `PermissionMode`, `applyMode`, `PermissionGate`, `PermissionGateContext`, `PermissionGateDecision`. Additive + backward-compatible (`evaluate` mode defaults to `default`; `onAsk` still works). `updatedInput` (arg rewrite) is intentionally deferred — the `pre_tool_call` seam is veto-only today.
431
448
 
432
- Grounded in a deep OpenCode + Codex permission-model comparison (SDK Evolution roadmap SE1).
449
+ Grounded in a survey of state-of-the-art agent permission models (SDK Evolution roadmap SE1).
433
450
 
434
451
  - f9001bb: **SE2 — typed runtime event stream (opt-in `SendOptions.onRunEvent`).**
435
452
 
@@ -441,11 +458,11 @@
441
458
 
442
459
  New exports: `RunEvent` (+ the 7 member types), `RunEventSink`, `emitRunEvent`. Additive + backward-compatible.
443
460
 
444
- Grounded in the SDK Evolution roadmap SE2 (Anthropic Agent SDK comparison).
461
+ Grounded in the SDK Evolution roadmap SE2.
445
462
 
446
463
  - eec7d55: **SE3 — multi-agent provenance (`origin`).**
447
464
 
448
- New public `MessageOrigin` discriminated union that stamps WHO triggered a turn in the multi-agent path (Squad / a2a / handoff / background-delegation) and is **forwarded onto the run result** — so consumers can attribute or route turns by their trigger. Metadata-only: zero change to routing or dispatch. Mirrors the Anthropic Agent SDK's `origin` shape.
465
+ New public `MessageOrigin` discriminated union that stamps WHO triggered a turn in the multi-agent path (Squad / a2a / handoff / background-delegation) and is **forwarded onto the run result** — so consumers can attribute or route turns by their trigger. Metadata-only: zero change to routing or dispatch.
449
466
 
450
467
  - `MessageOrigin` union: `{ kind: "human" }` | `{ kind: "peer"; from }` | `{ kind: "task-notification" }` | `{ kind: "coordinator"; from? }` | `{ kind: "auto-continuation" }`. Absence = a direct human turn.
451
468
  - `SendOptions.origin?: MessageOrigin` — the caller stamps the provenance; `RunResult.origin?: MessageOrigin` — forwarded onto the result (both fixture and real runtimes).
@@ -455,7 +472,7 @@
455
472
 
456
473
  New exports: `MessageOrigin`, plus `origin` fields on `SendOptions` / `RunResult` / `A2AMessage` / `AgentStep` and the `agentStep(..., { origin })` option. Additive + backward-compatible.
457
474
 
458
- Grounded in the SDK Evolution roadmap SE3 (Anthropic Agent SDK comparison).
475
+ Grounded in the SDK Evolution roadmap SE3.
459
476
 
460
477
  - 8606f5b: **SE4 — session-management surface (`createSessionManager`).**
461
478
 
@@ -469,7 +486,7 @@
469
486
 
470
487
  New exports: `createSessionManager`, `SessionManager`, `SessionSummary`, `SessionListOptions`, `SessionCapabilityResult`, `SessionMeta`, `SessionMetaPatch`. Additive + backward-compatible.
471
488
 
472
- Grounded in the SDK Evolution roadmap SE4 (Anthropic Agent SDK comparison).
489
+ Grounded in the SDK Evolution roadmap SE4.
473
490
 
474
491
  - ce9b375: **SE7 — structured/multimodal tool results + `ToolError`.**
475
492
 
@@ -486,17 +503,17 @@
486
503
 
487
504
  - 3722208: **SE8 — model bare-string shorthand.**
488
505
 
489
- Every public model-accepting surface — `AgentOptions.model`, `SendOptions.model`, `AgentBuilder.model()`, and `GenerateObjectOptions.model` / `structuringModel` / `StreamObjectOptions.model` — now accepts a bare-string model id (`model: "openai/gpt-4o-mini"`) in addition to the `{ id }` object, matching every peer SDK's `"provider/model"` shorthand. Additive + fully backward-compatible: the object form (and `{ id, params }` for tuning) is unchanged.
506
+ Every public model-accepting surface — `AgentOptions.model`, `SendOptions.model`, `AgentBuilder.model()`, and `GenerateObjectOptions.model` / `structuringModel` / `StreamObjectOptions.model` — now accepts a bare-string model id (`model: "openai/gpt-4o-mini"`) in addition to the `{ id }` object the familiar `"provider/model"` shorthand. Additive + fully backward-compatible: the object form (and `{ id, params }` for tuning) is unchanged.
490
507
 
491
508
  - A bare string is normalized to `{ id }` at ONE boundary seam (`normalizeModel`), so all downstream code keeps seeing a `ModelSelection`. The id still parses a `provider/` prefix for routing.
492
509
  - Use the object form when you need `params` (reasoning/temperature tuning): `model: { id: "...", params: [...] }`.
493
510
  - An empty / whitespace-only string throws a typed `ConfigurationError` (`code: "invalid_model_selection"`).
494
511
 
495
- From the DX comparison against OpenAI Agents / LangChain `create_agent` / Vercel AI SDK `ToolLoopAgent` / Mastra (all take a bare string). Grounded in ROADMAP SE8.
512
+ A bare-string model id is the common shorthand across agent SDKs. Grounded in ROADMAP SE8.
496
513
 
497
514
  - d039cd6: **SE9 — integrated structured output on `agent.generate()`.**
498
515
 
499
- New typed `agent.generate(input, { output: schema, ...sendOptions })` method: runs the agent's NORMAL tool loop (the user's tools run first) and then coerces the final answer into a Zod schema, returning a validated, **inferred-typed** object — in one call, instead of a separate `generateObject`. This closes the biggest DX-capability gap vs LangChain `response_format` / Vercel AI SDK `Output.object`.
516
+ New typed `agent.generate(input, { output: schema, ...sendOptions })` method: runs the agent's NORMAL tool loop (the user's tools run first) and then coerces the final answer into a Zod schema, returning a validated, **inferred-typed** object — in one call, instead of a separate `generateObject`. This delivers structured output straight off the agent's own tool loop.
500
517
 
501
518
  - `agent.generate<T>(input, { output: T, ...SendOptions }): Promise<GenerateRunResult<z.infer<T>>>` — `{ object, result, raw, usage }`. `object` carries the inferred type; `result` is the underlying tool-loop `RunResult` (status/usage/model).
502
519
  - **Sugar over `Agent.generateObject` (ADR D33), not a fork:** phase 1 is the user's own `agent.send()` run; phase 2 reuses `generateObjectImpl` (the synthetic forced-`output`-tool + Zod validation + retries) over the run's final answer.
@@ -504,7 +521,7 @@
504
521
  - **Typed failure:** a run that errors before an answer surfaces a typed `GenerateObjectError` (no structuring over a failed run); a persistent parse-failure is governed by `errorStrategy`.
505
522
  - Available on both local and cloud agents.
506
523
 
507
- New exports: `GenerateOptions`, `GenerateRunResult` (+ the `SDKAgent.generate` method). Additive + backward-compatible. From the DX comparison vs OpenAI Agents / LangChain / AI SDK / Mastra. Grounded in ROADMAP SE9.
524
+ New exports: `GenerateOptions`, `GenerateRunResult` (+ the `SDKAgent.generate` method). Additive + backward-compatible. Grounded in ROADMAP SE9.
508
525
 
509
526
  ### Patch Changes
510
527
 
@@ -633,25 +650,25 @@
633
650
 
634
651
  ### Patch Changes
635
652
 
636
- - 6336f81: Suppress the leaked-dialect tool-call from the visible stream (R7). When `extractToolCallsFromContent` is enabled and a model leaks a `<function=NAME>` tool call as assistant text, the OpenAI-compat streaming now HOLDS that text back at the stream boundary (a small suspicion-buffer FSM that reuses the request-scoped allowlist from R5) instead of emitting it as `text_delta` events — so the raw dialect no longer flashes by in the live stream or lands in the final assistant text. `finish()` still recovers the call (unchanged). Fail-open: a never-closing marker or un-suppressable input is flushed as visible text (never held forever). Flag-off streaming is byte-for-byte unchanged. Grounded in openclaw's stream-normalizer FSM.
653
+ - 6336f81: Suppress the leaked-dialect tool-call from the visible stream (R7). When `extractToolCallsFromContent` is enabled and a model leaks a `<function=NAME>` tool call as assistant text, the OpenAI-compat streaming now HOLDS that text back at the stream boundary (a small suspicion-buffer FSM that reuses the request-scoped allowlist from R5) instead of emitting it as `text_delta` events — so the raw dialect no longer flashes by in the live stream or lands in the final assistant text. `finish()` still recovers the call (unchanged). Fail-open: a never-closing marker or un-suppressable input is flushed as visible text (never held forever). Flag-off streaming is byte-for-byte unchanged. Grounded in a survey of state-of-the-art stream-normalizer FSMs.
637
654
 
638
655
  ## 2.15.1
639
656
 
640
657
  ### Patch Changes
641
658
 
642
- - bec2077: Make the leaked-dialect recovery **request-scoped (R5)**. The opt-in `extractToolCallsFromContent` recovery previously promoted ANY `<function=NAME>` block leaked into assistant text on an enabled route, so a code assistant printing a literal `<function=example>` in a fenced code block could be wrongly turned into a tool call. Recovery now gates on an exact, case-sensitive allowlist derived automatically from the current request's declared tools (`request.tools`): the per-route flag stays the coarse enable, and the allowlist is the precise false-positive guard. A request with no tools recovers nothing; a gated-out block keeps its text visible (it is not silently deleted). No public API change — the allowlist is derived from the tools you already pass. Mirrors openclaw's `@openclaw/tool-call-repair` allowlist.
659
+ - bec2077: Make the leaked-dialect recovery **request-scoped (R5)**. The opt-in `extractToolCallsFromContent` recovery previously promoted ANY `<function=NAME>` block leaked into assistant text on an enabled route, so a code assistant printing a literal `<function=example>` in a fenced code block could be wrongly turned into a tool call. Recovery now gates on an exact, case-sensitive allowlist derived automatically from the current request's declared tools (`request.tools`): the per-route flag stays the coarse enable, and the allowlist is the precise false-positive guard. A request with no tools recovers nothing; a gated-out block keeps its text visible (it is not silently deleted). No public API change — the allowlist is derived from the tools you already pass, following the standard tool-call-repair allowlist approach.
643
660
 
644
661
  ## 2.15.0
645
662
 
646
663
  ### Minor Changes
647
664
 
648
- - d7057f2: Add a **doom-loop / no-progress guard** to the agent loop. The loop now detects when the model repeats IDENTICAL tool calls (same name + same canonical input) that make no progress — the qwen3-coder `read_file`/`not_found` failure mode where the model retries the same failing call and the run grinds to the iteration ceiling — and stops early with a typed `no_progress` terminal instead of hanging. A pure `DoomLoopTracker` (canonical key-sorted-JSON signature + a consecutive-identical counter) escalates from a one-time guidance nudge at a soft threshold to a hard stop; the hard stop surfaces on `RunResult.stoppedByDoomLoop` and, through the continuation driver, as `terminal: "no_progress"` (so the outer loop does not re-send). It complements — does not replace — the existing empty-round `no_progress` (a different failure mode: model stuck repeating vs model gone silent). On by default with generous thresholds (soft 3 / hard 5); tune or disable per send via `SendOptions.doomLoop` (`false` to disable, or `{ softThreshold, hardThreshold }` to tune). Dependency-free. Grounded in a SOTA study of cline's LoopDetectionTracker + opencode's doom-loop.
665
+ - d7057f2: Add a **doom-loop / no-progress guard** to the agent loop. The loop now detects when the model repeats IDENTICAL tool calls (same name + same canonical input) that make no progress — the qwen3-coder `read_file`/`not_found` failure mode where the model retries the same failing call and the run grinds to the iteration ceiling — and stops early with a typed `no_progress` terminal instead of hanging. A pure `DoomLoopTracker` (canonical key-sorted-JSON signature + a consecutive-identical counter) escalates from a one-time guidance nudge at a soft threshold to a hard stop; the hard stop surfaces on `RunResult.stoppedByDoomLoop` and, through the continuation driver, as `terminal: "no_progress"` (so the outer loop does not re-send). It complements — does not replace — the existing empty-round `no_progress` (a different failure mode: model stuck repeating vs model gone silent). On by default with generous thresholds (soft 3 / hard 5); tune or disable per send via `SendOptions.doomLoop` (`false` to disable, or `{ softThreshold, hardThreshold }` to tune). Dependency-free. Grounded in a survey of state-of-the-art loop-detection and doom-loop guards.
649
666
 
650
667
  ## 2.14.0
651
668
 
652
669
  ### Minor Changes
653
670
 
654
- - 6ee4217: Add a public, isolated tool-input **sanitization** primitive on the new `@theokit/sdk/sanitize` subpath, plus a declarative `defineTool({ sanitize })` opt-in. Custom tools can now clean the raw arguments a model emits before they reach the tool schema: `sanitizeToolInput(input, options?)` trims whitespace by default and — opt-in — coerces string values toward their expected type (`"5"`→`5`, `"true"`→`true`, JSON-encoded strings→arrays/objects) and repairs malformed JSON (via `jsonrepair`). Coercion is guarded against silent corruption: numeric coercion round-trips and stays finite, so ID-like strings (`"12345678901234567890"`, `"007"`) and `NaN`/`Infinity` are left as strings; JSON repair only runs on JSON-looking values; a non-object input is returned untouched (the primitive is total — it never throws). When a Zod object schema is passed, coercion is schema-aware (a `z.string()` field keeps `"5"` a string). `defineTool({ sanitize: true })` trims the raw args before validation; `defineTool({ sanitize: { coerce: true } })` additionally coerces toward the tool's own schema — absent, `defineTool` behaviour is unchanged. Internally, the leaked-dialect recovery (`hermes-tool-extract`) now reuses the same primitive, so the public surface and the internal path never diverge. Grounded in a SOTA study of openclaw / agentfw / opencode / cline / vercel-ai-sdk.
671
+ - 6ee4217: Add a public, isolated tool-input **sanitization** primitive on the new `@theokit/sdk/sanitize` subpath, plus a declarative `defineTool({ sanitize })` opt-in. Custom tools can now clean the raw arguments a model emits before they reach the tool schema: `sanitizeToolInput(input, options?)` trims whitespace by default and — opt-in — coerces string values toward their expected type (`"5"`→`5`, `"true"`→`true`, JSON-encoded strings→arrays/objects) and repairs malformed JSON (via `jsonrepair`). Coercion is guarded against silent corruption: numeric coercion round-trips and stays finite, so ID-like strings (`"12345678901234567890"`, `"007"`) and `NaN`/`Infinity` are left as strings; JSON repair only runs on JSON-looking values; a non-object input is returned untouched (the primitive is total — it never throws). When a Zod object schema is passed, coercion is schema-aware (a `z.string()` field keeps `"5"` a string). `defineTool({ sanitize: true })` trims the raw args before validation; `defineTool({ sanitize: { coerce: true } })` additionally coerces toward the tool's own schema — absent, `defineTool` behaviour is unchanged. Internally, the leaked-dialect recovery (`hermes-tool-extract`) now reuses the same primitive, so the public surface and the internal path never diverge. Grounded in a survey of state-of-the-art agent runtimes.
655
672
 
656
673
  ## 2.13.1
657
674
 
@@ -927,7 +944,7 @@
927
944
 
928
945
  - 461c020: `createSquad` sequential agent-team convenience + `Agent.batch` boundary validation — first real npm publish.
929
946
 
930
- - **`createSquad(options)`** — composes `Workflow.create()` + `agentStep` into a sequential agent team (own identity; not a framework copy). Throws `ConfigurationError` (`invalid_squad` for empty agents, `squad_process_unsupported` for hierarchical). Cross-validation Gap 1.
947
+ - **`createSquad(options)`** — composes `Workflow.create()` + `agentStep` into a sequential agent team (own identity, built on the SDK's own primitives). Throws `ConfigurationError` (`invalid_squad` for empty agents, `squad_process_unsupported` for hierarchical). Cross-validation Gap 1.
931
948
  - **`Agent.batch`** now fail-fast validates `concurrency` + prompt items at the public boundary (`ConfigurationError` with `invalid_concurrency` / `invalid_batch_item`) before any side effect. Cross-validation Gap 3.
932
949
 
933
950
  Note: these features were tagged as `v1.8.0` but that version's npm publish failed (CI build cycle, fixed in `turbo.json`); `1.8.0` / `1.8.1` on npm predate them. They are published to npm for the first time in `1.9.0`. The `[1.8.0]` CHANGELOG section is retained as the GitHub-released record and is not rewritten.
@@ -1124,7 +1141,7 @@
1124
1141
  - `SubscriptionCtx`, `SubscriptionDescriptor<TInput, TOutput>`, `TrackedEnvelope<T>` (types)
1125
1142
  - **3 typed error classes:** `SubscriptionError`, `SubscriptionInputError` (carries Zod `issues`), `SubscriptionDisconnectError` (carries `closeCode`/`closeReason`). All extend `TheokitAgentError`.
1126
1143
  - **`ws@>=8.0.0` + `@types/ws@>=8.0.0` optional peer deps** — Node WS adapter loads `ws` via dynamic `import()` with actionable error when missing (D426). SSE-only consumers pay zero cost.
1127
- - **W3C-spec SSE encoder + parser** — independent of D38 Vercel AI Data Stream v1 wire format (which stays locked for `streamAssistant` LLM streaming). Both coexist (D429).
1144
+ - **W3C-spec SSE encoder + parser** — independent of the D38 Data Stream v1 wire format (which stays locked for `streamAssistant` LLM streaming). Both coexist (D429).
1128
1145
  - **Server integration primitives** — `scanSubscriptions({appDir, outFile})` emits `.theo/subscriptions.json` mirroring G6 routes scanner; `mountSubscriptions({manifest, appDir})` returns `{handleSseRequest, handleWsUpgrade}` ready to wire into `http.Server`. theokit-side Vite plugin + dev-server wiring is a cross-repo follow-up (D430).
1129
1146
 
1130
1147
  ### ADRs absorbed
@@ -1135,7 +1152,7 @@
1135
1152
  - **D426** — `ws` Node canonical (optional peer); CF Workers / Bun / Deno deferred to v1.8.x as separate packages
1136
1153
  - **D427** — `defineSubscription` AsyncGenerator + Zod input/output
1137
1154
  - **D428** — `subscribe` lives at `@theokit/sdk/subscription` sub-path only (NOT promoted to `Theokit.subscribe` due to pre-existing `agent.ts ↔ fork-agent.ts` rollup-dts cycle; same isolation pattern as `path-safety`)
1138
- - **D429** — W3C SSE wire format (independent of D38 Vercel AI Data Stream)
1155
+ - **D429** — W3C SSE wire format (independent of the D38 Data Stream)
1139
1156
  - **D430** — Server auto-route via `theokit.subscriptions` scanner (cross-repo follow-up for theokit-side wiring)
1140
1157
 
1141
1158
  ### Security threats addressed
@@ -1160,7 +1177,7 @@
1160
1177
 
1161
1178
  ### Notes
1162
1179
 
1163
- - v1.7.0 is **additive** — no breaking changes. Existing `streamAssistant` (Vercel AI Data Stream, D38) untouched.
1180
+ - v1.7.0 is **additive** — no breaking changes. Existing `streamAssistant` (Data Stream wire format, D38) untouched.
1164
1181
  - Tests: **45 GREEN + 1 honest-SKIP** under `tests/subscription/` + `tests/integration/subscription-resume.test.ts` (real `ws.WebSocketServer` + `http.Server` real SSE roundtrip + lastEventId resume) + `tests/integration/subscription-real-llm.test.ts` (env-gated `OPENROUTER_API_KEY` — verified GREEN against real OpenRouter `openai/gpt-4o-mini` per `real-llm-validation.md`).
1165
1182
  - Build: `dist/subscription/index.{js,cjs,d.ts,d.cts}` emitted; JS+CJS via tsup, DTS via tsc + `tsconfig.tools-dts.json` (mirrors `tools/` + `path-safety` pattern to avoid pre-existing `types/agent.ts ↔ fork-agent.ts` rollup-dts cycle).
1166
1183
 
@@ -1424,7 +1441,7 @@ FAIL before the fix).
1424
1441
  - OpenAI / OpenRouter SSE accumulator parses 5 token buckets:
1425
1442
  `prompt_tokens_details.cached_tokens` → `cacheReadTokens`,
1426
1443
  `completion_tokens_details.reasoning_tokens` → `reasoningTokens`, plus the
1427
- cline#10266 top-level `cache_read_input_tokens` /
1444
+ top-level `cache_read_input_tokens` /
1428
1445
  `cache_creation_input_tokens` fallback for Anthropic-on-OpenRouter.
1429
1446
  - `stream_options: { include_usage: true }` is now sent on every
1430
1447
  Chat Completions request so the final usage chunk arrives reliably.
@@ -1483,7 +1500,7 @@ FAIL before the fix).
1483
1500
  - **`ConversationStorageAdapter`** interface + `FileSystemConversationStorage` (default) + `InMemoryConversationStorage`. New `AgentOptions.conversationStorage` opt-in. Postgres + Redis recipes in `docs/recipes/`. Strict resume integrity check via `requiresCustomStorage` marker (D325).
1484
1501
  - **`Agent.registry`** — LRU + idle-timeout GC for live `SDKAgent` instances. `configure / evict / evictAll / size / ids` + `onEvict` listener. Defaults: `maxAgents: 100`, `idleTimeoutMs: 30 min`. Eliminates OOM in 24/7 Node deploys.
1485
1502
  - **`AgentRunErrorCode`** discriminated union (16 codes including `quota_exceeded`, `tool_runtime_error`, `aborted`, `invalid_model`, `safety_blocked`, `provider_unreachable`). Plus `AgentRunError.requestId` / `.conversationId` fields and `.retriable` / `.retryAfterMs` / `.providerError` getters. Anti-leak invariant: `providerError` never in `.message`.
1486
- - **`SendOptions.signal`** propagates end-to-end to LLM `fetch({ signal })`. Tokens stop billing on caller cancel. `anySignal` ponyfill for Vercel Edge subsets without native `AbortSignal.any`. `agent.dispose()` fires lifecycle abort. Aborted runs throw `AgentRunError({ code: "aborted" })`; no partial assistant message persists.
1503
+ - **`SendOptions.signal`** propagates end-to-end to LLM `fetch({ signal })`. Tokens stop billing on caller cancel. `anySignal` ponyfill for edge runtimes without native `AbortSignal.any`. `agent.dispose()` fires lifecycle abort. Aborted runs throw `AgentRunError({ code: "aborted" })`; no partial assistant message persists.
1487
1504
  - **`AgentOptions.onToolStart` / `onToolEnd` / `onToolError`** — observation callbacks with `callId` pair correlation + `durationMs`. Hook errors swallowed (do not crash run).
1488
1505
  - **`AgentOptions.onBeforeCreate` / `onBeforeSend`** — admission gates for multi-tenant quota. Errors propagate (NOT swallowed — these are blockers, not observers).
1489
1506
 
@@ -1511,7 +1528,7 @@ Closes Gap 6 of the TheoKit cross-repo handoff. Lets multi-tenant SaaS deploys e
1511
1528
 
1512
1529
  Closes Gap 4 of the TheoKit cross-repo handoff. Cost tracking, audit log, per-tool retry/alerting without writing a plugin.
1513
1530
 
1514
- - **`AgentOptions.onToolStart`**, **`onToolEnd`**, **`onToolError`** callbacks accepted in `AgentOptions` (top-level — no plugin needed; D315). Match Vercel AI SDK `onChunk`/`onFinish` ergonomics.
1531
+ - **`AgentOptions.onToolStart`**, **`onToolEnd`**, **`onToolError`** callbacks accepted in `AgentOptions` (top-level — no plugin needed; D315). Familiar `onChunk`/`onFinish`-style callback ergonomics.
1515
1532
  - **`callId` propagated** through the start/end (or start/error) pair from the existing `generateCallId()` in dispatch (D316). Consumers correlate without managing their own counter.
1516
1533
  - **`durationMs`** measured between start hook fire and end/error hook fire — handler latency.
1517
1534
  - **Hook errors swallowed** via single `safeEmitToolHook` chokepoint (D317). Listener throws logged to stderr but never crash the run.
@@ -1526,7 +1543,7 @@ Closes Gap 5 of the TheoKit cross-repo handoff. Tokens stop billing the moment a
1526
1543
 
1527
1544
  - **`SendOptions.signal`** (already typed) now flows from `LocalAgent.send` → `dispatchRun` → `real-local-run.buildLoopInputs` → `AgentLoopInputs.signal` → `streamLlmTurn` → LLM client `fetch({ signal })`. The infrastructure was already in place at every LLM client; only the orchestrator wiring was missing.
1528
1545
  - **`LocalAgent.#lifecycleAbortController`**: every agent owns a private controller fired by `dispose()`. `send()` composes `[userSignal, lifecycleSignal]` via `anySignal` so eviction (`Agent.registry.evict`) cancels in-flight LLM calls promptly.
1529
- - **`anySignal` ponyfill** (`internal/runtime/abort-utils.ts`) absorbs EC-5: native `AbortSignal.any` when available, ponyfill for runtimes (Vercel Edge subset) that lag. Single-signal short-circuit, undefined entries filtered, abort `reason` propagated.
1546
+ - **`anySignal` ponyfill** (`internal/runtime/abort-utils.ts`) absorbs EC-5: native `AbortSignal.any` when available, ponyfill for edge runtimes that lag. Single-signal short-circuit, undefined entries filtered, abort `reason` propagated.
1530
1547
  - **`AgentLoopInputs.signal`** new optional field; loop uses caller's signal when present, never-aborting placeholder otherwise (legacy behavior preserved when nothing wired).
1531
1548
  - **Aborted runs surface as `AgentRunError({ code: "aborted", retriable: false })`** (D321 + T3.5 finalization). `err.cause` preserves the original `DOMException`.
1532
1549
  - **Aborted runs do not persist partial assistant messages** (D320): the user message persists at entry; the abort path skips the assistant append, preserving conversation history invariant.
@@ -1576,7 +1593,7 @@ Closes Gap 2 of the TheoKit cross-repo handoff. Eliminates OOM in 24/7 Node depl
1576
1593
 
1577
1594
  ### Added (`ConversationStorageAdapter` — pluggable conversation persistence — Production-Readiness #1)
1578
1595
 
1579
- Closes Gap 1 of the TheoKit cross-repo production-readiness handoff (`docs/handoffs/from-theokit/2026-05-25-production-readiness.md`). Unblocks serverless (Vercel, Cloudflare Workers, Lambda) and multi-host (K8s replicas, TheoCloud canary) deploys that cannot use the default `<cwd>/.theokit/agents/<id>/messages.jsonl` filesystem persistence.
1596
+ Closes Gap 1 of the TheoKit cross-repo production-readiness handoff (`docs/handoffs/from-theokit/2026-05-25-production-readiness.md`). Unblocks serverless (Cloudflare Workers, AWS Lambda) and multi-host (K8s replicas, TheoCloud canary) deploys that cannot use the default `<cwd>/.theokit/agents/<id>/messages.jsonl` filesystem persistence.
1580
1597
 
1581
1598
  - **`ConversationStorageAdapter`** interface exported from `@theokit/sdk`. 5 methods (`getMessages`, `appendMessage`, `deleteConversation`, optional `listConversationIds`, optional `compact`, optional `dispose`). Implementations return `Promise<>` uniformly for adapter polymorphism (ADR D306).
1582
1599
  - **`FileSystemConversationStorage`** exported. Default when `AgentOptions.conversationStorage` is unset (zero migration — existing apps unaffected). Wraps the pre-D303 byte-identical behavior including redaction (D68) + compaction every 50 appends (D18). Path-traversal guard re-applied in `deleteConversation` (EC-1, ADR D304); ENOENT swallowed in `listConversationIds` for first-run deploys (EC-2).
@@ -1647,7 +1664,7 @@ and llama.cpp sibling profiles. 100% local, zero remote API keys required.**
1647
1664
  into `OpenAIClient` via new optional `providerName` constructor option.
1648
1665
  - **Provider inference from model.id prefix** (D186). `model: "ollama/llama3.2:3b"`
1649
1666
  routes to the Ollama profile and sends `llama3.2:3b` as the model name to the
1650
- LLM body. Aligned with OpenRouter / Hermes / Mastra patterns. Aliases
1667
+ LLM body. Aligned with OpenRouter / Hermes provider-prefix routing patterns. Aliases
1651
1668
  `llama-cpp`/`llama.cpp` → `llamacpp`, `lm-studio` → `lmstudio`.
1652
1669
  - **CredentialPool no-op for `authType: "none"`** (D187). `apiKeys: { ollama: [...] }`
1653
1670
  is silently ignored with one-shot stderr warn instead of building a meaningless
@@ -2343,10 +2360,10 @@ tool-call-failure-recovery.md` (Hermes v0.2 #444, v0.3 #1300,
2343
2360
  - D63 — WAL primary, DELETE journal fallback on NFS/SMB; warn once per label
2344
2361
  - D64 — FTS5 sanitizer 6-step + CJK auto-detection (trigram routing deferred to v1.4)
2345
2362
 
2346
- ### Added (v1.2 features — paridade técnica com Vercel AI / Mastra)
2363
+ ### Added (v1.2 features)
2347
2364
 
2348
2365
  - **`Agent.streamObject<T>({ schema, prompt, ... })`** — typed structured output WITH partial-object streaming via synthetic forced tool (ADR D39). Returns `AsyncIterator<StreamObjectEvent<T>>` emitting zero or more `{ type: "partial", partial: DeepPartial<T>, attempt }` events plus exactly one `{ type: "complete", object: z.infer<T>, ... }` at the end. Reuses 80% of `generateObject` infrastructure. EC-4 (cancellation cleanup), EC-5 (refine/transform fallback), EC-6 (parallel tool-use dedup) covered by tests.
2349
- - **`@theokit/react` v1.2.0 — family of 3 hooks** (ADR D40): `useTheoChat` (multi-turn, existing) + `useTheoCompletion` (single-shot text gen, equivalent to Vercel `useCompletion`) + `useTheoAssistant<T>` (object-shaped streaming, wraps `Agent.streamObject`). Each hook has a matching server-side handler: `streamTheoChat`, `streamCompletion`, `streamAssistant`. Shared SSE parser in `internal/sse-parser.ts` handles all wire codes including new `o:`/`O:` for object streaming (ADR D45).
2366
+ - **`@theokit/react` v1.2.0 — family of 3 hooks** (ADR D40): `useTheoChat` (multi-turn, existing) + `useTheoCompletion` (single-shot text gen) + `useTheoAssistant<T>` (object-shaped streaming, wraps `Agent.streamObject`). Each hook has a matching server-side handler: `streamTheoChat`, `streamCompletion`, `streamAssistant`. Shared SSE parser in `internal/sse-parser.ts` handles all wire codes including new `o:`/`O:` for object streaming (ADR D45).
2350
2367
  - **OAuth 2.1 PKCE for MCP HTTP servers** (ADR D41). `McpAuthConfig.oauth` opts into the flow. Two modes: `manual` (paste callback URL via stdin, SSH-friendly) and `localhost` (auto-spawned http.createServer on a free port). Token storage prefers OS keychain (`keytar`, optional peer dep) with `~/.theokit/mcp-tokens.json` (chmod 600) fallback. EC-2 (state CSRF validation), EC-9 (concurrent refresh serialization), EC-10 (default expires_in 3600s) covered.
2351
2368
  - **Auto-instrumentation of telemetry vendors** (ADR D42). `tracer.ts` feature-detects `@langfuse/node` v3+, `@sentry/node`, and `posthog-node` via `createRequire`. When present + `telemetry.enabled: true`, registers OTel exporter automatically. Opt-out via `telemetry.autoDetect: false` OR `telemetry.disable: ["langfuse"]`. EC-12 (double-billing prevention) covered.
2352
2369
  - **LanceDB backend for Memory.index** (ADR D43). `Memory.create({ index: { backend: "lance" } })` activates `@lancedb/lancedb` (optional peer dep). SQLite remains default. Lance scales to 100k+ facts. Filters use Lance's structured filter API — NO string interpolation, EC-1 MUST FIX. EC-8 (embedding dim mismatch) typed error.
@@ -2371,7 +2388,7 @@ tool-call-failure-recovery.md` (Hermes v0.2 #444, v0.3 #1300,
2371
2388
 
2372
2389
  - **`Agent.generateObject<T>({ schema, prompt })`** — typed structured output via synthetic forced tool (ADR D33). Returns `{ object: z.infer<T>, raw, usage, finishReason }`. Retry-on-parse-fail with `maxRetries` (default 1). Transient agent disposed AND hard-deleted from registry across retries (EC-3 no leak). Same provider routing/fallback as `agent.send`.
2373
2390
  - **`AgentOptions.telemetry`** — opt-in OpenTelemetry spans for `agent.send`, `llm.call`, `tool.call` (ADR D34). Privacy-by-default: NO content logged unless `includeContent: true`. `@opentelemetry/api` is OPTIONAL peer dep loaded via `createRequire`. All OTel calls wrapped in `safe()` so exporter errors NEVER propagate to `agent.send` (EC-1).
2374
- - **`@theokit/react` v1.0.0** — new workspace package (ADR D32). `useTheoChat` React hook (HTTP fetch + SSE parser, AbortController on unmount, EC-6 5xx handling, EC-8 graceful close). `streamTheoChat` Next.js-compatible SSE handler (EC-2 pre-stream typed errors return HTTP 400/401). Wire format = Vercel AI Data Stream v1 (drop-in `useChat` migration; no `ai` package runtime dep). React peer dep `^18 || ^19`.
2391
+ - **`@theokit/react` v1.0.0** — new workspace package (ADR D32). `useTheoChat` React hook (HTTP fetch + SSE parser, AbortController on unmount, EC-6 5xx handling, EC-8 graceful close). `streamTheoChat` Next.js-compatible SSE handler (EC-2 pre-stream typed errors return HTTP 400/401). Wire format = Data Stream v1 (drop-in `useChat` migration; no `ai` package runtime dep). React peer dep `^18 || ^19`.
2375
2392
 
2376
2393
  ### Validations (v1.1 pillar audits)
2377
2394
 
@@ -2458,7 +2475,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2458
2475
 
2459
2476
  ### Added (multimodal demo `examples/telegram-pro`)
2460
2477
 
2461
- - **New `examples/telegram-pro/`** — ~600 LoC Telegram bot that reproduces the 5 highest-value patterns from OpenClaw's `extensions/telegram` (187 production files) on top of `@theokit/sdk` 1.0.0:
2478
+ - **New `examples/telegram-pro/`** — ~600 LoC Telegram bot demonstrating the 5 highest-value Telegram integration patterns on top of `@theokit/sdk` 1.0.0:
2462
2479
  - **Voice transcription** ([`src/transcribe.ts`](../../examples/telegram-pro/src/transcribe.ts)) — downloads the OGG/Opus from Telegram, POSTs multipart to Whisper. Provider order: `OPENAI_API_KEY` → `GROQ_API_KEY` → graceful "voice not configured" reply. Transcript is injected into the agent loop as `[voice transcript: ...]`.
2463
2480
  - **Vision** ([`src/vision.ts`](../../examples/telegram-pro/src/vision.ts)) — photo and sticker descriptions via `google/gemini-2.0-flash-001` multimodal on OpenRouter. Disk-cached at `.theokit/cache/vision/<sha256>.txt` keyed by Telegram's `file_unique_id`, so repeated stickers (common in groups) skip the LLM roundtrip.
2464
2481
  - **Inline buttons** ([`src/buttons.ts`](../../examples/telegram-pro/src/buttons.ts)) — agent emits `[BUTTONS: A | B | C]` at end of reply; example strips the marker, renders a grammy `InlineKeyboard`, and routes button taps back to the agent as `[user tapped button: A]` so conversation history stays consistent.
@@ -2587,15 +2604,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2587
2604
  - **`placeholderScript` renamed to `unusedFixtureScript`** in `real-local-run.ts` + `real-cloud-run.ts` with a clarifying comment — the FixtureScript shape is required by the base Run class but never consumed by the real-LLM path.
2588
2605
  - **`index-schema.ts` comment** corrected — `meta` table description matches what the code actually persists (embedding identity), and the `embeddings` virtual table is now documented.
2589
2606
 
2590
- ### Changed (memory-system-openclaw-parity, Increment D — Dogfood follow-ups)
2607
+ ### Changed (memory-system parity, Increment D — Dogfood follow-ups)
2591
2608
 
2592
2609
  - **`local-agent.ts` decomposed** — memory glue (lazy IndexManager + tools cache + Active Memory breaker + summary cache) extracted to `local-agent-memory.ts`. Brings `local-agent.ts` under the G8 400-LoC cap.
2593
2610
  - **`legacyMemoryJsonPath` centralized in `memory/types.ts`** — removes the 9-line jscpd clone between `migration.ts` and `runtime/memory-store.ts`. Both now call the leaf-module helper.
2594
2611
 
2595
- ### Added (memory-system-openclaw-parity, Increment C — Dogfood examples + Memory namespace)
2612
+ ### Added (memory-system parity, Increment C — Dogfood examples + Memory namespace)
2596
2613
 
2597
2614
  - **`Memory` public namespace** exported from `@theokit/sdk` — `Memory.runDreamingSweep({ cwd, embedding })` lets users trigger consolidation outside of `agent.send()` (e.g. from a cron job handler).
2598
- - **`MemoryEmbeddingRuntime` public type** — `embedding` now accepts either a built-in provider id (`{ provider, model? }`) OR a BYO runtime (`{ runtime: MemoryEmbeddingRuntime }`). Enables self-hosted/local embedding models and self-contained demos without external API creds. Mirrors OpenClaw's `EmbeddingRuntime` shape from ADR D3.
2615
+ - **`MemoryEmbeddingRuntime` public type** — `embedding` now accepts either a built-in provider id (`{ provider, model? }`) OR a BYO runtime (`{ runtime: MemoryEmbeddingRuntime }`). Enables self-hosted/local embedding models and self-contained demos without external API creds. Follows the `EmbeddingRuntime` shape from ADR D3.
2599
2616
  - **4 new example apps** under `examples/`:
2600
2617
  - **`memory-search`** — LLM uses `memory_search` to find facts in MEMORY.md.
2601
2618
  - **`memory-get`** — LLM uses `memory_get` for bounded reads of `notes/*.md`.
@@ -2603,44 +2620,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2603
2620
  - **`memory-dreaming`** — `Memory.runDreamingSweep` consolidates duplicates + clusters + writes a dream-diary entry. Ships with a deterministic local-demo embedding fallback so the example runs without `OPENAI_API_KEY` / `MISTRAL_API_KEY`.
2604
2621
  - **`examples/README.md` inventory** updated with all 4 new examples marked ✅ Full.
2605
2622
 
2606
- ### Added (memory-system-openclaw-parity, Increment B — Active Memory wire-up)
2623
+ ### Added (memory-system parity, Increment B — Active Memory wire-up)
2607
2624
 
2608
2625
  - **`memory.activeRecall.enabled`** runtime wire-up — when `true`, the SDK calls `runActiveMemory` before every `send()` and prepends the recall summary as a `<active-memory>` block to the LLM system prompt (priority 5 — above context/skills/memory).
2609
2626
  - **Per-agent `CircuitBreaker` + `ActiveMemoryCache`** — instantiated lazily on first send with active recall enabled. Keyed by `agentId` so multiple agents in the same process don't share state.
2610
2627
  - **Stub-server E2E proof** — captured Anthropic request body contains `<active-memory>` when enabled, and does NOT when disabled.
2611
2628
  - **Active recall config surface** — `queryMode` (`"message"` / `"recent"` / `"full"`), `timeoutMs`, `maxSummaryChars`, `persistTranscripts` are all wired from `MemorySettings.activeRecall` through to `runActiveMemory`.
2612
2629
 
2613
- ### Added (memory-system-openclaw-parity, Increment A — Agent.create/send wire-up)
2630
+ ### Added (memory-system parity, Increment A — Agent.create/send wire-up)
2614
2631
 
2615
2632
  - **`MemorySettings.index`** public field — `{ tools?: boolean; backend?: "sqlite-vec" | "lancedb"; embedding?: { provider, model? } }`. When `memory.enabled === true` and `index.tools !== false`, the SDK lazily opens an `IndexManager` on first send + registers `memory_search` and `memory_get` with the LLM. Default backend is `sqlite-vec`; default embedding is none (FTS-only mode).
2616
2633
  - **`MemorySettings.activeRecall`** public field — reserved for Phase 7 wire-up (next increment). Type surface live today; runtime hookup pending.
2617
2634
  - **Stub-server E2E tests** prove memory tools appear in the captured Anthropic request body's `tools` array when memory is enabled, and are absent when disabled or opted-out via `index.tools: false`.
2618
2635
  - **Lazy embedding adapter resolution** — when `index.embedding.provider` is set, the SDK looks the adapter up via `MEMORY_EMBEDDING_ADAPTERS` and instantiates it on first send. Adapter failures degrade gracefully to FTS-only mode with a stderr warning.
2619
2636
 
2620
- ### Added (memory-system-openclaw-parity, Phase 13)
2637
+ ### Added (memory-system parity, Phase 13)
2621
2638
 
2622
- - **Cross-validation report** at `.claude/knowledge-base/reviews/cross-validation/memory-system-openclaw-parity-xval-2026-05-16.md`. Verdict **APROVADO COM RESSALVAS**, zero BLOCKERs. All 10 ADRs cross-checked against shipped code; all 13 edge cases verified resolved or documented.
2639
+ - **Cross-validation report** at `.claude/knowledge-base/reviews/cross-validation/memory-system-parity-xval-2026-05-16.md`. Verdict **APROVADO COM RESSALVAS**, zero BLOCKERs. All 10 ADRs cross-checked against shipped code; all 13 edge cases verified resolved or documented.
2623
2640
 
2624
- ### Added (memory-system-openclaw-parity, Phase 12)
2641
+ ### Added (memory-system parity, Phase 12)
2625
2642
 
2626
2643
  - **Backend selector** — `IndexManager.open({ backend: "sqlite-vec" | "lancedb" })`. Default `"sqlite-vec"`. `"lancedb"` reserved for Phase 12.1; throws `ConfigurationError(code: "memory_backend_not_implemented")` today (same KISS pattern as the Phase 11 stub embedding adapters).
2627
2644
 
2628
- ### Added (memory-system-openclaw-parity, Phase 11)
2645
+ ### Added (memory-system parity, Phase 11)
2629
2646
 
2630
- - **`MEMORY_EMBEDDING_ADAPTERS` catalog** exports all 7 OpenClaw provider ids: `openai`, `mistral`, `voyage`, `deepinfra`, `lmstudio`, `google`, `bedrock`. Switching is one config field.
2647
+ - **`MEMORY_EMBEDDING_ADAPTERS` catalog** exports all 7 provider ids: `openai`, `mistral`, `voyage`, `deepinfra`, `lmstudio`, `google`, `bedrock`. Switching is one config field.
2631
2648
  - **Mistral adapter** fully implemented — `mistral-embed` (1024 dims) via shared OpenAI-compatible factory (`POST /v1/embeddings`). Honors `MISTRAL_API_KEY` + `MISTRAL_API_BASE_URL`.
2632
2649
  - **`createOpenAiCompatibleRuntime` shared factory** — extracted from the OpenAI adapter so any provider exposing the `{ model, input }` → `{ data: [{ embedding }] }` REST shape can plug in with a one-file thin wrapper.
2633
2650
  - **5 stub adapters** (Voyage, DeepInfra, LMStudio, Google, Bedrock) — metadata-only. `embed()` throws `ConfigurationError(code: "adapter_not_implemented")` so callers detect the gap without crashing the agent loop.
2634
2651
 
2635
- ### Added (memory-system-openclaw-parity, Phase 10)
2652
+ ### Added (memory-system parity, Phase 10)
2636
2653
 
2637
2654
  - **Wiki supplements** — files under `.theokit/memory/wiki/*.md` are read-only auxiliary corpora discovered by `discoverWikiFiles`. Indexed alongside `MEMORY.md` + `notes/*.md` with `source: "wiki"` tag in the `files` table.
2638
- - **Corpus filtering in search** — `IndexManager.search(query, { sources: ["wiki"] })` returns only wiki hits; default search returns memory + wiki together. `memory_search` tool already honors `corpus: "wiki" | "memory" | "all"` per the OpenClaw schema from Phase 6.
2655
+ - **Corpus filtering in search** — `IndexManager.search(query, { sources: ["wiki"] })` returns only wiki hits; default search returns memory + wiki together. `memory_search` tool already honors `corpus: "wiki" | "memory" | "all"` per the tool schema from Phase 6.
2639
2656
  - **Source coercion on conflict** — `upsertFile` accepts an explicit `source` arg so reclassifying a file (moving a note into the wiki dir, etc.) updates the tag on next sync via `ON CONFLICT DO UPDATE SET source = excluded.source`.
2640
2657
 
2641
- ### Added (memory-system-openclaw-parity, Phase 9)
2658
+ ### Added (memory-system parity, Phase 9)
2642
2659
 
2643
- - **`runDreamingSweep`** — cron-driven memory consolidation (ADR D7). Three phases mirror OpenClaw:
2660
+ - **`runDreamingSweep`** — cron-driven memory consolidation (ADR D7). Three phases:
2644
2661
  - **light** — drop near-duplicate facts via cosine similarity (default threshold 0.95).
2645
2662
  - **REM** — single-link agglomerative clustering by cosine similarity (default threshold 0.75).
2646
2663
  - **deep** — write a `notes/dreamed-<ts>.md` per sweep with consolidated clusters.
@@ -2648,53 +2665,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2648
2665
  - **All dreaming writes are atomic (EC-3)** — `replaceFileAtomic` for notes and diary; per-cwd mutex held for the whole sweep so concurrent `Remember:` appends can't race.
2649
2666
  - **LLM narrative summarization deferred to Phase 9.1** — v1 ships deterministic clustering only. The interface is stable enough to plug an LLM-mediated `narrative.ts` later without changing the orchestrator.
2650
2667
 
2651
- ### Added (memory-system-openclaw-parity, Phase 8)
2668
+ ### Added (memory-system parity, Phase 8)
2652
2669
 
2653
2670
  - **CircuitBreaker** for Active Memory — `{ maxTimeouts: 3, cooldownMs: 60000 }` defaults. After N consecutive timeouts, `shouldSkip(key)` returns `true` until cooldown elapses. `recordSuccess` resets the counter immediately. Per-key isolation (multiple agents in one process don't share state).
2654
2671
  - **`ActiveMemoryCache`** — TTL-bounded LRU keyed by `sha256(userText + queryMode)`. Default TTL 15s, capacity 1000. Cache hits skip the IndexManager search entirely.
2655
2672
  - **`runActiveMemory` integration** — accepts optional `breaker` + `cache` + `agentKey` + `runId` + `persistTranscripts` + `cwd`. Breaker is consulted on entry and updated by status; cache stores results on the way out; transcripts written under `.theokit/memory/transcripts/active-memory/<runId>.json` when enabled.
2656
2673
  - **`persistActiveMemoryTranscript`** — JSON transcript persistence. Failures swallowed with stderr warning so transcript IO never crashes the agent run.
2657
2674
 
2658
- ### Added (memory-system-openclaw-parity, Phase 7)
2675
+ ### Added (memory-system parity, Phase 7)
2659
2676
 
2660
2677
  - **`runActiveMemory`** — blocking pre-send recall (ADR D6). Default `mode: "search"` calls `IndexManager.search` deterministically; `mode: "subagent"` (LLM-mediated curation) is stubbed for Phase 7.1. Query modes: `"message"` (only the user text), `"recent"` (user text + last N user turns, default 2), `"full"` (entire conversation). Hard timeout via `Promise.race` (default 15000ms) — returns `status: "timeout"` instead of throwing.
2661
2678
  - **Status discriminator** — `ActiveMemoryStatus` covers `"ok" | "timeout" | "skipped" | "no-recall" | "error"`. Caller-side dispatch is one switch statement.
2662
2679
  - **`ActiveMemoryPromptProvider`** at priority 5 (before context/skills/memory) — contributes the `<active-memory>` block via `SystemPromptAssemblyContext.activeMemorySummary`. Summary is XML-escaped (D9). Block omitted when summary is empty.
2663
2680
  - **Pipeline auto-registration** — `SystemPromptPipeline.default()` now wires 5 providers: ActiveMemory (5) → Context (10) → Skills (20) → Memory (30) → Base (100).
2664
2681
 
2665
- ### Added (memory-system-openclaw-parity, Phase 6)
2682
+ ### Added (memory-system parity, Phase 6)
2666
2683
 
2667
- - **`memory_search` + `memory_get` tools** (ADR D5) with OpenClaw-mirrored JSON schemas and descriptions. `memory_search` returns ranked hits with `{ path, startLine, endLine, score, snippet, citation, source }`; `memory_get` returns bounded excerpts with truncation info.
2684
+ - **`memory_search` + `memory_get` tools** (ADR D5) with well-defined JSON schemas and descriptions. `memory_search` returns ranked hits with `{ path, startLine, endLine, score, snippet, citation, source }`; `memory_get` returns bounded excerpts with truncation info.
2668
2685
  - **Path-traversal guard (EC-2)** — `memory_get` resolves the requested path against the memory root and throws `ConfigurationError(code: "memory_path_escapes_root")` if the resolved path escapes (e.g. `../../etc/passwd`).
2669
2686
  - **Result-size cap (EC-10)** — `memory_search` truncates the response when concatenated snippets exceed `maxTotalChars` (default 16384). Low-rank hits are dropped first; `truncated: true` marker on the payload.
2670
2687
  - **Agent-loop integration** — new `AgentLoopInputs.memoryTools?: MemoryToolSpec[]` field; `collectTools` appends memory tools alongside shell + MCP tools; `tool-dispatch` routes `origin === "memory"` calls through a dedicated handler that wraps JSON-encoded results.
2671
2688
 
2672
- ### Added (memory-system-openclaw-parity, Phase 5)
2689
+ ### Added (memory-system parity, Phase 5)
2673
2690
 
2674
2691
  - **sqlite-vec vector index** under the existing SQLite DB (ADR D2). `vec0` virtual table stores per-chunk embeddings; `vectorSearch` runs KNN with `MATCH` syntax. `loadSqliteVecExtension` wraps the native load with a typed `sqlite_vec_unavailable` ConfigurationError (EC-8) instead of a raw native exception.
2675
2692
  - **`meta` table tracks embedding identity** (`providerId` + `model` + `dimension`). On `IndexManager.open`, current adapter config is compared against stored meta — any mismatch drops the `embeddings` table and forces a full re-embed on next `sync()` (EC-1).
2676
2693
  - **Hybrid scoring** (ADR D4): FTS top-K + vector top-K merged, scores combined via `vectorScore * vectorWeight + textScore * textWeight` (defaults `0.6` / `0.4`, configurable per-call). Vector-only hits surface alongside FTS hits via a chunk-id outer join. `MemorySearchHit.vectorScore` exposed when vector backend is active.
2677
2694
  - **`IndexManager.open({ cwd, embedding? })`** — embedding-aware constructor. FTS-only still works when `embedding` is omitted; backend reported via `status().backend` as `"fts-only"` or `"hybrid"`.
2678
2695
 
2679
- ### Added (memory-system-openclaw-parity, Phase 4)
2696
+ ### Added (memory-system parity, Phase 4)
2680
2697
 
2681
- - **`MemoryEmbeddingProviderAdapter` interface** (ADR D3) mirrors OpenClaw's contract: `id`, `defaultModel`, `transport`, `authProviderId`, `autoSelectPriority`, `create(options) → EmbeddingRuntime`. Adapters live under `internal/memory/adapters/`.
2698
+ - **`MemoryEmbeddingProviderAdapter` interface** (ADR D3) defines the adapter contract: `id`, `defaultModel`, `transport`, `authProviderId`, `autoSelectPriority`, `create(options) → EmbeddingRuntime`. Adapters live under `internal/memory/adapters/`.
2682
2699
  - **OpenAI embedding adapter** (`openai-embedding.ts`) — native fetch only, no `openai` SDK dep. Batches at 100 texts/call. Retries once on 429 + 5xx with linear backoff (EC-9). Empty inputs skipped. Honors `OPENAI_API_KEY` + `OPENAI_API_BASE_URL`. Default model `text-embedding-3-small` (1536 dims).
2683
2700
  - **LRU embedding cache** keyed by `sha256(model+text)`. Max 5000 entries; oldest evicted first. Observable via `runtime.stats()` (`cacheHits` / `cacheMisses` / `httpCalls` / `retries`).
2684
2701
 
2685
- ### Added (memory-system-openclaw-parity, Phase 3)
2702
+ ### Added (memory-system parity, Phase 3)
2686
2703
 
2687
2704
  - **SQLite + FTS5 index** at `.theokit/memory/.index/memory.sqlite` (ADR D2). Schema: `files`, `chunks`, `chunks_fts` (FTS5 virtual table), `meta`. Triggers keep FTS in sync with `chunks` on insert/delete. WAL mode, foreign keys on. Backed by `better-sqlite3` (optional peer dep) — `node:sqlite` fallback path documented for Node 22.5+.
2688
2705
  - **`IndexManager.open / sync / search / status / close`** — full lifecycle. `sync()` walks `MEMORY.md` + `notes/*.md`, computes content hashes, skips unchanged files, deletes old chunks before reindexing changed ones. `search()` runs FTS5 BM25 ranking, returns `MemorySearchHit[]` with `path`, `startLine`, `endLine`, `score`, `textScore`, `snippet`, `source`, `citation` (path:startLine-endLine).
2689
2706
  - **Corrupt-DB recovery (EC-7)** — when opening fails with "malformed" / "not a database" / "encrypted" errors, the file is renamed to `<path>.corrupt-<ts>` (plus `-wal` and `-shm` siblings) and the schema is rebuilt from scratch. Diagnostic line emitted to stderr.
2690
2707
 
2691
- ### Added (memory-system-openclaw-parity, Phase 2)
2708
+ ### Added (memory-system parity, Phase 2)
2692
2709
 
2693
2710
  - **`chunkMarkdown`** splits markdown by heading boundaries + blank-line paragraph boundaries. Oversize paragraphs split on word-boundary nearest the cap (EC-6) — never mid-word. Each chunk carries `startLine` / `endLine` / `text` / `hash` (sha256) / optional `heading`.
2694
- - **`readMemoryFileBounded`** — bounded read with `from` (1-indexed) + `lines` (default 200, mirrors OpenClaw's `DEFAULT_MEMORY_READ_LINES`). Returns `linesReturned`, `totalLines`, `remainingLines`, `truncated` (true when content remains past the slice). Foundation for Phase 6's `memory_get` tool.
2695
- - Public types `MemoryChunk`, `MemoryReadResult`, `MemoryFileEntry` in `internal/memory/types.ts` mirroring OpenClaw's engine-storage shapes.
2711
+ - **`readMemoryFileBounded`** — bounded read with `from` (1-indexed) + `lines` (default 200, matching the `DEFAULT_MEMORY_READ_LINES` constant). Returns `linesReturned`, `totalLines`, `remainingLines`, `truncated` (true when content remains past the slice). Foundation for Phase 6's `memory_get` tool.
2712
+ - Public types `MemoryChunk`, `MemoryReadResult`, `MemoryFileEntry` in `internal/memory/types.ts` capturing the engine-storage shapes.
2696
2713
 
2697
- ### Added (memory-system-openclaw-parity, Phase 1)
2714
+ ### Added (memory-system parity, Phase 1)
2698
2715
 
2699
2716
  - **Markdown-first memory storage** (ADR D1) — facts now persist to `.theokit/memory/MEMORY.md` under a `## Facts` section, human-editable and git-friendly. The legacy JSON file (`.theokit/memory/<namespace>/<scope>-<userId>.json`) migrates one-shot on first read and is deleted afterward (ADR D8). Behavior is preserved: `readMemoryFacts` + `appendMemoryFact` keep their signatures.
2700
2717
  - **`replaceFileAtomic` + per-cwd mutex** — every append writes to `<file>.tmp`, fsync, rename; concurrent appends within the same process serialize through a per-`cwd` mutex (edge-case review EC-4). Multi-process safety is out of scope for v1 (documented).