@warlock.js/ai 4.5.0 → 4.6.1

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 (159) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cjs/index.cjs +20 -1
  3. package/cjs/{src-DFibP2FQ.cjs → src-Bmajk4Qg.cjs} +1 -1
  4. package/cjs/{src-C02yzsLs.cjs → src-OZyDYHxm.cjs} +2789 -691
  5. package/cjs/src-OZyDYHxm.cjs.map +1 -0
  6. package/esm/agent/agent-config.type.d.mts +29 -0
  7. package/esm/agent/agent-config.type.d.mts.map +1 -1
  8. package/esm/agent/agent.d.mts.map +1 -1
  9. package/esm/agent/agent.mjs +126 -7
  10. package/esm/agent/agent.mjs.map +1 -1
  11. package/esm/agent/signature.mjs +57 -0
  12. package/esm/agent/signature.mjs.map +1 -0
  13. package/esm/agent/snapshot.mjs +101 -0
  14. package/esm/agent/snapshot.mjs.map +1 -0
  15. package/esm/ai-openai/src/image.mjs +5 -0
  16. package/esm/ai-openai/src/index.mjs +3 -0
  17. package/esm/ai-openai/src/sdk.mjs +3 -0
  18. package/esm/ai-openai/src/speech.mjs +5 -0
  19. package/esm/ai-openai/src/transcription.mjs +6 -0
  20. package/esm/ai-openai/src/utils/index.mjs +1 -0
  21. package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
  22. package/esm/ai.d.mts +45 -0
  23. package/esm/ai.d.mts.map +1 -1
  24. package/esm/ai.mjs +37 -1
  25. package/esm/ai.mjs.map +1 -1
  26. package/esm/contracts/agent/agent-options.type.d.mts +22 -2
  27. package/esm/contracts/agent/agent-options.type.d.mts.map +1 -1
  28. package/esm/contracts/agent/agent-snapshot.type.d.mts +90 -0
  29. package/esm/contracts/agent/agent-snapshot.type.d.mts.map +1 -0
  30. package/esm/contracts/agent/agent.contract.d.mts +29 -1
  31. package/esm/contracts/agent/agent.contract.d.mts.map +1 -1
  32. package/esm/contracts/agent/index.d.mts +2 -1
  33. package/esm/contracts/image-model.contract.d.mts +156 -0
  34. package/esm/contracts/image-model.contract.d.mts.map +1 -0
  35. package/esm/contracts/index.d.mts +8 -3
  36. package/esm/contracts/planner/index.d.mts +3 -2
  37. package/esm/contracts/planner/planner-config.type.d.mts +30 -0
  38. package/esm/contracts/planner/planner-config.type.d.mts.map +1 -1
  39. package/esm/contracts/planner/planner-execute-options.type.d.mts +13 -1
  40. package/esm/contracts/planner/planner-execute-options.type.d.mts.map +1 -1
  41. package/esm/contracts/planner/planner-snapshot.type.d.mts +77 -0
  42. package/esm/contracts/planner/planner-snapshot.type.d.mts.map +1 -0
  43. package/esm/contracts/planner/planner.contract.d.mts +21 -1
  44. package/esm/contracts/planner/planner.contract.d.mts.map +1 -1
  45. package/esm/contracts/result/base-report.type.d.mts +1 -1
  46. package/esm/contracts/result/base-report.type.d.mts.map +1 -1
  47. package/esm/contracts/result/base-report.type.mjs.map +1 -1
  48. package/esm/contracts/sdk-adapter.contract.d.mts +37 -0
  49. package/esm/contracts/sdk-adapter.contract.d.mts.map +1 -1
  50. package/esm/contracts/speech-model.contract.d.mts +97 -0
  51. package/esm/contracts/speech-model.contract.d.mts.map +1 -0
  52. package/esm/contracts/transcription-model.contract.d.mts +101 -0
  53. package/esm/contracts/transcription-model.contract.d.mts.map +1 -0
  54. package/esm/errors/agent-drift-error.d.mts +32 -0
  55. package/esm/errors/agent-drift-error.d.mts.map +1 -0
  56. package/esm/errors/agent-drift-error.mjs +31 -0
  57. package/esm/errors/agent-drift-error.mjs.map +1 -0
  58. package/esm/errors/error-code.type.d.mts +1 -1
  59. package/esm/errors/index.d.mts +2 -0
  60. package/esm/errors/index.mjs +2 -0
  61. package/esm/errors/planner-drift-error.d.mts +34 -0
  62. package/esm/errors/planner-drift-error.d.mts.map +1 -0
  63. package/esm/errors/planner-drift-error.mjs +33 -0
  64. package/esm/errors/planner-drift-error.mjs.map +1 -0
  65. package/esm/image/image-cost.d.mts +32 -0
  66. package/esm/image/image-cost.d.mts.map +1 -0
  67. package/esm/image/image-cost.mjs +55 -0
  68. package/esm/image/image-cost.mjs.map +1 -0
  69. package/esm/image/image.d.mts +92 -0
  70. package/esm/image/image.d.mts.map +1 -0
  71. package/esm/image/image.mjs +113 -0
  72. package/esm/image/image.mjs.map +1 -0
  73. package/esm/image/index.mjs +4 -0
  74. package/esm/index.d.mts +26 -4
  75. package/esm/index.mjs +20 -1
  76. package/esm/mock/index.d.mts +3 -0
  77. package/esm/mock/index.mjs +3 -0
  78. package/esm/mock/mock-config.type.d.mts +22 -0
  79. package/esm/mock/mock-config.type.d.mts.map +1 -1
  80. package/esm/mock/mock-image-model.d.mts +41 -0
  81. package/esm/mock/mock-image-model.d.mts.map +1 -0
  82. package/esm/mock/mock-image-model.mjs +52 -0
  83. package/esm/mock/mock-image-model.mjs.map +1 -0
  84. package/esm/mock/mock-sdk.d.mts +7 -1
  85. package/esm/mock/mock-sdk.d.mts.map +1 -1
  86. package/esm/mock/mock-sdk.mjs +27 -0
  87. package/esm/mock/mock-sdk.mjs.map +1 -1
  88. package/esm/mock/mock-speech-model.d.mts +31 -0
  89. package/esm/mock/mock-speech-model.d.mts.map +1 -0
  90. package/esm/mock/mock-speech-model.mjs +39 -0
  91. package/esm/mock/mock-speech-model.mjs.map +1 -0
  92. package/esm/mock/mock-transcription-model.d.mts +32 -0
  93. package/esm/mock/mock-transcription-model.d.mts.map +1 -0
  94. package/esm/mock/mock-transcription-model.mjs +36 -0
  95. package/esm/mock/mock-transcription-model.mjs.map +1 -0
  96. package/esm/planner/planner-run.d.mts +8 -0
  97. package/esm/planner/planner-run.d.mts.map +1 -1
  98. package/esm/planner/planner-run.mjs +161 -6
  99. package/esm/planner/planner-run.mjs.map +1 -1
  100. package/esm/planner/planner.d.mts.map +1 -1
  101. package/esm/planner/planner.mjs +25 -1
  102. package/esm/planner/planner.mjs.map +1 -1
  103. package/esm/planner/snapshot.mjs +95 -0
  104. package/esm/planner/snapshot.mjs.map +1 -0
  105. package/esm/rag/index.d.mts +7 -0
  106. package/esm/rag/index.mjs +7 -0
  107. package/esm/rag/loaders/errors.d.mts +19 -0
  108. package/esm/rag/loaders/errors.d.mts.map +1 -0
  109. package/esm/rag/loaders/errors.mjs +25 -0
  110. package/esm/rag/loaders/errors.mjs.map +1 -0
  111. package/esm/rag/loaders/index.mjs +7 -0
  112. package/esm/rag/loaders/load-html.d.mts +26 -0
  113. package/esm/rag/loaders/load-html.d.mts.map +1 -0
  114. package/esm/rag/loaders/load-html.mjs +138 -0
  115. package/esm/rag/loaders/load-html.mjs.map +1 -0
  116. package/esm/rag/loaders/load-pdf.d.mts +38 -0
  117. package/esm/rag/loaders/load-pdf.d.mts.map +1 -0
  118. package/esm/rag/loaders/load-pdf.mjs +150 -0
  119. package/esm/rag/loaders/load-pdf.mjs.map +1 -0
  120. package/esm/rag/loaders/load-text.d.mts +47 -0
  121. package/esm/rag/loaders/load-text.d.mts.map +1 -0
  122. package/esm/rag/loaders/load-text.mjs +60 -0
  123. package/esm/rag/loaders/load-text.mjs.map +1 -0
  124. package/esm/rag/loaders/load-web.d.mts +42 -0
  125. package/esm/rag/loaders/load-web.d.mts.map +1 -0
  126. package/esm/rag/loaders/load-web.mjs +89 -0
  127. package/esm/rag/loaders/load-web.mjs.map +1 -0
  128. package/esm/rag/loaders/loader.type.d.mts +89 -0
  129. package/esm/rag/loaders/loader.type.d.mts.map +1 -0
  130. package/esm/rag/store/pg-vector-store.d.mts +139 -0
  131. package/esm/rag/store/pg-vector-store.d.mts.map +1 -0
  132. package/esm/rag/store/pg-vector-store.mjs +328 -0
  133. package/esm/rag/store/pg-vector-store.mjs.map +1 -0
  134. package/esm/speech/index.mjs +3 -0
  135. package/esm/speech/speech.d.mts +65 -0
  136. package/esm/speech/speech.d.mts.map +1 -0
  137. package/esm/speech/speech.mjs +123 -0
  138. package/esm/speech/speech.mjs.map +1 -0
  139. package/esm/supervisor/entries.mjs +2 -2
  140. package/esm/supervisor/entries.mjs.map +1 -1
  141. package/esm/transcribe/audio-input.d.mts +47 -0
  142. package/esm/transcribe/audio-input.d.mts.map +1 -0
  143. package/esm/transcribe/audio-input.mjs +84 -0
  144. package/esm/transcribe/audio-input.mjs.map +1 -0
  145. package/esm/transcribe/index.mjs +4 -0
  146. package/esm/transcribe/transcribe.d.mts +64 -0
  147. package/esm/transcribe/transcribe.d.mts.map +1 -0
  148. package/esm/transcribe/transcribe.mjs +128 -0
  149. package/esm/transcribe/transcribe.mjs.map +1 -0
  150. package/llms-full.txt +753 -0
  151. package/llms.txt +5 -0
  152. package/package.json +3 -3
  153. package/skills/README.md +4 -0
  154. package/skills/durable-agent-runs/SKILL.md +135 -0
  155. package/skills/generate-images/SKILL.md +138 -0
  156. package/skills/generate-speech/SKILL.md +139 -0
  157. package/skills/rag-loaders-and-stores/SKILL.md +164 -0
  158. package/skills/transcribe-audio/SKILL.md +157 -0
  159. package/cjs/src-C02yzsLs.cjs.map +0 -1
package/llms.txt CHANGED
@@ -12,10 +12,13 @@
12
12
  - [attach-ai-middleware](@warlock.js/ai/attach-ai-middleware/SKILL.md): Wire agent middleware — ai.middleware.budget (token / USD caps + SLO/cost contract w/ maxLatencyMs + onViolation fallback), ai.middleware.guardrail (pre / post content checks), ai.middleware.semanticCache (exact + vector cache), supervisor-level middleware, plus authoring custom hooks (execute / trip / tool). Triggers: `ai.middleware.budget`, `ai.middleware.guardrail`, `ai.middleware.semanticCache`, `ai.middleware.compose`, `ai.middleware.forTool`, `AgentMiddleware`, `BudgetExceededError`, `GuardrailViolationError`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`; 'cap token cost', 'SLO budget', 'block pii in prompts', 'semantic cache before LLM', 'supervisor-level middleware', 'write custom hook'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; cache drivers — `@warlock.js/ai/persist-ai-data/SKILL.md`; competing libs `langchain` callbacks.
13
13
  - [define-ai-tool](@warlock.js/ai/define-ai-tool/SKILL.md): Define tools with ai.tool({...}) — typed validated async functions the model can call. Covers name / description / action / mode (feedback / silent) / input / execute, `ctx.artifacts` side-channel, `ToolExecutionError`. Triggers: `ai.tool`, `ToolContract`, `ToolContext`, `ToolCall`, `ToolExecutionError`, `artifactsSchema`, `mode: "silent"`, `workflow.asTool`; 'define a tool', 'wire tool into agent', 'tool input validation', 'side-channel artifacts'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; supervisor artifacts — `@warlock.js/ai/run-supervisor/SKILL.md`; competing libs `langchain` tools, raw `openai` function-calling.
14
14
  - [detect-and-redact-pii](@warlock.js/ai/detect-and-redact-pii/SKILL.md): Detect and redact PII (and run model-graded moderation) with @warlock.js/ai-guard detectors — `ai.guardrail.pii(...)` and the optional `ai.guardrail.moderation(...)` peer. Triggers: `ai.guardrail.pii`, `piiDetector`, `PiiDetectorOptions`, `PiiCategory`, `mask`, `{label}`, `dictionary`, `onMatch`, `ai.guardrail.moderation`, `openAiModeration`, `OpenAiModerationOptions`, `blockOn`, `omni-moderation-latest`; 'redact PII from model output', 'mask SSN / credit card / email / phone / IP', 'stop PII leaking into a tool call', 'scrub sensitive data', 'add OpenAI moderation', 'block violent / self-harm content'; typical import `import "@warlock.js/ai-guard"` (registers `ai.guardrail.pii` / `.moderation`) or `import { pii, moderation } from "@warlock.js/ai-guard"`. Skip: composing the guard / wiring it into an agent — `@warlock.js/ai-guard/guard-input-output/SKILL.md`; routing a block to a human — `@warlock.js/ai-guard/escalate-block-to-human/SKILL.md`.
15
+ - [durable-agent-runs](@warlock.js/ai/durable-agent-runs/SKILL.md): Mid-run crash-resume for agents AND planners — opt in with durable: { store, deleteOnComplete? } on the config, pass a stable runId to execute(), and call agent.resume(runId) / planner.resume(runId) after a crash to continue from the last settled trip / plan node. Reuses the ai.snapshot.{memory,pg,redis} stores; checkpoints per-trip (agent) / per-node (planner); completed trips + nodes never re-run their tools and usage is never double-counted; a drifted definition throws AgentDriftError / PlannerDriftError (bypass with { force: true }). Triggers: `durable`, `agent.resume`, `planner.resume`, `resume(runId)`, `runId`, `AgentSnapshot`, `PlannerSnapshot`, `AgentSnapshotStatus`, `PlannerSnapshotStatus`, `AgentDriftError`, `PlannerDriftError`, `computeAgentSignature`, `agent.signature`, `deleteOnComplete`, `defaultSnapshotStore`, `ai.snapshot.pg`, `ai.snapshot.memory`, `SnapshotStore`, `force: true`; 'resume an agent after a crash', 'durable agent run', 'continue a planner from where it crashed', 'checkpoint agent state', 'idempotent tool re-run on resume', 'signature drift on resume'; typical import `import { ai } from "@warlock.js/ai"`. Skip: durable human-in-the-loop approval resume (ai.human.resume of a PendingInterrupt) — `@warlock.js/ai/durable-resume/SKILL.md`; supervisor/workflow iterate-mid-turn snapshot resume + the store contracts themselves — `@warlock.js/ai/manage-ai-stores/SKILL.md`; competing libs `temporal`, `inngest`, `restate`.
15
16
  - [durable-resume](@warlock.js/ai/durable-resume/SKILL.md): Persist a gated tool call and resume it from another process hours later — ships in @warlock.js/ai core: `ai.human.resume(interruptId, decision, options)`, the `InterruptStore` (`ai.human.interrupt.{memory,pg,redis}()`), `PendingInterrupt`, and the `InterruptSuspendedError` suspend sentinel. Triggers: `ai.human.resume`, `resume(interruptId, decision)`, `InterruptStore`, `ai.human.interrupt.memory`, `ai.human.interrupt.pg`, `ai.human.interrupt.redis`, `interruptMemory`, `interruptPg`, `interruptRedis`, `PendingInterrupt`, `InterruptSuspendedError`, `ResumeOptions`, `ResumeResult`, `PgClientLike`, `RedisClientLike`; 'approve hours later from a webhook', 'persist the approval request and resume in another process', 'durable human-in-the-loop', 'store the interrupt in Postgres/Redis', 're-run the agent turn once the human approves'. Typical import `import { ai, InterruptSuspendedError } from "@warlock.js/ai"`. Skip: the in-process await gate and the policy/decision shapes — `@warlock.js/ai/approve-tool-calls/SKILL.md`.
16
17
  - [embed-text](@warlock.js/ai/embed-text/SKILL.md): Text-to-vector via sdk.embedder({...}) — embed(string) for single, embedMany(string[]) for batch. Peer primitive on the SDK adapter, not wired into agents. Compose into RAG tools, workflow run steps, or ai.middleware.semanticCache. Triggers: `sdk.embedder`, `EmbedderContract`, `embedder.embed`, `embedder.embedMany`, `EmbeddingResult`, `EmbeddingBatchResult`, `dimensions`; 'embed text', 'build RAG tool', 'populate vector store', 'embedding batch'; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: cache similarity — `@warlock.js/cache/use-cache-similarity/SKILL.md`; pgvector queries — `@warlock.js/cascade/search-by-vector/SKILL.md`; competing libs `langchain` embeddings, raw `openai.embeddings.create`.
17
18
  - [escalate-block-to-human](@warlock.js/ai/escalate-block-to-human/SKILL.md): Route a hard guardrail block to a human-review surface with @warlock.js/ai-guard — the `escalation.onBlock` seam and an `escalate: true` verdict. Triggers: `escalation`, `onBlock`, `GuardrailEscalation`, `GuardrailBlockEvent`, `escalate: true`, `{ type: "block", escalate: true }`, 'escalate a block to a human', 'human review queue for guardrail', 'page an operator on a guardrail block', 'human-in-the-loop guardrail', 'compose a block with a review surface', 'custom detector that escalates'; typical import `import "@warlock.js/ai-guard"` then `ai.guardrail({ escalation: { onBlock } })`. Skip: composing the guard / phases / verdict model — `@warlock.js/ai-guard/guard-input-output/SKILL.md`; PII/moderation detectors — `@warlock.js/ai-guard/detect-and-redact-pii/SKILL.md`; durable suspend/resume human-step machinery (deferred) — not in this package.
18
19
  - [eval-datasets-and-ci](@warlock.js/ai/eval-datasets-and-ci/SKILL.md): Datasets + regression-gated eval CI with ai.dataset({...}) feeding agent.eval({cases,baseline,tolerance}). Covers the immutable filterable/shardable dataset (cases / fromFile JSONL), DatasetEntry tags, EvalReport.regression (regressed/added/removed/passed) against a baseline, and the ai.eval reporters toJUnit / toJSON / fromJSON for CI artifacts + committed baselines. Triggers: `ai.dataset`, `DatasetContract`, `DatasetEntry`, `DatasetOptions`, `dataset.filter`, `dataset.shard`, `fromFile`, `agent.eval`, `EvalOptions`, `EvalReport`, `EvalCaseResult`, `EvalRegression`, `baseline`, `tolerance`, `ai.eval.toJUnit`, `ai.eval.toJSON`, `ai.eval.fromJSON`, `diff`, JSONL; 'eval dataset from a JSONL file', 'shard an eval suite across CI jobs', 'fail CI on an eval regression', 'emit a JUnit report', 'snapshot an eval baseline'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the scorers + LLM-as-judge + Vitest matchers themselves — `@warlock.js/ai/ai-dx-helpers/SKILL.md` (registerAiMatchers / ai.eval.exact|contains|predicate|judge); record/replay of model calls for deterministic tests — `@warlock.js/ai/record-replay-llm/SKILL.md`; competing libs `promptfoo`, `braintrust`.
20
+ - [generate-images](@warlock.js/ai/generate-images/SKILL.md): Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
21
+ - [generate-speech](@warlock.js/ai/generate-speech/SKILL.md): Text-to-speech via ai.speech({ model: sdk.speech({ name }), text }) — the audio-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter's speech() factory: OpenAI tts-1 / tts-1-hd (per-character) or gpt-4o-mini-tts (per-token). Synthesized audio is a discriminated GeneratedAudio = { type: "base64"; base64; mediaType }. Options: voice / format / speed / instructions / signal. Triggers: `ai.speech`, `sdk.speech`, `openai.speech`, `SpeechModelContract`, `GeneratedAudio`, `SpeechModelPricing`, `SpeechOptions`, `MockSpeechModel`; 'text to speech', 'TTS', 'synthesize voice', 'read this aloud', 'tts-1', 'gpt-4o-mini-tts', 'voice narration', 'audio output', 'speak this text'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: speech-to-text / transcribing a voice note — [[transcribe-audio]]; image OUTPUT — [[generate-images]]; competing libs raw `openai.audio.speech.create`, `elevenlabs` SDK.
19
22
  - [guard-input-output](@warlock.js/ai/guard-input-output/SKILL.md): Build the composed guardrail middleware with @warlock.js/ai-guard and wire it into an agent — `ai.guardrail({ input, output, tool, toolNames, escalation })`. Triggers: `ai.guardrail`, `guard`, `GuardOptions`, `GuardrailVerdict`, `GuardrailDetector`, `GuardrailPhase`, `GuardrailMatch`, `GuardrailViolationError`, `ai.guardrail.topic`, `ai.guardrail.injection`, `topicFilter`, `injectionDetector`, `toolNames`, `forTool`; 'add a guardrail to my agent', 'block prompt injection', 'filter banned topics', 'guard agent input and output', 'stop the model leaking data into a tool call', 'scope a detector to one tool'; typical import `import "@warlock.js/ai-guard"` (registers `ai.guardrail`) or `import { guard } from "@warlock.js/ai-guard"`. Skip: PII detection/redaction specifically — `@warlock.js/ai-guard/detect-and-redact-pii/SKILL.md`; routing a block to a human — `@warlock.js/ai-guard/escalate-block-to-human/SKILL.md`; the core middleware pipeline / hook contract — `@warlock.js/ai/run-ai-agent/SKILL.md`.
20
23
  - [handle-ai-errors](@warlock.js/ai/handle-ai-errors/SKILL.md): Typed AIError hierarchy with stable code strings + coarse category for retry-policy dispatch. execute() never throws — errors surface via result.error (the sole exception: OrchestratorConfigError throws at construction). Triggers: `AIError`, `ProviderRateLimitError`, `ProviderAuthError`, `ContextLengthExceededError`, `ContentFilterError`, `SchemaValidationError`, `ToolExecutionError`, `WorkflowDriftError`, `SupervisorDriftError`, `SupervisorFailedError`, `SupervisorRoutingError`, `OrchestratorFailedError`, `OrchestratorDriftError`, `OrchestratorConfigError`, `OrchestratorCancelledError`, `PlannerFailedError`, `PlannerPlanInvalidError`, `PlannerCancelledError`, `BudgetExceededError`, `GuardrailViolationError`, `error.code`, `error.category`; 'handle ai error', 'retry on rate limit', 'branch on error code', 'ORCHESTRATOR_DRIFT', 'PLANNER_PLAN_INVALID', 'build fallback ladder'; typical import `import { AIError } from "@warlock.js/ai"`. Skip: log surfacing — `@warlock.js/ai/log-ai-calls/SKILL.md`; native `try / catch` on raw `openai`.
21
24
  - [log-ai-calls](@warlock.js/ai/log-ai-calls/SKILL.md): Framework logging delegated to @warlock.js/logger — every primitive emits via the log singleton, configure channels / levels / redaction once at boot. Four-arg call convention (module, action, message, context). Triggers: `log.configure`, `log.setMinLevel`, `log.setChannels`, `ConsoleLog`, `FileLog`, `LogChannel`, `redact.paths`, `ai.agent.<name>` / `ai.workflow.<name>` / `ai.supervisor.<name>` modules; 'configure ai logging', 'mask prompts in logs', 'silence logs in tests', 'capture log entries'; typical import `import { log } from "@warlock.js/logger"`. Skip: error hierarchy — `@warlock.js/ai/handle-ai-errors/SKILL.md`; competing libs `pino`, `winston`, `console.log`.
@@ -24,6 +27,7 @@
24
27
  - [observe-ai-flows](@warlock.js/ai/observe-ai-flows/SKILL.md): The core Observer seam — a generic, tool-agnostic observability hook every flow routes its completed ExecutionReport through. Covers the per-flow `observe?: boolean | Observer` option on ai.agent / workflow / supervisor / team, the global registry (registerObserver / getObservers / setObserveAll / isObserveAll / clearObservers), resolveObservers / notifyObservers resolution, the opt-in AgentConfig.captureMessages → AgentReport.messages full-history capture, the onConfigApplied dependency-inversion seam, and that @warlock.js/ai-panoptic is the batteries-included Observer. Triggers: `Observer`, `observe`, `registerObserver`, `getObservers`, `setObserveAll`, `isObserveAll`, `clearObservers`, `resolveObservers`, `notifyObservers`, `FlowObserveOption`, `ExecutionReport`, `captureMessages`, `AgentReport.messages`, `CapturedMessage`, `onConfigApplied`, `observeAll`; 'observe an agent run', 'send finished reports to a collector', 'capture the full message history', 'observe every flow by default', 'wire panoptic / tracing'; typical import `import { ai, registerObserver } from "@warlock.js/ai"`. Skip: structured logging of events — `@warlock.js/ai/log-ai-calls/SKILL.md`; reading the report tree shape (trips / children) — `@warlock.js/ai/run-ai-agent/SKILL.md`; per-call cost / usage rollup — `@warlock.js/ai/handle-ai-errors/SKILL.md`. The batteries-included Observer is the `@warlock.js/ai-panoptic` package.
25
28
  - [persist-ai-data](@warlock.js/ai/persist-ai-data/SKILL.md): Persistence delegated to @warlock.js/cache — workflow + supervisor snapshot resume via snapshotStore (4.3.0: now a SnapshotStore from ai.snapshot.*, ⚠ moved off raw CacheDriver), semantic cache + memory via vector-capable CacheDriver, global defaults via ai.config({defaultStore}) + ai.config({defaultSnapshotStore}). Covers drift detection + three recovery paths. Triggers: `ai.config`, `defaultStore`, `defaultSnapshotStore`, `snapshotStore`, `ai.snapshot`, `wf.resume`, `supervisor.resume`, `WorkflowSnapshot`, `SupervisorSnapshot`, `WorkflowDriftError`, `SupervisorDriftError`, `force: true`; 'resume a workflow run', 'configure snapshot store', 'handle signature drift', 'wire pg vector cache'; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator checkpoint/snapshot store factories — `@warlock.js/ai/manage-ai-stores/SKILL.md`; cache driver catalog — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `temporal`, `inngest`.
26
29
  - [pick-ai-provider](@warlock.js/ai/pick-ai-provider/SKILL.md): Choose an AI provider adapter — @warlock.js/ai-openai (shipped, also handles OpenRouter / Azure via baseURL), @warlock.js/ai-anthropic, @warlock.js/ai-bedrock, @warlock.js/ai-google, @warlock.js/ai-ollama — plus cost truth: ModelPricing (per-1M tokens), Usage cost breakdown, the cachedTokens / cacheWriteTokens / reasoningTokens channels, and capability flags. Triggers: `OpenAISDK`, `SDKAdapterContract`, `ModelContract`, `ModelPricing`, `ModelCapabilities`, `sdk.model`, `sdk.embedder`, `capabilities.vision`, `capabilities.structuredOutput`, `capabilities.reasoning`, `capabilities.promptCaching`, `pricing`, `Usage.cost`, `cachedTokens`, `cacheWriteTokens`, `reasoningTokens`, `reasoning.effort`, `cacheControl`, `baseURL`, `provider: "openrouter"`; 'pick a provider', 'openai vs openrouter', 'does this model support vision/reasoning', 'configure pricing', 'how much did reasoning cost', 'prompt cache tokens'; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: agent factory — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs raw `openai`, `@anthropic-ai/sdk`, `@aws-sdk/client-bedrock-runtime`.
30
+ - [rag-loaders-and-stores](@warlock.js/ai/rag-loaders-and-stores/SKILL.md): Turn any source into a RagDocument and index it in a production vector store — the document loaders ai.rag.loadText / loadHtml / loadWeb (SSRF-safe via guardedFetch) / loadPdf (lazy pdf-parse peer), plus the swappable stores ai.rag.pgVectorStore({client}) (pgvector + ensureSchema DDL + hnsw/ivfflat index) and ai.rag.cacheVectorStore(driver), both satisfying VectorStoreContract (upsert / query / removeNamespace). Loaders return the exact RagDocument[] that kb.index() consumes — no adapter. Triggers: `ai.rag.loadText`, `ai.rag.loadHtml`, `ai.rag.loadWeb`, `ai.rag.loadPdf`, `loadText`, `loadHtml`, `loadWeb`, `loadPdf`, `ai.rag.pgVectorStore`, `ai.rag.cacheVectorStore`, `pgVectorStore`, `cacheVectorStore`, `VectorStore`, `PgVectorStoreOptions`, `PgVectorStoreInstance`, `ensureSchema`, `schema()`, `RagLoaderResult`, `LoadWebOptions`, `LoadPdfOptions`, `perPage`, `OutboundPolicy`, `guardedFetch`, `hnsw`, `ivfflat`, `pgvector`, `dimensions`, `PgClientLike`, `PDF_PARSE_INSTALL_INSTRUCTIONS`; 'load a website into a knowledge base', 'index a PDF for RAG', 'strip HTML to text for embedding', 'pgvector store for RAG', 'SSRF-safe document fetch', 'one document per PDF page', 'swap the vector store'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the chunk → embed → retrieve → rerank → cite pipeline that consumes these — `@warlock.js/ai/run-ai-rag/SKILL.md`; the raw embedder primitive — `@warlock.js/ai/embed-text/SKILL.md`; cache similarity internals — `@warlock.js/cache/use-cache-similarity/SKILL.md`; competing libs `langchain` loaders, `llamaindex` readers.
27
31
  - [record-replay-llm](@warlock.js/ai/record-replay-llm/SKILL.md): Deterministic, offline LLM tests with ai.vcr(model,{path,mode}) — a record/replay decorator over ANY ModelContract that intercepts only complete()/stream(), delegates name/provider/capabilities/pricing to the inner model, and hashes each request against a JSON cassette on disk. Covers the three modes (record / replay / auto), the cassette format, save(), VcrCassetteMissError, streaming round-trip, hashOptions, and composing below fallbackModel. Triggers: `ai.vcr`, `vcr`, `VcrModel`, `VcrOptions`, `VcrMode`, `Cassette`, `CassetteEntry`, `VcrCassetteMissError`, `hashRequest`, `DEFAULT_HASH_OPTIONS`, `mode`, `path`, `hashOptions`, `save`, `cassette`, record, replay, cassette; 'record LLM responses for tests', 'replay model calls offline in CI', 'deterministic agent test without hitting the provider', 'cassette for model calls'; typical import `import { ai } from "@warlock.js/ai"`. Skip: eval scoring + regression gating — `@warlock.js/ai/eval-datasets-and-ci/SKILL.md`; the Vitest matchers + mockRouter — `@warlock.js/ai/ai-dx-helpers/SKILL.md`; choosing a provider adapter — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing libs `nock`, `polly.js`.
28
32
  - [run-ai-agent](@warlock.js/ai/run-ai-agent/SKILL.md): Build agents with ai.agent({...}) — the single-LLM-turn primitive. Covers execute / stream, attachments, structured output, placeholders, events, agent.eval scoring, the judge-safe preset for resilient LLM-as-judge / verdict classifiers (ai.agent.judge / judge: true — lenient JSON parse + repair + never-throw, for Nova-class models), and auto-adapting raw executables in tools:[]. Triggers: `ai.agent`, `ai.agent.judge`, `agent.execute`, `agent.stream`, `agent.eval`, `AgentResult`, `AgentReport`, `AgentToolEntry`, `JudgeConfig`, `JudgeAgentConfig`, `judge`, `repairAttempts`, `streamingToolGuard`, `attachments`, `repair`, `maxTrips`, `sessionId`, `spawnSubAgent`, `SpawnSubAgentSpec`; 'run an agent', 'stream an agent response', 'structured output schema', 'pass image to agent', 'evaluate an agent', 'LLM-as-judge that survives malformed JSON', 'grade with a Nova model without crashing', 'put a supervisor in tools', 'cancel an agent run', 'spawn a one-shot sub-agent with a per-task budget'; typical import `import { ai } from "@warlock.js/ai"`. Skip: tool definition — `@warlock.js/ai/define-ai-tool/SKILL.md`; workflows — `@warlock.js/ai/run-ai-workflow/SKILL.md`; eval matchers / batch / fallback detail — `@warlock.js/ai/ai-dx-helpers/SKILL.md`; competing libs `langchain`, `ai` (Vercel), raw `openai`.
29
33
  - [run-ai-rag](@warlock.js/ai/run-ai-rag/SKILL.md): Retrieval-augmented generation with ai.rag({...}) — a chunk → embed → vector-store → retrieve → rerank → cite pipeline that reuses ai.embedder + a @warlock.js/cache CacheDriver. Covers index() / retrieve() / clear() / asTool(), chunking strategies (recursive | markdown | sentence | fixed), Citation / RetrievedChunk provenance, and the opt-in rerankers ai.rag.keywordReranker / ai.rag.llmReranker. Triggers: `ai.rag`, `rag.index`, `rag.retrieve`, `rag.clear`, `rag.asTool`, `RagConfig`, `RagDocument`, `RetrieveOptions`, `RetrieveResult`, `RetrievedChunk`, `Citation`, `ChunkOptions`, `ChunkType`, `ai.rag.keywordReranker`, `ai.rag.llmReranker`, `cacheVectorStore`, `VectorStore`, `topK`, `threshold`, `candidates`; 'build a knowledge base', 'retrieve relevant chunks for a query', 'cite the source of an answer', 'chunk markdown for embedding', 'rerank retrieval results', 'expose retrieval as a tool'; typical import `import { ai } from "@warlock.js/ai"`. Skip: raw single-string embedding — `@warlock.js/ai/embed-text/SKILL.md`; exact + vector LLM-response cache — `@warlock.js/ai/attach-ai-middleware/SKILL.md` (ai.middleware.semanticCache); tool wiring — `@warlock.js/ai/define-ai-tool/SKILL.md`; competing libs `langchain`, `llamaindex`.
@@ -32,6 +36,7 @@
32
36
  - [run-orchestrator](@warlock.js/ai/run-orchestrator/SKILL.md): Durable stateful sessions with ai.orchestrator({...}) — the capstone of the 4-primitive ladder. Wraps a supervisor with cross-turn session state (checkpointStore), per-turn windowing, drift detection, post-turn compaction, mid-turn resume (iterate: true + snapshotStore), per-turn memory, typed commands, asTool, and a 3-tier event model. Triggers: `ai.orchestrator`, `orchestrator.execute`, `orchestrator.resume`, `orchestrator.command`, `orchestrator.stream`, `OrchestratorConfig`, `OrchestratorResult`, `OrchestratorReport`, `OrchestratorContract`, `CheckpointStore`, `OrchestratorDriftError`, `sessionId`, `iterate`, `historyWindow`, `summarize`, `keepSnapshots`, `awaiting-input`, `turns[]`, `TurnSnapshot`, `CompactionResult`, `initialAgent`, `checkpointStore`; 'multi-turn conversation that persists', 'durable session across calls', 'resume an interrupted turn', 'compact session history', 'per-session memory'; typical import `import { ai } from "@warlock.js/ai"`. Skip: a single routing turn with no session — `@warlock.js/ai/run-supervisor/SKILL.md`; a fixed pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; the store factories themselves — `@warlock.js/ai/manage-ai-stores/SKILL.md`; competing libs `langgraph`, `crewai`.
33
37
  - [run-planner](@warlock.js/ai/run-planner/SKILL.md): Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered execution plan over your registered capabilities (agents / workflows / supervisors / tools), then the planner EXECUTES it, threading each step output into the next, and returns the unified {data, report, usage, error} envelope with report.type "planner". Supports DAG scheduling (dag:true + maxConcurrency off dependsOn), adaptive re-planning (replan:{maxReplans} + the onStep continue/abort/replan directive), and plan-only / approval (mode:"plan-only" → status "awaiting-approval" → approvedPlan). A plan step may delegate via ai.spawnSubAgent({...}) — a GENERAL one-shot-agent helper covered in `@warlock.js/ai/run-ai-agent/SKILL.md`; it is not planner-specific. Triggers: `ai.planner`, `planner.execute`, `spawnSubAgent`, `PlannerConfig`, `PlannerCapability`, `PlannerResult`, `PlannerReport`, `PlannerPlan`, `PlannerStep`, `PlannerStepDirective`, `PlannerPlanInvalidError`, `maxSteps`, `dag`, `maxConcurrency`, `dependsOn`, `replan`, `onStep`, `mode`, `approvedPlan`, `awaiting-approval`, `report.plan`, `report.executedSteps`; 'let the model plan the steps', 'dynamic plan from a goal', 'run independent steps in parallel', 're-plan when a step fails', 'generate a plan for approval before running it'; typical import `import { ai } from "@warlock.js/ai"`. Skip: a FIXED known pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; routing one input to a specialist each turn — `@warlock.js/ai/run-supervisor/SKILL.md`; a single model + tools call — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.
34
38
  - [run-supervisor](@warlock.js/ai/run-supervisor/SKILL.md): Multi-intent routing with ai.supervisor({...}) — classifier (iter-0 dispatch), router agent OR route callback, intents as agents / workflows / callbacks, fan-out, evaluate quality loop, ack receptionist, supervisor-level middleware. A callback that calls agent.execute() directly auto-nests agent → tool under the callback span (ambient RunFrame) with usage / cost rolled up — same for team members and orchestrator turns. Triggers: `ai.supervisor`, `ai.router`, `ai.fanOut`, `supervisor.execute`, `supervisor.resume`, `intents`, `router`, `route`, `classifier`, `evaluate`, `ack`, `artifactsSchema`, `middleware`, `END`, `ctx.intents.X.execute`, `ctx.run`, `RunFrame`, `callback span`, `children`, `parentRunId`, `rootRunId`, `trace nesting`, `sub-agent`; 'route one input across specialists', 'multi-intent dispatch', 'fan-out then evaluate', 'classifier then router', 'supervisor middleware', 'self-consistency / voting', 'why is my callback agent not nested / cost is $0', 'nest a sub-agent under a callback'; typical import `import { ai } from "@warlock.js/ai"`. Skip: durable multi-turn sessions — `@warlock.js/ai/run-orchestrator/SKILL.md`; fixed pipelines — `@warlock.js/ai/run-ai-workflow/SKILL.md`; single agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.
39
+ - [transcribe-audio](@warlock.js/ai/transcribe-audio/SKILL.md): Speech-to-text via ai.transcribe({ model: sdk.transcribe({ name }), audio }) — the audio-INPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Feed it an AudioInput = { base64; mediaType; filename? } — build one with ai.audioFromFile(path) (reads disk, infers media type incl. WhatsApp .ogg/.opus) or ai.audioFromBuffer(bytes, mediaType). Models: OpenAI whisper-1 (verbose_json, per-minute, segments + durationSeconds) or gpt-4o-transcribe (json, per-token). Triggers: `ai.transcribe`, `ai.audioFromFile`, `ai.audioFromBuffer`, `sdk.transcribe`, `openai.transcribe`, `TranscriptionModelContract`, `AudioInput`, `TranscriptionSegment`, `MockTranscriptionModel`; 'speech to text', 'transcribe audio', 'voice note to text', 'WhatsApp voice message', 'whisper', 'gpt-4o-transcribe', 'subtitle segments', 'audio input'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: text-to-speech / synthesizing a voice — [[generate-speech]]; competing libs raw `openai.audio.transcriptions.create`, `whisper.cpp`.
35
40
  - [use-ai-memory](@warlock.js/ai/use-ai-memory/SKILL.md): Agent memory with ai.memory({...}) — a provider-neutral store with FOUR tiers: WORKING (in-run scratch, recalled by recency), SEMANTIC (durable facts by cosine similarity over a @warlock.js/cache vector driver via .similar()), EPISODIC (durable events, similarity blended with recency), and PROCEDURAL (durable how-tos, similarity blended with reinforcement). remember() / recall() / clear(); wire it into ai.orchestrator({ memory }). Triggers: `ai.memory`, `memory.remember`, `memory.recall`, `memory.clear`, `MemoryContract`, `MemoryConfig`, `MemoryItem`, `RecalledMemory`, `MemoryTier`, `SemanticMemoryConfig`, `EpisodicMemoryConfig`, `ProceduralMemoryConfig`, `working`, `semantic`, `episodic`, `procedural`, `defaultTier`, `threshold`, `recencyWeight`, `halfLifeMs`, `reinforcementWeight`, `injectKey`; 'give the agent memory', 'remember user preferences', 'semantic recall', 'per-session working memory', 'episodic / event memory', 'procedural / how-to memory', 'recency-weighted recall', 'reinforce a procedure'; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator wiring of the memory — `@warlock.js/ai/run-orchestrator/SKILL.md`; the vector cache driver itself — `@warlock.js/cache/cache-basics/SKILL.md`; embeddings primitive — `@warlock.js/ai/embed-text/SKILL.md`; competing libs `mem0`, `langchain` memory.
36
41
  - [use-runtime-skills](@warlock.js/ai/use-runtime-skills/SKILL.md): Progressive-disclosure agent skills with ai.skills({...}) and the first-class `skills` option on ai.agent — an always-injected cheap metadata catalog plus an on-demand loadSkill tool, backed by directory / url / store sources. Covers inject ("all" | {select:"semantic",topK,embedder}), maxLoadsPerRun, scope tags, the MockSkillsStore, semantic preload, and the inert-by-default Phase-2 self-authoring (saveSkill + default-DENY review gate → promote). Triggers: `ai.skills`, `SkillsConfig`, `SkillsContract`, `SkillSource`, `SkillInjectMode`, `SkillRecord`, `SkillCatalogEntry`, `loadSkill`, `loadSkillTool`, `saveSkill`, `saveSkillTool`, `SkillReviewGate`, `runReviewGate`, `MockSkillsStore`, `proceduralSkillStore`, `maxLoadsPerRun`, `inject`, `scope`, `review`, the agent `skills:` option; 'give an agent loadable skills', 'progressive disclosure of instructions', 'catalog of skills the model pulls on demand', 'semantic preload of skill bodies', 'let an agent author and review a skill'; typical import `import { ai } from "@warlock.js/ai"`. Skip: composing static system prompts — `@warlock.js/ai/write-system-prompt/SKILL.md`; durable agent memory tiers — `@warlock.js/ai/use-ai-memory/SKILL.md`; defining callable tools — `@warlock.js/ai/define-ai-tool/SKILL.md`.
37
42
  - [write-system-prompt](@warlock.js/ai/write-system-prompt/SKILL.md): Compose system prompts via ai.systemPrompt() / ai.persona() / ai.instruction() — immutable builders with {{placeholder}} substitution, plus ai.systemPrompt.fromFile(path) to seed from a file read once at construction. Carry identity with .meta({ name, version, description, required }) (a name auto-registers in ai.prompts) and compose with merge(...blocks) / merge(contract) / merge(name, { fromVersion }) (provenance in meta.composedFrom). Triggers: `ai.systemPrompt`, `ai.systemPrompt.fromFile`, `ai.persona`, `ai.instruction`, `SystemPromptBlockContract`, `SystemPromptContract`, `SystemPromptMeta`, `SystemPromptMergeOptions`, `PersonaContract`, `InstructionContract`, `meta`, `merge`, `composedFrom`, `fromVersion`, `placeholders`, `{{placeholder|default}}`, `InvalidRequestError`; 'write a system prompt', 'compose persona + instructions', 'prompt from a file', 'name and version a prompt', 'merge prompts together', 'per-call prompt override', 'mustache placeholder'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the named/versioned prompt registry (register / resolve / tag / diff / export / validate) — `@warlock.js/ai/manage-prompts/SKILL.md`; agent factory wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langchain` `PromptTemplate`, raw f-strings.
package/package.json CHANGED
@@ -15,14 +15,14 @@
15
15
  "@standard-schema/spec": "^1.0.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "@warlock.js/cache": "4.5.0",
19
- "@warlock.js/logger": "4.5.0",
18
+ "@warlock.js/cache": "4.6.1",
19
+ "@warlock.js/logger": "4.6.1",
20
20
  "langfuse": "*",
21
21
  "openai": "*",
22
22
  "pg": "*",
23
23
  "redis": "*"
24
24
  },
25
- "version": "4.5.0",
25
+ "version": "4.6.1",
26
26
  "main": "./cjs/index.cjs",
27
27
  "module": "./esm/index.mjs",
28
28
  "types": "./esm/index.d.mts",
package/skills/README.md CHANGED
@@ -36,6 +36,10 @@ Text-to-vector via sdk.embedder({...}) — embed(string) for single, embedMany(s
36
36
 
37
37
  Datasets + regression-gated eval CI with ai.dataset({...}) feeding agent.eval({cases,baseline,tolerance}). Covers the immutable filterable/shardable dataset (cases / fromFile JSONL), DatasetEntry tags, EvalReport.regression (regressed/added/removed/passed) against a baseline, and the ai.eval reporters toJUnit / toJSON / fromJSON for CI artifacts + committed baselines. Triggers: `ai.dataset`, `DatasetContract`, `DatasetEntry`, `DatasetOptions`, `dataset.filter`, `dataset.shard`, `fromFile`, `agent.eval`, `EvalOptions`, `EvalReport`, `EvalCaseResult`, `EvalRegression`, `baseline`, `tolerance`, `ai.eval.toJUnit`, `ai.eval.toJSON`, `ai.eval.fromJSON`, `diff`, JSONL; 'eval dataset from a JSONL file', 'shard an eval suite across CI jobs', 'fail CI on an eval regression', 'emit a JUnit report', 'snapshot an eval baseline'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the scorers + LLM-as-judge + Vitest matchers themselves — `@warlock.js/ai/ai-dx-helpers/SKILL.md` (registerAiMatchers / ai.eval.exact|contains|predicate|judge); record/replay of model calls for deterministic tests — `@warlock.js/ai/record-replay-llm/SKILL.md`; competing libs `promptfoo`, `braintrust`.
38
38
 
39
+ ### [`generate-images/`](./generate-images/SKILL.md)
40
+
41
+ Text-to-image with ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws {data, error, usage, report} envelope with cost-truth + panoptic observation. Models come from an adapter's image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = {type:"base64"} | {type:"url"}. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`, `perImage`; 'generate an image', 'text to image', 'gpt-image', 'dall-e', 'imagen', 'product thumbnail', 'image output'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.
42
+
39
43
  ### [`handle-ai-errors/`](./handle-ai-errors/SKILL.md)
40
44
 
41
45
  Typed AIError hierarchy with stable code strings + coarse category for retry-policy dispatch. execute() never throws — errors surface via result.error. Load when inspecting result.error, branching on error.code / error.category, designing retry / fallback logic, or wrapping framework errors in HTTP errors.
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: durable-agent-runs
3
+ description: 'Mid-run crash-resume for agents AND planners — opt in with durable: { store, deleteOnComplete? } on the config, pass a stable runId to execute(), and call agent.resume(runId) / planner.resume(runId) after a crash to continue from the last settled trip / plan node. Reuses the ai.snapshot.{memory,pg,redis} stores; checkpoints per-trip (agent) / per-node (planner); completed trips + nodes never re-run their tools and usage is never double-counted; a drifted definition throws AgentDriftError / PlannerDriftError (bypass with { force: true }). Triggers: `durable`, `agent.resume`, `planner.resume`, `resume(runId)`, `runId`, `AgentSnapshot`, `PlannerSnapshot`, `AgentSnapshotStatus`, `PlannerSnapshotStatus`, `AgentDriftError`, `PlannerDriftError`, `computeAgentSignature`, `agent.signature`, `deleteOnComplete`, `defaultSnapshotStore`, `ai.snapshot.pg`, `ai.snapshot.memory`, `SnapshotStore`, `force: true`; ''resume an agent after a crash'', ''durable agent run'', ''continue a planner from where it crashed'', ''checkpoint agent state'', ''idempotent tool re-run on resume'', ''signature drift on resume''; typical import `import { ai } from "@warlock.js/ai"`. Skip: durable human-in-the-loop approval resume (ai.human.resume of a PendingInterrupt) — `@warlock.js/ai/durable-resume/SKILL.md`; supervisor/workflow iterate-mid-turn snapshot resume + the store contracts themselves — `@warlock.js/ai/manage-ai-stores/SKILL.md`; competing libs `temporal`, `inngest`, `restate`.'
4
+ ---
5
+
6
+ # Durable agent + planner runs — resume from the last checkpoint
7
+
8
+ Opt-in mid-run crash-resume for the two long-running primitives. Turn it on, give the run a stable `runId`, and after a process crash `resume(runId)` re-hydrates the persisted state and continues from where it stopped — never re-issuing a settled trip's model call or re-invoking a completed node's capability.
9
+
10
+ > **Not the same as [[durable-resume]].** That skill is `ai.human.resume(interruptId, decision)` — resuming a **gated tool call** hours later after a human rules (a `PendingInterrupt` in an `InterruptStore`). *This* skill is **crash-resume of an in-flight run** (an `AgentSnapshot` / `PlannerSnapshot` in a `SnapshotStore`): the process died mid-run, you restart, and continue the same trip / plan. Different trigger (a crash, not a human), different store, different verb (`agent.resume` / `planner.resume`, not `ai.human.resume`).
11
+
12
+ ## Opt in — `durable` on the config
13
+
14
+ ```ts
15
+ import { ai } from "@warlock.js/ai";
16
+
17
+ const writer = ai.agent({
18
+ name: "writer",
19
+ model,
20
+ tools: [searchTool, draftTool],
21
+ durable: {
22
+ store: ai.snapshot.pg({ client: pgPool }), // reuses the ai.snapshot.* stores
23
+ deleteOnComplete: false, // default — keep for the completed-run short-circuit + audit
24
+ },
25
+ });
26
+ ```
27
+
28
+ `durable` shape (identical on the agent and planner config):
29
+
30
+ - **`store?`** — a `SnapshotStore`. Falls back to `ai.config({ defaultSnapshotStore })`. When neither resolves, snapshot writes **silently skip** and `resume()` throws.
31
+ - **`deleteOnComplete?`** — drop the snapshot once the run completes successfully. Default `false`.
32
+
33
+ **Absent `durable` ⇒ zero behavior change** — the loop starts at trip 0 / the first node, never writes a snapshot, and runs byte-for-byte as before.
34
+
35
+ ## Run with a stable `runId`, then resume
36
+
37
+ The `runId` is the store key. Pass a stable one to `execute()` (or read the generated one off `result.report.runId`) so a later `resume()` can find the snapshot:
38
+
39
+ ```ts
40
+ const result = await writer.execute("research X", { runId: "run-42" });
41
+
42
+ // ...process crashes mid-run, restarts...
43
+
44
+ const recovered = await writer.resume("run-42");
45
+ // continues from the next unsettled trip; `recovered.report.status === "completed"`
46
+ ```
47
+
48
+ Planners are the mirror image — `durable` on the config, `runId` on `execute(goal)`, `planner.resume(runId)`:
49
+
50
+ ```ts
51
+ const research = ai.planner({
52
+ name: "research-assistant",
53
+ model,
54
+ capabilities: [{ name: "search", executable: searchAgent }, { name: "write", executable: writerAgent }],
55
+ durable: { store: ai.snapshot.pg({ client: pgPool }) },
56
+ });
57
+
58
+ const first = await research.execute("compare A vs B", { runId: "plan-7" });
59
+ // ...crash...
60
+ const done = await research.resume("plan-7");
61
+ ```
62
+
63
+ ## Checkpoint granularity
64
+
65
+ | Primitive | Written | Contains | Resume continues at |
66
+ |---|---|---|---|
67
+ | **agent** | after every settled **trip** (`runTrip` end) | `messages`, `trips`, `toolCalls`, `usage`, resolved `systemPrompt` / `responseSchema`, `signature`, `status` | `trips.length` (the next trip index) |
68
+ | **planner** | after every settled **plan node** (`executeStep` end) | the frozen `plan`, `executedSteps` ledger, `usage`, child `children` reports, `replanCount`, `signature`, `status` | the unfinished frontier (from `executedSteps`) |
69
+
70
+ The write happens only where the persisted arrays are mutually consistent — for the agent, after every tool a trip requested has been dispatched and its result appended. A crash **mid-trip** loses only that in-flight trip (never checkpointed), which the resume re-issues cleanly. The planner **never re-calls the planning LLM** on resume — the plan is frozen on the first run; re-asking would burn tokens and risk a plan that no longer matches the ledger. Every field on both snapshots is JSON-serializable, so they round-trip through any `ai.snapshot.{memory,pg,redis}` backend verbatim.
71
+
72
+ ## Idempotency — what does and doesn't re-run
73
+
74
+ ```ts
75
+ // Completed run: resume is a no-op that re-returns the stored result.
76
+ const again = await writer.resume("run-42"); // runs nothing when status === "completed"
77
+ ```
78
+
79
+ - **Completed trips / nodes never re-run their tools.** On agent resume, `trips.length` is the starting trip index — earlier trips' model calls are not replayed and their tool dispatches are not re-invoked. On planner resume, a completed node's capability dispatch is skipped (the sequential skip-guard / DAG re-seed derive the completed set from `executedSteps`).
80
+ - **Usage is never double-counted.** The running `usage` total is restored from the snapshot; only the newly-executed trips / nodes add to it.
81
+ - **Caveat — a crash MID-trip re-runs that trip's tools.** The in-flight trip was never checkpointed, so on resume its tools fire again. **Side-effectful tools (charging a card, sending an email) must be idempotent** — the same caller-responsibility boundary the supervisor and workflow primitives document. Guard them with your own dedupe key (e.g. `${runId}:${toolCallId}`).
82
+
83
+ ## Drift — definition changed since the snapshot
84
+
85
+ Every agent / planner carries a structural `signature` (`agent.signature` — computed at factory time by `computeAgentSignature`), stamped on each snapshot. `resume()` compares the stored signature against the current definition; a mismatch throws before executing anything:
86
+
87
+ - **agent** covers: model name + provider, sorted tool names, `maxTrips`, whether a default `output` schema is set, `version`. It does **not** cover system-prompt text, middleware, per-event handlers, placeholders, or `modelOptions` — runtime knobs that don't change a resumable run's shape.
88
+ - **planner** covers: name + ordered capability names. A mid-run **re-plan is NOT drift** (the plan changed, not the definition); `replanCount` is persisted so the replan budget survives a resume.
89
+
90
+ ```ts
91
+ import { AgentDriftError } from "@warlock.js/ai";
92
+
93
+ try {
94
+ await writer.resume("run-42");
95
+ } catch (error) {
96
+ if (error instanceof AgentDriftError) {
97
+ // The definition changed (a tool was added, the model swapped). Either roll the
98
+ // definition back, or — only when you've verified the change is snapshot-safe:
99
+ await writer.resume("run-42", { force: true }); // bypasses the drift check
100
+ }
101
+ }
102
+ ```
103
+
104
+ `{ force: true }` is the escape hatch (mirror `PlannerDriftError` for planners). `resume()` also throws `AgentExecutionError` / `PlannerFailedError` when no store is configured or no snapshot exists for the `runId`.
105
+
106
+ ## Pattern — a boot-drain resume loop
107
+
108
+ On restart, resume every run the store still has in flight. Snapshots carry a `status` (`"running" | "completed" | "cancelled" | "failed"`), so you only resume the live ones:
109
+
110
+ ```ts
111
+ const store = ai.snapshot.pg({ client: pgPool });
112
+ const runIds = (await store.list?.()) ?? [];
113
+
114
+ for (const runId of runIds) {
115
+ const snapshot = await store.load(runId);
116
+ if (snapshot?.status === "running") {
117
+ await writer.resume(runId); // completed/failed snapshots short-circuit or re-throw — skip them
118
+ }
119
+ }
120
+ ```
121
+
122
+ Pair `deleteOnComplete: true` with this loop when you don't need the completed-run audit trail — the store then holds only genuinely-unfinished runs, so the drain never touches settled ones.
123
+
124
+ ## Cost + testing
125
+
126
+ - **Checkpointing cost is one store write per settled trip / node** — a `JSONB` upsert on `pg`, an in-process `Map` set on `memory`. A failed checkpoint is surfaced via logs, not thrown: it loses resume-ability from that point but never breaks an otherwise-healthy run.
127
+ - **Resume saves the tokens of every settled trip / node** — their model calls are not replayed. A completed-run resume spends nothing (it rebuilds the result from the snapshot). The planning LLM is never re-called on planner resume.
128
+ - **Test with `ai.snapshot.memory()`.** Drive `execute(input, { runId })` against a flaky model that throws once, assert the tool spy was called once, flip the failure off, `resume(runId)`, and assert (a) `status === "completed"`, (b) the tool spy count is unchanged (no re-invoke), and (c) `usage.total` counts each trip's tokens exactly once. Drift is testable by mutating the definition (add a tool) between `execute` and `resume` and asserting `AgentDriftError` — then `{ force: true }` proceeds.
129
+
130
+ ## See also
131
+
132
+ - [[handle-ai-errors]] — the typed `AgentDriftError` / `PlannerDriftError` / `AgentExecutionError` / `PlannerFailedError` and how `result.error` surfaces a failed run.
133
+ - [[manage-ai-stores]] — the `ai.snapshot.{memory,pg,redis}()` factories, the `SnapshotStore` contract, dev-owned `pg` / `redis` clients, and never-auto-migrated `schema()`.
134
+ - [[persist-ai-data]] — supervisor / workflow snapshot resume (the sibling `iterate`-style durability) and the SnapshotStore migration notes.
135
+ - [[durable-resume]] — the OTHER resume: `ai.human.resume` of a gated tool call (human-in-the-loop), not a crash.
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: generate-images
3
+ description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
4
+ ---
5
+
6
+ # Generate images — the image-output verb (`ai.image`)
7
+
8
+ `ai.image()` is the output counterpart to `ai.agent` for the image modality (the first verb of the output-modality track, Theme I). Prompt-in / images-out, wrapped in the same uniform result contract every executable returns — so it slots into cost dashboards and panoptic traces exactly like an agent run.
9
+
10
+ This is image **output** (generation). For image/PDF/audio **input** to a chat agent (vision), see [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md).
11
+
12
+ ## Shape
13
+
14
+ ```ts
15
+ // 1. Build an image model from an adapter's image() factory.
16
+ const model = openai.image({ name: "gpt-image-1" }); // ImageModelContract
17
+
18
+ // 2. Run the verb — never throws; failures land on result.error.
19
+ const { data, error, usage, report } = await ai.image({ model, prompt: "a red bicycle" });
20
+
21
+ if (error) {
22
+ console.warn(error.code); // typed AIError (auth / rate-limit / content-filter / …)
23
+ } else {
24
+ for (const img of data.images) { // GeneratedImage[]
25
+ if (img.type === "base64") save(Buffer.from(img.base64, "base64"), img.mediaType);
26
+ else download(img.url);
27
+ }
28
+ }
29
+ ```
30
+
31
+ `ImageModelContract` mirrors `EmbedderContract` — a peer primitive on the SDK adapter, produced by the optional `image?()` factory. An adapter without an image API simply doesn't define `image()`, so `ai.anthropic.image(...)` is a **compile-time** error, not a silent runtime failure.
32
+
33
+ ## The result envelope
34
+
35
+ ```ts
36
+ type ImageResult = {
37
+ type: "image";
38
+ data?: { images: GeneratedImage[] }; // undefined on failure
39
+ error?: AIError; // undefined on success — NEVER thrown
40
+ usage: Usage; // tokens (gpt-image) + cost when priced
41
+ report: ImageReport; // type:"image", model, imageCount, lineage
42
+ };
43
+
44
+ type GeneratedImage =
45
+ | { type: "base64"; base64: string; mediaType: string; revisedPrompt?: string }
46
+ | { type: "url"; url: string; mediaType?: string; revisedPrompt?: string };
47
+ ```
48
+
49
+ ## Generation options (provider-neutral)
50
+
51
+ ```ts
52
+ await ai.image({
53
+ model,
54
+ prompt: "an isometric office desk, soft studio lighting",
55
+ count: 2, // n images
56
+ size: "1024x1024", // OpenAI WxH (also resolves perImageBySize pricing)
57
+ quality: "high", // OpenAI quality tier
58
+ aspectRatio: "16:9", // Imagen ratio
59
+ negativePrompt: "blurry, watermark", // Imagen
60
+ format: "png", // output container hint
61
+ signal, // AbortSignal
62
+ observe: collector, // route the report to an Observer (panoptic), like agents
63
+ sessionId: "checkout-123",
64
+ options: { background: "transparent" }, // provider-specific passthrough
65
+ });
66
+ ```
67
+
68
+ Each adapter maps the options its API supports and ignores the rest. `options` is the escape hatch for provider-specific knobs (OpenAI `background`, DALL·E `responseFormat: "url"`, Imagen `imageSize` / `personGeneration`).
69
+
70
+ ## OpenAI — gpt-image (token-metered) + DALL·E (per-image)
71
+
72
+ ```ts
73
+ import { OpenAISDK } from "@warlock.js/ai-openai";
74
+
75
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
76
+
77
+ // gpt-image-1 always returns base64 bytes; priced per TOKEN.
78
+ const gpt = openai.image({ name: "gpt-image-1", pricing: { input: 5, output: 40 } });
79
+
80
+ // DALL·E 3 — per-image pricing; defaults to base64 (opt into url with options).
81
+ const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } });
82
+ ```
83
+
84
+ A non-image model id (`openai.image({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** — fail fast, like the embedder/vision guards.
85
+
86
+ ## Google — Imagen (per-image)
87
+
88
+ ```ts
89
+ import { GoogleSDK } from "@warlock.js/ai-google";
90
+
91
+ const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
92
+ const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perImage: 0.04 } });
93
+
94
+ const { data } = await ai.image({ model: imagen, prompt: "a watercolor lighthouse at dawn", aspectRatio: "3:4" });
95
+ ```
96
+
97
+ Imagen returns base64 bytes (no hosted URL). When every candidate is safety-filtered, `ai.image` surfaces a typed `ContentFilterError` on `result.error`.
98
+
99
+ ## Cost-truth — one rollup, two metering models
100
+
101
+ `ai.image` fills `usage.cost` (a `ModelPricing`-shaped USD breakdown) so image spend folds into the **same** `Usage.cost` rollup as text — no second accounting path:
102
+
103
+ - **Token-metered** (gpt-image-1): `{ input, output }` USD-per-1M-tokens → standard `computeCost` against the returned token usage.
104
+ - **Per-image** (DALL·E, Imagen): `{ perImage }` (or `perImageBySize["1792x1024"]`) × image count → `cost.output`.
105
+
106
+ Unpriced model → `usage.cost` stays `undefined` (honest "cost unknown", never a false zero). A pre-priced adapter response is honored, not overwritten.
107
+
108
+ ## Pattern — catalog thumbnail in a workflow `run` step
109
+
110
+ ```ts
111
+ ai.step({
112
+ name: "thumbnail",
113
+ run: async (ctx) => {
114
+ const { data, error } = await ai.image({
115
+ model: openai.image({ name: "gpt-image-1" }),
116
+ prompt: `product photo, white background: ${ctx.steps.extract.output.title}`,
117
+ size: "1024x1024",
118
+ });
119
+ if (error) throw error; // step retry/backoff handles transient provider faults
120
+ ctx.state.thumb = data.images[0];
121
+ },
122
+ });
123
+ ```
124
+
125
+ ## Observability
126
+
127
+ The completed `ImageReport` routes to any registered `Observer` (panoptic, OTel, …) through the shared `observe` seam — pass `observe: true` (global), an `Observer` object (flow-local), or rely on observe-all. Cost + latency attribute to `report.model` for free. See [`@warlock.js/ai/observe-ai-flows/SKILL.md`](@warlock.js/ai/observe-ai-flows/SKILL.md).
128
+
129
+ ## Testing
130
+
131
+ `MockSDK({ imageResponses, imagePricing }).image({ name })` returns a deterministic `MockImageModel` — no HTTP. Script images/usage/errors and inspect `model.calls`.
132
+
133
+ ```ts
134
+ import { MockSDK } from "@warlock.js/ai";
135
+
136
+ const mock = MockSDK({ imageResponses: [{}], imagePricing: { perImage: 0.04 } });
137
+ const { data, usage } = await ai.image({ model: mock.image({ name: "mock-image" }), prompt: "x" });
138
+ ```
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: generate-speech
3
+ description: 'Text-to-speech via ai.speech({ model: sdk.speech({ name }), text }) — the audio-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s speech() factory: OpenAI tts-1 / tts-1-hd (per-character) or gpt-4o-mini-tts (per-token). Synthesized audio is a discriminated GeneratedAudio = { type: "base64"; base64; mediaType }. Options: voice / format / speed / instructions / signal. Triggers: `ai.speech`, `sdk.speech`, `openai.speech`, `SpeechModelContract`, `GeneratedAudio`, `SpeechModelPricing`, `SpeechOptions`, `MockSpeechModel`; ''text to speech'', ''TTS'', ''synthesize voice'', ''read this aloud'', ''tts-1'', ''gpt-4o-mini-tts'', ''voice narration'', ''audio output'', ''speak this text''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: speech-to-text / transcribing a voice note — [[transcribe-audio]]; image OUTPUT — [[generate-images]]; competing libs raw `openai.audio.speech.create`, `elevenlabs` SDK.'
4
+ ---
5
+
6
+ # Generate speech — the text-to-speech verb (`ai.speech`)
7
+
8
+ `ai.speech()` is the audio-output counterpart to `ai.image()` on the output-modality track (Theme I). Text-in / audio-out, wrapped in the same uniform result contract every executable returns — so a synthesized voicemail slots into cost dashboards and panoptic traces exactly like an agent run.
9
+
10
+ This is audio **output** (TTS). For audio **input** (speech-to-text on a WhatsApp voice note or a meeting recording), see [[transcribe-audio]].
11
+
12
+ ## Shape
13
+
14
+ ```ts
15
+ // 1. Build a speech model from an adapter's speech() factory.
16
+ const model = openai.speech({ name: "tts-1", voice: "alloy" }); // SpeechModelContract
17
+
18
+ // 2. Run the verb — never throws; failures land on result.error.
19
+ const { data, error, usage, report } = await ai.speech({ model, text: "Your order has shipped." });
20
+
21
+ if (error) {
22
+ console.warn(error.code); // typed AIError (auth / rate-limit / content-filter / …)
23
+ } else {
24
+ const { base64, mediaType } = data.audio; // GeneratedAudio (always base64 today)
25
+ await fs.writeFile("ship.mp3", Buffer.from(base64, "base64"));
26
+ }
27
+ ```
28
+
29
+ `SpeechModelContract` mirrors `EmbedderContract` / `ImageModelContract` — a peer primitive produced by the adapter's optional `speech?()` factory. An adapter without a TTS API simply doesn't define `speech()`, so calling it is a **compile-time** error, not a silent runtime failure. A non-TTS model id (`openai.speech({ name: "gpt-4o" })`) throws `InvalidRequestError` **at construction** — fail fast, like the embedder/image guards.
30
+
31
+ ## The result envelope
32
+
33
+ ```ts
34
+ type SpeechResult = {
35
+ type: "speech";
36
+ data?: { audio: GeneratedAudio }; // undefined on failure
37
+ error?: AIError; // undefined on success — NEVER thrown
38
+ usage: Usage; // tokens (gpt-4o-mini-tts) + cost when priced
39
+ report: SpeechReport; // type:"speech", model, characters, lineage
40
+ };
41
+
42
+ type GeneratedAudio = {
43
+ type: "base64";
44
+ base64: string; // base64-encoded audio bytes
45
+ mediaType: string; // IANA type, e.g. "audio/mpeg", "audio/wav"
46
+ };
47
+ ```
48
+
49
+ `GeneratedAudio` is a discriminated union with a single `base64` variant today — the union leaves room for a future hosted-`url` variant without a breaking change, so always branch on `audio.type` rather than assuming `base64`.
50
+
51
+ ## Generation options (provider-neutral)
52
+
53
+ ```ts
54
+ await ai.speech({
55
+ model,
56
+ text: "Welcome aboard. Let's get you set up.",
57
+ voice: "verse", // voice id/name; overrides the model's default
58
+ format: "wav", // "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm"
59
+ speed: 1.25, // playback multiplier (OpenAI 0.25–4.0)
60
+ instructions: "calm, warm", // tone/delivery steering (gpt-4o-mini-tts only)
61
+ signal, // AbortSignal
62
+ observe: collector, // route the report to an Observer (panoptic), like agents
63
+ sessionId: "onboarding-42", // group into a session for flat cost/trace queries
64
+ options: { /* provider passthrough */ },
65
+ });
66
+ ```
67
+
68
+ Each adapter maps the options its API supports and forwards `options` verbatim. On OpenAI the container defaults to `mp3` (→ `audio/mpeg`); `speed` and `instructions` are only sent when set, and the default voice is `alloy` when neither the call nor the model config supplies one.
69
+
70
+ ## OpenAI — tts-1 (per-character) + gpt-4o-mini-tts (per-token)
71
+
72
+ ```ts
73
+ import { ai } from "@warlock.js/ai";
74
+ import { OpenAISDK } from "@warlock.js/ai-openai";
75
+
76
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
77
+
78
+ // tts-1 / tts-1-hd — billed per INPUT CHARACTER.
79
+ const classic = openai.speech({ name: "tts-1", voice: "alloy", pricing: { perMillionCharacters: 15 } });
80
+
81
+ // gpt-4o-mini-tts — billed per TOKEN like a chat model; supports `instructions`.
82
+ const steered = openai.speech({ name: "gpt-4o-mini-tts", pricing: { input: 0.6, output: 12 } });
83
+
84
+ const { data } = await ai.speech({ model: steered, text: "Read this warmly.", instructions: "gentle" });
85
+ ```
86
+
87
+ ## Cost-truth — one rollup, two metering models
88
+
89
+ `ai.speech` fills `usage.cost` (a USD breakdown) so TTS spend folds into the **same** `Usage.cost` rollup as text — no second accounting path:
90
+
91
+ - **Per-character** (`tts-1` / `tts-1-hd`): `{ perMillionCharacters }` × `report.characters` → `cost.input`. The Speech API reports no token usage, so `usage` tokens stay `{ 0, 0, 0 }` and spend is priced entirely from the input character count.
92
+ - **Token-metered** (`gpt-4o-mini-tts`): `{ input, output }` USD-per-1M-tokens → standard `computeCost` against the returned token usage.
93
+
94
+ Per-character wins when both are set. An unpriced model leaves `usage.cost` **`undefined`** (honest "cost unknown", never a false zero); a pre-priced adapter response is honored, not overwritten.
95
+
96
+ ## Pattern — order-confirmation voice line in a workflow `run` step
97
+
98
+ ```ts
99
+ ai.step({
100
+ name: "voiceLine",
101
+ run: async (ctx) => {
102
+ const { data, error } = await ai.speech({
103
+ model: openai.speech({ name: "tts-1", voice: "alloy" }),
104
+ text: `Order ${ctx.steps.order.output.id} confirmed. Thank you!`,
105
+ format: "mp3",
106
+ });
107
+ if (error) throw error; // step retry/backoff handles transient provider faults
108
+ ctx.state.audio = data.audio; // { type:"base64", base64, mediaType:"audio/mpeg" }
109
+ },
110
+ });
111
+ ```
112
+
113
+ ## Observability
114
+
115
+ The completed `SpeechReport` (with `report.characters` and cost/latency attributed to `report.model`) routes to any registered `Observer` (panoptic, OTel, …) through the shared `observe` seam — pass `observe: true` (global), an `Observer` object (flow-local), or rely on observe-all. See [[observe-ai-flows]]. Provider faults surface as typed `AIError`s on `result.error`; see [[handle-ai-errors]].
116
+
117
+ ## Testing
118
+
119
+ `MockSpeechModel(name, responses, pricing?)` is a deterministic `SpeechModelContract` double — no HTTP. Script audio/usage/errors and inspect `model.calls`. `MockSDK({ speechResponses, speechPricing }).speech({ name })` wires the same double behind a full adapter.
120
+
121
+ ```ts
122
+ import { MockSpeechModel } from "@warlock.js/ai";
123
+ import { speech } from "@warlock.js/ai";
124
+
125
+ const model = new MockSpeechModel("tts-1", [{}], { perMillionCharacters: 15 });
126
+ const { data, usage } = await speech({ model, text: "abcdefghij" }); // 10 chars
127
+ // data.audio → { type:"base64", base64:"AAAA", mediaType:"audio/mpeg" }
128
+ // usage.cost.input → (10 * 15) / 1_000_000
129
+ // model.calls[0] records { text, options } for assertions
130
+ ```
131
+
132
+ Scripting `[{ error: new ProviderRateLimitError("slow down") }]` drives the never-throws path — `result.error` is the typed error and `result.data` is `undefined`.
133
+
134
+ ## See also
135
+
136
+ - [[transcribe-audio]] — the inverse verb (`ai.transcribe`), audio → text
137
+ - [[generate-images]] — the sibling image-output verb (`ai.image`)
138
+ - [[observe-ai-flows]] — routing the `SpeechReport` to panoptic / OTel
139
+ - [[handle-ai-errors]] — the typed `AIError` taxonomy on `result.error`