@warlock.js/ai 5.2.3 → 5.2.4
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.
- package/cjs/index.cjs +1 -1
- package/cjs/index.cjs.map +1 -1
- package/cjs/{magic-string.es-BQeqHJ-a.cjs → magic-string.es-G6Xl81ni.cjs} +2 -2
- package/cjs/{magic-string.es-BQeqHJ-a.cjs.map → magic-string.es-G6Xl81ni.cjs.map} +1 -1
- package/cjs/matcher-logic-07fFOz7r.cjs.map +1 -1
- package/cjs/{matchers-CINm4ojZ.cjs → matchers-D7PBk4ut.cjs} +7 -7
- package/cjs/{matchers-CINm4ojZ.cjs.map → matchers-D7PBk4ut.cjs.map} +1 -1
- package/esm/agent/agent-input-builder.mjs.map +1 -1
- package/esm/agent/agent-stream.d.mts.map +1 -1
- package/esm/agent/agent-stream.mjs.map +1 -1
- package/esm/agent/agent.d.mts.map +1 -1
- package/esm/agent/agent.mjs.map +1 -1
- package/esm/agent/json-stream-guard.mjs.map +1 -1
- package/esm/agent/signature.mjs.map +1 -1
- package/esm/agent/snapshot.mjs.map +1 -1
- package/esm/agent/spawn-sub-agent.d.mts.map +1 -1
- package/esm/batch/batch.d.mts.map +1 -1
- package/esm/batch/batch.mjs.map +1 -1
- package/esm/checkpoint/memory.d.mts.map +1 -1
- package/esm/checkpoint/pg.mjs.map +1 -1
- package/esm/checkpoint/redis.mjs.map +1 -1
- package/esm/config.d.mts.map +1 -1
- package/esm/eval/dataset.d.mts.map +1 -1
- package/esm/eval/dataset.mjs.map +1 -1
- package/esm/eval/eval-runner.d.mts.map +1 -1
- package/esm/eval/eval-runner.mjs.map +1 -1
- package/esm/eval/judge-scorer.d.mts.map +1 -1
- package/esm/eval/regression.d.mts.map +1 -1
- package/esm/eval/regression.mjs.map +1 -1
- package/esm/eval/report-json.d.mts.map +1 -1
- package/esm/eval/report-junit.mjs.map +1 -1
- package/esm/eval/scorers.d.mts.map +1 -1
- package/esm/eval/scorers.mjs.map +1 -1
- package/esm/guard/detectors/injection.mjs.map +1 -1
- package/esm/guard/detectors/moderation.mjs.map +1 -1
- package/esm/guard/detectors/pii.mjs.map +1 -1
- package/esm/guard/detectors/topic.mjs.map +1 -1
- package/esm/human/human-approval.mjs.map +1 -1
- package/esm/human/resume.d.mts.map +1 -1
- package/esm/human/stores/memory.d.mts.map +1 -1
- package/esm/human/stores/pg.mjs.map +1 -1
- package/esm/human/stores/redis.mjs.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/memory/derive-id.mjs.map +1 -1
- package/esm/memory/episodic-memory.mjs.map +1 -1
- package/esm/memory/memory.mjs.map +1 -1
- package/esm/memory/procedural-memory.mjs.map +1 -1
- package/esm/memory/semantic-memory.mjs.map +1 -1
- package/esm/memory/working-memory.mjs.map +1 -1
- package/esm/middleware/builtins/budget.mjs.map +1 -1
- package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
- package/esm/middleware/helpers/compose.d.mts.map +1 -1
- package/esm/middleware/helpers/for-tool.mjs.map +1 -1
- package/esm/middleware/pipeline.d.mts.map +1 -1
- package/esm/middleware/utils/extract-user-text.mjs.map +1 -1
- package/esm/middleware/utils/namespaced-state.d.mts.map +1 -1
- package/esm/mock/mock-agent.d.mts.map +1 -1
- package/esm/mock/mock-agent.mjs.map +1 -1
- package/esm/mock/mock-model.d.mts.map +1 -1
- package/esm/mock/mock-model.mjs.map +1 -1
- package/esm/mock/mock-router.d.mts.map +1 -1
- package/esm/model/fallback-model.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.6.0/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.mjs +2 -2
- package/esm/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.mjs.map +1 -1
- package/esm/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.mjs.map +1 -1
- package/esm/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs +1 -1
- package/esm/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs.map +1 -1
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/rpc.MzXet3jl.mjs.map +1 -1
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/test.DNmyFkvJ.mjs.map +1 -1
- package/esm/object-stream/parse-partial-json.mjs.map +1 -1
- package/esm/object-stream/stream-object.d.mts.map +1 -1
- package/esm/object-stream/stream-object.mjs.map +1 -1
- package/esm/observe/observer-registry.d.mts.map +1 -1
- package/esm/orchestrator/as-tool.d.mts.map +1 -1
- package/esm/orchestrator/checkpoint.mjs.map +1 -1
- package/esm/orchestrator/compaction.mjs.map +1 -1
- package/esm/orchestrator/emitter.d.mts.map +1 -1
- package/esm/orchestrator/emitter.mjs.map +1 -1
- package/esm/orchestrator/execution.d.mts.map +1 -1
- package/esm/orchestrator/execution.mjs.map +1 -1
- package/esm/orchestrator/memory.mjs.map +1 -1
- package/esm/orchestrator/orchestrator-stream.d.mts.map +1 -1
- package/esm/orchestrator/orchestrator-stream.mjs.map +1 -1
- package/esm/orchestrator/orchestrator.d.mts.map +1 -1
- package/esm/orchestrator/orchestrator.mjs.map +1 -1
- package/esm/orchestrator/session-lock.d.mts.map +1 -1
- package/esm/orchestrator/signature.mjs.map +1 -1
- package/esm/planner/dag-scheduler.mjs.map +1 -1
- package/esm/planner/plan-prompt.mjs.map +1 -1
- package/esm/planner/planner-run.mjs.map +1 -1
- package/esm/planner/planner.d.mts.map +1 -1
- package/esm/planner/planner.mjs.map +1 -1
- package/esm/planner/signature.mjs.map +1 -1
- package/esm/planner/snapshot.mjs.map +1 -1
- package/esm/prompt/prompt-langfuse-sync.mjs.map +1 -1
- package/esm/prompt/prompt-validate.mjs.map +1 -1
- package/esm/prompt/prompt.mjs.map +1 -1
- package/esm/prompts/prompts-manager.d.mts.map +1 -1
- package/esm/prompts/prompts-manager.mjs.map +1 -1
- package/esm/prompts/prompts-validate.mjs.map +1 -1
- package/esm/rag/chunk/chunk.mjs.map +1 -1
- package/esm/rag/chunk/markdown.mjs.map +1 -1
- package/esm/rag/chunk/sentence.mjs.map +1 -1
- package/esm/rag/hybrid/bm25.mjs.map +1 -1
- package/esm/rag/hybrid/hybrid-rank.mjs.map +1 -1
- package/esm/rag/hybrid/rrf.mjs.map +1 -1
- package/esm/rag/loaders/load-html.mjs.map +1 -1
- package/esm/rag/loaders/load-pdf.mjs.map +1 -1
- package/esm/rag/loaders/load-text.mjs.map +1 -1
- package/esm/rag/rerank/keyword-reranker.mjs.map +1 -1
- package/esm/rag/rerank/llm-reranker.mjs.map +1 -1
- package/esm/rag/retrieve.mjs.map +1 -1
- package/esm/rag/store/cache-vector-store.mjs.map +1 -1
- package/esm/rag/store/pg-vector-store.mjs.map +1 -1
- package/esm/rag/transforms/multi-query.mjs.map +1 -1
- package/esm/security/outbound-policy.mjs.map +1 -1
- package/esm/security/private-ip.mjs.map +1 -1
- package/esm/security/redact.d.mts.map +1 -1
- package/esm/security/redact.mjs.map +1 -1
- package/esm/serve/serve.d.mts.map +1 -1
- package/esm/serve/serve.mjs.map +1 -1
- package/esm/serve/stream-to-sse.d.mts.map +1 -1
- package/esm/skills/catalog.mjs.map +1 -1
- package/esm/skills/skills.mjs.map +1 -1
- package/esm/skills/sources/directory-source.mjs.map +1 -1
- package/esm/skills/sources/parse-frontmatter.mjs.map +1 -1
- package/esm/skills/sources/url-source.mjs.map +1 -1
- package/esm/skills/store/mock-skills-store.mjs.map +1 -1
- package/esm/skills/store/procedural-skill-store.mjs.map +1 -1
- package/esm/snapshot/memory.d.mts.map +1 -1
- package/esm/snapshot/pg.mjs.map +1 -1
- package/esm/speech/speech.mjs.map +1 -1
- package/esm/supervisor/as-tool.d.mts.map +1 -1
- package/esm/supervisor/cancellation.mjs.map +1 -1
- package/esm/supervisor/emitter.d.mts.map +1 -1
- package/esm/supervisor/emitter.mjs.map +1 -1
- package/esm/supervisor/entries.mjs.map +1 -1
- package/esm/supervisor/execution.d.mts.map +1 -1
- package/esm/supervisor/execution.mjs.map +1 -1
- package/esm/supervisor/fan-out.mjs.map +1 -1
- package/esm/supervisor/router-factory.mjs.map +1 -1
- package/esm/supervisor/router-prompt.mjs.map +1 -1
- package/esm/supervisor/signature.mjs.map +1 -1
- package/esm/supervisor/snapshot.mjs.map +1 -1
- package/esm/supervisor/supervisor-stream.d.mts.map +1 -1
- package/esm/supervisor/supervisor-stream.mjs.map +1 -1
- package/esm/supervisor/supervisor.d.mts.map +1 -1
- package/esm/supervisor/supervisor.mjs.map +1 -1
- package/esm/system-prompt/refined-system-prompt.d.mts.map +1 -1
- package/esm/system-prompt/refined-system-prompt.mjs.map +1 -1
- package/esm/system-prompt/system-prompt.d.mts.map +1 -1
- package/esm/system-prompt/system-prompt.mjs.map +1 -1
- package/esm/team/team.d.mts.map +1 -1
- package/esm/testing/matcher-logic.mjs.map +1 -1
- package/esm/testing/register-lazy.d.mts.map +1 -1
- package/esm/tool/executable-as-tool.d.mts.map +1 -1
- package/esm/tool/tool.d.mts.map +1 -1
- package/esm/tool/tool.mjs.map +1 -1
- package/esm/transcribe/audio-input.mjs.map +1 -1
- package/esm/transcribe/transcribe.mjs.map +1 -1
- package/esm/utils/extract-json-payload.mjs.map +1 -1
- package/esm/utils/generate-run-id.mjs.map +1 -1
- package/esm/utils/prepare-attachment-part.mjs.map +1 -1
- package/esm/utils/run-context.d.mts.map +1 -1
- package/esm/utils/safe-json-parse.d.mts.map +1 -1
- package/esm/vcr/cassette-io.mjs.map +1 -1
- package/esm/vcr/hash-request.mjs.map +1 -1
- package/esm/vcr/vcr.mjs.map +1 -1
- package/esm/workflow/cancellation.mjs.map +1 -1
- package/esm/workflow/emitter.mjs.map +1 -1
- package/esm/workflow/engine.mjs.map +1 -1
- package/esm/workflow/retry.mjs.map +1 -1
- package/esm/workflow/router.mjs.map +1 -1
- package/esm/workflow/signature.d.mts.map +1 -1
- package/esm/workflow/signature.mjs.map +1 -1
- package/esm/workflow/snapshot.mjs.map +1 -1
- package/esm/workflow/step-runner.mjs.map +1 -1
- package/esm/workflow/step.d.mts.map +1 -1
- package/esm/workflow/step.mjs.map +1 -1
- package/esm/workflow/workflow.d.mts.map +1 -1
- package/esm/workflow/workflow.mjs.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.mjs","names":[],"sources":["../../../../../../../ai/src/system-prompt/system-prompt.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { Placeholders } from \"../contracts/placeholders.type\";\nimport type {\n InstructionContract,\n PersonaContract,\n RefinedSystemPromptContract,\n RefinedSystemPromptOptions,\n SystemPromptBlockContract,\n SystemPromptContract,\n SystemPromptMergeOptions,\n SystemPromptMeta,\n} from \"../contracts/system-prompt.contract\";\nimport { InvalidRequestError } from \"../errors\";\nimport { defaultPromptsManager, promptKey } from \"../prompts/prompts-manager\";\nimport type {\n PromptValidationResult,\n PromptsValidateOptions,\n} from \"../prompts/prompts-manager.type\";\nimport { Instruction } from \"./instruction\";\nimport { Persona } from \"./persona\";\nimport { RefinedSystemPrompt } from \"./refined-system-prompt\";\n\n/**\n * Monotonic source of the internal, non-registry display id every\n * `SystemPrompt` carries. Anonymous (unnamed) prompts have nothing else to\n * identify them by; this id never feeds the registry and is never derived from\n * the wall clock, so it stays stable and order-deterministic across a run.\n */\nlet displayIdCounter = 0;\n\n/**\n * Narrow an arbitrary value to a `SystemPromptContract` — true when it exposes\n * the builder surface (`blocks` array + a callable `resolve`). Used by the\n * registry-aware `merge` overload to tell a folded contract from a raw block\n * or a registry name string, robustly across duplicate package copies.\n */\nfunction isSystemPromptContract(\n value: unknown,\n): value is SystemPromptContract {\n return (\n typeof value === \"object\" &&\n value !== null &&\n Array.isArray((value as { blocks?: unknown }).blocks) &&\n typeof (value as { resolve?: unknown }).resolve === \"function\"\n );\n}\n\n/**\n * Build the deterministic provenance label for a prompt — `name@version` when\n * it is registered, otherwise its internal display id. No random suffixes, so\n * the same source always yields the same `composedFrom` entry.\n */\nfunction provenanceLabel(prompt: SystemPromptContract): string {\n const meta = prompt.meta();\n\n if (meta?.name) {\n return promptKey(meta.name, meta.version ?? \"1\");\n }\n\n return prompt instanceof SystemPrompt ? prompt.id : \"anonymous\";\n}\n\n/**\n * Concrete `SystemPromptContract` — an immutable layered prompt builder.\n *\n * **Role.** The top-level composer for a system prompt: it holds an ordered\n * list of typed blocks (persona + instructions) and resolves the whole\n * stack into one final string when the agent is about to call the model.\n *\n * **Responsibility.**\n * - Owns: the ordered `blocks` list and the block-join rules (insertion\n * order, blank-line separator, trim).\n * - Does NOT own: how any individual block is rendered (delegated to each\n * block's `resolve()`), the placeholder syntax (delegated to\n * `renderPlaceholders`), or any knowledge of the agent, model, or\n * session consuming the resolved text.\n *\n * Blocks are discriminated by a string `type` tag (`\"persona\"` /\n * `\"instruction\"`) rather than `instanceof`, so user-supplied blocks that\n * implement `SystemPromptBlockContract` interoperate seamlessly with blocks\n * built via `ai.persona()` / `ai.instruction()` — even across duplicate\n * package copies or bundler scope boundaries.\n *\n * The builder is **immutable** — every `.persona()` / `.instruction()`\n * call returns a fresh `SystemPrompt` instance sharing nothing mutable\n * with its parent. This makes forking a base prompt into specialized\n * variants a safe, side-effect-free operation.\n *\n * Users construct via the `ai.systemPrompt()` factory — `new SystemPrompt()`\n * is not the public API (see §4.2 of code-style.md). Modeled as a class so\n * that methods live on the prototype (one copy shared across every forked\n * instance) and downstream code can branch via `instanceof SystemPrompt`.\n *\n * @example\n * // Chainable form\n * const alex = ai.persona(\"You are Alex, a TypeScript expert.\");\n * const replyIn = ai.instruction(\"Respond in {{language|English}}.\");\n *\n * const base = ai.systemPrompt().persona(alex).instruction(replyIn);\n * const arabicVariant = base.instruction(\"Prefer Arabic comments.\");\n *\n * base.resolve({ language: \"English\" });\n * arabicVariant.resolve({ language: \"Arabic\" });\n *\n * @example\n * // Array form — insertion order is preserved exactly\n * const prompt = ai.systemPrompt([\n * ai.persona(\"You are Alex, a TypeScript expert.\"),\n * ai.instruction(\"Respond in {{language|English}}.\"),\n * ]);\n */\nexport class SystemPrompt implements SystemPromptContract {\n /**\n * Internal, non-registry id for display / provenance. Stable for the life of\n * the instance; sourced from a monotonic counter, never the wall clock.\n * Anonymous prompts are identified solely by this id.\n */\n public readonly id: string;\n\n public constructor(\n public readonly blocks: readonly SystemPromptBlockContract[] = [],\n private readonly metaData?: SystemPromptMeta,\n ) {\n this.id = `prompt#${displayIdCounter++}`;\n\n // Auto-register the moment a builder acquires a name — whether through the\n // `systemPrompt(input, { name })` factory or a `.meta({ name })` rename.\n // Forks built by `persona()` / `instruction()` / `merge()` deliberately\n // drop the name (they pass no meta), so they stay anonymous and never land\n // in the registry unless explicitly re-named.\n if (metaData?.name) {\n defaultPromptsManager().register(this);\n }\n }\n\n /**\n * Read the current metadata snapshot (no argument) or derive a renamed\n * builder (with `meta`). The accessor returns `undefined` for an anonymous\n * prompt; the updater shallow-merges `meta` onto the current metadata and\n * returns a fresh builder. Naming the result registers it in `ai.prompts`.\n */\n public meta(): SystemPromptMeta | undefined;\n public meta(meta: SystemPromptMeta): SystemPromptContract;\n public meta(\n meta?: SystemPromptMeta,\n ): SystemPromptMeta | undefined | SystemPromptContract {\n if (meta === undefined) {\n return this.metaData;\n }\n\n return new SystemPrompt(this.blocks, { ...this.metaData, ...meta });\n }\n\n /**\n * Build a system prompt by reading the file at `path` once, synchronously,\n * at construction time. The file's UTF-8 contents seed a single instruction\n * block — the same semantics as the string-seed form of `systemPrompt()` —\n * so placeholders inside the file (`{{language|English}}`) resolve at\n * `resolve()` time and the result can be forked with further\n * `.persona()` / `.instruction()` calls.\n *\n * One-shot by design: the file is read exactly once here, never re-read on\n * `resolve()`. Reads are synchronous so the call stays a drop-in for the\n * synchronous `systemPrompt()` factory and the synchronous `resolve()` API.\n *\n * Throws `InvalidRequestError` when the file cannot be read (missing path,\n * permission denied) — surfacing the underlying cause so a typo in the\n * prompt path fails loudly at construction instead of silently producing an\n * empty prompt.\n *\n * @param path - Filesystem path to the prompt template file.\n *\n * @example\n * const prompt = SystemPrompt.fromFile(\"./prompts/support-agent.md\");\n *\n * const localized = prompt.instruction(\"Respond in {{language|English}}.\");\n * localized.resolve({ language: \"Arabic\" });\n */\n public static fromFile(path: string): SystemPrompt {\n let contents: string;\n\n try {\n contents = readFileSync(path, \"utf8\");\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to read system prompt file \"${path}\" — ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path }, cause: error },\n );\n }\n\n return new SystemPrompt([new Instruction(contents)]);\n }\n\n /**\n * Return a new builder with the persona block set. If a persona already\n * exists it's replaced in place (preserving its position in `blocks`);\n * otherwise the new persona is prepended so persona-first remains the\n * default for chain-built prompts. Accepts either raw text (auto-wrapped\n * via `new Persona`) or an existing `PersonaContract` instance for reuse\n * across prompts.\n */\n public persona(value: PersonaContract | string): SystemPromptContract {\n const block = typeof value === \"string\" ? new Persona(value) : value;\n const existingIndex = this.blocks.findIndex(\n candidate => candidate.type === \"persona\",\n );\n\n if (existingIndex >= 0) {\n const next = [...this.blocks];\n next[existingIndex] = block;\n\n return new SystemPrompt(next) as this;\n }\n\n return new SystemPrompt([block, ...this.blocks]);\n }\n\n /**\n * Return a new builder with the given instruction appended. Instructions\n * render in insertion order. Accepts either raw text (auto-wrapped via\n * `new Instruction`) or an existing `InstructionContract` instance for\n * cross-prompt reuse.\n */\n public instruction(\n value: InstructionContract | string,\n ): SystemPromptContract {\n const block = typeof value === \"string\" ? new Instruction(value) : value;\n\n return new SystemPrompt([...this.blocks, block]);\n }\n\n /**\n * Fold predefined blocks, another prompt contract, or a registered prompt\n * name into this builder. Three forms share one method:\n *\n * - `merge(...blocks)` — N pre-built `ai.persona()` / `ai.instruction()`\n * blocks. A `persona` block sets/replaces the single, leading persona;\n * every other block appends in order. `base.merge(reviewer, style, lang)`\n * equals `base.persona(reviewer).instruction(style).instruction(lang)`.\n * - `merge(contract)` — another prompt; its blocks fold in (persona\n * replaces, instructions append) and `meta.composedFrom` records the\n * provenance of both sides.\n * - `merge(name, { fromVersion })` — a prompt resolved from `ai.prompts`\n * (latest version unless `fromVersion` selects another); throws\n * `InvalidRequestError` when the name / version is unregistered.\n *\n * Immutable — the original builder is untouched; passing zero blocks returns\n * an equivalent builder. The folded result is anonymous (no `name`), so it\n * is never auto-registered even though it carries `composedFrom` provenance.\n */\n public merge(\n ...blocks: readonly SystemPromptBlockContract[]\n ): SystemPromptContract;\n public merge(source: SystemPromptContract): SystemPromptContract;\n public merge(\n name: string,\n options?: SystemPromptMergeOptions,\n ): SystemPromptContract;\n public merge(\n first?:\n | SystemPromptBlockContract\n | SystemPromptContract\n | string,\n // `undefined` is part of the element union so the `merge(name, options?)`\n // overload's optional trailing `options?` (i.e. `… | undefined`) stays\n // assignable to this implementation signature.\n ...rest: readonly (\n | SystemPromptBlockContract\n | SystemPromptMergeOptions\n | undefined\n )[]\n ): SystemPromptContract {\n // Registry-name form: resolve from ai.prompts at the chosen version.\n if (typeof first === \"string\") {\n const options = rest[0] as SystemPromptMergeOptions | undefined;\n const resolved = defaultPromptsManager().get(first, options?.fromVersion);\n\n return this.mergeContract(resolved);\n }\n\n // Contract form: fold another prompt's blocks + record provenance.\n if (isSystemPromptContract(first)) {\n return this.mergeContract(first);\n }\n\n // Variadic block form (the original behavior).\n const all = [\n ...(first ? [first] : []),\n ...rest,\n ] as readonly SystemPromptBlockContract[];\n\n return this.foldBlocks(this, all);\n }\n\n /**\n * Fold an ordered list of blocks onto a starting prompt: persona blocks\n * set/replace the single leading persona; every other block appends in\n * order. The shared core of the variadic-block `merge` and the contract fold.\n */\n private foldBlocks(\n start: SystemPromptContract,\n blocks: readonly SystemPromptBlockContract[],\n ): SystemPromptContract {\n return blocks.reduce<SystemPromptContract>((prompt, block) => {\n if (block.type === \"persona\") {\n return prompt.persona(block as PersonaContract);\n }\n\n return new SystemPrompt([...prompt.blocks, block]);\n }, start);\n }\n\n /**\n * Fold another prompt contract into this one (persona replaces, instructions\n * append) and stamp the deterministic `composedFrom` provenance — this\n * prompt's existing provenance (or its own label) followed by the folded\n * source's label. The result is anonymous so it never auto-registers.\n */\n private mergeContract(\n source: SystemPromptContract,\n ): SystemPromptContract {\n const folded = this.foldBlocks(this, source.blocks);\n\n const baseProvenance =\n this.metaData?.composedFrom ??\n (this.metaData?.name ? [provenanceLabel(this)] : []);\n\n const composedFrom = [...baseProvenance, provenanceLabel(source)];\n\n // Carry forward only provenance — never the name — so the merged result is\n // a fresh anonymous prompt (immutable rename = new key; original stays).\n return new SystemPrompt(folded.blocks, { composedFrom });\n }\n\n /**\n * Resolve every block against the placeholder map, join the results with\n * blank-line separators (in insertion order), and trim. Returns an empty\n * string when no blocks are present — callers treat that as \"no system\n * message\".\n */\n public resolve(placeholders?: Placeholders): string {\n return this.blocks\n .map(block => block.resolve(placeholders))\n .join(\"\\n\\n\")\n .trim();\n }\n\n /**\n * Validate this prompt via the process-wide `ai.prompts` manager — sugar for\n * `ai.prompts.validate(this, options)`. Runs the deterministic placeholder\n * check and, when `options.judge` is supplied, the Nova-safe LLM-as-judge\n * pass. Never throws on a judge failure; `ok` tracks the deterministic\n * verdict alone.\n */\n public validate(\n options?: PromptsValidateOptions,\n ): Promise<PromptValidationResult> {\n return defaultPromptsManager().validate(this, options);\n }\n\n /**\n * Derive the compiled form of this prompt — a lazy wrapper that rewrites\n * the human-authored text into a model-optimized version on first use,\n * pins the result, and serves the pin thereafter. See\n * {@link RefinedSystemPromptContract} for the full semantics (lockfile\n * pinning, placeholder parity, advisory fallback, `refine()` /\n * `refinePrompt()`).\n *\n * The wrapper's collaborators are injected here rather than imported by\n * `refined-system-prompt.ts` — importing this module (or the prompts\n * manager) back from there would close an import cycle.\n */\n public refined(\n options: RefinedSystemPromptOptions,\n ): RefinedSystemPromptContract {\n return new RefinedSystemPrompt(this, options, {\n buildPrompt: (blocks, meta) => new SystemPrompt([...blocks], meta),\n validatePrompt: (target, validateOptions) =>\n defaultPromptsManager().validate(target, validateOptions),\n });\n }\n}\n\n/**\n * Public factory for `SystemPrompt`, callable directly or via its\n * `fromFile` static. Exists as a named interface so the callable signature\n * and the `fromFile` attachment travel together as one public type.\n */\nexport interface SystemPromptFactory {\n (\n input?: string | ReadonlyArray<SystemPromptBlockContract>,\n meta?: SystemPromptMeta,\n ): SystemPrompt;\n\n /**\n * Build a system prompt from a file read once at construction. Delegates\n * to {@link SystemPrompt.fromFile}, so `ai.systemPrompt.fromFile(path)` and\n * `SystemPrompt.fromFile(path)` behave identically.\n *\n * @example\n * const prompt = ai.systemPrompt.fromFile(\"./prompts/support-agent.md\");\n */\n fromFile(path: string): SystemPrompt;\n}\n\nfunction systemPromptFactory(\n input?: string | ReadonlyArray<SystemPromptBlockContract>,\n meta?: SystemPromptMeta,\n): SystemPrompt {\n if (input === undefined) {\n return new SystemPrompt([], meta);\n }\n\n if (typeof input === \"string\") {\n return new SystemPrompt([new Instruction(input)], meta);\n }\n\n return new SystemPrompt([...input], meta);\n}\n\n/**\n * Create a new immutable system-prompt builder.\n *\n * **Role.** Public factory for `SystemPrompt` — keeps user-facing code\n * free of `new` and consistent with `ai.tool()`, `ai.agent()`,\n * `ai.persona()`, `ai.instruction()`.\n *\n * Input forms:\n * - No argument → empty builder, chain `.persona()` / `.instruction()`\n * - Single string → seeded with one instruction for quick one-shot prompts\n * - Array of blocks → used verbatim, preserving insertion order\n * - `.fromFile(path)` → seeded from a file read once at construction\n *\n * Pass a second `meta` argument to name the prompt — a named prompt\n * auto-registers in `ai.prompts` under `name@version` (version defaults to the\n * next integer). Forks (`.persona()`, `.instruction()`, `.merge()`) are\n * anonymous unless re-named via `.meta({ name })`.\n *\n * @example\n * // Composed builder\n * const prompt = systemPrompt()\n * .persona(\"You are Alex, a senior TypeScript engineer.\")\n * .instruction(\"Always include working code examples.\")\n * .instruction(\"Respond in {{language|English}}.\");\n *\n * prompt.resolve({ language: \"Arabic\" });\n *\n * @example\n * // One-shot seed\n * const prompt = systemPrompt(\"Answer only with JSON matching the schema.\");\n *\n * @example\n * // From a file, read once at construction\n * const prompt = systemPrompt.fromFile(\"./prompts/support-agent.md\");\n *\n * @example\n * // Array form — fully declarative\n * const prompt = systemPrompt([\n * ai.persona(\"You are Alex.\"),\n * ai.instruction(\"Always cite sources.\"),\n * ai.instruction(\"Respond in {{language|English}}.\"),\n * ]);\n */\nexport const systemPrompt: SystemPromptFactory = Object.assign(\n systemPromptFactory,\n { fromFile: SystemPrompt.fromFile },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"system-prompt.mjs","names":[],"sources":["../../../../../../../ai/src/system-prompt/system-prompt.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { Placeholders } from \"../contracts/placeholders.type\";\nimport type {\n InstructionContract,\n PersonaContract,\n RefinedSystemPromptContract,\n RefinedSystemPromptOptions,\n SystemPromptBlockContract,\n SystemPromptContract,\n SystemPromptMergeOptions,\n SystemPromptMeta,\n} from \"../contracts/system-prompt.contract\";\nimport { InvalidRequestError } from \"../errors\";\nimport { defaultPromptsManager, promptKey } from \"../prompts/prompts-manager\";\nimport type {\n PromptValidationResult,\n PromptsValidateOptions,\n} from \"../prompts/prompts-manager.type\";\nimport { Instruction } from \"./instruction\";\nimport { Persona } from \"./persona\";\nimport { RefinedSystemPrompt } from \"./refined-system-prompt\";\n\n/**\n * Monotonic source of the internal, non-registry display id every\n * `SystemPrompt` carries. Anonymous (unnamed) prompts have nothing else to\n * identify them by; this id never feeds the registry and is never derived from\n * the wall clock, so it stays stable and order-deterministic across a run.\n */\nlet displayIdCounter = 0;\n\n/**\n * Narrow an arbitrary value to a `SystemPromptContract` — true when it exposes\n * the builder surface (`blocks` array + a callable `resolve`). Used by the\n * registry-aware `merge` overload to tell a folded contract from a raw block\n * or a registry name string, robustly across duplicate package copies.\n */\nfunction isSystemPromptContract(\n value: unknown,\n): value is SystemPromptContract {\n return (\n typeof value === \"object\" &&\n value !== null &&\n Array.isArray((value as { blocks?: unknown }).blocks) &&\n typeof (value as { resolve?: unknown }).resolve === \"function\"\n );\n}\n\n/**\n * Build the deterministic provenance label for a prompt — `name@version` when\n * it is registered, otherwise its internal display id. No random suffixes, so\n * the same source always yields the same `composedFrom` entry.\n */\nfunction provenanceLabel(prompt: SystemPromptContract): string {\n const meta = prompt.meta();\n\n if (meta?.name) {\n return promptKey(meta.name, meta.version ?? \"1\");\n }\n\n return prompt instanceof SystemPrompt ? prompt.id : \"anonymous\";\n}\n\n/**\n * Concrete `SystemPromptContract` — an immutable layered prompt builder.\n *\n * **Role.** The top-level composer for a system prompt: it holds an ordered\n * list of typed blocks (persona + instructions) and resolves the whole\n * stack into one final string when the agent is about to call the model.\n *\n * **Responsibility.**\n * - Owns: the ordered `blocks` list and the block-join rules (insertion\n * order, blank-line separator, trim).\n * - Does NOT own: how any individual block is rendered (delegated to each\n * block's `resolve()`), the placeholder syntax (delegated to\n * `renderPlaceholders`), or any knowledge of the agent, model, or\n * session consuming the resolved text.\n *\n * Blocks are discriminated by a string `type` tag (`\"persona\"` /\n * `\"instruction\"`) rather than `instanceof`, so user-supplied blocks that\n * implement `SystemPromptBlockContract` interoperate seamlessly with blocks\n * built via `ai.persona()` / `ai.instruction()` — even across duplicate\n * package copies or bundler scope boundaries.\n *\n * The builder is **immutable** — every `.persona()` / `.instruction()`\n * call returns a fresh `SystemPrompt` instance sharing nothing mutable\n * with its parent. This makes forking a base prompt into specialized\n * variants a safe, side-effect-free operation.\n *\n * Users construct via the `ai.systemPrompt()` factory — `new SystemPrompt()`\n * is not the public API (see §4.2 of code-style.md). Modeled as a class so\n * that methods live on the prototype (one copy shared across every forked\n * instance) and downstream code can branch via `instanceof SystemPrompt`.\n *\n * @example\n * // Chainable form\n * const alex = ai.persona(\"You are Alex, a TypeScript expert.\");\n * const replyIn = ai.instruction(\"Respond in {{language|English}}.\");\n *\n * const base = ai.systemPrompt().persona(alex).instruction(replyIn);\n * const arabicVariant = base.instruction(\"Prefer Arabic comments.\");\n *\n * base.resolve({ language: \"English\" });\n * arabicVariant.resolve({ language: \"Arabic\" });\n *\n * @example\n * // Array form — insertion order is preserved exactly\n * const prompt = ai.systemPrompt([\n * ai.persona(\"You are Alex, a TypeScript expert.\"),\n * ai.instruction(\"Respond in {{language|English}}.\"),\n * ]);\n */\nexport class SystemPrompt implements SystemPromptContract {\n /**\n * Internal, non-registry id for display / provenance. Stable for the life of\n * the instance; sourced from a monotonic counter, never the wall clock.\n * Anonymous prompts are identified solely by this id.\n */\n public readonly id: string;\n\n public constructor(\n public readonly blocks: readonly SystemPromptBlockContract[] = [],\n private readonly metaData?: SystemPromptMeta,\n ) {\n this.id = `prompt#${displayIdCounter++}`;\n\n // Auto-register the moment a builder acquires a name — whether through the\n // `systemPrompt(input, { name })` factory or a `.meta({ name })` rename.\n // Forks built by `persona()` / `instruction()` / `merge()` deliberately\n // drop the name (they pass no meta), so they stay anonymous and never land\n // in the registry unless explicitly re-named.\n if (metaData?.name) {\n defaultPromptsManager().register(this);\n }\n }\n\n /**\n * Read the current metadata snapshot (no argument) or derive a renamed\n * builder (with `meta`). The accessor returns `undefined` for an anonymous\n * prompt; the updater shallow-merges `meta` onto the current metadata and\n * returns a fresh builder. Naming the result registers it in `ai.prompts`.\n */\n public meta(): SystemPromptMeta | undefined;\n public meta(meta: SystemPromptMeta): SystemPromptContract;\n public meta(\n meta?: SystemPromptMeta,\n ): SystemPromptMeta | undefined | SystemPromptContract {\n if (meta === undefined) {\n return this.metaData;\n }\n\n return new SystemPrompt(this.blocks, { ...this.metaData, ...meta });\n }\n\n /**\n * Build a system prompt by reading the file at `path` once, synchronously,\n * at construction time. The file's UTF-8 contents seed a single instruction\n * block — the same semantics as the string-seed form of `systemPrompt()` —\n * so placeholders inside the file (`{{language|English}}`) resolve at\n * `resolve()` time and the result can be forked with further\n * `.persona()` / `.instruction()` calls.\n *\n * One-shot by design: the file is read exactly once here, never re-read on\n * `resolve()`. Reads are synchronous so the call stays a drop-in for the\n * synchronous `systemPrompt()` factory and the synchronous `resolve()` API.\n *\n * Throws `InvalidRequestError` when the file cannot be read (missing path,\n * permission denied) — surfacing the underlying cause so a typo in the\n * prompt path fails loudly at construction instead of silently producing an\n * empty prompt.\n *\n * @param path - Filesystem path to the prompt template file.\n *\n * @example\n * const prompt = SystemPrompt.fromFile(\"./prompts/support-agent.md\");\n *\n * const localized = prompt.instruction(\"Respond in {{language|English}}.\");\n * localized.resolve({ language: \"Arabic\" });\n */\n public static fromFile(path: string): SystemPrompt {\n let contents: string;\n\n try {\n contents = readFileSync(path, \"utf8\");\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to read system prompt file \"${path}\" — ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path }, cause: error },\n );\n }\n\n return new SystemPrompt([new Instruction(contents)]);\n }\n\n /**\n * Return a new builder with the persona block set. If a persona already\n * exists it's replaced in place (preserving its position in `blocks`);\n * otherwise the new persona is prepended so persona-first remains the\n * default for chain-built prompts. Accepts either raw text (auto-wrapped\n * via `new Persona`) or an existing `PersonaContract` instance for reuse\n * across prompts.\n */\n public persona(value: PersonaContract | string): SystemPromptContract {\n const block = typeof value === \"string\" ? new Persona(value) : value;\n const existingIndex = this.blocks.findIndex(\n candidate => candidate.type === \"persona\",\n );\n\n if (existingIndex >= 0) {\n const next = [...this.blocks];\n next[existingIndex] = block;\n\n return new SystemPrompt(next) as this;\n }\n\n return new SystemPrompt([block, ...this.blocks]);\n }\n\n /**\n * Return a new builder with the given instruction appended. Instructions\n * render in insertion order. Accepts either raw text (auto-wrapped via\n * `new Instruction`) or an existing `InstructionContract` instance for\n * cross-prompt reuse.\n */\n public instruction(\n value: InstructionContract | string,\n ): SystemPromptContract {\n const block = typeof value === \"string\" ? new Instruction(value) : value;\n\n return new SystemPrompt([...this.blocks, block]);\n }\n\n /**\n * Fold predefined blocks, another prompt contract, or a registered prompt\n * name into this builder. Three forms share one method:\n *\n * - `merge(...blocks)` — N pre-built `ai.persona()` / `ai.instruction()`\n * blocks. A `persona` block sets/replaces the single, leading persona;\n * every other block appends in order. `base.merge(reviewer, style, lang)`\n * equals `base.persona(reviewer).instruction(style).instruction(lang)`.\n * - `merge(contract)` — another prompt; its blocks fold in (persona\n * replaces, instructions append) and `meta.composedFrom` records the\n * provenance of both sides.\n * - `merge(name, { fromVersion })` — a prompt resolved from `ai.prompts`\n * (latest version unless `fromVersion` selects another); throws\n * `InvalidRequestError` when the name / version is unregistered.\n *\n * Immutable — the original builder is untouched; passing zero blocks returns\n * an equivalent builder. The folded result is anonymous (no `name`), so it\n * is never auto-registered even though it carries `composedFrom` provenance.\n */\n public merge(\n ...blocks: readonly SystemPromptBlockContract[]\n ): SystemPromptContract;\n public merge(source: SystemPromptContract): SystemPromptContract;\n public merge(\n name: string,\n options?: SystemPromptMergeOptions,\n ): SystemPromptContract;\n public merge(\n first?:\n | SystemPromptBlockContract\n | SystemPromptContract\n | string,\n // `undefined` is part of the element union so the `merge(name, options?)`\n // overload's optional trailing `options?` (i.e. `… | undefined`) stays\n // assignable to this implementation signature.\n ...rest: readonly (\n | SystemPromptBlockContract\n | SystemPromptMergeOptions\n | undefined\n )[]\n ): SystemPromptContract {\n // Registry-name form: resolve from ai.prompts at the chosen version.\n if (typeof first === \"string\") {\n const options = rest[0] as SystemPromptMergeOptions | undefined;\n const resolved = defaultPromptsManager().get(first, options?.fromVersion);\n\n return this.mergeContract(resolved);\n }\n\n // Contract form: fold another prompt's blocks + record provenance.\n if (isSystemPromptContract(first)) {\n return this.mergeContract(first);\n }\n\n // Variadic block form (the original behavior).\n const all = [\n ...(first ? [first] : []),\n ...rest,\n ] as readonly SystemPromptBlockContract[];\n\n return this.foldBlocks(this, all);\n }\n\n /**\n * Fold an ordered list of blocks onto a starting prompt: persona blocks\n * set/replace the single leading persona; every other block appends in\n * order. The shared core of the variadic-block `merge` and the contract fold.\n */\n private foldBlocks(\n start: SystemPromptContract,\n blocks: readonly SystemPromptBlockContract[],\n ): SystemPromptContract {\n return blocks.reduce<SystemPromptContract>((prompt, block) => {\n if (block.type === \"persona\") {\n return prompt.persona(block as PersonaContract);\n }\n\n return new SystemPrompt([...prompt.blocks, block]);\n }, start);\n }\n\n /**\n * Fold another prompt contract into this one (persona replaces, instructions\n * append) and stamp the deterministic `composedFrom` provenance — this\n * prompt's existing provenance (or its own label) followed by the folded\n * source's label. The result is anonymous so it never auto-registers.\n */\n private mergeContract(\n source: SystemPromptContract,\n ): SystemPromptContract {\n const folded = this.foldBlocks(this, source.blocks);\n\n const baseProvenance =\n this.metaData?.composedFrom ??\n (this.metaData?.name ? [provenanceLabel(this)] : []);\n\n const composedFrom = [...baseProvenance, provenanceLabel(source)];\n\n // Carry forward only provenance — never the name — so the merged result is\n // a fresh anonymous prompt (immutable rename = new key; original stays).\n return new SystemPrompt(folded.blocks, { composedFrom });\n }\n\n /**\n * Resolve every block against the placeholder map, join the results with\n * blank-line separators (in insertion order), and trim. Returns an empty\n * string when no blocks are present — callers treat that as \"no system\n * message\".\n */\n public resolve(placeholders?: Placeholders): string {\n return this.blocks\n .map(block => block.resolve(placeholders))\n .join(\"\\n\\n\")\n .trim();\n }\n\n /**\n * Validate this prompt via the process-wide `ai.prompts` manager — sugar for\n * `ai.prompts.validate(this, options)`. Runs the deterministic placeholder\n * check and, when `options.judge` is supplied, the Nova-safe LLM-as-judge\n * pass. Never throws on a judge failure; `ok` tracks the deterministic\n * verdict alone.\n */\n public validate(\n options?: PromptsValidateOptions,\n ): Promise<PromptValidationResult> {\n return defaultPromptsManager().validate(this, options);\n }\n\n /**\n * Derive the compiled form of this prompt — a lazy wrapper that rewrites\n * the human-authored text into a model-optimized version on first use,\n * pins the result, and serves the pin thereafter. See\n * {@link RefinedSystemPromptContract} for the full semantics (lockfile\n * pinning, placeholder parity, advisory fallback, `refine()` /\n * `refinePrompt()`).\n *\n * The wrapper's collaborators are injected here rather than imported by\n * `refined-system-prompt.ts` — importing this module (or the prompts\n * manager) back from there would close an import cycle.\n */\n public refined(\n options: RefinedSystemPromptOptions,\n ): RefinedSystemPromptContract {\n return new RefinedSystemPrompt(this, options, {\n buildPrompt: (blocks, meta) => new SystemPrompt([...blocks], meta),\n validatePrompt: (target, validateOptions) =>\n defaultPromptsManager().validate(target, validateOptions),\n });\n }\n}\n\n/**\n * Public factory for `SystemPrompt`, callable directly or via its\n * `fromFile` static. Exists as a named interface so the callable signature\n * and the `fromFile` attachment travel together as one public type.\n */\nexport interface SystemPromptFactory {\n (\n input?: string | ReadonlyArray<SystemPromptBlockContract>,\n meta?: SystemPromptMeta,\n ): SystemPrompt;\n\n /**\n * Build a system prompt from a file read once at construction. Delegates\n * to {@link SystemPrompt.fromFile}, so `ai.systemPrompt.fromFile(path)` and\n * `SystemPrompt.fromFile(path)` behave identically.\n *\n * @example\n * const prompt = ai.systemPrompt.fromFile(\"./prompts/support-agent.md\");\n */\n fromFile(path: string): SystemPrompt;\n}\n\nfunction systemPromptFactory(\n input?: string | ReadonlyArray<SystemPromptBlockContract>,\n meta?: SystemPromptMeta,\n): SystemPrompt {\n if (input === undefined) {\n return new SystemPrompt([], meta);\n }\n\n if (typeof input === \"string\") {\n return new SystemPrompt([new Instruction(input)], meta);\n }\n\n return new SystemPrompt([...input], meta);\n}\n\n/**\n * Create a new immutable system-prompt builder.\n *\n * **Role.** Public factory for `SystemPrompt` — keeps user-facing code\n * free of `new` and consistent with `ai.tool()`, `ai.agent()`,\n * `ai.persona()`, `ai.instruction()`.\n *\n * Input forms:\n * - No argument → empty builder, chain `.persona()` / `.instruction()`\n * - Single string → seeded with one instruction for quick one-shot prompts\n * - Array of blocks → used verbatim, preserving insertion order\n * - `.fromFile(path)` → seeded from a file read once at construction\n *\n * Pass a second `meta` argument to name the prompt — a named prompt\n * auto-registers in `ai.prompts` under `name@version` (version defaults to the\n * next integer). Forks (`.persona()`, `.instruction()`, `.merge()`) are\n * anonymous unless re-named via `.meta({ name })`.\n *\n * @example\n * // Composed builder\n * const prompt = systemPrompt()\n * .persona(\"You are Alex, a senior TypeScript engineer.\")\n * .instruction(\"Always include working code examples.\")\n * .instruction(\"Respond in {{language|English}}.\");\n *\n * prompt.resolve({ language: \"Arabic\" });\n *\n * @example\n * // One-shot seed\n * const prompt = systemPrompt(\"Answer only with JSON matching the schema.\");\n *\n * @example\n * // From a file, read once at construction\n * const prompt = systemPrompt.fromFile(\"./prompts/support-agent.md\");\n *\n * @example\n * // Array form — fully declarative\n * const prompt = systemPrompt([\n * ai.persona(\"You are Alex.\"),\n * ai.instruction(\"Always cite sources.\"),\n * ai.instruction(\"Respond in {{language|English}}.\"),\n * ]);\n */\nexport const systemPrompt: SystemPromptFactory = Object.assign(\n systemPromptFactory,\n { fromFile: SystemPrompt.fromFile },\n);\n"],"mappings":"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"}
|
package/esm/team/team.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team.d.mts","names":[],"sources":["../../../../../../../ai/src/team/team.ts"],"mappings":";;;;;;AAmDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,IAAA,6BAEL,OAAA,mBACQ,MAAA,SAAe,eAAA,IAAmB,MAAA,SAAe,eAAA,
|
|
1
|
+
{"version":3,"file":"team.d.mts","names":[],"sources":["../../../../../../../ai/src/team/team.ts"],"mappings":";;;;;;AAmDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,IAAA,6BAEL,OAAA,mBACQ,MAAA,SAAe,eAAA,IAAmB,MAAA,SAAe,eAAA,GAClE,MAAA,EAAQ,UAAA,CAAW,OAAA,EAAS,MAAA,EAAQ,QAAA,IAAY,kBAAA,CAAmB,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"matcher-logic.mjs","names":[],"sources":["../../../../../../../ai/src/testing/matcher-logic.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { END } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { SupervisorReport } from \"../contracts/result/supervisor-result.type\";\nimport type { WorkflowReport } from \"../contracts/result/workflow-result.type\";\n\n/**\n * Normalized matcher verdict — the library-agnostic shape every\n * matcher in this module returns. Vitest's `expect.extend` consumes\n * the same `{ pass, message }` contract, so the registration layer\n * forwards these verbatim.\n */\nexport type MatcherVerdict = {\n /** Whether the assertion passed. */\n pass: boolean;\n /** Lazy message factory — Vitest calls it only when reporting. */\n message: () => string;\n};\n\n/**\n * Any unified result envelope a matcher can be handed. Accepts the\n * full `{ report, ... }` result or a bare `BaseReport` so callers can\n * assert against either `await x.execute()` or `result.report`.\n */\ntype ReportLike = BaseReport | { report: BaseReport };\n\n/**\n * Coerce a matcher target into its `BaseReport`. Accepts the result\n * envelope (`{ report }`) or a report directly.\n */\nfunction toReport(received: ReportLike): BaseReport {\n if (\"report\" in received && received.report) {\n return received.report;\n }\n\n return received as BaseReport;\n}\n\n/** Narrow a `BaseReport` to a `SupervisorReport` by its discriminator. */\nfunction asSupervisorReport(report: BaseReport): SupervisorReport | undefined {\n return report.type === \"supervisor\" ? (report as SupervisorReport) : undefined;\n}\n\n/** Narrow a `BaseReport` to a `WorkflowReport` by its discriminator. */\nfunction asWorkflowReport(report: BaseReport): WorkflowReport | undefined {\n return report.type === \"workflow\" ? (report as WorkflowReport) : undefined;\n}\n\n/**\n * Collect every intent name dispatched by a supervisor across all\n * iterations — the keys of each iteration snapshot's `result` record.\n */\nfunction dispatchedIntents(report: SupervisorReport): string[] {\n const intents = new Set<string>();\n\n for (const snapshot of report.snapshots) {\n for (const intent of Object.keys(snapshot.result)) {\n intents.add(intent);\n }\n }\n\n return [...intents];\n}\n\n/**\n * Assert that a supervisor routed to (dispatched) the named intent at\n * least once across its iterations. Targets a `SupervisorResult` or a\n * `SupervisorReport`.\n *\n * @example\n * expect(await supervisor.execute(input)).toRouteTo(\"critic\");\n */\nexport function matchRouteTo(received: ReportLike, intent: string): MatcherVerdict {\n const report = toReport(received);\n const supervisorReport = asSupervisorReport(report);\n\n if (!supervisorReport) {\n return {\n pass: false,\n message: () =>\n `toRouteTo expects a supervisor result, but received a \"${report.type}\" report`,\n };\n }\n\n const intents = dispatchedIntents(supervisorReport);\n const pass = intents.includes(intent);\n\n return {\n pass,\n message: () =>\n pass\n ? `expected supervisor not to route to \"${intent}\", but it did`\n : `expected supervisor to route to \"${intent}\", but it routed to [${intents.join(\", \")}]`,\n };\n}\n\n/**\n * Assert that a supervisor converged — terminated on its own decision\n * (`router` / `route` / `evaluate` / `classifier`) with a\n * `\"completed\"` status, rather than hitting the iteration cap, being\n * cancelled, or erroring. Targets a `SupervisorResult` or\n * `SupervisorReport`.\n *\n * @example\n * expect(await supervisor.execute(input)).toConverge();\n */\nexport function matchConverge(received: ReportLike): MatcherVerdict {\n const report = toReport(received);\n const supervisorReport = asSupervisorReport(report);\n\n if (!supervisorReport) {\n return {\n pass: false,\n message: () =>\n `toConverge expects a supervisor result, but received a \"${report.type}\" report`,\n };\n }\n\n const nonConvergent = new Set([\"max-iterations\", \"cancelled\", \"error\"]);\n const pass =\n supervisorReport.status === \"completed\" &&\n !nonConvergent.has(supervisorReport.terminatedBy);\n\n return {\n pass,\n message: () =>\n pass\n ? `expected supervisor not to converge, but it terminated via \"${supervisorReport.terminatedBy}\"`\n : `expected supervisor to converge, but status=\"${supervisorReport.status}\" terminatedBy=\"${supervisorReport.terminatedBy}\" after ${supervisorReport.iterations} iteration(s)`,\n };\n}\n\n/**\n * Assert that a workflow step completed successfully. Targets a\n * `WorkflowResult` or `WorkflowReport`; looks the step up by name in\n * `report.steps` and checks its status is `\"completed\"`.\n *\n * @example\n * expect(await workflow.execute(input)).toPassStep(\"draft\");\n */\nexport function matchPassStep(received: ReportLike, stepName: string): MatcherVerdict {\n const report = toReport(received);\n const workflowReport = asWorkflowReport(report);\n\n if (!workflowReport) {\n return {\n pass: false,\n message: () =>\n `toPassStep expects a workflow result, but received a \"${report.type}\" report`,\n };\n }\n\n const step = workflowReport.steps[stepName];\n\n if (!step) {\n const known = Object.keys(workflowReport.steps).join(\", \");\n return {\n pass: false,\n message: () =>\n `expected workflow to have a step \"${stepName}\", but steps are [${known}]`,\n };\n }\n\n const pass = step.status === \"completed\";\n\n return {\n pass,\n message: () =>\n pass\n ? `expected step \"${stepName}\" not to pass, but it completed`\n : `expected step \"${stepName}\" to pass, but its status was \"${step.status}\"`,\n };\n}\n\n/**\n * Result envelope carrying a typed `data` payload — what\n * `matchOutputShape` validates against a schema.\n */\ntype DataResult = { data?: unknown };\n\n/**\n * Assert that a result's `data` validates against a Standard Schema.\n * Targets any result envelope with a `data` field (agent / workflow /\n * supervisor). Runs the schema's `~standard.validate` and passes only\n * when it reports no issues.\n *\n * Synchronous-only: a schema whose `validate` returns a Promise is\n * rejected with a clear message rather than silently passing — the\n * async variant belongs on a dedicated async matcher if needed.\n *\n * @example\n * expect(await agent.execute(input, { output: schema })).toOutputShape(schema);\n */\nexport function matchOutputShape(\n received: DataResult,\n schema: StandardSchemaV1,\n): MatcherVerdict {\n const data = received.data;\n\n if (data === undefined) {\n return {\n pass: false,\n message: () => \"toOutputShape expected result.data to be defined, but it was undefined\",\n };\n }\n\n const validation = schema[\"~standard\"].validate(data);\n\n if (validation instanceof Promise) {\n return {\n pass: false,\n message: () =>\n \"toOutputShape received an async schema; use a synchronous Standard Schema for this matcher\",\n };\n }\n\n const pass = validation.issues === undefined;\n\n return {\n pass,\n message: () => {\n if (pass) {\n return \"expected result.data not to match the schema, but it did\";\n }\n\n const summary = (validation.issues ?? []).map((issue) => issue.message).join(\"; \");\n return `expected result.data to match the schema, but validation failed: ${summary}`;\n },\n };\n}\n\n// Re-exported so matcher consumers and tests can reference the\n// termination sentinel without reaching into contracts.\nexport { END };\n"],"mappings":";;;;;;;AA8BA,SAAS,SAAS,UAAkC;CAClD,IAAI,YAAY,YAAY,SAAS,QACnC,OAAO,SAAS;CAGlB,OAAO;AACT;;AAGA,SAAS,mBAAmB,QAAkD;CAC5E,OAAO,OAAO,SAAS,eAAgB,SAA8B;AACvE;;AAGA,SAAS,iBAAiB,QAAgD;CACxE,OAAO,OAAO,SAAS,aAAc,SAA4B;AACnE;;;;;AAMA,SAAS,kBAAkB,QAAoC;CAC7D,MAAM,0BAAU,IAAI,IAAY;CAEhC,KAAK,MAAM,YAAY,OAAO,WAC5B,KAAK,MAAM,UAAU,OAAO,KAAK,SAAS,MAAM,GAC9C,QAAQ,IAAI,MAAM;CAItB,OAAO,CAAC,GAAG,OAAO;AACpB;;;;;;;;;AAUA,SAAgB,aAAa,UAAsB,QAAgC;CACjF,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,mBAAmB,mBAAmB,MAAM;CAElD,IAAI,CAAC,kBACH,OAAO;EACL,MAAM;EACN,eACE,0DAA0D,OAAO,KAAK;CAC1E;CAGF,MAAM,UAAU,kBAAkB,gBAAgB;CAClD,MAAM,OAAO,QAAQ,SAAS,MAAM;CAEpC,OAAO;EACL;EACA,eACE,OACI,wCAAwC,OAAO,iBAC/C,oCAAoC,OAAO,uBAAuB,QAAQ,KAAK,IAAI,EAAE;CAC7F;AACF;;;;;;;;;;;AAYA,SAAgB,cAAc,UAAsC;CAClE,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,mBAAmB,mBAAmB,MAAM;CAElD,IAAI,CAAC,kBACH,OAAO;EACL,MAAM;EACN,eACE,2DAA2D,OAAO,KAAK;CAC3E;CAGF,MAAM,gBAAgB,IAAI,IAAI;EAAC;EAAkB;EAAa;CAAO,CAAC;CACtE,MAAM,OACJ,iBAAiB,WAAW,eAC5B,CAAC,cAAc,IAAI,iBAAiB,YAAY;CAElD,OAAO;EACL;EACA,eACE,OACI,+DAA+D,iBAAiB,aAAa,KAC7F,gDAAgD,iBAAiB,OAAO,kBAAkB,iBAAiB,aAAa,UAAU,iBAAiB,WAAW;CACtK;AACF;;;;;;;;;AAUA,SAAgB,cAAc,UAAsB,UAAkC;CACpF,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,iBAAiB,iBAAiB,MAAM;CAE9C,IAAI,CAAC,gBACH,OAAO;EACL,MAAM;EACN,eACE,yDAAyD,OAAO,KAAK;CACzE;CAGF,MAAM,OAAO,eAAe,MAAM;CAElC,IAAI,CAAC,MAAM;EACT,MAAM,QAAQ,OAAO,KAAK,eAAe,KAAK,
|
|
1
|
+
{"version":3,"file":"matcher-logic.mjs","names":[],"sources":["../../../../../../../ai/src/testing/matcher-logic.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { END } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { SupervisorReport } from \"../contracts/result/supervisor-result.type\";\nimport type { WorkflowReport } from \"../contracts/result/workflow-result.type\";\n\n/**\n * Normalized matcher verdict — the library-agnostic shape every\n * matcher in this module returns. Vitest's `expect.extend` consumes\n * the same `{ pass, message }` contract, so the registration layer\n * forwards these verbatim.\n */\nexport type MatcherVerdict = {\n /** Whether the assertion passed. */\n pass: boolean;\n /** Lazy message factory — Vitest calls it only when reporting. */\n message: () => string;\n};\n\n/**\n * Any unified result envelope a matcher can be handed. Accepts the\n * full `{ report, ... }` result or a bare `BaseReport` so callers can\n * assert against either `await x.execute()` or `result.report`.\n */\ntype ReportLike = BaseReport | { report: BaseReport };\n\n/**\n * Coerce a matcher target into its `BaseReport`. Accepts the result\n * envelope (`{ report }`) or a report directly.\n */\nfunction toReport(received: ReportLike): BaseReport {\n if (\"report\" in received && received.report) {\n return received.report;\n }\n\n return received as BaseReport;\n}\n\n/** Narrow a `BaseReport` to a `SupervisorReport` by its discriminator. */\nfunction asSupervisorReport(report: BaseReport): SupervisorReport | undefined {\n return report.type === \"supervisor\" ? (report as SupervisorReport) : undefined;\n}\n\n/** Narrow a `BaseReport` to a `WorkflowReport` by its discriminator. */\nfunction asWorkflowReport(report: BaseReport): WorkflowReport | undefined {\n return report.type === \"workflow\" ? (report as WorkflowReport) : undefined;\n}\n\n/**\n * Collect every intent name dispatched by a supervisor across all\n * iterations — the keys of each iteration snapshot's `result` record.\n */\nfunction dispatchedIntents(report: SupervisorReport): string[] {\n const intents = new Set<string>();\n\n for (const snapshot of report.snapshots) {\n for (const intent of Object.keys(snapshot.result)) {\n intents.add(intent);\n }\n }\n\n return [...intents];\n}\n\n/**\n * Assert that a supervisor routed to (dispatched) the named intent at\n * least once across its iterations. Targets a `SupervisorResult` or a\n * `SupervisorReport`.\n *\n * @example\n * expect(await supervisor.execute(input)).toRouteTo(\"critic\");\n */\nexport function matchRouteTo(received: ReportLike, intent: string): MatcherVerdict {\n const report = toReport(received);\n const supervisorReport = asSupervisorReport(report);\n\n if (!supervisorReport) {\n return {\n pass: false,\n message: () =>\n `toRouteTo expects a supervisor result, but received a \"${report.type}\" report`,\n };\n }\n\n const intents = dispatchedIntents(supervisorReport);\n const pass = intents.includes(intent);\n\n return {\n pass,\n message: () =>\n pass\n ? `expected supervisor not to route to \"${intent}\", but it did`\n : `expected supervisor to route to \"${intent}\", but it routed to [${intents.join(\", \")}]`,\n };\n}\n\n/**\n * Assert that a supervisor converged — terminated on its own decision\n * (`router` / `route` / `evaluate` / `classifier`) with a\n * `\"completed\"` status, rather than hitting the iteration cap, being\n * cancelled, or erroring. Targets a `SupervisorResult` or\n * `SupervisorReport`.\n *\n * @example\n * expect(await supervisor.execute(input)).toConverge();\n */\nexport function matchConverge(received: ReportLike): MatcherVerdict {\n const report = toReport(received);\n const supervisorReport = asSupervisorReport(report);\n\n if (!supervisorReport) {\n return {\n pass: false,\n message: () =>\n `toConverge expects a supervisor result, but received a \"${report.type}\" report`,\n };\n }\n\n const nonConvergent = new Set([\"max-iterations\", \"cancelled\", \"error\"]);\n const pass =\n supervisorReport.status === \"completed\" &&\n !nonConvergent.has(supervisorReport.terminatedBy);\n\n return {\n pass,\n message: () =>\n pass\n ? `expected supervisor not to converge, but it terminated via \"${supervisorReport.terminatedBy}\"`\n : `expected supervisor to converge, but status=\"${supervisorReport.status}\" terminatedBy=\"${supervisorReport.terminatedBy}\" after ${supervisorReport.iterations} iteration(s)`,\n };\n}\n\n/**\n * Assert that a workflow step completed successfully. Targets a\n * `WorkflowResult` or `WorkflowReport`; looks the step up by name in\n * `report.steps` and checks its status is `\"completed\"`.\n *\n * @example\n * expect(await workflow.execute(input)).toPassStep(\"draft\");\n */\nexport function matchPassStep(received: ReportLike, stepName: string): MatcherVerdict {\n const report = toReport(received);\n const workflowReport = asWorkflowReport(report);\n\n if (!workflowReport) {\n return {\n pass: false,\n message: () =>\n `toPassStep expects a workflow result, but received a \"${report.type}\" report`,\n };\n }\n\n const step = workflowReport.steps[stepName];\n\n if (!step) {\n const known = Object.keys(workflowReport.steps).join(\", \");\n return {\n pass: false,\n message: () =>\n `expected workflow to have a step \"${stepName}\", but steps are [${known}]`,\n };\n }\n\n const pass = step.status === \"completed\";\n\n return {\n pass,\n message: () =>\n pass\n ? `expected step \"${stepName}\" not to pass, but it completed`\n : `expected step \"${stepName}\" to pass, but its status was \"${step.status}\"`,\n };\n}\n\n/**\n * Result envelope carrying a typed `data` payload — what\n * `matchOutputShape` validates against a schema.\n */\ntype DataResult = { data?: unknown };\n\n/**\n * Assert that a result's `data` validates against a Standard Schema.\n * Targets any result envelope with a `data` field (agent / workflow /\n * supervisor). Runs the schema's `~standard.validate` and passes only\n * when it reports no issues.\n *\n * Synchronous-only: a schema whose `validate` returns a Promise is\n * rejected with a clear message rather than silently passing — the\n * async variant belongs on a dedicated async matcher if needed.\n *\n * @example\n * expect(await agent.execute(input, { output: schema })).toOutputShape(schema);\n */\nexport function matchOutputShape(\n received: DataResult,\n schema: StandardSchemaV1,\n): MatcherVerdict {\n const data = received.data;\n\n if (data === undefined) {\n return {\n pass: false,\n message: () => \"toOutputShape expected result.data to be defined, but it was undefined\",\n };\n }\n\n const validation = schema[\"~standard\"].validate(data);\n\n if (validation instanceof Promise) {\n return {\n pass: false,\n message: () =>\n \"toOutputShape received an async schema; use a synchronous Standard Schema for this matcher\",\n };\n }\n\n const pass = validation.issues === undefined;\n\n return {\n pass,\n message: () => {\n if (pass) {\n return \"expected result.data not to match the schema, but it did\";\n }\n\n const summary = (validation.issues ?? []).map((issue) => issue.message).join(\"; \");\n return `expected result.data to match the schema, but validation failed: ${summary}`;\n },\n };\n}\n\n// Re-exported so matcher consumers and tests can reference the\n// termination sentinel without reaching into contracts.\nexport { END };\n"],"mappings":";;;;;;;AA8BA,SAAS,SAAS,UAAkC;CAClD,IAAI,YAAY,YAAY,SAAS,QACnC,OAAO,SAAS;CAGlB,OAAO;AACT;;AAGA,SAAS,mBAAmB,QAAkD;CAC5E,OAAO,OAAO,SAAS,eAAgB,SAA8B;AACvE;;AAGA,SAAS,iBAAiB,QAAgD;CACxE,OAAO,OAAO,SAAS,aAAc,SAA4B;AACnE;;;;;AAMA,SAAS,kBAAkB,QAAoC;CAC7D,MAAM,0BAAU,IAAI,IAAY;CAEhC,KAAK,MAAM,YAAY,OAAO,WAC5B,KAAK,MAAM,UAAU,OAAO,KAAK,SAAS,MAAM,GAC9C,QAAQ,IAAI,MAAM;CAItB,OAAO,CAAC,GAAG,OAAO;AACpB;;;;;;;;;AAUA,SAAgB,aAAa,UAAsB,QAAgC;CACjF,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,mBAAmB,mBAAmB,MAAM;CAElD,IAAI,CAAC,kBACH,OAAO;EACL,MAAM;EACN,eACE,0DAA0D,OAAO,KAAK;CAC1E;CAGF,MAAM,UAAU,kBAAkB,gBAAgB;CAClD,MAAM,OAAO,QAAQ,SAAS,MAAM;CAEpC,OAAO;EACL;EACA,eACE,OACI,wCAAwC,OAAO,iBAC/C,oCAAoC,OAAO,uBAAuB,QAAQ,KAAK,IAAI,EAAE;CAC7F;AACF;;;;;;;;;;;AAYA,SAAgB,cAAc,UAAsC;CAClE,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,mBAAmB,mBAAmB,MAAM;CAElD,IAAI,CAAC,kBACH,OAAO;EACL,MAAM;EACN,eACE,2DAA2D,OAAO,KAAK;CAC3E;CAGF,MAAM,gBAAgB,IAAI,IAAI;EAAC;EAAkB;EAAa;CAAO,CAAC;CACtE,MAAM,OACJ,iBAAiB,WAAW,eAC5B,CAAC,cAAc,IAAI,iBAAiB,YAAY;CAElD,OAAO;EACL;EACA,eACE,OACI,+DAA+D,iBAAiB,aAAa,KAC7F,gDAAgD,iBAAiB,OAAO,kBAAkB,iBAAiB,aAAa,UAAU,iBAAiB,WAAW;CACtK;AACF;;;;;;;;;AAUA,SAAgB,cAAc,UAAsB,UAAkC;CACpF,MAAM,SAAS,SAAS,QAAQ;CAChC,MAAM,iBAAiB,iBAAiB,MAAM;CAE9C,IAAI,CAAC,gBACH,OAAO;EACL,MAAM;EACN,eACE,yDAAyD,OAAO,KAAK;CACzE;CAGF,MAAM,OAAO,eAAe,MAAM;CAElC,IAAI,CAAC,MAAM;EACT,MAAM,QAAQ,OAAO,KAAK,eAAe,KAAK,CAAC,CAAC,KAAK,IAAI;EACzD,OAAO;GACL,MAAM;GACN,eACE,qCAAqC,SAAS,oBAAoB,MAAM;EAC5E;CACF;CAEA,MAAM,OAAO,KAAK,WAAW;CAE7B,OAAO;EACL;EACA,eACE,OACI,kBAAkB,SAAS,mCAC3B,kBAAkB,SAAS,iCAAiC,KAAK,OAAO;CAChF;AACF;;;;;;;;;;;;;;AAqBA,SAAgB,iBACd,UACA,QACgB;CAChB,MAAM,OAAO,SAAS;CAEtB,IAAI,SAAS,QACX,OAAO;EACL,MAAM;EACN,eAAe;CACjB;CAGF,MAAM,aAAa,OAAO,YAAY,CAAC,SAAS,IAAI;CAEpD,IAAI,sBAAsB,SACxB,OAAO;EACL,MAAM;EACN,eACE;CACJ;CAGF,MAAM,OAAO,WAAW,WAAW;CAEnC,OAAO;EACL;EACA,eAAe;GACb,IAAI,MACF,OAAO;GAIT,OAAO,qEADU,WAAW,UAAU,CAAC,EAAC,CAAE,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,IACI;EACnF;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-lazy.d.mts","names":[],"sources":["../../../../../../../ai/src/testing/register-lazy.ts"],"mappings":";;AA2BA;;;;AAAmD;;;;;;;;;;iBAA7B,kBAAA,
|
|
1
|
+
{"version":3,"file":"register-lazy.d.mts","names":[],"sources":["../../../../../../../ai/src/testing/register-lazy.ts"],"mappings":";;AA2BA;;;;AAAmD;;;;;;;;;;iBAA7B,kBAAA,IAAsB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executable-as-tool.d.mts","names":[],"sources":["../../../../../../../ai/src/tool/executable-as-tool.ts"],"mappings":";;;;;;;;;;AAK4D;;;;KASvD,kBAAA,YAA8B,UAAA;EACjC,IAAA,GAAO,OAAA;EACP,KAAA,GAAQ,OAAA;EACR,MAAA,EAAQ,UAAA;AAAA;;;;;;;;;;AAAU;AAsBpB;;;;;;;;;KAAY,cAAA;EAAA,SACD,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,GAAc,gBAAA,CAAiB,MAAA;EACxC,OAAA,CAAQ,KAAA,EAAO,MAAA,EAAQ,OAAA,aAAoB,OAAA,CAAQ,kBAAA,CAAmB,OAAA;EACtE,MAAA;AAAA;;;;;;;KASU,cAAA,wCACR,YAAA,CAAa,MAAA,EAAQ,OAAA,IACrB,cAAA,CAAe,MAAA,EAAQ,OAAA;;;;;AAXnB;AASR;;iBA4BgB,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc;;;;;;;;;;;;;;iBAuBzD,gBAAA,
|
|
1
|
+
{"version":3,"file":"executable-as-tool.d.mts","names":[],"sources":["../../../../../../../ai/src/tool/executable-as-tool.ts"],"mappings":";;;;;;;;;;AAK4D;;;;KASvD,kBAAA,YAA8B,UAAA;EACjC,IAAA,GAAO,OAAA;EACP,KAAA,GAAQ,OAAA;EACR,MAAA,EAAQ,UAAA;AAAA;;;;;;;;;;AAAU;AAsBpB;;;;;;;;;KAAY,cAAA;EAAA,SACD,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,GAAc,gBAAA,CAAiB,MAAA;EACxC,OAAA,CAAQ,KAAA,EAAO,MAAA,EAAQ,OAAA,aAAoB,OAAA,CAAQ,kBAAA,CAAmB,OAAA;EACtE,MAAA;AAAA;;;;;;;KASU,cAAA,wCACR,YAAA,CAAa,MAAA,EAAQ,OAAA,IACrB,cAAA,CAAe,MAAA,EAAQ,OAAA;;;;;AAXnB;AASR;;iBA4BgB,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc;;;;;;;;;;;;;;iBAuBzD,gBAAA,kBACd,UAAA,EAAY,cAAA,CAAe,MAAA,EAAQ,OAAA,IAClC,YAAA,CAAa,MAAA,EAAQ,OAAA;;;;AAnDU;AA0BlC;;;;;iBA0EgB,mBAAA,CACd,KAAA,EAAO,aAAA,CAAc,cAAA,gBACpB,YAAA"}
|
package/esm/tool/tool.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.mts","names":[],"sources":["../../../../../../../ai/src/tool/tool.ts"],"mappings":";;;;;;;;;;AAwCA;;;;;;;;;;;;;;;;;KAAY,gBAAA;EAQQ,iFANlB,IAAA,GAAO,OAAA,EAgBQ;EAdf,KAAA,GAAQ,OAAA,EAcmB;EAZ3B,KAAA,EAAO,KAAA,EAcP;EAZA,MAAA,EAAQ,UAAA;AAAA;;;;;;;;UAUO,YAAA,8CAA0D,UAAA,CACzE,MAAA,EACA,OAAA;EADA;;;;;;;;;;AAoB8E;AA6JhF;;;;;;EA7JE,MAAA,CAAO,QAAA,WAAmB,GAAA,GAAM,WAAA,GAAc,OAAA,CAAQ,gBAAA,CAAiB,OAAA;AAAA;AAAA,iBA6JzD,IAAA,
|
|
1
|
+
{"version":3,"file":"tool.d.mts","names":[],"sources":["../../../../../../../ai/src/tool/tool.ts"],"mappings":";;;;;;;;;;AAwCA;;;;;;;;;;;;;;;;;KAAY,gBAAA;EAQQ,iFANlB,IAAA,GAAO,OAAA,EAgBQ;EAdf,KAAA,GAAQ,OAAA,EAcmB;EAZ3B,KAAA,EAAO,KAAA,EAcP;EAZA,MAAA,EAAQ,UAAA;AAAA;;;;;;;;UAUO,YAAA,8CAA0D,UAAA,CACzE,MAAA,EACA,OAAA;EADA;;;;;;;;;;AAoB8E;AA6JhF;;;;;;EA7JE,MAAA,CAAO,QAAA,WAAmB,GAAA,GAAM,WAAA,GAAc,OAAA,CAAQ,gBAAA,CAAiB,OAAA;AAAA;AAAA,iBA6JzD,IAAA,kBACd,QAAA,EAAU,UAAA,CAAW,MAAA,EAAQ,OAAA,IAC5B,YAAA,CAAa,MAAA,EAAQ,OAAA"}
|
package/esm/tool/tool.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.mjs","names":[],"sources":["../../../../../../../ai/src/tool/tool.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type { ToolConfig, ToolContext } from \"../contracts/tool.contract\";\nimport { AIError, SchemaValidationError, ToolExecutionError } from \"../errors\";\nimport { generateRunId } from \"../utils/generate-run-id\";\n\n/**\n * Degraded `ToolContext` supplied when no caller threads one through\n * (`tool.invoke(input)` standalone, batch scripts, tests). Per\n * decisions §35 — mutations on the empty bag are harmless no-ops;\n * production paths under a supervisor receive a real ctx with the\n * iteration's shared bag.\n */\nfunction defaultToolContext(): ToolContext {\n return { artifacts: {} };\n}\n\nconst EMPTY_USAGE: Usage = Object.freeze({ input: 0, output: 0, total: 0 });\n\n/**\n * Result returned by `ToolContract.invoke()`.\n *\n * **Canonical destructure:** `const { data, usage, report, error }` —\n * matches every other executable (`AgentResult`, `WorkflowResult`,\n * `SupervisorResult`) so parent agents can treat every tool dispatch\n * uniformly.\n *\n * **Shape.** `data` / `error` carry the outcome; `usage` and `report`\n * are always present. For leaf tools, `usage` is zero and `report`\n * is a framework-synthesized {@link BaseReport} (`type: \"tool\"`,\n * `children: []`, real timing) so parents never have to nil-check.\n * For composites wrapped via `asTool()`, `usage` and `report` mirror\n * the inner primitive's — the nested tree lives in `report.children`.\n *\n * @example\n * const result = await myTool.invoke({ city: \"Cairo\" });\n * if (result.error) console.error(result.error.message);\n * else console.log(result.data, result.report.duration);\n */\nexport type ToolInvokeResult<TOutput> = {\n /** Successfully-returned output. Undefined if execution or validation failed. */\n data?: TOutput;\n /** Typed AI error produced by validation or execute(), if any. */\n error?: AIError;\n /** Rolled-up usage (zero for leaf tools, populated for composites). */\n usage: Usage;\n /** Recursive execution report — `report.children` carries nested executables. */\n report: BaseReport;\n};\n\n/**\n * A `ToolConfig` augmented with a safe `invoke()` entry point for the agent runtime.\n *\n * @example\n * const wrapped: ToolContract<{ city: string }, { temp: number }> = tool(contract);\n * const result = await wrapped.invoke({ city: \"Cairo\" });\n */\nexport interface ToolContract<TInput = unknown, TOutput = unknown> extends ToolConfig<\n TInput,\n TOutput\n> {\n /**\n * Agent-runtime entry point. Validates raw input against the tool's schema,\n * calls execute(), catches errors, and reports duration.\n * Never throws — errors surface in the returned `error` field as\n * typed `AIError` subclasses.\n *\n * The optional second argument is a `ToolContext` (Phase 5 /\n * decisions §35) — when supplied, threaded into `execute(input, ctx)`\n * so tools can write system-only side data into `ctx.artifacts`.\n * Standalone callers may omit it; the framework supplies a\n * degraded `{ artifacts: {} }` so single-arg legacy handlers keep\n * working unchanged.\n *\n * @example\n * const result = await myTool.invoke(rawLLMArgs);\n * if (result.error) handleError(result.error);\n */\n invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>>;\n}\n\n/**\n * Wraps a raw `ToolConfig` and adds a safe `invoke()` method for the agent runtime.\n * The returned object preserves all original contract fields unchanged.\n *\n * Error categorization:\n * - Input schema rejects model args → `SchemaValidationError` (issues preserved).\n * - Schema's `validate()` itself throws → `SchemaValidationError` wrapping the cause.\n * - `execute()` throws → `ToolExecutionError` wrapping the cause.\n *\n * @example\n * const weatherTool = tool({\n * name: \"getWeather\",\n * description: \"Fetch current weather for a city\",\n * input: z.object({ city: z.string() }),\n * execute: async ({ city }) => ({ temp: 72 }),\n * });\n *\n * const result = await weatherTool.invoke({ city: \"Cairo\" });\n */\n/**\n * Internal factory for `asTool()` wrappers on composite primitives\n * (agent / workflow / supervisor). Unlike the public `tool()` factory\n * (which synthesizes a leaf `BaseReport` every time), this variant\n * lets the composite's own `ExecuteResult` flow through: the inner\n * primitive's `report` becomes the sole child of the outer tool-call\n * node, and the inner `usage` is surfaced so parents can roll it up.\n *\n * The caller supplies `execute()` returning `{ data, usage, report }`\n * from the composite's own `execute()` method. Validation failures\n * and thrown errors still produce a synthesized failed leaf report —\n * the inner-report propagation is strictly a success-path concern.\n *\n * Not exported from the package barrel — used by `agent.asTool()`,\n * `workflow.asTool()`, `supervisor.asTool()` only.\n */\nexport function compositeAsTool<TInput, TOutput>(contract: {\n name: string;\n description?: string;\n version?: string;\n meta?: ToolConfig[\"meta\"];\n input: StandardSchemaV1<TInput>;\n /**\n * Runs the underlying composite and returns its full envelope. The\n * optional `ctx` relays the outer run's cancellation `signal` so a\n * cancelled parent aborts the nested primitive instead of letting it\n * outlive the cancellation (C2).\n */\n execute: (input: TInput, ctx?: ToolContext) => Promise<{\n data?: TOutput;\n error?: AIError;\n usage: Usage;\n report: BaseReport;\n }>;\n}): ToolContract<TInput, TOutput> {\n // The underlying `ToolConfig<TInput, TOutput>.execute` is typed as\n // `(input) => Promise<TOutput>`, but composite wrappers return an\n // envelope object instead. Surface a contract-shaped view that\n // extracts `.data` on demand for any code that still treats this\n // like a plain tool.\n const publicExecute = async (input: TInput): Promise<TOutput> => {\n const envelope = await contract.execute(input);\n if (envelope.error) throw envelope.error;\n return envelope.data as TOutput;\n };\n\n return {\n name: contract.name,\n description: contract.description ?? `Composite tool \"${contract.name}\".`,\n meta: contract.meta,\n input: contract.input,\n execute: publicExecute,\n\n async invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>> {\n // Composite tools (asTool-wrapped agent/workflow/supervisor) run in\n // their own state/scope — the ctx's `artifacts` bag is NOT shared\n // into the inner primitive (an inner supervisor gets a fresh bag).\n // The cancellation `signal`, however, IS relayed (below, into\n // `contract.execute`) so a cancelled outer run aborts the nested\n // primitive instead of letting it outlive the cancellation (C2).\n const startedAtDate = new Date();\n const start = performance.now();\n const runId = generateRunId(\"tool\");\n\n const failLeaf = (error: AIError): ToolInvokeResult<TOutput> => {\n const endedAt = new Date().toISOString();\n const duration = performance.now() - start;\n return {\n error,\n usage: EMPTY_USAGE,\n report: {\n runId,\n rootRunId: runId,\n name: contract.name,\n version: contract.version,\n type: \"tool\",\n status: \"failed\",\n startedAt: startedAtDate.toISOString(),\n endedAt,\n duration,\n usage: EMPTY_USAGE,\n children: [],\n },\n };\n };\n\n let validationResult: StandardSchemaV1.Result<TInput>;\n try {\n const schema = contract.input as StandardSchemaV1<TInput>;\n validationResult = await schema[\"~standard\"].validate(rawInput);\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n return failLeaf(\n new SchemaValidationError(\n `Schema validation threw for tool \"${contract.name}\": ${message}`,\n { cause: thrown, context: { toolName: contract.name } },\n ),\n );\n }\n\n if (validationResult.issues) {\n const summary = validationResult.issues.map((issue) => issue.message).join(\"; \");\n return failLeaf(\n new SchemaValidationError(`Validation failed: ${summary}`, {\n issues: validationResult.issues,\n context: { toolName: contract.name },\n }),\n );\n }\n\n try {\n const composite = await contract.execute(validationResult.value, ctx);\n // Surface the inner primitive's full envelope. The outer\n // ToolInvokeResult carries the composite's usage and report\n // verbatim; the agent runtime nests the report as a child of\n // the tool-dispatch node it records.\n return {\n data: composite.data,\n error: composite.error,\n usage: composite.usage,\n report: composite.report,\n };\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n return failLeaf(\n new ToolExecutionError(message, {\n cause: thrown,\n toolName: contract.name,\n }),\n );\n }\n },\n };\n}\n\nexport function tool<TInput, TOutput>(\n contract: ToolConfig<TInput, TOutput>,\n): ToolContract<TInput, TOutput> {\n return {\n ...contract,\n\n async invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>> {\n const startedAtDate = new Date();\n const start = performance.now();\n const runId = generateRunId(\"tool\");\n const handlerCtx = ctx ?? defaultToolContext();\n\n const finish = (partial: { data?: TOutput; error?: AIError }): ToolInvokeResult<TOutput> => {\n const endedAt = new Date().toISOString();\n const duration = performance.now() - start;\n const status: BaseReport[\"status\"] = partial.error ? \"failed\" : \"completed\";\n const report: BaseReport = {\n runId,\n rootRunId: runId,\n name: contract.name,\n version: contract.version,\n type: \"tool\",\n status,\n startedAt: startedAtDate.toISOString(),\n endedAt,\n duration,\n usage: EMPTY_USAGE,\n children: [],\n };\n\n return {\n ...partial,\n usage: EMPTY_USAGE,\n report,\n };\n };\n\n let validationResult: StandardSchemaV1.Result<TInput>;\n if (contract.input) {\n try {\n validationResult = await contract.input[\"~standard\"].validate(rawInput);\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return finish({\n error: new SchemaValidationError(\n `Schema validation threw for tool \"${contract.name}\": ${message}`,\n { cause: thrown, context: { toolName: contract.name } },\n ),\n });\n }\n } else {\n // `input` is optional on ToolConfig — this is a no-argument tool\n // (e.g. view_cart, checkout). With no schema there is nothing to\n // validate, so pass the raw model args straight to execute()\n // instead of dereferencing a missing schema's `~standard`.\n validationResult = { value: rawInput as TInput };\n }\n\n if (validationResult.issues) {\n const summary = validationResult.issues.map((issue) => issue.message).join(\"; \");\n\n return finish({\n error: new SchemaValidationError(`Validation failed: ${summary}`, {\n issues: validationResult.issues,\n context: { toolName: contract.name },\n }),\n });\n }\n\n try {\n const output = await contract.execute(validationResult.value, handlerCtx);\n return finish({ data: output });\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return finish({\n error: new ToolExecutionError(message, {\n cause: thrown,\n toolName: contract.name,\n }),\n });\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAS,qBAAkC;CACzC,OAAO,EAAE,WAAW,CAAC,EAAE;AACzB;AAEA,MAAM,cAAqB,OAAO,OAAO;CAAE,OAAO;CAAG,QAAQ;CAAG,OAAO;AAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmG1E,SAAgB,gBAAiC,UAkBf;CAMhC,MAAM,gBAAgB,OAAO,UAAoC;EAC/D,MAAM,WAAW,MAAM,SAAS,QAAQ,KAAK;EAC7C,IAAI,SAAS,OAAO,MAAM,SAAS;EACnC,OAAO,SAAS;CAClB;CAEA,OAAO;EACL,MAAM,SAAS;EACf,aAAa,SAAS,eAAe,mBAAmB,SAAS,KAAK;EACtE,MAAM,SAAS;EACf,OAAO,SAAS;EAChB,SAAS;EAET,MAAM,OAAO,UAAmB,KAAuD;GAOrF,MAAM,gCAAgB,IAAI,KAAK;GAC/B,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,QAAQ,cAAc,MAAM;GAElC,MAAM,YAAY,UAA8C;IAC9D,MAAM,2BAAU,IAAI,KAAK,GAAE,YAAY;IACvC,MAAM,WAAW,YAAY,IAAI,IAAI;IACrC,OAAO;KACL;KACA,OAAO;KACP,QAAQ;MACN;MACA,WAAW;MACX,MAAM,SAAS;MACf,SAAS,SAAS;MAClB,MAAM;MACN,QAAQ;MACR,WAAW,cAAc,YAAY;MACrC;MACA;MACA,OAAO;MACP,UAAU,CAAC;KACb;IACF;GACF;GAEA,IAAI;GACJ,IAAI;IAEF,mBAAmB,MADJ,SAAS,MACQ,aAAa,SAAS,QAAQ;GAChE,SAAS,QAAQ;IACf,MAAM,UAAU,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;IACxE,OAAO,SACL,IAAI,sBACF,qCAAqC,SAAS,KAAK,KAAK,WACxD;KAAE,OAAO;KAAQ,SAAS,EAAE,UAAU,SAAS,KAAK;IAAE,CACxD,CACF;GACF;GAEA,IAAI,iBAAiB,QAEnB,OAAO,SACL,IAAI,sBAAsB,sBAFZ,iBAAiB,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAEnB,KAAK;IACzD,QAAQ,iBAAiB;IACzB,SAAS,EAAE,UAAU,SAAS,KAAK;GACrC,CAAC,CACH;GAGF,IAAI;IACF,MAAM,YAAY,MAAM,SAAS,QAAQ,iBAAiB,OAAO,GAAG;IAKpE,OAAO;KACL,MAAM,UAAU;KAChB,OAAO,UAAU;KACjB,OAAO,UAAU;KACjB,QAAQ,UAAU;IACpB;GACF,SAAS,QAAQ;IAEf,OAAO,SACL,IAAI,mBAFU,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,GAEtC;KAC9B,OAAO;KACP,UAAU,SAAS;IACrB,CAAC,CACH;GACF;EACF;CACF;AACF;AAEA,SAAgB,KACd,UAC+B;CAC/B,OAAO;EACL,GAAG;EAEH,MAAM,OAAO,UAAmB,KAAuD;GACrF,MAAM,gCAAgB,IAAI,KAAK;GAC/B,MAAM,QAAQ,YAAY,IAAI;GAC9B,MAAM,QAAQ,cAAc,MAAM;GAClC,MAAM,aAAa,OAAO,mBAAmB;GAE7C,MAAM,UAAU,YAA4E;IAC1F,MAAM,2BAAU,IAAI,KAAK,GAAE,YAAY;IACvC,MAAM,WAAW,YAAY,IAAI,IAAI;IACrC,MAAM,SAA+B,QAAQ,QAAQ,WAAW;IAChE,MAAM,SAAqB;KACzB;KACA,WAAW;KACX,MAAM,SAAS;KACf,SAAS,SAAS;KAClB,MAAM;KACN;KACA,WAAW,cAAc,YAAY;KACrC;KACA;KACA,OAAO;KACP,UAAU,CAAC;IACb;IAEA,OAAO;KACL,GAAG;KACH,OAAO;KACP;IACF;GACF;GAEA,IAAI;GACJ,IAAI,SAAS,OACX,IAAI;IACF,mBAAmB,MAAM,SAAS,MAAM,aAAa,SAAS,QAAQ;GACxE,SAAS,QAAQ;IACf,MAAM,UAAU,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;IAExE,OAAO,OAAO,EACZ,OAAO,IAAI,sBACT,qCAAqC,SAAS,KAAK,KAAK,WACxD;KAAE,OAAO;KAAQ,SAAS,EAAE,UAAU,SAAS,KAAK;IAAE,CACxD,EACF,CAAC;GACH;QAMA,mBAAmB,EAAE,OAAO,SAAmB;GAGjD,IAAI,iBAAiB,QAGnB,OAAO,OAAO,EACZ,OAAO,IAAI,sBAAsB,sBAHnB,iBAAiB,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,KAAK,IAGZ,KAAK;IAChE,QAAQ,iBAAiB;IACzB,SAAS,EAAE,UAAU,SAAS,KAAK;GACrC,CAAC,EACH,CAAC;GAGH,IAAI;IAEF,OAAO,OAAO,EAAE,MAAM,MADD,SAAS,QAAQ,iBAAiB,OAAO,UAAU,EAC3C,CAAC;GAChC,SAAS,QAAQ;IAGf,OAAO,OAAO,EACZ,OAAO,IAAI,mBAHG,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,GAG/B;KACrC,OAAO;KACP,UAAU,SAAS;IACrB,CAAC,EACH,CAAC;GACH;EACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"tool.mjs","names":[],"sources":["../../../../../../../ai/src/tool/tool.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type { ToolConfig, ToolContext } from \"../contracts/tool.contract\";\nimport { AIError, SchemaValidationError, ToolExecutionError } from \"../errors\";\nimport { generateRunId } from \"../utils/generate-run-id\";\n\n/**\n * Degraded `ToolContext` supplied when no caller threads one through\n * (`tool.invoke(input)` standalone, batch scripts, tests). Per\n * decisions §35 — mutations on the empty bag are harmless no-ops;\n * production paths under a supervisor receive a real ctx with the\n * iteration's shared bag.\n */\nfunction defaultToolContext(): ToolContext {\n return { artifacts: {} };\n}\n\nconst EMPTY_USAGE: Usage = Object.freeze({ input: 0, output: 0, total: 0 });\n\n/**\n * Result returned by `ToolContract.invoke()`.\n *\n * **Canonical destructure:** `const { data, usage, report, error }` —\n * matches every other executable (`AgentResult`, `WorkflowResult`,\n * `SupervisorResult`) so parent agents can treat every tool dispatch\n * uniformly.\n *\n * **Shape.** `data` / `error` carry the outcome; `usage` and `report`\n * are always present. For leaf tools, `usage` is zero and `report`\n * is a framework-synthesized {@link BaseReport} (`type: \"tool\"`,\n * `children: []`, real timing) so parents never have to nil-check.\n * For composites wrapped via `asTool()`, `usage` and `report` mirror\n * the inner primitive's — the nested tree lives in `report.children`.\n *\n * @example\n * const result = await myTool.invoke({ city: \"Cairo\" });\n * if (result.error) console.error(result.error.message);\n * else console.log(result.data, result.report.duration);\n */\nexport type ToolInvokeResult<TOutput> = {\n /** Successfully-returned output. Undefined if execution or validation failed. */\n data?: TOutput;\n /** Typed AI error produced by validation or execute(), if any. */\n error?: AIError;\n /** Rolled-up usage (zero for leaf tools, populated for composites). */\n usage: Usage;\n /** Recursive execution report — `report.children` carries nested executables. */\n report: BaseReport;\n};\n\n/**\n * A `ToolConfig` augmented with a safe `invoke()` entry point for the agent runtime.\n *\n * @example\n * const wrapped: ToolContract<{ city: string }, { temp: number }> = tool(contract);\n * const result = await wrapped.invoke({ city: \"Cairo\" });\n */\nexport interface ToolContract<TInput = unknown, TOutput = unknown> extends ToolConfig<\n TInput,\n TOutput\n> {\n /**\n * Agent-runtime entry point. Validates raw input against the tool's schema,\n * calls execute(), catches errors, and reports duration.\n * Never throws — errors surface in the returned `error` field as\n * typed `AIError` subclasses.\n *\n * The optional second argument is a `ToolContext` (Phase 5 /\n * decisions §35) — when supplied, threaded into `execute(input, ctx)`\n * so tools can write system-only side data into `ctx.artifacts`.\n * Standalone callers may omit it; the framework supplies a\n * degraded `{ artifacts: {} }` so single-arg legacy handlers keep\n * working unchanged.\n *\n * @example\n * const result = await myTool.invoke(rawLLMArgs);\n * if (result.error) handleError(result.error);\n */\n invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>>;\n}\n\n/**\n * Wraps a raw `ToolConfig` and adds a safe `invoke()` method for the agent runtime.\n * The returned object preserves all original contract fields unchanged.\n *\n * Error categorization:\n * - Input schema rejects model args → `SchemaValidationError` (issues preserved).\n * - Schema's `validate()` itself throws → `SchemaValidationError` wrapping the cause.\n * - `execute()` throws → `ToolExecutionError` wrapping the cause.\n *\n * @example\n * const weatherTool = tool({\n * name: \"getWeather\",\n * description: \"Fetch current weather for a city\",\n * input: z.object({ city: z.string() }),\n * execute: async ({ city }) => ({ temp: 72 }),\n * });\n *\n * const result = await weatherTool.invoke({ city: \"Cairo\" });\n */\n/**\n * Internal factory for `asTool()` wrappers on composite primitives\n * (agent / workflow / supervisor). Unlike the public `tool()` factory\n * (which synthesizes a leaf `BaseReport` every time), this variant\n * lets the composite's own `ExecuteResult` flow through: the inner\n * primitive's `report` becomes the sole child of the outer tool-call\n * node, and the inner `usage` is surfaced so parents can roll it up.\n *\n * The caller supplies `execute()` returning `{ data, usage, report }`\n * from the composite's own `execute()` method. Validation failures\n * and thrown errors still produce a synthesized failed leaf report —\n * the inner-report propagation is strictly a success-path concern.\n *\n * Not exported from the package barrel — used by `agent.asTool()`,\n * `workflow.asTool()`, `supervisor.asTool()` only.\n */\nexport function compositeAsTool<TInput, TOutput>(contract: {\n name: string;\n description?: string;\n version?: string;\n meta?: ToolConfig[\"meta\"];\n input: StandardSchemaV1<TInput>;\n /**\n * Runs the underlying composite and returns its full envelope. The\n * optional `ctx` relays the outer run's cancellation `signal` so a\n * cancelled parent aborts the nested primitive instead of letting it\n * outlive the cancellation (C2).\n */\n execute: (input: TInput, ctx?: ToolContext) => Promise<{\n data?: TOutput;\n error?: AIError;\n usage: Usage;\n report: BaseReport;\n }>;\n}): ToolContract<TInput, TOutput> {\n // The underlying `ToolConfig<TInput, TOutput>.execute` is typed as\n // `(input) => Promise<TOutput>`, but composite wrappers return an\n // envelope object instead. Surface a contract-shaped view that\n // extracts `.data` on demand for any code that still treats this\n // like a plain tool.\n const publicExecute = async (input: TInput): Promise<TOutput> => {\n const envelope = await contract.execute(input);\n if (envelope.error) throw envelope.error;\n return envelope.data as TOutput;\n };\n\n return {\n name: contract.name,\n description: contract.description ?? `Composite tool \"${contract.name}\".`,\n meta: contract.meta,\n input: contract.input,\n execute: publicExecute,\n\n async invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>> {\n // Composite tools (asTool-wrapped agent/workflow/supervisor) run in\n // their own state/scope — the ctx's `artifacts` bag is NOT shared\n // into the inner primitive (an inner supervisor gets a fresh bag).\n // The cancellation `signal`, however, IS relayed (below, into\n // `contract.execute`) so a cancelled outer run aborts the nested\n // primitive instead of letting it outlive the cancellation (C2).\n const startedAtDate = new Date();\n const start = performance.now();\n const runId = generateRunId(\"tool\");\n\n const failLeaf = (error: AIError): ToolInvokeResult<TOutput> => {\n const endedAt = new Date().toISOString();\n const duration = performance.now() - start;\n return {\n error,\n usage: EMPTY_USAGE,\n report: {\n runId,\n rootRunId: runId,\n name: contract.name,\n version: contract.version,\n type: \"tool\",\n status: \"failed\",\n startedAt: startedAtDate.toISOString(),\n endedAt,\n duration,\n usage: EMPTY_USAGE,\n children: [],\n },\n };\n };\n\n let validationResult: StandardSchemaV1.Result<TInput>;\n try {\n const schema = contract.input as StandardSchemaV1<TInput>;\n validationResult = await schema[\"~standard\"].validate(rawInput);\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n return failLeaf(\n new SchemaValidationError(\n `Schema validation threw for tool \"${contract.name}\": ${message}`,\n { cause: thrown, context: { toolName: contract.name } },\n ),\n );\n }\n\n if (validationResult.issues) {\n const summary = validationResult.issues.map((issue) => issue.message).join(\"; \");\n return failLeaf(\n new SchemaValidationError(`Validation failed: ${summary}`, {\n issues: validationResult.issues,\n context: { toolName: contract.name },\n }),\n );\n }\n\n try {\n const composite = await contract.execute(validationResult.value, ctx);\n // Surface the inner primitive's full envelope. The outer\n // ToolInvokeResult carries the composite's usage and report\n // verbatim; the agent runtime nests the report as a child of\n // the tool-dispatch node it records.\n return {\n data: composite.data,\n error: composite.error,\n usage: composite.usage,\n report: composite.report,\n };\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n return failLeaf(\n new ToolExecutionError(message, {\n cause: thrown,\n toolName: contract.name,\n }),\n );\n }\n },\n };\n}\n\nexport function tool<TInput, TOutput>(\n contract: ToolConfig<TInput, TOutput>,\n): ToolContract<TInput, TOutput> {\n return {\n ...contract,\n\n async invoke(rawInput: unknown, ctx?: ToolContext): Promise<ToolInvokeResult<TOutput>> {\n const startedAtDate = new Date();\n const start = performance.now();\n const runId = generateRunId(\"tool\");\n const handlerCtx = ctx ?? defaultToolContext();\n\n const finish = (partial: { data?: TOutput; error?: AIError }): ToolInvokeResult<TOutput> => {\n const endedAt = new Date().toISOString();\n const duration = performance.now() - start;\n const status: BaseReport[\"status\"] = partial.error ? \"failed\" : \"completed\";\n const report: BaseReport = {\n runId,\n rootRunId: runId,\n name: contract.name,\n version: contract.version,\n type: \"tool\",\n status,\n startedAt: startedAtDate.toISOString(),\n endedAt,\n duration,\n usage: EMPTY_USAGE,\n children: [],\n };\n\n return {\n ...partial,\n usage: EMPTY_USAGE,\n report,\n };\n };\n\n let validationResult: StandardSchemaV1.Result<TInput>;\n if (contract.input) {\n try {\n validationResult = await contract.input[\"~standard\"].validate(rawInput);\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return finish({\n error: new SchemaValidationError(\n `Schema validation threw for tool \"${contract.name}\": ${message}`,\n { cause: thrown, context: { toolName: contract.name } },\n ),\n });\n }\n } else {\n // `input` is optional on ToolConfig — this is a no-argument tool\n // (e.g. view_cart, checkout). With no schema there is nothing to\n // validate, so pass the raw model args straight to execute()\n // instead of dereferencing a missing schema's `~standard`.\n validationResult = { value: rawInput as TInput };\n }\n\n if (validationResult.issues) {\n const summary = validationResult.issues.map((issue) => issue.message).join(\"; \");\n\n return finish({\n error: new SchemaValidationError(`Validation failed: ${summary}`, {\n issues: validationResult.issues,\n context: { toolName: contract.name },\n }),\n });\n }\n\n try {\n const output = await contract.execute(validationResult.value, handlerCtx);\n return finish({ data: output });\n } catch (thrown) {\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return finish({\n error: new ToolExecutionError(message, {\n cause: thrown,\n toolName: contract.name,\n }),\n });\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-input.mjs","names":[],"sources":["../../../../../../../ai/src/transcribe/audio-input.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { basename, extname } from \"node:path\";\nimport type { AudioInput } from \"../contracts/transcription-model.contract\";\n\n/**\n * File-extension → IANA audio media type map covering the formats the\n * common STT providers accept — including the **WhatsApp voice-note**\n * formats (`.ogg` / `.opus`, Opus-in-Ogg on Android; `.m4a` on iOS).\n */\nconst AUDIO_MEDIA_TYPES: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".mpeg\": \"audio/mpeg\",\n \".mpga\": \"audio/mpeg\",\n \".m4a\": \"audio/mp4\",\n \".mp4\": \"audio/mp4\",\n \".wav\": \"audio/wav\",\n \".webm\": \"audio/webm\",\n \".weba\": \"audio/webm\",\n \".ogg\": \"audio/ogg\",\n \".oga\": \"audio/ogg\",\n \".opus\": \"audio/ogg\",\n \".flac\": \"audio/flac\",\n \".aac\": \"audio/aac\",\n};\n\n/**\n * Resolve the audio media type from a filename's extension, or\n * `undefined` when the extension is unknown. Case-insensitive.\n *\n * @example\n * audioMediaTypeForFilename(\"voice-note.opus\"); // \"audio/ogg\"\n */\nexport function audioMediaTypeForFilename(filename: string): string | undefined {\n return AUDIO_MEDIA_TYPES[extname(filename).toLowerCase()];\n}\n\n/**\n * Package raw audio bytes as an {@link AudioInput} for `ai.transcribe()`.\n * Pure plumbing — no AI, no I/O. Use when you already hold the bytes\n * (an upload buffer, a downloaded blob).\n *\n * @example\n * const audio = audioFromBuffer(uploadBuffer, \"audio/ogg\", \"note.ogg\");\n * const { data } = await ai.transcribe({ model: openai.transcribe({ name: \"whisper-1\" }), audio });\n */\nexport function audioFromBuffer(\n data: Uint8Array,\n mediaType: string,\n filename?: string,\n): AudioInput {\n return {\n base64: Buffer.from(data).toString(\"base64\"),\n mediaType,\n ...(filename ? { filename } : {}),\n };\n}\n\n/**\n * Read an audio file from disk and package it as an {@link AudioInput}\n * for `ai.transcribe()` — the one-line bridge from a file on disk\n * (WhatsApp `.ogg`/`.opus`, a meeting `.m4a`, a `.wav`) to the\n * transcription verb. **Pure utility — no AI here**; the actual text\n * extraction is the AI step (`ai.transcribe`).\n *\n * The media type is inferred from the file extension (override via\n * `options.mediaType` for extensionless or mislabeled files).\n *\n * @example\n * // WhatsApp voice note → text, end to end:\n * const audio = await audioFromFile(\"./voice-note.ogg\");\n * const { data, error } = await ai.transcribe({\n * model: openai.transcribe({ name: \"whisper-1\" }),\n * audio,\n * language: \"en\",\n * });\n * if (!error) console.log(data.text);\n */\nexport async function audioFromFile(\n filePath: string,\n options?: { mediaType?: string },\n): Promise<AudioInput> {\n const buffer = await readFile(filePath);\n const filename = basename(filePath);\n const mediaType = options?.mediaType ?? audioMediaTypeForFilename(filename) ?? \"audio/mpeg\";\n\n return { base64: buffer.toString(\"base64\"), mediaType, filename };\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,oBAA4C;CAChD,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;AACV;;;;;;;;AASA,SAAgB,0BAA0B,UAAsC;CAC9E,OAAO,kBAAkB,QAAQ,QAAQ,
|
|
1
|
+
{"version":3,"file":"audio-input.mjs","names":[],"sources":["../../../../../../../ai/src/transcribe/audio-input.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { basename, extname } from \"node:path\";\nimport type { AudioInput } from \"../contracts/transcription-model.contract\";\n\n/**\n * File-extension → IANA audio media type map covering the formats the\n * common STT providers accept — including the **WhatsApp voice-note**\n * formats (`.ogg` / `.opus`, Opus-in-Ogg on Android; `.m4a` on iOS).\n */\nconst AUDIO_MEDIA_TYPES: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".mpeg\": \"audio/mpeg\",\n \".mpga\": \"audio/mpeg\",\n \".m4a\": \"audio/mp4\",\n \".mp4\": \"audio/mp4\",\n \".wav\": \"audio/wav\",\n \".webm\": \"audio/webm\",\n \".weba\": \"audio/webm\",\n \".ogg\": \"audio/ogg\",\n \".oga\": \"audio/ogg\",\n \".opus\": \"audio/ogg\",\n \".flac\": \"audio/flac\",\n \".aac\": \"audio/aac\",\n};\n\n/**\n * Resolve the audio media type from a filename's extension, or\n * `undefined` when the extension is unknown. Case-insensitive.\n *\n * @example\n * audioMediaTypeForFilename(\"voice-note.opus\"); // \"audio/ogg\"\n */\nexport function audioMediaTypeForFilename(filename: string): string | undefined {\n return AUDIO_MEDIA_TYPES[extname(filename).toLowerCase()];\n}\n\n/**\n * Package raw audio bytes as an {@link AudioInput} for `ai.transcribe()`.\n * Pure plumbing — no AI, no I/O. Use when you already hold the bytes\n * (an upload buffer, a downloaded blob).\n *\n * @example\n * const audio = audioFromBuffer(uploadBuffer, \"audio/ogg\", \"note.ogg\");\n * const { data } = await ai.transcribe({ model: openai.transcribe({ name: \"whisper-1\" }), audio });\n */\nexport function audioFromBuffer(\n data: Uint8Array,\n mediaType: string,\n filename?: string,\n): AudioInput {\n return {\n base64: Buffer.from(data).toString(\"base64\"),\n mediaType,\n ...(filename ? { filename } : {}),\n };\n}\n\n/**\n * Read an audio file from disk and package it as an {@link AudioInput}\n * for `ai.transcribe()` — the one-line bridge from a file on disk\n * (WhatsApp `.ogg`/`.opus`, a meeting `.m4a`, a `.wav`) to the\n * transcription verb. **Pure utility — no AI here**; the actual text\n * extraction is the AI step (`ai.transcribe`).\n *\n * The media type is inferred from the file extension (override via\n * `options.mediaType` for extensionless or mislabeled files).\n *\n * @example\n * // WhatsApp voice note → text, end to end:\n * const audio = await audioFromFile(\"./voice-note.ogg\");\n * const { data, error } = await ai.transcribe({\n * model: openai.transcribe({ name: \"whisper-1\" }),\n * audio,\n * language: \"en\",\n * });\n * if (!error) console.log(data.text);\n */\nexport async function audioFromFile(\n filePath: string,\n options?: { mediaType?: string },\n): Promise<AudioInput> {\n const buffer = await readFile(filePath);\n const filename = basename(filePath);\n const mediaType = options?.mediaType ?? audioMediaTypeForFilename(filename) ?? \"audio/mpeg\";\n\n return { base64: buffer.toString(\"base64\"), mediaType, filename };\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,oBAA4C;CAChD,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;AACV;;;;;;;;AASA,SAAgB,0BAA0B,UAAsC;CAC9E,OAAO,kBAAkB,QAAQ,QAAQ,CAAC,CAAC,YAAY;AACzD;;;;;;;;;;AAWA,SAAgB,gBACd,MACA,WACA,UACY;CACZ,OAAO;EACL,QAAQ,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,QAAQ;EAC3C;EACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CACjC;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,cACpB,UACA,SACqB;CACrB,MAAM,SAAS,MAAM,SAAS,QAAQ;CACtC,MAAM,WAAW,SAAS,QAAQ;CAClC,MAAM,YAAY,SAAS,aAAa,0BAA0B,QAAQ,KAAK;CAE/E,OAAO;EAAE,QAAQ,OAAO,SAAS,QAAQ;EAAG;EAAW;CAAS;AAClE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcribe.mjs","names":[],"sources":["../../../../../../../ai/src/transcribe/transcribe.ts"],"sourcesContent":["import type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type {\n AudioInput,\n TranscriptionModelContract,\n TranscriptionModelPricing,\n TranscriptionSegment,\n} from \"../contracts/transcription-model.contract\";\nimport { AIError } from \"../errors/ai-error\";\nimport { ProviderError } from \"../errors/provider-error\";\nimport type { FlowObserveOption } from \"../observe/resolve-observers\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport { computeCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\n\n/** Parameters for {@link transcribe}. `model` comes from `sdk.transcribe({ name })`. */\nexport type TranscribeParams = {\n /** The STT model to transcribe with. */\n model: TranscriptionModelContract;\n /** The audio to transcribe (inlined base64 bytes + media type). */\n audio: AudioInput;\n /** BCP-47 language hint. */\n language?: string;\n /** Optional priming prompt (spelling/style hints). */\n prompt?: string;\n /** Provider response-format override (e.g. `\"verbose_json\"`). */\n format?: string;\n /** Cancellation handle. */\n signal?: AbortSignal;\n /** Observability routing — same `observe` seam as agents. */\n observe?: FlowObserveOption;\n /** Groups this call into a session for flat cost/trace queries. */\n sessionId?: string;\n /** Report node name (defaults to `\"transcription\"`). */\n name?: string;\n /** Provider-specific options forwarded verbatim to the adapter. */\n options?: Record<string, unknown>;\n};\n\n/** Success payload of a {@link transcribe} run. */\nexport type TranscriptionData = {\n /** The full transcript text. */\n text: string;\n /** Timestamped segments when the provider returned them. */\n segments?: TranscriptionSegment[];\n};\n\n/** The report node a {@link transcribe} run produces (`type: \"transcription\"`). */\nexport type TranscriptionReport = BaseReport & {\n type: \"transcription\";\n /** Identity of the STT model this run used. */\n model: { name: string; provider: string };\n /** Input audio duration in seconds, when the provider reported it. */\n durationSeconds?: number;\n};\n\n/** Result envelope of {@link transcribe} — the uniform `{ data, error, usage, report }`. */\nexport type TranscriptionResult = ExecuteResult<TranscriptionData> & {\n type: \"transcription\";\n report: TranscriptionReport;\n};\n\n/**\n * Transcribe audio to text — the speech-to-text verb of the\n * output-modality track (Theme I), inverse of `ai.speech()`. Wraps a\n * {@link TranscriptionModelContract} (from `openai.transcribe(...)`) in\n * the uniform result contract:\n *\n * - **Never throws.** Provider failures surface as a typed `AIError` on\n * `result.error`.\n * - **Cost-truth.** `result.usage.cost` is filled per-minute\n * (`whisper-1`) or per-token (`gpt-4o-transcribe`).\n * - **Observable.** The completed {@link TranscriptionReport} routes to\n * any registered `Observer` via the `observe` seam.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey });\n * const { data, error } = await ai.transcribe({\n * model: openai.transcribe({ name: \"whisper-1\" }),\n * audio: { base64, mediaType: \"audio/mpeg\", filename: \"voicemail.mp3\" },\n * language: \"en\",\n * });\n * if (!error) console.log(data.text);\n */\nexport async function transcribe(params: TranscribeParams): Promise<TranscriptionResult> {\n const { model, audio } = params;\n\n const runId = generateRunId(\"transcription\");\n const startedAt = new Date().toISOString();\n const startPerf = performance.now();\n\n const usage: Usage = { input: 0, output: 0, total: 0 };\n let data: TranscriptionData | undefined;\n let error: AIError | undefined;\n let status: TranscriptionReport[\"status\"] = \"completed\";\n let durationSeconds: number | undefined;\n\n try {\n const response = await model.transcribe(audio, {\n language: params.language,\n prompt: params.prompt,\n format: params.format,\n signal: params.signal,\n ...params.options,\n });\n\n Object.assign(usage, response.usage);\n durationSeconds = response.durationSeconds;\n\n if (usage.cost === undefined) {\n const cost = computeTranscriptionCost(usage, durationSeconds, model.pricing);\n if (cost !== undefined) {\n usage.cost = cost;\n }\n }\n\n data = { text: response.text, ...(response.segments ? { segments: response.segments } : {}) };\n } catch (thrown) {\n error =\n thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });\n status = params.signal?.aborted ? \"cancelled\" : \"failed\";\n }\n\n const report: TranscriptionReport = {\n runId,\n rootRunId: runId,\n name: params.name ?? \"transcription\",\n type: \"transcription\",\n status,\n error,\n startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - startPerf,\n usage,\n children: [],\n model: { name: model.name, provider: model.provider },\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n\n stampReportLineage(report, { rootRunId: runId, sessionId: params.sessionId });\n\n await notifyObservers(params.observe, report);\n\n return { type: \"transcription\", data, error, usage, report };\n}\n\n/**\n * Price an STT run: `perMinute × (durationSeconds / 60)` (per-minute\n * metering, attributed to `cost.input`) wins when configured, otherwise\n * the standard token math. Returns `undefined` when no usable pricing\n * is present (e.g. per-minute pricing but the provider didn't report a\n * duration).\n */\nfunction computeTranscriptionCost(\n usage: Usage,\n durationSeconds: number | undefined,\n pricing: TranscriptionModelPricing | undefined,\n): ModelPricing | undefined {\n if (!pricing) {\n return undefined;\n }\n\n if (pricing.perMinute !== undefined) {\n if (durationSeconds === undefined) {\n return undefined;\n }\n return { input: (durationSeconds / 60) * pricing.perMinute, output: 0 };\n }\n\n if (pricing.input !== undefined && pricing.output !== undefined) {\n return computeCost(usage, { input: pricing.input, output: pricing.output });\n }\n\n return undefined;\n}\n\n/** Best-effort message for a non-`AIError` thrown value. */\nfunction toMessage(thrown: unknown): string {\n return thrown instanceof Error ? thrown.message : String(thrown);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,eAAsB,WAAW,QAAwD;CACvF,MAAM,EAAE,OAAO,UAAU;CAEzB,MAAM,QAAQ,cAAc,eAAe;CAC3C,MAAM,6BAAY,IAAI,KAAK,
|
|
1
|
+
{"version":3,"file":"transcribe.mjs","names":[],"sources":["../../../../../../../ai/src/transcribe/transcribe.ts"],"sourcesContent":["import type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type {\n AudioInput,\n TranscriptionModelContract,\n TranscriptionModelPricing,\n TranscriptionSegment,\n} from \"../contracts/transcription-model.contract\";\nimport { AIError } from \"../errors/ai-error\";\nimport { ProviderError } from \"../errors/provider-error\";\nimport type { FlowObserveOption } from \"../observe/resolve-observers\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport { computeCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\n\n/** Parameters for {@link transcribe}. `model` comes from `sdk.transcribe({ name })`. */\nexport type TranscribeParams = {\n /** The STT model to transcribe with. */\n model: TranscriptionModelContract;\n /** The audio to transcribe (inlined base64 bytes + media type). */\n audio: AudioInput;\n /** BCP-47 language hint. */\n language?: string;\n /** Optional priming prompt (spelling/style hints). */\n prompt?: string;\n /** Provider response-format override (e.g. `\"verbose_json\"`). */\n format?: string;\n /** Cancellation handle. */\n signal?: AbortSignal;\n /** Observability routing — same `observe` seam as agents. */\n observe?: FlowObserveOption;\n /** Groups this call into a session for flat cost/trace queries. */\n sessionId?: string;\n /** Report node name (defaults to `\"transcription\"`). */\n name?: string;\n /** Provider-specific options forwarded verbatim to the adapter. */\n options?: Record<string, unknown>;\n};\n\n/** Success payload of a {@link transcribe} run. */\nexport type TranscriptionData = {\n /** The full transcript text. */\n text: string;\n /** Timestamped segments when the provider returned them. */\n segments?: TranscriptionSegment[];\n};\n\n/** The report node a {@link transcribe} run produces (`type: \"transcription\"`). */\nexport type TranscriptionReport = BaseReport & {\n type: \"transcription\";\n /** Identity of the STT model this run used. */\n model: { name: string; provider: string };\n /** Input audio duration in seconds, when the provider reported it. */\n durationSeconds?: number;\n};\n\n/** Result envelope of {@link transcribe} — the uniform `{ data, error, usage, report }`. */\nexport type TranscriptionResult = ExecuteResult<TranscriptionData> & {\n type: \"transcription\";\n report: TranscriptionReport;\n};\n\n/**\n * Transcribe audio to text — the speech-to-text verb of the\n * output-modality track (Theme I), inverse of `ai.speech()`. Wraps a\n * {@link TranscriptionModelContract} (from `openai.transcribe(...)`) in\n * the uniform result contract:\n *\n * - **Never throws.** Provider failures surface as a typed `AIError` on\n * `result.error`.\n * - **Cost-truth.** `result.usage.cost` is filled per-minute\n * (`whisper-1`) or per-token (`gpt-4o-transcribe`).\n * - **Observable.** The completed {@link TranscriptionReport} routes to\n * any registered `Observer` via the `observe` seam.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey });\n * const { data, error } = await ai.transcribe({\n * model: openai.transcribe({ name: \"whisper-1\" }),\n * audio: { base64, mediaType: \"audio/mpeg\", filename: \"voicemail.mp3\" },\n * language: \"en\",\n * });\n * if (!error) console.log(data.text);\n */\nexport async function transcribe(params: TranscribeParams): Promise<TranscriptionResult> {\n const { model, audio } = params;\n\n const runId = generateRunId(\"transcription\");\n const startedAt = new Date().toISOString();\n const startPerf = performance.now();\n\n const usage: Usage = { input: 0, output: 0, total: 0 };\n let data: TranscriptionData | undefined;\n let error: AIError | undefined;\n let status: TranscriptionReport[\"status\"] = \"completed\";\n let durationSeconds: number | undefined;\n\n try {\n const response = await model.transcribe(audio, {\n language: params.language,\n prompt: params.prompt,\n format: params.format,\n signal: params.signal,\n ...params.options,\n });\n\n Object.assign(usage, response.usage);\n durationSeconds = response.durationSeconds;\n\n if (usage.cost === undefined) {\n const cost = computeTranscriptionCost(usage, durationSeconds, model.pricing);\n if (cost !== undefined) {\n usage.cost = cost;\n }\n }\n\n data = { text: response.text, ...(response.segments ? { segments: response.segments } : {}) };\n } catch (thrown) {\n error =\n thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });\n status = params.signal?.aborted ? \"cancelled\" : \"failed\";\n }\n\n const report: TranscriptionReport = {\n runId,\n rootRunId: runId,\n name: params.name ?? \"transcription\",\n type: \"transcription\",\n status,\n error,\n startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - startPerf,\n usage,\n children: [],\n model: { name: model.name, provider: model.provider },\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n\n stampReportLineage(report, { rootRunId: runId, sessionId: params.sessionId });\n\n await notifyObservers(params.observe, report);\n\n return { type: \"transcription\", data, error, usage, report };\n}\n\n/**\n * Price an STT run: `perMinute × (durationSeconds / 60)` (per-minute\n * metering, attributed to `cost.input`) wins when configured, otherwise\n * the standard token math. Returns `undefined` when no usable pricing\n * is present (e.g. per-minute pricing but the provider didn't report a\n * duration).\n */\nfunction computeTranscriptionCost(\n usage: Usage,\n durationSeconds: number | undefined,\n pricing: TranscriptionModelPricing | undefined,\n): ModelPricing | undefined {\n if (!pricing) {\n return undefined;\n }\n\n if (pricing.perMinute !== undefined) {\n if (durationSeconds === undefined) {\n return undefined;\n }\n return { input: (durationSeconds / 60) * pricing.perMinute, output: 0 };\n }\n\n if (pricing.input !== undefined && pricing.output !== undefined) {\n return computeCost(usage, { input: pricing.input, output: pricing.output });\n }\n\n return undefined;\n}\n\n/** Best-effort message for a non-`AIError` thrown value. */\nfunction toMessage(thrown: unknown): string {\n return thrown instanceof Error ? thrown.message : String(thrown);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,eAAsB,WAAW,QAAwD;CACvF,MAAM,EAAE,OAAO,UAAU;CAEzB,MAAM,QAAQ,cAAc,eAAe;CAC3C,MAAM,6BAAY,IAAI,KAAK,EAAC,CAAC,YAAY;CACzC,MAAM,YAAY,YAAY,IAAI;CAElC,MAAM,QAAe;EAAE,OAAO;EAAG,QAAQ;EAAG,OAAO;CAAE;CACrD,IAAI;CACJ,IAAI;CACJ,IAAI,SAAwC;CAC5C,IAAI;CAEJ,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,WAAW,OAAO;GAC7C,UAAU,OAAO;GACjB,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,GAAG,OAAO;EACZ,CAAC;EAED,OAAO,OAAO,OAAO,SAAS,KAAK;EACnC,kBAAkB,SAAS;EAE3B,IAAI,MAAM,SAAS,QAAW;GAC5B,MAAM,OAAO,yBAAyB,OAAO,iBAAiB,MAAM,OAAO;GAC3E,IAAI,SAAS,QACX,MAAM,OAAO;EAEjB;EAEA,OAAO;GAAE,MAAM,SAAS;GAAM,GAAI,SAAS,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;EAAG;CAC9F,SAAS,QAAQ;EACf,QACE,kBAAkB,UAAU,SAAS,IAAI,cAAc,UAAU,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;EAC7F,SAAS,OAAO,QAAQ,UAAU,cAAc;CAClD;CAEA,MAAM,SAA8B;EAClC;EACA,WAAW;EACX,MAAM,OAAO,QAAQ;EACrB,MAAM;EACN;EACA;EACA;EACA,0BAAS,IAAI,KAAK,EAAC,CAAC,YAAY;EAChC,UAAU,YAAY,IAAI,IAAI;EAC9B;EACA,UAAU,CAAC;EACX,OAAO;GAAE,MAAM,MAAM;GAAM,UAAU,MAAM;EAAS;EACpD,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;EAC3D;CACF;CAEA,mBAAmB,QAAQ;EAAE,WAAW;EAAO,WAAW,OAAO;CAAU,CAAC;CAE5E,MAAM,gBAAgB,OAAO,SAAS,MAAM;CAE5C,OAAO;EAAE,MAAM;EAAiB;EAAM;EAAO;EAAO;CAAO;AAC7D;;;;;;;;AASA,SAAS,yBACP,OACA,iBACA,SAC0B;CAC1B,IAAI,CAAC,SACH;CAGF,IAAI,QAAQ,cAAc,QAAW;EACnC,IAAI,oBAAoB,QACtB;EAEF,OAAO;GAAE,OAAQ,kBAAkB,KAAM,QAAQ;GAAW,QAAQ;EAAE;CACxE;CAEA,IAAI,QAAQ,UAAU,UAAa,QAAQ,WAAW,QACpD,OAAO,YAAY,OAAO;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAO,CAAC;AAI9E;;AAGA,SAAS,UAAU,QAAyB;CAC1C,OAAO,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AACjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract-json-payload.mjs","names":[],"sources":["../../../../../../../ai/src/utils/extract-json-payload.ts"],"sourcesContent":["/**\n * Strip markdown code fences from an LLM response before JSON parsing.\n *\n * Models — especially Claude, smaller models, and local models — routinely\n * wrap JSON output in fenced code blocks (` ```json\\n{...}\\n``` `) even when\n * instructed otherwise. Sometimes they also precede the fence with prose\n * (\"Here you go:\\n```json\\n...\\n```\"). This helper finds the first fenced\n * block regardless of language tag and returns its trimmed contents.\n *\n * Returns the trimmed original text unchanged when no fence is present, so\n * clean JSON passes through as a no-op.\n *\n * Deliberately does NOT fall back to \"find first `{` and last `}` and slice\n * between them\" — that heuristic silently corrupts data when prose contains\n * stray braces. Failing loudly at `JSON.parse` is safer.\n *\n * @example\n * extractJsonPayload('```json\\n{\"a\":1}\\n```');\n * // => '{\"a\":1}'\n *\n * @example\n * extractJsonPayload('Here you go:\\n```\\n{\"a\":1}\\n```\\nHope this helps.');\n * // => '{\"a\":1}'\n *\n * @example\n * extractJsonPayload('{\"a\":1}');\n * // => '{\"a\":1}' (no fence → unchanged)\n */\nexport function extractJsonPayload(text: string): string {\n const trimmed = text.trim();\n\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n\n return trimmed;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,mBAAmB,MAAsB;CACvD,MAAM,UAAU,KAAK,KAAK;CAE1B,MAAM,aAAa,QAAQ,MAAM,oCAAoC;CAErE,IAAI,YACF,OAAO,WAAW,
|
|
1
|
+
{"version":3,"file":"extract-json-payload.mjs","names":[],"sources":["../../../../../../../ai/src/utils/extract-json-payload.ts"],"sourcesContent":["/**\n * Strip markdown code fences from an LLM response before JSON parsing.\n *\n * Models — especially Claude, smaller models, and local models — routinely\n * wrap JSON output in fenced code blocks (` ```json\\n{...}\\n``` `) even when\n * instructed otherwise. Sometimes they also precede the fence with prose\n * (\"Here you go:\\n```json\\n...\\n```\"). This helper finds the first fenced\n * block regardless of language tag and returns its trimmed contents.\n *\n * Returns the trimmed original text unchanged when no fence is present, so\n * clean JSON passes through as a no-op.\n *\n * Deliberately does NOT fall back to \"find first `{` and last `}` and slice\n * between them\" — that heuristic silently corrupts data when prose contains\n * stray braces. Failing loudly at `JSON.parse` is safer.\n *\n * @example\n * extractJsonPayload('```json\\n{\"a\":1}\\n```');\n * // => '{\"a\":1}'\n *\n * @example\n * extractJsonPayload('Here you go:\\n```\\n{\"a\":1}\\n```\\nHope this helps.');\n * // => '{\"a\":1}'\n *\n * @example\n * extractJsonPayload('{\"a\":1}');\n * // => '{\"a\":1}' (no fence → unchanged)\n */\nexport function extractJsonPayload(text: string): string {\n const trimmed = text.trim();\n\n const fenceMatch = trimmed.match(/```(?:json)?\\s*\\n?([\\s\\S]*?)\\n?```/);\n\n if (fenceMatch) {\n return fenceMatch[1].trim();\n }\n\n return trimmed;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,mBAAmB,MAAsB;CACvD,MAAM,UAAU,KAAK,KAAK;CAE1B,MAAM,aAAa,QAAQ,MAAM,oCAAoC;CAErE,IAAI,YACF,OAAO,WAAW,EAAE,CAAC,KAAK;CAG5B,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-run-id.mjs","names":[],"sources":["../../../../../../../ai/src/utils/generate-run-id.ts"],"sourcesContent":["/**\n * Generates a stable, human-readable run id for any execution node\n * (tool invocation, agent run, workflow run, supervisor run). Format:\n * `${prefix}_${timestamp36}_${random36}` — compact, sortable by\n * prefix, collision-resistant within a run.\n *\n * Shared helper so every primitive emits the same id shape. The\n * prefix is conventionally the primitive kind (`\"tool\"`, `\"agent\"`,\n * `\"workflow\"`, `\"sup\"`) but callers can pass anything; the id is\n * purely for correlation, never parsed.\n *\n * @example\n * const runId = generateRunId(\"tool\");\n * // → \"tool_ld8x3m_7fq2j1kp\"\n */\nexport function generateRunId(prefix: string): string {\n return `${prefix}_${Date.now().toString(36)}_${Math.random()\n .toString(36)\n .slice(2, 10)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,cAAc,QAAwB;CACpD,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"generate-run-id.mjs","names":[],"sources":["../../../../../../../ai/src/utils/generate-run-id.ts"],"sourcesContent":["/**\n * Generates a stable, human-readable run id for any execution node\n * (tool invocation, agent run, workflow run, supervisor run). Format:\n * `${prefix}_${timestamp36}_${random36}` — compact, sortable by\n * prefix, collision-resistant within a run.\n *\n * Shared helper so every primitive emits the same id shape. The\n * prefix is conventionally the primitive kind (`\"tool\"`, `\"agent\"`,\n * `\"workflow\"`, `\"sup\"`) but callers can pass anything; the id is\n * purely for correlation, never parsed.\n *\n * @example\n * const runId = generateRunId(\"tool\");\n * // → \"tool_ld8x3m_7fq2j1kp\"\n */\nexport function generateRunId(prefix: string): string {\n return `${prefix}_${Date.now().toString(36)}_${Math.random()\n .toString(36)\n .slice(2, 10)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,cAAc,QAAwB;CACpD,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CACzD,SAAS,EAAE,CAAC,CACZ,MAAM,GAAG,EAAE;AAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare-attachment-part.mjs","names":["resolvePath"],"sources":["../../../../../../../ai/src/utils/prepare-attachment-part.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { extname, isAbsolute, relative, resolve as resolvePath } from \"node:path\";\nimport type { AttachmentPolicy } from \"../contracts/attachment-policy.type\";\nimport type { Attachment } from \"../contracts/attachment.type\";\nimport type { ContentPart } from \"../contracts/content-part.type\";\nimport { InvalidRequestError, OutboundPolicyError } from \"../errors\";\nimport { fetchTextWithPolicy } from \"../security/outbound-policy\";\nimport { resolveAttachment } from \"./resolve-attachment\";\n\nconst IMAGE_EXTENSIONS_TO_MEDIA_TYPE: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n \".gif\": \"image/gif\",\n};\n\nconst TEXT_EXTENSIONS = new Set([\".txt\"]);\n\nconst AUDIO_EXTENSIONS_TO_MEDIA_TYPE: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n \".m4a\": \"audio/mp4\",\n \".ogg\": \"audio/ogg\",\n \".weba\": \"audio/webm\",\n};\n\nconst PDF_EXTENSIONS = new Set([\".pdf\"]);\n\ntype AttachmentKind = \"image\" | \"text\" | \"pdf\" | \"audio\";\n\n/**\n * Convert a user-supplied `Attachment` into a provider-ready\n * `ContentPart` the model adapter can consume without doing any I/O of\n * its own.\n *\n * Kind resolution:\n * - Tagged `{ type: \"image\", source }` / `{ type: \"text\", source }`\n * trusts the caller's intent.\n * - Shorthand (raw string / `StorageFileShape`) infers from the file\n * extension. Image extensions (`.png`/`.jpg`/`.jpeg`/`.webp`/`.gif`)\n * map to `\"image\"`. `.txt` maps to `\"text\"`. Anything else throws\n * `InvalidRequestError` — silent inference on ambiguous inputs\n * causes silent bugs.\n *\n * Local paths are read from disk; images are base64-encoded inline,\n * text files are read as UTF-8 strings and returned as a `text`\n * `ContentPart`. Remote URLs for image attachments are passed through\n * unchanged; remote URLs for text attachments are fetched so the\n * adapter never needs network access.\n *\n * @example\n * await prepareAttachmentPart(\"./photo.png\");\n * // → { type: \"image\", source: { base64: \"...\", mediaType: \"image/png\" } }\n *\n * @example\n * await prepareAttachmentPart({ type: \"text\", source: \"./notes.txt\" });\n * // → { type: \"text\", text: \"<file contents>\" }\n *\n * **Trust boundary (S1).** Attachment references are often user-controlled,\n * so server-side I/O is policy-gated by `policy` ({@link AttachmentPolicy}):\n * remote text fetches are default-deny and, when enabled, run through the\n * shared `OutboundPolicy` (scheme/host/private-IP/max-bytes/timeout); local\n * reads honor an `allowedRoots` sandbox; bare-string local paths warn\n * (staged deprecation). URL *image* attachments are passed to the provider\n * untouched (never fetched here).\n */\nexport async function prepareAttachmentPart(\n attachment: Attachment,\n policy?: AttachmentPolicy,\n): Promise<ContentPart> {\n const kind = resolveKind(attachment);\n const bareString = typeof attachment === \"string\";\n\n if (kind === \"text\") {\n return prepareTextPart(attachment, policy, bareString);\n }\n\n if (kind === \"image\") {\n return prepareImagePart(attachment, policy, bareString);\n }\n\n return prepareBinaryPart(attachment, kind, policy, bareString);\n}\n\n/**\n * Decide whether the attachment is text or image. Tagged forms win\n * immediately; for shorthand we inspect the extension. Throws if the\n * shorthand doesn't look like anything we recognize.\n */\nfunction resolveKind(attachment: Attachment): AttachmentKind {\n if (isTaggedAttachment(attachment)) {\n return attachment.type;\n }\n\n const path = extractPath(attachment);\n const extension = path ? extname(stripQuery(path)).toLowerCase() : \"\";\n\n if (IMAGE_EXTENSIONS_TO_MEDIA_TYPE[extension]) {\n return \"image\";\n }\n\n if (TEXT_EXTENSIONS.has(extension)) {\n return \"text\";\n }\n\n if (PDF_EXTENSIONS.has(extension)) {\n return \"pdf\";\n }\n\n if (AUDIO_EXTENSIONS_TO_MEDIA_TYPE[extension]) {\n return \"audio\";\n }\n\n throw new InvalidRequestError(\n \"Cannot infer attachment type from input — pass an explicit `{ type: 'image' | 'text' | 'pdf' | 'audio', source: ... }` or use a recognized extension (.png, .jpg, .jpeg, .webp, .gif, .txt, .pdf, .mp3, .wav, .m4a, .ogg, .weba)\",\n );\n}\n\n/**\n * Produce a `pdf` / `audio` ContentPart (A2). URLs pass through; local\n * paths are read and base64-encoded with a media type inferred from the\n * kind (`application/pdf`) or extension (audio); inline base64 passes\n * through. Same `AttachmentPolicy` gating as image/text reads.\n */\nasync function prepareBinaryPart(\n attachment: Attachment,\n kind: \"pdf\" | \"audio\",\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n return { type: kind, source: { url: resolved.value } };\n }\n\n if (resolved.type === \"base64\") {\n return { type: kind, source: { base64: resolved.value, mediaType: resolved.mediaType } };\n }\n\n const mediaType =\n kind === \"pdf\" ? \"application/pdf\" : inferAudioMediaType(resolved.value);\n\n if (!mediaType) {\n throw new InvalidRequestError(\n `Cannot infer media type for ${kind} path \"${resolved.value}\" — use a recognized extension or pass ` +\n `\\`{ type: '${kind}', source: { base64, mediaType } }\\``,\n { context: { path: resolved.value } },\n );\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value);\n\n return { type: kind, source: { base64: bytes.toString(\"base64\"), mediaType } };\n}\n\n/** Infer an audio media type from a path's extension. */\nfunction inferAudioMediaType(path: string): string | undefined {\n return AUDIO_EXTENSIONS_TO_MEDIA_TYPE[extname(stripQuery(path)).toLowerCase()];\n}\n\n/**\n * Produce an `image` ContentPart. URLs pass through; paths are\n * read from disk and base64-encoded with an inferred media type.\n * Inline base64 attachments pass through unchanged.\n */\nasync function prepareImagePart(\n attachment: Attachment,\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const inferredMediaType = isTaggedAttachment(attachment)\n ? undefined\n : inferImageMediaType(attachment);\n\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n // URL images are handed to the provider as a URL — the provider\n // fetches them, not us — so there's no server-side SSRF surface here.\n return { type: \"image\", source: { url: resolved.value } };\n }\n\n if (resolved.type === \"base64\") {\n return {\n type: \"image\",\n source: { base64: resolved.value, mediaType: resolved.mediaType },\n };\n }\n\n const mediaType = inferredMediaType ?? inferImageMediaType(resolved.value);\n\n if (!mediaType) {\n throw new InvalidRequestError(\n `Cannot infer media type for path \"${resolved.value}\" — use a recognized image extension or pass ` +\n \"`{ type: 'image', source: { base64, mediaType } }`\",\n { context: { path: resolved.value } },\n );\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value);\n\n return {\n type: \"image\",\n source: { base64: bytes.toString(\"base64\"), mediaType },\n };\n}\n\n/**\n * Produce a `text` ContentPart. URLs are fetched as UTF-8, paths are\n * read from disk as UTF-8, inline base64 is decoded to UTF-8. The\n * result joins the conversation as an additional text part the model\n * sees before responding.\n */\nasync function prepareTextPart(\n attachment: Attachment,\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n // Default-deny: a remote text attachment is a server-side fetch of\n // user-controlled input — refuse unless the app explicitly opted in,\n // then run it through the shared OutboundPolicy (scheme/host/private-\n // IP/max-bytes/timeout).\n if (!policy?.allowRemoteFetch) {\n throw new OutboundPolicyError(\n `remote text attachment fetch is disabled by default — set \\`attachmentPolicy.allowRemoteFetch: true\\` (with an \\`outbound\\` policy) to fetch \"${resolved.value}\"`,\n { context: { url: resolved.value } },\n );\n }\n\n const result = await fetchTextWithPolicy(resolved.value, policy.outbound ?? {});\n\n if (!result.ok) {\n throw new InvalidRequestError(\n `Failed to fetch text attachment \"${resolved.value}\" — status ${result.status}`,\n { context: { url: resolved.value, status: result.status } },\n );\n }\n\n return { type: \"text\", text: result.text };\n }\n\n if (resolved.type === \"base64\") {\n const decoded = Buffer.from(resolved.value, \"base64\").toString(\"utf8\");\n\n return { type: \"text\", text: decoded };\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value, \"utf8\");\n\n return { type: \"text\", text: bytes };\n}\n\n/** Process-lifetime flag so the bare-string deprecation warns at most once. */\nlet warnedBareLocalPath = false;\n\n/**\n * Enforce the local-file half of {@link AttachmentPolicy} (S1):\n *\n * - **Bare-string local paths** are staged for deprecation. With\n * `allowBareLocalPaths: false` they hard-deny now; otherwise they warn\n * once (outside tests) — the typed `StorageFile.absolutePath` route is\n * the supported way to read a local file.\n * - **`allowedRoots` sandbox** — when set, the resolved path must live\n * inside one of the roots, else the read is refused.\n */\nfunction enforceLocalPathPolicy(\n path: string,\n bareString: boolean,\n policy: AttachmentPolicy | undefined,\n): void {\n if (bareString) {\n if (policy?.allowBareLocalPaths === false) {\n throw new OutboundPolicyError(\n `local file attachment via a bare string path (\"${path}\") is disabled — pass a typed \\`{ type, source: { absolutePath } }\\` StorageFile, or set \\`attachmentPolicy.allowBareLocalPaths: true\\``,\n { context: { path } },\n );\n }\n\n if (!warnedBareLocalPath && !process.env.VITEST && process.env.NODE_ENV !== \"test\") {\n warnedBareLocalPath = true;\n console.warn(\n \"[warlock-ai] reading a local file attachment from a bare string path is deprecated and will be denied by default in a future minor. \" +\n \"Pass a typed `{ type, source: { absolutePath } }` StorageFile and confine reads with `attachmentPolicy.allowedRoots`.\",\n );\n }\n }\n\n const roots = policy?.allowedRoots;\n if (roots && roots.length > 0) {\n const target = resolvePath(path);\n const inside = roots.some(root => {\n const rel = relative(resolvePath(root), target);\n return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n });\n\n if (!inside) {\n throw new OutboundPolicyError(\n `local file attachment \"${path}\" is outside the allowed roots`,\n { context: { path, allowedRoots: roots } },\n );\n }\n }\n}\n\nfunction isTaggedAttachment(\n attachment: Attachment,\n): attachment is Extract<Attachment, { type: string }> {\n return (\n typeof attachment === \"object\" &&\n attachment !== null &&\n \"type\" in attachment\n );\n}\n\nfunction inferImageMediaType(input: unknown): string | undefined {\n const path = extractPath(input);\n\n if (!path) {\n return undefined;\n }\n\n const extension = extname(stripQuery(path)).toLowerCase();\n\n return IMAGE_EXTENSIONS_TO_MEDIA_TYPE[extension];\n}\n\nfunction extractPath(input: unknown): string | undefined {\n if (typeof input === \"string\") {\n return input;\n }\n\n if (typeof input === \"object\" && input !== null) {\n const storage = input as { url?: string; absolutePath?: string };\n return storage.url ?? storage.absolutePath;\n }\n\n return undefined;\n}\n\nfunction stripQuery(path: string): string {\n const queryIndex = path.indexOf(\"?\");\n\n return queryIndex === -1 ? path : path.slice(0, queryIndex);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prepare-attachment-part.mjs","names":["resolvePath"],"sources":["../../../../../../../ai/src/utils/prepare-attachment-part.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { extname, isAbsolute, relative, resolve as resolvePath } from \"node:path\";\nimport type { AttachmentPolicy } from \"../contracts/attachment-policy.type\";\nimport type { Attachment } from \"../contracts/attachment.type\";\nimport type { ContentPart } from \"../contracts/content-part.type\";\nimport { InvalidRequestError, OutboundPolicyError } from \"../errors\";\nimport { fetchTextWithPolicy } from \"../security/outbound-policy\";\nimport { resolveAttachment } from \"./resolve-attachment\";\n\nconst IMAGE_EXTENSIONS_TO_MEDIA_TYPE: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n \".gif\": \"image/gif\",\n};\n\nconst TEXT_EXTENSIONS = new Set([\".txt\"]);\n\nconst AUDIO_EXTENSIONS_TO_MEDIA_TYPE: Record<string, string> = {\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n \".m4a\": \"audio/mp4\",\n \".ogg\": \"audio/ogg\",\n \".weba\": \"audio/webm\",\n};\n\nconst PDF_EXTENSIONS = new Set([\".pdf\"]);\n\ntype AttachmentKind = \"image\" | \"text\" | \"pdf\" | \"audio\";\n\n/**\n * Convert a user-supplied `Attachment` into a provider-ready\n * `ContentPart` the model adapter can consume without doing any I/O of\n * its own.\n *\n * Kind resolution:\n * - Tagged `{ type: \"image\", source }` / `{ type: \"text\", source }`\n * trusts the caller's intent.\n * - Shorthand (raw string / `StorageFileShape`) infers from the file\n * extension. Image extensions (`.png`/`.jpg`/`.jpeg`/`.webp`/`.gif`)\n * map to `\"image\"`. `.txt` maps to `\"text\"`. Anything else throws\n * `InvalidRequestError` — silent inference on ambiguous inputs\n * causes silent bugs.\n *\n * Local paths are read from disk; images are base64-encoded inline,\n * text files are read as UTF-8 strings and returned as a `text`\n * `ContentPart`. Remote URLs for image attachments are passed through\n * unchanged; remote URLs for text attachments are fetched so the\n * adapter never needs network access.\n *\n * @example\n * await prepareAttachmentPart(\"./photo.png\");\n * // → { type: \"image\", source: { base64: \"...\", mediaType: \"image/png\" } }\n *\n * @example\n * await prepareAttachmentPart({ type: \"text\", source: \"./notes.txt\" });\n * // → { type: \"text\", text: \"<file contents>\" }\n *\n * **Trust boundary (S1).** Attachment references are often user-controlled,\n * so server-side I/O is policy-gated by `policy` ({@link AttachmentPolicy}):\n * remote text fetches are default-deny and, when enabled, run through the\n * shared `OutboundPolicy` (scheme/host/private-IP/max-bytes/timeout); local\n * reads honor an `allowedRoots` sandbox; bare-string local paths warn\n * (staged deprecation). URL *image* attachments are passed to the provider\n * untouched (never fetched here).\n */\nexport async function prepareAttachmentPart(\n attachment: Attachment,\n policy?: AttachmentPolicy,\n): Promise<ContentPart> {\n const kind = resolveKind(attachment);\n const bareString = typeof attachment === \"string\";\n\n if (kind === \"text\") {\n return prepareTextPart(attachment, policy, bareString);\n }\n\n if (kind === \"image\") {\n return prepareImagePart(attachment, policy, bareString);\n }\n\n return prepareBinaryPart(attachment, kind, policy, bareString);\n}\n\n/**\n * Decide whether the attachment is text or image. Tagged forms win\n * immediately; for shorthand we inspect the extension. Throws if the\n * shorthand doesn't look like anything we recognize.\n */\nfunction resolveKind(attachment: Attachment): AttachmentKind {\n if (isTaggedAttachment(attachment)) {\n return attachment.type;\n }\n\n const path = extractPath(attachment);\n const extension = path ? extname(stripQuery(path)).toLowerCase() : \"\";\n\n if (IMAGE_EXTENSIONS_TO_MEDIA_TYPE[extension]) {\n return \"image\";\n }\n\n if (TEXT_EXTENSIONS.has(extension)) {\n return \"text\";\n }\n\n if (PDF_EXTENSIONS.has(extension)) {\n return \"pdf\";\n }\n\n if (AUDIO_EXTENSIONS_TO_MEDIA_TYPE[extension]) {\n return \"audio\";\n }\n\n throw new InvalidRequestError(\n \"Cannot infer attachment type from input — pass an explicit `{ type: 'image' | 'text' | 'pdf' | 'audio', source: ... }` or use a recognized extension (.png, .jpg, .jpeg, .webp, .gif, .txt, .pdf, .mp3, .wav, .m4a, .ogg, .weba)\",\n );\n}\n\n/**\n * Produce a `pdf` / `audio` ContentPart (A2). URLs pass through; local\n * paths are read and base64-encoded with a media type inferred from the\n * kind (`application/pdf`) or extension (audio); inline base64 passes\n * through. Same `AttachmentPolicy` gating as image/text reads.\n */\nasync function prepareBinaryPart(\n attachment: Attachment,\n kind: \"pdf\" | \"audio\",\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n return { type: kind, source: { url: resolved.value } };\n }\n\n if (resolved.type === \"base64\") {\n return { type: kind, source: { base64: resolved.value, mediaType: resolved.mediaType } };\n }\n\n const mediaType =\n kind === \"pdf\" ? \"application/pdf\" : inferAudioMediaType(resolved.value);\n\n if (!mediaType) {\n throw new InvalidRequestError(\n `Cannot infer media type for ${kind} path \"${resolved.value}\" — use a recognized extension or pass ` +\n `\\`{ type: '${kind}', source: { base64, mediaType } }\\``,\n { context: { path: resolved.value } },\n );\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value);\n\n return { type: kind, source: { base64: bytes.toString(\"base64\"), mediaType } };\n}\n\n/** Infer an audio media type from a path's extension. */\nfunction inferAudioMediaType(path: string): string | undefined {\n return AUDIO_EXTENSIONS_TO_MEDIA_TYPE[extname(stripQuery(path)).toLowerCase()];\n}\n\n/**\n * Produce an `image` ContentPart. URLs pass through; paths are\n * read from disk and base64-encoded with an inferred media type.\n * Inline base64 attachments pass through unchanged.\n */\nasync function prepareImagePart(\n attachment: Attachment,\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const inferredMediaType = isTaggedAttachment(attachment)\n ? undefined\n : inferImageMediaType(attachment);\n\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n // URL images are handed to the provider as a URL — the provider\n // fetches them, not us — so there's no server-side SSRF surface here.\n return { type: \"image\", source: { url: resolved.value } };\n }\n\n if (resolved.type === \"base64\") {\n return {\n type: \"image\",\n source: { base64: resolved.value, mediaType: resolved.mediaType },\n };\n }\n\n const mediaType = inferredMediaType ?? inferImageMediaType(resolved.value);\n\n if (!mediaType) {\n throw new InvalidRequestError(\n `Cannot infer media type for path \"${resolved.value}\" — use a recognized image extension or pass ` +\n \"`{ type: 'image', source: { base64, mediaType } }`\",\n { context: { path: resolved.value } },\n );\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value);\n\n return {\n type: \"image\",\n source: { base64: bytes.toString(\"base64\"), mediaType },\n };\n}\n\n/**\n * Produce a `text` ContentPart. URLs are fetched as UTF-8, paths are\n * read from disk as UTF-8, inline base64 is decoded to UTF-8. The\n * result joins the conversation as an additional text part the model\n * sees before responding.\n */\nasync function prepareTextPart(\n attachment: Attachment,\n policy: AttachmentPolicy | undefined,\n bareString: boolean,\n): Promise<ContentPart> {\n const resolved = resolveAttachment(attachment);\n\n if (resolved.type === \"url\") {\n // Default-deny: a remote text attachment is a server-side fetch of\n // user-controlled input — refuse unless the app explicitly opted in,\n // then run it through the shared OutboundPolicy (scheme/host/private-\n // IP/max-bytes/timeout).\n if (!policy?.allowRemoteFetch) {\n throw new OutboundPolicyError(\n `remote text attachment fetch is disabled by default — set \\`attachmentPolicy.allowRemoteFetch: true\\` (with an \\`outbound\\` policy) to fetch \"${resolved.value}\"`,\n { context: { url: resolved.value } },\n );\n }\n\n const result = await fetchTextWithPolicy(resolved.value, policy.outbound ?? {});\n\n if (!result.ok) {\n throw new InvalidRequestError(\n `Failed to fetch text attachment \"${resolved.value}\" — status ${result.status}`,\n { context: { url: resolved.value, status: result.status } },\n );\n }\n\n return { type: \"text\", text: result.text };\n }\n\n if (resolved.type === \"base64\") {\n const decoded = Buffer.from(resolved.value, \"base64\").toString(\"utf8\");\n\n return { type: \"text\", text: decoded };\n }\n\n enforceLocalPathPolicy(resolved.value, bareString, policy);\n const bytes = await readFile(resolved.value, \"utf8\");\n\n return { type: \"text\", text: bytes };\n}\n\n/** Process-lifetime flag so the bare-string deprecation warns at most once. */\nlet warnedBareLocalPath = false;\n\n/**\n * Enforce the local-file half of {@link AttachmentPolicy} (S1):\n *\n * - **Bare-string local paths** are staged for deprecation. With\n * `allowBareLocalPaths: false` they hard-deny now; otherwise they warn\n * once (outside tests) — the typed `StorageFile.absolutePath` route is\n * the supported way to read a local file.\n * - **`allowedRoots` sandbox** — when set, the resolved path must live\n * inside one of the roots, else the read is refused.\n */\nfunction enforceLocalPathPolicy(\n path: string,\n bareString: boolean,\n policy: AttachmentPolicy | undefined,\n): void {\n if (bareString) {\n if (policy?.allowBareLocalPaths === false) {\n throw new OutboundPolicyError(\n `local file attachment via a bare string path (\"${path}\") is disabled — pass a typed \\`{ type, source: { absolutePath } }\\` StorageFile, or set \\`attachmentPolicy.allowBareLocalPaths: true\\``,\n { context: { path } },\n );\n }\n\n if (!warnedBareLocalPath && !process.env.VITEST && process.env.NODE_ENV !== \"test\") {\n warnedBareLocalPath = true;\n console.warn(\n \"[warlock-ai] reading a local file attachment from a bare string path is deprecated and will be denied by default in a future minor. \" +\n \"Pass a typed `{ type, source: { absolutePath } }` StorageFile and confine reads with `attachmentPolicy.allowedRoots`.\",\n );\n }\n }\n\n const roots = policy?.allowedRoots;\n if (roots && roots.length > 0) {\n const target = resolvePath(path);\n const inside = roots.some(root => {\n const rel = relative(resolvePath(root), target);\n return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n });\n\n if (!inside) {\n throw new OutboundPolicyError(\n `local file attachment \"${path}\" is outside the allowed roots`,\n { context: { path, allowedRoots: roots } },\n );\n }\n }\n}\n\nfunction isTaggedAttachment(\n attachment: Attachment,\n): attachment is Extract<Attachment, { type: string }> {\n return (\n typeof attachment === \"object\" &&\n attachment !== null &&\n \"type\" in attachment\n );\n}\n\nfunction inferImageMediaType(input: unknown): string | undefined {\n const path = extractPath(input);\n\n if (!path) {\n return undefined;\n }\n\n const extension = extname(stripQuery(path)).toLowerCase();\n\n return IMAGE_EXTENSIONS_TO_MEDIA_TYPE[extension];\n}\n\nfunction extractPath(input: unknown): string | undefined {\n if (typeof input === \"string\") {\n return input;\n }\n\n if (typeof input === \"object\" && input !== null) {\n const storage = input as { url?: string; absolutePath?: string };\n return storage.url ?? storage.absolutePath;\n }\n\n return undefined;\n}\n\nfunction stripQuery(path: string): string {\n const queryIndex = path.indexOf(\"?\");\n\n return queryIndex === -1 ? path : path.slice(0, queryIndex);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-context.d.mts","names":[],"sources":["../../../../../../../ai/src/utils/run-context.ts"],"mappings":";;;;;AAgBA;;;;;;;;;;AAsBW;KAtBC,QAAA;EAqDgB;;;;EAhD1B,IAAA,EAAM,UAAU;EAgD8C;;;;;EA1C9D,SAAA;EA0C+C;;;AAAe;AAYhE;EAhDE,WAAA;EAgD6B;;;;EA3C7B,SAAA;AAAA;;AA2CgD;AAUlD;;;;AAA2C;AAsC3C;;;iBA5DgB,YAAA,
|
|
1
|
+
{"version":3,"file":"run-context.d.mts","names":[],"sources":["../../../../../../../ai/src/utils/run-context.ts"],"mappings":";;;;;AAgBA;;;;;;;;;;AAsBW;KAtBC,QAAA;EAqDgB;;;;EAhD1B,IAAA,EAAM,UAAU;EAgD8C;;;;;EA1C9D,SAAA;EA0C+C;;;AAAe;AAYhE;EAhDE,WAAA;EAgD6B;;;;EA3C7B,SAAA;AAAA;;AA2CgD;AAUlD;;;;AAA2C;AAsC3C;;;iBA5DgB,YAAA,IAAgB,KAAA,EAAO,QAAA,EAAU,EAAA,QAAU,CAAA,GAAI,CAAA;AA4DV;;;;;;;;AAAA,iBAhDrC,eAAA,IAAmB,EAAA,QAAU,CAAA,GAAI,CAAC;;;;;;;iBAUlC,eAAA,IAAmB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;iBAsC3B,kBAAA,CAAmB,MAAkB,EAAV,UAAU"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-json-parse.d.mts","names":[],"sources":["../../../../../../../ai/src/utils/safe-json-parse.ts"],"mappings":";;AASA;;;;;;;;iBAAgB,aAAA,
|
|
1
|
+
{"version":3,"file":"safe-json-parse.d.mts","names":[],"sources":["../../../../../../../ai/src/utils/safe-json-parse.ts"],"mappings":";;AASA;;;;;;;;iBAAgB,aAAA,SACd,IAAA,6BACA,YAAA,EAAc,MAAA,GACb,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cassette-io.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/cassette-io.ts"],"sourcesContent":["import type { Cassette } from \"./vcr.type\";\n\n/**\n * Lazily-resolved `node:fs/promises` module. VCR cassette I/O only touches\n * disk on construct (load) and on `save()` — keeping the import lazy means\n * importing the `vcr` factory never eagerly pulls `node:fs`, which keeps the\n * surface usable in non-node bundles that never call a disk path.\n */\ntype FsPromises = typeof import(\"node:fs/promises\");\n\nlet fsModule: FsPromises | undefined;\n\n/**\n * Resolve `node:fs/promises` once and memoize it.\n */\nasync function loadFs(): Promise<FsPromises> {\n if (!fsModule) {\n fsModule = await import(\"node:fs/promises\");\n }\n\n return fsModule;\n}\n\n/**\n * Build a fresh, empty cassette for a model identity. Used when the path\n * does not exist yet (first record run).\n */\nexport function emptyCassette(model: string, provider: string): Cassette {\n return {\n version: 1,\n model,\n provider,\n entries: [],\n };\n}\n\n/**\n * Load a cassette from disk. Returns a fresh empty cassette (not an error)\n * when the file does not exist — the common first-record case. Any other I/O\n * or parse failure rejects so corruption is never silently swallowed.\n */\nexport async function loadCassette(\n path: string,\n model: string,\n provider: string,\n): Promise<Cassette> {\n const fs = await loadFs();\n\n let raw: string;\n\n try {\n raw = await fs.readFile(path, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return emptyCassette(model, provider);\n }\n\n throw error;\n }\n\n const parsed = JSON.parse(raw) as Cassette;\n\n return {\n version: 1,\n model: parsed.model ?? model,\n provider: parsed.provider ?? provider,\n entries: Array.isArray(parsed.entries) ? parsed.entries : [],\n };\n}\n\n/**\n * Write a cassette to disk as pretty-printed JSON, creating the parent\n * directory if needed so a brand-new `./cassettes/foo.json` path just works.\n */\nexport async function saveCassette(path: string, cassette: Cassette): Promise<void> {\n const fs = await loadFs();\n const nodePath = await import(\"node:path\");\n const dir = nodePath.dirname(path);\n\n if (dir && dir !== \".\" && dir !== path) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n await fs.writeFile(path, JSON.stringify(cassette, undefined, 2), \"utf8\");\n}\n"],"mappings":";AAUA,IAAI;;;;AAKJ,eAAe,SAA8B;CAC3C,IAAI,CAAC,UACH,WAAW,MAAM,OAAO;CAG1B,OAAO;AACT;;;;;AAMA,SAAgB,cAAc,OAAe,UAA4B;CACvE,OAAO;EACL,SAAS;EACT;EACA;EACA,SAAS,CAAC;CACZ;AACF;;;;;;AAOA,eAAsB,aACpB,MACA,OACA,UACmB;CACnB,MAAM,KAAK,MAAM,OAAO;CAExB,IAAI;CAEJ,IAAI;EACF,MAAM,MAAM,GAAG,SAAS,MAAM,MAAM;CACtC,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAC5C,OAAO,cAAc,OAAO,QAAQ;EAGtC,MAAM;CACR;CAEA,MAAM,SAAS,KAAK,MAAM,GAAG;CAE7B,OAAO;EACL,SAAS;EACT,OAAO,OAAO,SAAS;EACvB,UAAU,OAAO,YAAY;EAC7B,SAAS,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,CAAC;CAC7D;AACF;;;;;AAMA,eAAsB,aAAa,MAAc,UAAmC;CAClF,MAAM,KAAK,MAAM,OAAO;CAExB,MAAM,OAAM,MADW,OAAO,
|
|
1
|
+
{"version":3,"file":"cassette-io.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/cassette-io.ts"],"sourcesContent":["import type { Cassette } from \"./vcr.type\";\n\n/**\n * Lazily-resolved `node:fs/promises` module. VCR cassette I/O only touches\n * disk on construct (load) and on `save()` — keeping the import lazy means\n * importing the `vcr` factory never eagerly pulls `node:fs`, which keeps the\n * surface usable in non-node bundles that never call a disk path.\n */\ntype FsPromises = typeof import(\"node:fs/promises\");\n\nlet fsModule: FsPromises | undefined;\n\n/**\n * Resolve `node:fs/promises` once and memoize it.\n */\nasync function loadFs(): Promise<FsPromises> {\n if (!fsModule) {\n fsModule = await import(\"node:fs/promises\");\n }\n\n return fsModule;\n}\n\n/**\n * Build a fresh, empty cassette for a model identity. Used when the path\n * does not exist yet (first record run).\n */\nexport function emptyCassette(model: string, provider: string): Cassette {\n return {\n version: 1,\n model,\n provider,\n entries: [],\n };\n}\n\n/**\n * Load a cassette from disk. Returns a fresh empty cassette (not an error)\n * when the file does not exist — the common first-record case. Any other I/O\n * or parse failure rejects so corruption is never silently swallowed.\n */\nexport async function loadCassette(\n path: string,\n model: string,\n provider: string,\n): Promise<Cassette> {\n const fs = await loadFs();\n\n let raw: string;\n\n try {\n raw = await fs.readFile(path, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return emptyCassette(model, provider);\n }\n\n throw error;\n }\n\n const parsed = JSON.parse(raw) as Cassette;\n\n return {\n version: 1,\n model: parsed.model ?? model,\n provider: parsed.provider ?? provider,\n entries: Array.isArray(parsed.entries) ? parsed.entries : [],\n };\n}\n\n/**\n * Write a cassette to disk as pretty-printed JSON, creating the parent\n * directory if needed so a brand-new `./cassettes/foo.json` path just works.\n */\nexport async function saveCassette(path: string, cassette: Cassette): Promise<void> {\n const fs = await loadFs();\n const nodePath = await import(\"node:path\");\n const dir = nodePath.dirname(path);\n\n if (dir && dir !== \".\" && dir !== path) {\n await fs.mkdir(dir, { recursive: true });\n }\n\n await fs.writeFile(path, JSON.stringify(cassette, undefined, 2), \"utf8\");\n}\n"],"mappings":";AAUA,IAAI;;;;AAKJ,eAAe,SAA8B;CAC3C,IAAI,CAAC,UACH,WAAW,MAAM,OAAO;CAG1B,OAAO;AACT;;;;;AAMA,SAAgB,cAAc,OAAe,UAA4B;CACvE,OAAO;EACL,SAAS;EACT;EACA;EACA,SAAS,CAAC;CACZ;AACF;;;;;;AAOA,eAAsB,aACpB,MACA,OACA,UACmB;CACnB,MAAM,KAAK,MAAM,OAAO;CAExB,IAAI;CAEJ,IAAI;EACF,MAAM,MAAM,GAAG,SAAS,MAAM,MAAM;CACtC,SAAS,OAAO;EACd,IAAK,MAAgC,SAAS,UAC5C,OAAO,cAAc,OAAO,QAAQ;EAGtC,MAAM;CACR;CAEA,MAAM,SAAS,KAAK,MAAM,GAAG;CAE7B,OAAO;EACL,SAAS;EACT,OAAO,OAAO,SAAS;EACvB,UAAU,OAAO,YAAY;EAC7B,SAAS,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,CAAC;CAC7D;AACF;;;;;AAMA,eAAsB,aAAa,MAAc,UAAmC;CAClF,MAAM,KAAK,MAAM,OAAO;CAExB,MAAM,OAAM,MADW,OAAO,aACV,CAAC,QAAQ,IAAI;CAEjC,IAAI,OAAO,QAAQ,OAAO,QAAQ,MAChC,MAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;CAGzC,MAAM,GAAG,UAAU,MAAM,KAAK,UAAU,UAAU,QAAW,CAAC,GAAG,MAAM;AACzE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash-request.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/hash-request.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type { ModelCallOptions } from \"../contracts/model.contract\";\nimport type { ToolConfig } from \"../contracts/tool.contract\";\n\n/**\n * Default `ModelCallOptions` fields folded into the request hash. These are\n * the inputs that materially change the model's output; everything else\n * (notably `signal` and unknown provider keys) is excluded so an otherwise\n * identical logical call still matches its recording.\n */\nexport const DEFAULT_HASH_OPTIONS: readonly string[] = [\n \"temperature\",\n \"maxTokens\",\n \"responseSchema\",\n \"tools\",\n \"reasoning\",\n];\n\n/**\n * Reduce a tool to the parts the model actually conditions on: its name,\n * description, and the *shape* of its input schema. Two tools that differ\n * only by object identity (a fresh schema instance per import) hash\n * identically; a real contract change (renamed field, new description)\n * invalidates the recording.\n *\n * The input schema is fingerprinted structurally — a Standard Schema is an\n * opaque object, so we serialize its enumerable own keys rather than\n * attempting to read its internals.\n */\nfunction fingerprintTool(tool: ToolConfig<unknown, unknown>): unknown {\n return {\n name: tool.name,\n description: tool.description,\n input: tool.input ? schemaShape(tool.input) : undefined,\n };\n}\n\n/**\n * Produce a stable, JSON-safe fingerprint of an arbitrary schema object.\n * Records only the structural skeleton (own enumerable keys, recursively)\n * so harmless instance differences don't perturb the hash while a genuine\n * structural change does.\n */\nfunction schemaShape(value: unknown, depth = 0): unknown {\n if (depth > 6 || value === null || typeof value !== \"object\") {\n return typeof value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => schemaShape(item, depth + 1));\n }\n\n const out: Record<string, unknown> = {};\n\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n out[key] = schemaShape((value as Record<string, unknown>)[key], depth + 1);\n }\n\n return out;\n}\n\n/**\n * Pick the hashable subset of `options`, normalizing `tools` into their\n * name+description+schema-shape fingerprint. `signal` and any field not in\n * `hashOptions` are dropped.\n */\nfunction pickOptions(\n options: ModelCallOptions | undefined,\n hashOptions: readonly string[],\n): Record<string, unknown> {\n if (!options) {\n return {};\n }\n\n const picked: Record<string, unknown> = {};\n\n for (const key of hashOptions) {\n const value = options[key];\n\n if (value === undefined) {\n continue;\n }\n\n if (key === \"tools\" && Array.isArray(value)) {\n picked[key] = (value as ToolConfig<unknown, unknown>[]).map(fingerprintTool);\n continue;\n }\n\n picked[key] = value;\n }\n\n return picked;\n}\n\n/**\n * Recursively sort object keys so two logically-equal payloads serialize to\n * byte-identical JSON regardless of property insertion order.\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n const out: Record<string, unknown> = {};\n\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n out[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n\n return out;\n}\n\n/**\n * Non-cryptographic 53-bit string hash (FNV-style, cyrb53). Deterministic\n * across runs and platforms; collision-resistant enough for a per-cassette\n * keyspace. Returned as a base-36 string.\n */\nfunction hashString(input: string): string {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n\n for (let i = 0; i < input.length; i++) {\n const ch = input.charCodeAt(i);\n\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n\n const combined = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n\n return combined.toString(36);\n}\n\n/**\n * Compute the stable VCR request hash for a model call.\n *\n * The hash covers the full `messages` array plus the picked, normalized\n * `options` subset (see {@link DEFAULT_HASH_OPTIONS}). Inputs are\n * canonicalized (recursive key sort) before serialization so property order\n * never affects the result. `signal` and unknown provider keys are excluded.\n *\n * @example\n * const a = hashRequest(messages, { temperature: 0.2 });\n * const b = hashRequest(messages, { temperature: 0.2, signal });\n * // a === b — signal is excluded.\n */\nexport function hashRequest(\n messages: Message[],\n options?: ModelCallOptions,\n hashOptions: readonly string[] = DEFAULT_HASH_OPTIONS,\n): string {\n const payload = canonicalize({\n messages,\n options: pickOptions(options, hashOptions),\n });\n\n return hashString(JSON.stringify(payload));\n}\n"],"mappings":";;;;;;;AAUA,MAAa,uBAA0C;CACrD;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;AAaA,SAAS,gBAAgB,MAA6C;CACpE,OAAO;EACL,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,OAAO,KAAK,QAAQ,YAAY,KAAK,KAAK,IAAI;CAChD;AACF;;;;;;;AAQA,SAAS,YAAY,OAAgB,QAAQ,GAAY;CACvD,IAAI,QAAQ,KAAK,UAAU,QAAQ,OAAO,UAAU,UAClD,OAAO,OAAO;CAGhB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,YAAY,MAAM,QAAQ,CAAC,CAAC;CAGzD,MAAM,MAA+B,CAAC;CAEtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,
|
|
1
|
+
{"version":3,"file":"hash-request.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/hash-request.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type { ModelCallOptions } from \"../contracts/model.contract\";\nimport type { ToolConfig } from \"../contracts/tool.contract\";\n\n/**\n * Default `ModelCallOptions` fields folded into the request hash. These are\n * the inputs that materially change the model's output; everything else\n * (notably `signal` and unknown provider keys) is excluded so an otherwise\n * identical logical call still matches its recording.\n */\nexport const DEFAULT_HASH_OPTIONS: readonly string[] = [\n \"temperature\",\n \"maxTokens\",\n \"responseSchema\",\n \"tools\",\n \"reasoning\",\n];\n\n/**\n * Reduce a tool to the parts the model actually conditions on: its name,\n * description, and the *shape* of its input schema. Two tools that differ\n * only by object identity (a fresh schema instance per import) hash\n * identically; a real contract change (renamed field, new description)\n * invalidates the recording.\n *\n * The input schema is fingerprinted structurally — a Standard Schema is an\n * opaque object, so we serialize its enumerable own keys rather than\n * attempting to read its internals.\n */\nfunction fingerprintTool(tool: ToolConfig<unknown, unknown>): unknown {\n return {\n name: tool.name,\n description: tool.description,\n input: tool.input ? schemaShape(tool.input) : undefined,\n };\n}\n\n/**\n * Produce a stable, JSON-safe fingerprint of an arbitrary schema object.\n * Records only the structural skeleton (own enumerable keys, recursively)\n * so harmless instance differences don't perturb the hash while a genuine\n * structural change does.\n */\nfunction schemaShape(value: unknown, depth = 0): unknown {\n if (depth > 6 || value === null || typeof value !== \"object\") {\n return typeof value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => schemaShape(item, depth + 1));\n }\n\n const out: Record<string, unknown> = {};\n\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n out[key] = schemaShape((value as Record<string, unknown>)[key], depth + 1);\n }\n\n return out;\n}\n\n/**\n * Pick the hashable subset of `options`, normalizing `tools` into their\n * name+description+schema-shape fingerprint. `signal` and any field not in\n * `hashOptions` are dropped.\n */\nfunction pickOptions(\n options: ModelCallOptions | undefined,\n hashOptions: readonly string[],\n): Record<string, unknown> {\n if (!options) {\n return {};\n }\n\n const picked: Record<string, unknown> = {};\n\n for (const key of hashOptions) {\n const value = options[key];\n\n if (value === undefined) {\n continue;\n }\n\n if (key === \"tools\" && Array.isArray(value)) {\n picked[key] = (value as ToolConfig<unknown, unknown>[]).map(fingerprintTool);\n continue;\n }\n\n picked[key] = value;\n }\n\n return picked;\n}\n\n/**\n * Recursively sort object keys so two logically-equal payloads serialize to\n * byte-identical JSON regardless of property insertion order.\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n const out: Record<string, unknown> = {};\n\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n out[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n\n return out;\n}\n\n/**\n * Non-cryptographic 53-bit string hash (FNV-style, cyrb53). Deterministic\n * across runs and platforms; collision-resistant enough for a per-cassette\n * keyspace. Returned as a base-36 string.\n */\nfunction hashString(input: string): string {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n\n for (let i = 0; i < input.length; i++) {\n const ch = input.charCodeAt(i);\n\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n\n const combined = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n\n return combined.toString(36);\n}\n\n/**\n * Compute the stable VCR request hash for a model call.\n *\n * The hash covers the full `messages` array plus the picked, normalized\n * `options` subset (see {@link DEFAULT_HASH_OPTIONS}). Inputs are\n * canonicalized (recursive key sort) before serialization so property order\n * never affects the result. `signal` and unknown provider keys are excluded.\n *\n * @example\n * const a = hashRequest(messages, { temperature: 0.2 });\n * const b = hashRequest(messages, { temperature: 0.2, signal });\n * // a === b — signal is excluded.\n */\nexport function hashRequest(\n messages: Message[],\n options?: ModelCallOptions,\n hashOptions: readonly string[] = DEFAULT_HASH_OPTIONS,\n): string {\n const payload = canonicalize({\n messages,\n options: pickOptions(options, hashOptions),\n });\n\n return hashString(JSON.stringify(payload));\n}\n"],"mappings":";;;;;;;AAUA,MAAa,uBAA0C;CACrD;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;AAaA,SAAS,gBAAgB,MAA6C;CACpE,OAAO;EACL,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,OAAO,KAAK,QAAQ,YAAY,KAAK,KAAK,IAAI;CAChD;AACF;;;;;;;AAQA,SAAS,YAAY,OAAgB,QAAQ,GAAY;CACvD,IAAI,QAAQ,KAAK,UAAU,QAAQ,OAAO,UAAU,UAClD,OAAO,OAAO;CAGhB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,YAAY,MAAM,QAAQ,CAAC,CAAC;CAGzD,MAAM,MAA+B,CAAC;CAEtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,CAAC,CAAC,KAAK,GACnE,IAAI,OAAO,YAAa,MAAkC,MAAM,QAAQ,CAAC;CAG3E,OAAO;AACT;;;;;;AAOA,SAAS,YACP,SACA,aACyB;CACzB,IAAI,CAAC,SACH,OAAO,CAAC;CAGV,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,OAAO,aAAa;EAC7B,MAAM,QAAQ,QAAQ;EAEtB,IAAI,UAAU,QACZ;EAGF,IAAI,QAAQ,WAAW,MAAM,QAAQ,KAAK,GAAG;GAC3C,OAAO,OAAQ,MAAyC,IAAI,eAAe;GAC3E;EACF;EAEA,OAAO,OAAO;CAChB;CAEA,OAAO;AACT;;;;;AAMA,SAAS,aAAa,OAAyB;CAC7C,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,YAAY;CAG/B,MAAM,MAA+B,CAAC;CAEtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,CAAC,CAAC,KAAK,GACnE,IAAI,OAAO,aAAc,MAAkC,IAAI;CAGjE,OAAO;AACT;;;;;;AAOA,SAAS,WAAW,OAAuB;CACzC,IAAI,KAAK;CACT,IAAI,KAAK;CAET,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,KAAK,MAAM,WAAW,CAAC;EAE7B,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;EAClC,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;CACpC;CAEA,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC5C,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAI5C,QAFiB,cAAc,UAAU,OAAO,OAAO,GAExC,CAAC,SAAS,EAAE;AAC7B;;;;;;;;;;;;;;AAeA,SAAgB,YACd,UACA,SACA,cAAiC,sBACzB;CACR,MAAM,UAAU,aAAa;EAC3B;EACA,SAAS,YAAY,SAAS,WAAW;CAC3C,CAAC;CAED,OAAO,WAAW,KAAK,UAAU,OAAO,CAAC;AAC3C"}
|
package/esm/vcr/vcr.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/vcr.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n ModelCallOptions,\n ModelCapabilities,\n ModelContract,\n ModelResponse,\n ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport { redact } from \"../security/redact\";\nimport { emptyCassette, loadCassette, saveCassette } from \"./cassette-io\";\nimport { VcrCassetteMissError } from \"./errors\";\nimport { DEFAULT_HASH_OPTIONS, hashRequest } from \"./hash-request\";\nimport type { Cassette, CassetteEntry, VcrMode, VcrModel, VcrOptions } from \"./vcr.type\";\n\n/**\n * Internal decorator that wraps an inner `ModelContract`, intercepting only\n * `complete()`/`stream()` and delegating every identity getter to the inner\n * model. Drives the record/replay state machine over a single in-memory\n * {@link Cassette}.\n *\n * **Why a class.** It holds mutable per-instance state (the loaded cassette,\n * the dirty flag, the load promise) behind a stable `ModelContract` surface;\n * the public API is the `vcr()` factory, never `new`.\n */\nclass Vcr implements VcrModel {\n private readonly mode: VcrMode;\n private readonly path: string;\n private readonly hashOptions: readonly string[];\n\n /** Loaded + newly recorded entries. Mutated in place as we record. */\n private loadedCassette: Cassette;\n\n /** Set when an entry is recorded so `save()` knows there's work to flush. */\n private dirty = false;\n\n /** One-shot lazy load of the on-disk cassette, shared across calls. */\n private loadPromise: Promise<void> | undefined;\n\n /** Persisted-body privacy controls (S2). */\n private readonly recordRequest: NonNullable<VcrOptions[\"recordRequest\"]>;\n private readonly redactRequestHook: VcrOptions[\"redactRequest\"];\n private readonly redactResponseHook: VcrOptions[\"redactResponse\"];\n private readonly redactErrorHook: VcrOptions[\"redactError\"];\n\n /** Verbatim-recording warning fires at most once per instance. */\n private warnedVerbatim = false;\n\n public constructor(\n private readonly inner: ModelContract,\n options: VcrOptions,\n ) {\n this.path = options.path;\n this.mode = options.mode ?? \"auto\";\n this.hashOptions = options.hashOptions ?? DEFAULT_HASH_OPTIONS;\n this.recordRequest = options.recordRequest ?? \"verbatim\";\n this.redactRequestHook = options.redactRequest;\n this.redactResponseHook = options.redactResponse;\n this.redactErrorHook = options.redactError;\n this.loadedCassette = emptyCassette(inner.name, inner.provider);\n }\n\n /** Inner model identifier — delegated verbatim. */\n public get name(): string {\n return this.inner.name;\n }\n\n /** Inner provider — delegated verbatim. */\n public get provider(): string {\n return this.inner.provider;\n }\n\n /** Inner capability flags — delegated verbatim. */\n public get capabilities(): ModelCapabilities | undefined {\n return this.inner.capabilities;\n }\n\n /** Inner pricing — delegated verbatim so cost accounting is unchanged. */\n public get pricing(): ModelPricing | undefined {\n return this.inner.pricing;\n }\n\n /** Loaded/recorded cassette, exposed for assertions. */\n public get cassette(): Cassette {\n return this.loadedCassette;\n }\n\n /**\n * Load the on-disk cassette exactly once. Pure `record` mode skips the\n * read — it always writes fresh — but the in-memory cassette still starts\n * empty so a record run never accidentally replays a stale entry.\n */\n private async ensureLoaded(): Promise<void> {\n if (this.loadPromise) {\n return this.loadPromise;\n }\n\n this.loadPromise =\n this.mode === \"record\"\n ? Promise.resolve()\n : (async () => {\n this.loadedCassette = await loadCassette(\n this.path,\n this.inner.name,\n this.inner.provider,\n );\n })();\n\n return this.loadPromise;\n }\n\n /** Find a recorded entry whose hash matches the current request. */\n private findEntry(hash: string): CassetteEntry | undefined {\n return this.loadedCassette.entries.find((entry) => entry.requestHash === hash);\n }\n\n /** Re-throw a recorded error by reconstructing a plain `Error`. */\n private throwRecordedError(entry: CassetteEntry): never {\n const error = new Error(entry.error?.message ?? \"Recorded error\");\n\n error.name = entry.error?.name ?? \"Error\";\n\n throw error;\n }\n\n /**\n * Non-streaming call. In `replay` a miss throws; in `auto`/`record` a miss\n * calls the inner model and records the outcome (response or error).\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n await this.ensureLoaded();\n\n const hash = hashRequest(messages, options, this.hashOptions);\n\n if (this.mode !== \"record\") {\n const entry = this.findEntry(hash);\n\n if (entry) {\n if (entry.error) {\n this.throwRecordedError(entry);\n }\n\n if (entry.response) {\n return entry.response;\n }\n }\n\n if (this.mode === \"replay\") {\n throw new VcrCassetteMissError(\n `No cassette entry for this request (model \"${this.inner.name}\", hash ${hash}).`,\n { requestHash: hash, path: this.path },\n );\n }\n }\n\n try {\n const response = await this.inner.complete(messages, options);\n\n this.record({ requestHash: hash, request: { messages, options }, response });\n\n return response;\n } catch (error) {\n this.record({\n requestHash: hash,\n request: { messages, options },\n error: { name: (error as Error).name, message: (error as Error).message },\n });\n\n throw error;\n }\n }\n\n /**\n * Streaming call. On replay the stored `chunks` are re-yielded in order\n * (reproducing the `delta`/`tool-call`/`done` sequence) or the stored\n * error is re-thrown. On record the inner stream is buffered into\n * `chunks[]` while being re-emitted, then recorded once exhausted.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n await this.ensureLoaded();\n\n const hash = hashRequest(messages, options, this.hashOptions);\n\n if (this.mode !== \"record\") {\n const entry = this.findEntry(hash);\n\n if (entry) {\n if (entry.error) {\n this.throwRecordedError(entry);\n }\n\n if (entry.chunks) {\n for (const chunk of entry.chunks) {\n yield chunk;\n }\n\n return;\n }\n }\n\n if (this.mode === \"replay\") {\n throw new VcrCassetteMissError(\n `No cassette entry for this request (model \"${this.inner.name}\", hash ${hash}).`,\n { requestHash: hash, path: this.path },\n );\n }\n }\n\n const chunks: ModelStreamChunk[] = [];\n\n try {\n for await (const chunk of this.inner.stream(messages, options)) {\n chunks.push(chunk);\n\n yield chunk;\n }\n } catch (error) {\n this.record({\n requestHash: hash,\n request: { messages, options },\n error: { name: (error as Error).name, message: (error as Error).message },\n });\n\n throw error;\n }\n\n this.record({ requestHash: hash, request: { messages, options }, chunks });\n }\n\n /**\n * Append an entry to the in-memory cassette and mark it dirty, applying\n * the configured request/response/error redaction first (S2). Pure\n * `replay` never reaches this path, so no replay run is ever dirtied.\n */\n private record(entry: CassetteEntry): void {\n this.loadedCassette.entries.push(this.applyRedaction(entry));\n this.dirty = true;\n this.maybeWarnVerbatim();\n }\n\n /**\n * Apply the persisted-body privacy controls to an entry before it is\n * stored. The request body follows `recordRequest`; response/error\n * redactors are applied only when supplied. Replay matching is by the\n * recomputed hash (kept verbatim), so none of this affects replay.\n */\n private applyRedaction(entry: CassetteEntry): CassetteEntry {\n const out: CassetteEntry = {\n requestHash: entry.requestHash,\n request: entry.request,\n };\n\n if (this.recordRequest === \"hash-only\") {\n out.request = { messages: [] };\n } else if (this.recordRequest === \"redacted\") {\n out.request = this.redactRequestHook\n ? this.redactRequestHook(entry.request)\n : redact(entry.request);\n }\n\n if (entry.response) {\n out.response = this.redactResponseHook\n ? this.redactResponseHook(entry.response)\n : entry.response;\n }\n if (entry.chunks) {\n out.chunks = entry.chunks;\n }\n if (entry.error) {\n out.error = this.redactErrorHook\n ? this.redactErrorHook(entry.error)\n : entry.error;\n }\n\n return out;\n }\n\n /**\n * Warn once (outside tests) when the cassette is recording verbatim\n * request bodies — they may carry prompts, tool args, and PII, so the\n * file is not safe to commit until sanitized.\n */\n private maybeWarnVerbatim(): void {\n if (this.warnedVerbatim || this.recordRequest !== \"verbatim\") return;\n if (process.env.VITEST || process.env.NODE_ENV === \"test\") return;\n\n this.warnedVerbatim = true;\n console.warn(\n `[warlock-ai] VCR is recording verbatim request bodies to \"${this.path}\" — prompts, tool args, and any PII are stored unredacted. ` +\n 'Sanitize before committing, or set recordRequest: \"redacted\" | \"hash-only\".',\n );\n }\n\n /**\n * Flush newly recorded entries to `path`. No-op when nothing was recorded\n * (pure replay, or a record/auto run that only ever hit cached entries).\n */\n public async save(): Promise<void> {\n if (!this.dirty) {\n return;\n }\n\n await saveCassette(this.path, this.loadedCassette);\n this.dirty = false;\n }\n}\n\n/**\n * Wrap any `ModelContract` in a record/replay decorator backed by a JSON\n * cassette on disk.\n *\n * **What it does.** Intercepts only `complete()`/`stream()` — the single\n * seam every agent trip funnels through — and delegates `name`, `provider`,\n * `capabilities`, and `pricing` to the inner model untouched. On a call it\n * computes a stable hash over `{ messages, picked options }` and, depending\n * on `mode`:\n *\n * - **`record`** — always calls the inner model and appends a cassette entry.\n * - **`replay`** — returns the matching entry (or re-yields its chunks /\n * re-throws its error); a miss throws `VcrCassetteMissError`, never a live\n * call.\n * - **`auto`** (default) — replays a hit, records a miss.\n *\n * Composes *below* `fallbackModel` and works with any adapter because it\n * depends only on `ModelContract`. Call `save()` to flush new entries.\n *\n * @example\n * const model = vcr(liveModel, { path: \"./cassettes/support.json\" });\n * const response = await model.complete(messages);\n * await model.save(); // first run records; later runs replay deterministically.\n */\nexport function vcr(model: ModelContract, options: VcrOptions): VcrModel {\n return new Vcr(model, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vcr.mjs","names":[],"sources":["../../../../../../../ai/src/vcr/vcr.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n ModelCallOptions,\n ModelCapabilities,\n ModelContract,\n ModelResponse,\n ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport { redact } from \"../security/redact\";\nimport { emptyCassette, loadCassette, saveCassette } from \"./cassette-io\";\nimport { VcrCassetteMissError } from \"./errors\";\nimport { DEFAULT_HASH_OPTIONS, hashRequest } from \"./hash-request\";\nimport type { Cassette, CassetteEntry, VcrMode, VcrModel, VcrOptions } from \"./vcr.type\";\n\n/**\n * Internal decorator that wraps an inner `ModelContract`, intercepting only\n * `complete()`/`stream()` and delegating every identity getter to the inner\n * model. Drives the record/replay state machine over a single in-memory\n * {@link Cassette}.\n *\n * **Why a class.** It holds mutable per-instance state (the loaded cassette,\n * the dirty flag, the load promise) behind a stable `ModelContract` surface;\n * the public API is the `vcr()` factory, never `new`.\n */\nclass Vcr implements VcrModel {\n private readonly mode: VcrMode;\n private readonly path: string;\n private readonly hashOptions: readonly string[];\n\n /** Loaded + newly recorded entries. Mutated in place as we record. */\n private loadedCassette: Cassette;\n\n /** Set when an entry is recorded so `save()` knows there's work to flush. */\n private dirty = false;\n\n /** One-shot lazy load of the on-disk cassette, shared across calls. */\n private loadPromise: Promise<void> | undefined;\n\n /** Persisted-body privacy controls (S2). */\n private readonly recordRequest: NonNullable<VcrOptions[\"recordRequest\"]>;\n private readonly redactRequestHook: VcrOptions[\"redactRequest\"];\n private readonly redactResponseHook: VcrOptions[\"redactResponse\"];\n private readonly redactErrorHook: VcrOptions[\"redactError\"];\n\n /** Verbatim-recording warning fires at most once per instance. */\n private warnedVerbatim = false;\n\n public constructor(\n private readonly inner: ModelContract,\n options: VcrOptions,\n ) {\n this.path = options.path;\n this.mode = options.mode ?? \"auto\";\n this.hashOptions = options.hashOptions ?? DEFAULT_HASH_OPTIONS;\n this.recordRequest = options.recordRequest ?? \"verbatim\";\n this.redactRequestHook = options.redactRequest;\n this.redactResponseHook = options.redactResponse;\n this.redactErrorHook = options.redactError;\n this.loadedCassette = emptyCassette(inner.name, inner.provider);\n }\n\n /** Inner model identifier — delegated verbatim. */\n public get name(): string {\n return this.inner.name;\n }\n\n /** Inner provider — delegated verbatim. */\n public get provider(): string {\n return this.inner.provider;\n }\n\n /** Inner capability flags — delegated verbatim. */\n public get capabilities(): ModelCapabilities | undefined {\n return this.inner.capabilities;\n }\n\n /** Inner pricing — delegated verbatim so cost accounting is unchanged. */\n public get pricing(): ModelPricing | undefined {\n return this.inner.pricing;\n }\n\n /** Loaded/recorded cassette, exposed for assertions. */\n public get cassette(): Cassette {\n return this.loadedCassette;\n }\n\n /**\n * Load the on-disk cassette exactly once. Pure `record` mode skips the\n * read — it always writes fresh — but the in-memory cassette still starts\n * empty so a record run never accidentally replays a stale entry.\n */\n private async ensureLoaded(): Promise<void> {\n if (this.loadPromise) {\n return this.loadPromise;\n }\n\n this.loadPromise =\n this.mode === \"record\"\n ? Promise.resolve()\n : (async () => {\n this.loadedCassette = await loadCassette(\n this.path,\n this.inner.name,\n this.inner.provider,\n );\n })();\n\n return this.loadPromise;\n }\n\n /** Find a recorded entry whose hash matches the current request. */\n private findEntry(hash: string): CassetteEntry | undefined {\n return this.loadedCassette.entries.find((entry) => entry.requestHash === hash);\n }\n\n /** Re-throw a recorded error by reconstructing a plain `Error`. */\n private throwRecordedError(entry: CassetteEntry): never {\n const error = new Error(entry.error?.message ?? \"Recorded error\");\n\n error.name = entry.error?.name ?? \"Error\";\n\n throw error;\n }\n\n /**\n * Non-streaming call. In `replay` a miss throws; in `auto`/`record` a miss\n * calls the inner model and records the outcome (response or error).\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n await this.ensureLoaded();\n\n const hash = hashRequest(messages, options, this.hashOptions);\n\n if (this.mode !== \"record\") {\n const entry = this.findEntry(hash);\n\n if (entry) {\n if (entry.error) {\n this.throwRecordedError(entry);\n }\n\n if (entry.response) {\n return entry.response;\n }\n }\n\n if (this.mode === \"replay\") {\n throw new VcrCassetteMissError(\n `No cassette entry for this request (model \"${this.inner.name}\", hash ${hash}).`,\n { requestHash: hash, path: this.path },\n );\n }\n }\n\n try {\n const response = await this.inner.complete(messages, options);\n\n this.record({ requestHash: hash, request: { messages, options }, response });\n\n return response;\n } catch (error) {\n this.record({\n requestHash: hash,\n request: { messages, options },\n error: { name: (error as Error).name, message: (error as Error).message },\n });\n\n throw error;\n }\n }\n\n /**\n * Streaming call. On replay the stored `chunks` are re-yielded in order\n * (reproducing the `delta`/`tool-call`/`done` sequence) or the stored\n * error is re-thrown. On record the inner stream is buffered into\n * `chunks[]` while being re-emitted, then recorded once exhausted.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n await this.ensureLoaded();\n\n const hash = hashRequest(messages, options, this.hashOptions);\n\n if (this.mode !== \"record\") {\n const entry = this.findEntry(hash);\n\n if (entry) {\n if (entry.error) {\n this.throwRecordedError(entry);\n }\n\n if (entry.chunks) {\n for (const chunk of entry.chunks) {\n yield chunk;\n }\n\n return;\n }\n }\n\n if (this.mode === \"replay\") {\n throw new VcrCassetteMissError(\n `No cassette entry for this request (model \"${this.inner.name}\", hash ${hash}).`,\n { requestHash: hash, path: this.path },\n );\n }\n }\n\n const chunks: ModelStreamChunk[] = [];\n\n try {\n for await (const chunk of this.inner.stream(messages, options)) {\n chunks.push(chunk);\n\n yield chunk;\n }\n } catch (error) {\n this.record({\n requestHash: hash,\n request: { messages, options },\n error: { name: (error as Error).name, message: (error as Error).message },\n });\n\n throw error;\n }\n\n this.record({ requestHash: hash, request: { messages, options }, chunks });\n }\n\n /**\n * Append an entry to the in-memory cassette and mark it dirty, applying\n * the configured request/response/error redaction first (S2). Pure\n * `replay` never reaches this path, so no replay run is ever dirtied.\n */\n private record(entry: CassetteEntry): void {\n this.loadedCassette.entries.push(this.applyRedaction(entry));\n this.dirty = true;\n this.maybeWarnVerbatim();\n }\n\n /**\n * Apply the persisted-body privacy controls to an entry before it is\n * stored. The request body follows `recordRequest`; response/error\n * redactors are applied only when supplied. Replay matching is by the\n * recomputed hash (kept verbatim), so none of this affects replay.\n */\n private applyRedaction(entry: CassetteEntry): CassetteEntry {\n const out: CassetteEntry = {\n requestHash: entry.requestHash,\n request: entry.request,\n };\n\n if (this.recordRequest === \"hash-only\") {\n out.request = { messages: [] };\n } else if (this.recordRequest === \"redacted\") {\n out.request = this.redactRequestHook\n ? this.redactRequestHook(entry.request)\n : redact(entry.request);\n }\n\n if (entry.response) {\n out.response = this.redactResponseHook\n ? this.redactResponseHook(entry.response)\n : entry.response;\n }\n if (entry.chunks) {\n out.chunks = entry.chunks;\n }\n if (entry.error) {\n out.error = this.redactErrorHook\n ? this.redactErrorHook(entry.error)\n : entry.error;\n }\n\n return out;\n }\n\n /**\n * Warn once (outside tests) when the cassette is recording verbatim\n * request bodies — they may carry prompts, tool args, and PII, so the\n * file is not safe to commit until sanitized.\n */\n private maybeWarnVerbatim(): void {\n if (this.warnedVerbatim || this.recordRequest !== \"verbatim\") return;\n if (process.env.VITEST || process.env.NODE_ENV === \"test\") return;\n\n this.warnedVerbatim = true;\n console.warn(\n `[warlock-ai] VCR is recording verbatim request bodies to \"${this.path}\" — prompts, tool args, and any PII are stored unredacted. ` +\n 'Sanitize before committing, or set recordRequest: \"redacted\" | \"hash-only\".',\n );\n }\n\n /**\n * Flush newly recorded entries to `path`. No-op when nothing was recorded\n * (pure replay, or a record/auto run that only ever hit cached entries).\n */\n public async save(): Promise<void> {\n if (!this.dirty) {\n return;\n }\n\n await saveCassette(this.path, this.loadedCassette);\n this.dirty = false;\n }\n}\n\n/**\n * Wrap any `ModelContract` in a record/replay decorator backed by a JSON\n * cassette on disk.\n *\n * **What it does.** Intercepts only `complete()`/`stream()` — the single\n * seam every agent trip funnels through — and delegates `name`, `provider`,\n * `capabilities`, and `pricing` to the inner model untouched. On a call it\n * computes a stable hash over `{ messages, picked options }` and, depending\n * on `mode`:\n *\n * - **`record`** — always calls the inner model and appends a cassette entry.\n * - **`replay`** — returns the matching entry (or re-yields its chunks /\n * re-throws its error); a miss throws `VcrCassetteMissError`, never a live\n * call.\n * - **`auto`** (default) — replays a hit, records a miss.\n *\n * Composes *below* `fallbackModel` and works with any adapter because it\n * depends only on `ModelContract`. Call `save()` to flush new entries.\n *\n * @example\n * const model = vcr(liveModel, { path: \"./cassettes/support.json\" });\n * const response = await model.complete(messages);\n * await model.save(); // first run records; later runs replay deterministically.\n */\nexport function vcr(model: ModelContract, options: VcrOptions): VcrModel {\n return new Vcr(model, options);\n}\n"],"mappings":"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"}
|