@warlock.js/ai 5.1.0 → 5.2.2
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/CHANGELOG.md +4 -0
- package/README.md +4 -0
- package/cjs/index.cjs +1 -1
- package/cjs/index.cjs.map +1 -1
- package/cjs/{magic-string.es-BoSa5xIt.cjs → magic-string.es-BQeqHJ-a.cjs} +22 -17
- package/cjs/magic-string.es-BQeqHJ-a.cjs.map +1 -0
- package/cjs/matcher-logic-07fFOz7r.cjs.map +1 -1
- package/cjs/{matchers-DnV47KR_.cjs → matchers-CINm4ojZ.cjs} +27 -27
- package/cjs/matchers-CINm4ojZ.cjs.map +1 -0
- 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/{@jridgewell → .pnpm/@jridgewell_sourcemap-codec@1.6.0/node_modules/@jridgewell}/sourcemap-codec/dist/sourcemap-codec.mjs +19 -14
- package/esm/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.6.0/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_expect@4.1.10/node_modules/@vitest}/expect/dist/index.mjs +8 -8
- package/esm/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest}/pretty-format/dist/index.mjs +4 -4
- package/esm/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_runner@4.1.10/node_modules/@vitest}/runner/dist/chunk-artifact.mjs +5 -5
- package/esm/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest}/snapshot/dist/index.mjs +7 -7
- package/esm/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_spy@4.1.10/node_modules/@vitest}/spy/dist/index.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/chunk-pathe.M-eThtNZ.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/diff.mjs +4 -4
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/display.mjs +3 -3
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/error.mjs +2 -2
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/error.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/helpers.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/offset.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/serialize.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/source-map.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.mjs.map +1 -0
- package/esm/node_modules/{@vitest → .pnpm/@vitest_utils@4.1.10/node_modules/@vitest}/utils/dist/timers.mjs +1 -1
- package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/timers.mjs.map +1 -0
- package/esm/node_modules/{chai → .pnpm/chai@6.2.2/node_modules/chai}/index.mjs +1 -1
- package/esm/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.mjs.map +1 -0
- package/esm/node_modules/{magic-string → .pnpm/magic-string@0.30.21/node_modules/magic-string}/dist/magic-string.es.mjs +3 -3
- package/esm/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs.map +1 -0
- package/esm/node_modules/{tinyrainbow → .pnpm/tinyrainbow@3.1.1/node_modules/tinyrainbow}/dist/index.mjs +1 -1
- package/esm/node_modules/.pnpm/tinyrainbow@3.1.1/node_modules/tinyrainbow/dist/index.mjs.map +1 -0
- package/esm/node_modules/{vitest → .pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest}/dist/chunks/_commonjsHelpers.D26ty3Ew.mjs +1 -1
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.mjs.map +1 -0
- package/esm/node_modules/{vitest → .pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest}/dist/chunks/rpc.MzXet3jl.mjs +1 -1
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/rpc.MzXet3jl.mjs.map +1 -0
- package/esm/node_modules/{vitest → .pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest}/dist/chunks/test.DNmyFkvJ.mjs +11 -11
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/test.DNmyFkvJ.mjs.map +1 -0
- package/esm/node_modules/{vitest → .pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest}/dist/chunks/utils.BX5Fg8C4.mjs +2 -2
- package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.mjs.map +1 -0
- 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.d.mts.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/matchers.mjs +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/llms-full.txt +2 -0
- package/package.json +7 -4
- package/skills/ai-basics/SKILL.md +2 -0
- package/cjs/magic-string.es-BoSa5xIt.cjs.map +0 -1
- package/cjs/matchers-DnV47KR_.cjs.map +0 -1
- package/esm/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +0 -1
- package/esm/node_modules/@vitest/expect/dist/index.mjs.map +0 -1
- package/esm/node_modules/@vitest/pretty-format/dist/index.mjs.map +0 -1
- package/esm/node_modules/@vitest/runner/dist/chunk-artifact.mjs.map +0 -1
- package/esm/node_modules/@vitest/snapshot/dist/index.mjs.map +0 -1
- package/esm/node_modules/@vitest/spy/dist/index.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/diff.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/display.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/error.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/helpers.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/offset.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/serialize.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/source-map.mjs.map +0 -1
- package/esm/node_modules/@vitest/utils/dist/timers.mjs.map +0 -1
- package/esm/node_modules/chai/index.mjs.map +0 -1
- package/esm/node_modules/magic-string/dist/magic-string.es.mjs.map +0 -1
- package/esm/node_modules/tinyrainbow/dist/index.mjs.map +0 -1
- package/esm/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.mjs.map +0 -1
- package/esm/node_modules/vitest/dist/chunks/rpc.MzXet3jl.mjs.map +0 -1
- package/esm/node_modules/vitest/dist/chunks/test.DNmyFkvJ.mjs.map +0 -1
- package/esm/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.mjs.map +0 -1
- /package/esm/node_modules/{@vitest → .pnpm/@vitest_runner@4.1.10/node_modules/@vitest}/runner/dist/index.mjs +0 -0
- /package/esm/node_modules/{@vitest → .pnpm/@vitest_runner@4.1.10/node_modules/@vitest}/runner/dist/utils.mjs +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-stream-guard.mjs","names":[],"sources":["../../../../../../../ai/src/agent/json-stream-guard.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { ModelToolCallRequest } from \"../contracts/model-tool-call-request.type\";\nimport type { ToolContract } from \"../tool/tool\";\n\n/**\n * Default cap on bytes accumulated in a single suspect buffer before\n * the guard gives up, flushes as text, and resets to pass-through.\n * Real envelope payloads observed in production leaks are well under\n * 1 KB; this is a safety valve against runaway / adversarial input.\n */\nconst DEFAULT_MAX_BUFFER_BYTES = 4096;\n\n/**\n * Fence opener the guard recognizes in pass-through mode. Targets the\n * lowercase form ```` ```json ```` only — that is the form models\n * actually emit in the wild when they fence-wrap a JSON tool envelope.\n * Other languages / casings flush as plain text.\n */\nconst FENCE_OPENER = \"```json\";\n\n/**\n * Closing fence sequence inside `bufferingFence` mode. Three backticks,\n * no language tag.\n */\nconst FENCE_CLOSER = \"```\";\n\n/**\n * Options passed when constructing a `JsonStreamGuard`.\n *\n * The guard is deliberately framework-agnostic of *how* deltas are\n * emitted or how recovered calls are dispatched — callers wire those\n * via `onSafeDelta` / `onRecoveredCall`. This keeps the unit-testable\n * surface tiny and lets the agent loop own all event-emission policy.\n */\nexport type JsonStreamGuardOptions = {\n /** Tools the agent has registered for this trip. Envelope lookups use `.name`. */\n tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n /**\n * Hard cap on a single suspect buffer's size. When exceeded, the\n * buffer is flushed verbatim as text and the guard returns to\n * pass-through. Defaults to {@link DEFAULT_MAX_BUFFER_BYTES}.\n */\n maxBufferBytes?: number;\n /**\n * Called for every chunk of text that survived the guard — exactly\n * what the consumer should treat as the visible delta. May be\n * called many times per `feed()` call, possibly with a single\n * character or with a multi-character flush.\n */\n onSafeDelta: (delta: string) => void;\n /**\n * Called once per envelope the guard successfully classifies as a\n * tool-call recovery. The request carries `recoveredFrom:\n * \"stream-text\"` so downstream consumers can distinguish synthesized\n * calls from real ones.\n */\n onRecoveredCall: (request: ModelToolCallRequest) => void;\n};\n\n/**\n * Per-trip state machine that intercepts streamed text deltas, detects\n * JSON envelopes the model has emitted as plain text (the\n * tool-call-leakage symptom), and synthesizes real `ModelToolCallRequest`\n * entries for them while suppressing the JSON from visible output.\n *\n * **Role.** A `JsonStreamGuard` is the per-trip implementation of the\n * opt-in `streamingToolGuard` config. It sits between the model\n * adapter's `delta` chunks and the agent's `agent.trip.streaming`\n * emit + `content` accumulator — text that survives the guard is what\n * the consumer sees and what the trip records as `output`.\n *\n * **Responsibility.**\n * - Owns: a small character-level state machine (pass-through,\n * brace-buffering, fence-buffering), string-literal-aware brace\n * tracking, fence-opener / fence-closer detection, named-envelope\n * matching against registered tool schemas, buffer-cap enforcement.\n * - Does NOT own: event emission (delegated via callbacks), tool\n * dispatch, `finishReason` normalization, dedupe vs. real tool\n * calls — the agent loop handles all four.\n *\n * **Matcher tier — named envelope only (v1).** A buffer matches when\n * it parses as a JSON object containing both:\n * - a `name` or `tool` key resolving to a registered tool name, AND\n * - an `arguments` or `input` key whose value validates against the\n * resolved tool's `~standard` schema.\n * Bare-object matching (where any registered tool's schema is the\n * sole signal) is deferred until tool input schemas are tight enough\n * to distinguish — `v.record(v.any())` would match everything.\n *\n * **Per-trip lifecycle.** One instance per trip. The agent loop calls\n * `feed(chunk)` for every `delta` chunk and `finalize()` exactly once\n * after the stream's `done` chunk. Mid-stream cancellation: the loop\n * simply stops calling `feed`; any open buffer is discarded with the\n * guard instance.\n *\n * Modeled as a class (see §4.2 of code-style.md — per-call execution\n * state across phases): the machine has 3 states, accumulators for\n * brace depth, string-literal escape tracking, and a synthesized-call\n * counter for stable ids across the trip.\n *\n * @example\n * // Inside the agent's streaming trip body:\n * const guard = new JsonStreamGuard({\n * tools: this.config.tools ?? [],\n * maxBufferBytes: guardConfig.maxBufferBytes,\n * onSafeDelta: (delta) => {\n * content += delta;\n * this.emit(\"agent.trip.streaming\", { delta, tripIndex });\n * },\n * onRecoveredCall: (request) => recoveredCalls.push(request),\n * });\n *\n * for await (const chunk of model.stream(messages, callOptions)) {\n * if (chunk.type === \"delta\") await guard.feed(chunk.content);\n * // ... other chunk types\n * }\n *\n * await guard.finalize();\n */\nexport class JsonStreamGuard {\n private readonly tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n private readonly maxBufferBytes: number;\n private readonly onSafeDelta: (delta: string) => void;\n private readonly onRecoveredCall: (request: ModelToolCallRequest) => void;\n\n private mode: \"passThrough\" | \"bufferingBrace\" | \"bufferingFence\" = \"passThrough\";\n\n /**\n * Characters held back in pass-through mode while we resolve whether\n * a partial fence opener (`` ` ``, `` `` ``, `` ``` ``, `` ```j ``, …)\n * will complete or break. Always a strict prefix of {@link FENCE_OPENER};\n * emptied (and emitted verbatim) the moment a non-matching character\n * arrives.\n */\n private holdback = \"\";\n\n /**\n * Accumulator while `mode === \"bufferingBrace\"` or `\"bufferingFence\"`.\n * In brace mode it carries the JSON including the outermost `{`/`}`.\n * In fence mode it carries everything between the opener and the\n * closer (the opener and closer themselves are NOT in the buffer —\n * they are reconstructed only on a flush-as-text fallback).\n */\n private buffer = \"\";\n\n /**\n * Brace-depth counter for `bufferingBrace` mode. Increments on `{`,\n * decrements on `}` — but only when {@link inString} is false, so a\n * `{` inside a JSON string literal does not skew the depth. Buffer\n * closes when depth returns to zero.\n */\n private braceDepth = 0;\n\n /** True while the scanner is inside a `\"...\"` JSON string literal. */\n private inString = false;\n\n /**\n * True when the previous character inside a string literal was a\n * backslash, so the current character is escaped (`\\\"` does not end\n * the string; `\\\\` resets the flag without escaping anything else).\n */\n private escapeNext = false;\n\n /**\n * Trailing tail of the fence buffer used to detect the closing\n * ```` ``` ```` sequence. Length capped at the closer length; rotated\n * forward as new characters arrive.\n */\n private fenceCloseTail = \"\";\n\n /**\n * Count of envelopes the guard has successfully synthesized this\n * trip. Used to assign deterministic, collision-free ids on\n * recovered `ModelToolCallRequest` entries.\n */\n private recoveredCount = 0;\n\n public constructor(options: JsonStreamGuardOptions) {\n this.tools = options.tools;\n this.maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n this.onSafeDelta = options.onSafeDelta;\n this.onRecoveredCall = options.onRecoveredCall;\n }\n\n /**\n * Feed the next raw delta from the model. Splits the chunk into\n * characters and runs each through the state machine, awaiting\n * envelope classification whenever a buffer closes mid-chunk.\n *\n * The hot path (pass-through prose with no `{` / `` ` ``) is fully\n * synchronous — `await` here only blocks at buffer-close points,\n * which are rare in normal traffic.\n */\n public async feed(chunk: string): Promise<void> {\n for (let i = 0; i < chunk.length; i++) {\n await this.processChar(chunk[i]);\n }\n }\n\n /**\n * Stream ended. Anything still in the holdback was prose\n * misclassified as a partial fence opener — emit it. Anything still\n * in an open buffer never closed — emit it as text too (a leak\n * truncated mid-flight is still text the user partially saw).\n */\n public async finalize(): Promise<void> {\n if (this.holdback.length > 0) {\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n\n if (this.mode === \"bufferingBrace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n this.flushFenceBufferAsText();\n }\n }\n\n /**\n * True when at least one envelope was recovered this trip. The\n * agent loop reads this to override `finishReason` from `\"stop\"` to\n * `\"tool_calls\"` when the model reported a natural stop but the\n * guard found tool calls hiding in the text channel.\n */\n public hasRecoveredCalls(): boolean {\n return this.recoveredCount > 0;\n }\n\n /**\n * Route a single character based on the current mode. The\n * `passThrough` branch handles holdback expansion / flushing\n * iteratively (no recursion) so a character that \"breaks\" a fence\n * opener can be re-evaluated as a fresh pass-through input in the\n * same call.\n */\n private async processChar(char: string): Promise<void> {\n if (this.mode === \"bufferingBrace\") {\n await this.processBraceChar(char);\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n await this.processFenceChar(char);\n return;\n }\n\n let current = char;\n\n while (true) {\n if (this.holdback.length === 0 && current === \"{\") {\n this.openBraceBuffer(current);\n return;\n }\n\n const extended = this.holdback + current;\n\n if (this.isFenceOpenerPrefix(extended)) {\n this.holdback = extended;\n\n if (extended === FENCE_OPENER) {\n this.openFenceBuffer();\n }\n\n return;\n }\n\n if (this.holdback.length === 0) {\n this.onSafeDelta(current);\n return;\n }\n\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n }\n\n /**\n * Recognize any strict prefix of {@link FENCE_OPENER} including the\n * full string. Used to decide whether to keep extending the holdback\n * or flush it as plain text.\n */\n private isFenceOpenerPrefix(candidate: string): boolean {\n return candidate.length <= FENCE_OPENER.length && FENCE_OPENER.startsWith(candidate);\n }\n\n /**\n * Enter `bufferingBrace` mode with the seed `{` as the first buffer\n * character and the initial brace depth set to one. Any holdback at\n * this point was already a non-fence sequence so it stays empty.\n */\n private openBraceBuffer(seed: string): void {\n this.mode = \"bufferingBrace\";\n this.buffer = seed;\n this.braceDepth = 1;\n this.inString = false;\n this.escapeNext = false;\n }\n\n /**\n * Enter `bufferingFence` mode immediately after the opener\n * ```` ```json ```` matched in the holdback. Holdback resets;\n * subsequent characters accumulate into the buffer until the\n * closing fence is seen.\n */\n private openFenceBuffer(): void {\n this.mode = \"bufferingFence\";\n this.buffer = \"\";\n this.fenceCloseTail = \"\";\n this.holdback = \"\";\n }\n\n /**\n * Process one character while accumulating a brace-delimited JSON\n * object. Tracks string-literal context so `{` / `}` inside `\"...\"`\n * do not skew brace depth. Closes (and classifies) on balanced\n * braces; flushes-as-text on cap overflow.\n */\n private async processBraceChar(char: string): Promise<void> {\n this.buffer += char;\n\n if (this.inString) {\n if (this.escapeNext) {\n this.escapeNext = false;\n return;\n }\n\n if (char === \"\\\\\") {\n this.escapeNext = true;\n return;\n }\n\n if (char === '\"') {\n this.inString = false;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === '\"') {\n this.inString = true;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"{\") {\n this.braceDepth++;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"}\") {\n this.braceDepth--;\n\n if (this.braceDepth === 0) {\n await this.closeBraceBuffer();\n return;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n this.guardBufferCap(\"brace\");\n }\n\n /**\n * Process one character while accumulating a fence-delimited JSON\n * block. The closing fence ```` ``` ```` ends the block; the closing\n * characters are NOT included in the classified buffer (they are\n * re-emitted only when the block flushes back to text).\n */\n private async processFenceChar(char: string): Promise<void> {\n this.fenceCloseTail += char;\n\n if (this.fenceCloseTail.length > FENCE_CLOSER.length) {\n this.fenceCloseTail = this.fenceCloseTail.slice(-FENCE_CLOSER.length);\n }\n\n if (this.fenceCloseTail === FENCE_CLOSER) {\n const innerLength = this.buffer.length - (FENCE_CLOSER.length - 1);\n this.buffer = this.buffer.slice(0, Math.max(0, innerLength));\n\n await this.closeFenceBuffer();\n return;\n }\n\n this.buffer += char;\n this.guardBufferCap(\"fence\");\n }\n\n /**\n * Enforce the buffer-byte cap. When the current buffer exceeds the\n * cap, flush it back to the consumer as plain text and reset to\n * pass-through. Acts as a runaway / adversarial-input safety valve.\n */\n private guardBufferCap(source: \"brace\" | \"fence\"): void {\n if (this.buffer.length <= this.maxBufferBytes) {\n return;\n }\n\n if (source === \"brace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n this.flushFenceBufferAsText();\n }\n\n /**\n * Run the envelope matcher against the closed brace buffer. On a\n * match, synthesize a recovered `ModelToolCallRequest`; on no\n * match, flush the buffer back as plain text. Resets state to\n * pass-through either way.\n */\n private async closeBraceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(closed);\n }\n\n /**\n * Run the envelope matcher against the closed fence buffer. On a\n * match, synthesize a recovered call; on no match, flush as text\n * **with** the original opener and closer reconstructed so the\n * customer sees exactly the markdown the model emitted.\n */\n private async closeFenceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(`${FENCE_OPENER}${closed}${FENCE_CLOSER}`);\n }\n\n /**\n * Emit the brace-buffer verbatim as text and reset to pass-through.\n * Used on cap overflow and on `finalize()` for an unclosed buffer.\n */\n private flushBraceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(closed);\n }\n\n /**\n * Emit the fence-buffer verbatim as text, reconstructing the\n * opener and closer so the original markdown structure is\n * preserved for the consumer.\n */\n private flushFenceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(`${FENCE_OPENER}${closed}`);\n }\n\n /**\n * Reset all per-buffer state back to the pass-through baseline.\n * Called whenever a buffer closes — by recovery, by flush, or by\n * cap overflow — so the next character starts a fresh scan.\n */\n private resetToPassThrough(): void {\n this.mode = \"passThrough\";\n this.buffer = \"\";\n this.braceDepth = 0;\n this.inString = false;\n this.escapeNext = false;\n this.fenceCloseTail = \"\";\n }\n\n /**\n * Attempt to classify a closed buffer as a tool-call envelope. On\n * success, invoke `onRecoveredCall` with a synthesized request and\n * return `true`; on failure return `false` so the caller can flush\n * the buffer back as text.\n */\n private async tryMatchEnvelope(raw: string): Promise<boolean> {\n const parsed = safeParseJson(raw);\n\n if (parsed === undefined || typeof parsed !== \"object\" || parsed === null) {\n return false;\n }\n\n const envelope = parsed as Record<string, unknown>;\n const candidateName = readString(envelope, \"name\") ?? readString(envelope, \"tool\");\n const candidateInput = readObject(envelope, \"arguments\") ?? readObject(envelope, \"input\");\n\n if (!candidateName || !candidateInput) {\n return false;\n }\n\n const tool = this.tools.find((entry) => entry.name === candidateName);\n\n if (!tool || !tool.input) {\n return false;\n }\n\n const schema = tool.input as StandardSchemaV1<unknown>;\n\n let validationResult: StandardSchemaV1.Result<unknown>;\n\n try {\n validationResult = await schema[\"~standard\"].validate(candidateInput);\n } catch {\n return false;\n }\n\n if (validationResult.issues) {\n return false;\n }\n\n this.recoveredCount++;\n\n this.onRecoveredCall({\n id: `synth_${candidateName}_${this.recoveredCount}`,\n name: candidateName,\n input: validationResult.value,\n recoveredFrom: \"stream-text\",\n });\n\n return true;\n }\n}\n\n/**\n * Parse a JSON string returning `undefined` on any failure. Local to\n * the guard so it can distinguish \"not JSON\" from a parsed `null`\n * value, which `safeJsonParse` cannot — a parsed `null` is a valid\n * JSON value but not a valid envelope, and we want the difference.\n */\nfunction safeParseJson(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Read a string-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is non-string —\n * the matcher rejects either case.\n */\nfunction readString(envelope: Record<string, unknown>, key: string): string | undefined {\n const value = envelope[key];\n\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/**\n * Read an object-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is not a\n * plain object (rejects arrays, primitives, null) — tool input\n * schemas always validate against an object root.\n */\nfunction readObject(\n envelope: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined {\n const value = envelope[key];\n\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n\n return value as Record<string, unknown>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"json-stream-guard.mjs","names":[],"sources":["../../../../../../../ai/src/agent/json-stream-guard.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { ModelToolCallRequest } from \"../contracts/model-tool-call-request.type\";\nimport type { ToolContract } from \"../tool/tool\";\n\n/**\n * Default cap on bytes accumulated in a single suspect buffer before\n * the guard gives up, flushes as text, and resets to pass-through.\n * Real envelope payloads observed in production leaks are well under\n * 1 KB; this is a safety valve against runaway / adversarial input.\n */\nconst DEFAULT_MAX_BUFFER_BYTES = 4096;\n\n/**\n * Fence opener the guard recognizes in pass-through mode. Targets the\n * lowercase form ```` ```json ```` only — that is the form models\n * actually emit in the wild when they fence-wrap a JSON tool envelope.\n * Other languages / casings flush as plain text.\n */\nconst FENCE_OPENER = \"```json\";\n\n/**\n * Closing fence sequence inside `bufferingFence` mode. Three backticks,\n * no language tag.\n */\nconst FENCE_CLOSER = \"```\";\n\n/**\n * Options passed when constructing a `JsonStreamGuard`.\n *\n * The guard is deliberately framework-agnostic of *how* deltas are\n * emitted or how recovered calls are dispatched — callers wire those\n * via `onSafeDelta` / `onRecoveredCall`. This keeps the unit-testable\n * surface tiny and lets the agent loop own all event-emission policy.\n */\nexport type JsonStreamGuardOptions = {\n /** Tools the agent has registered for this trip. Envelope lookups use `.name`. */\n tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n /**\n * Hard cap on a single suspect buffer's size. When exceeded, the\n * buffer is flushed verbatim as text and the guard returns to\n * pass-through. Defaults to {@link DEFAULT_MAX_BUFFER_BYTES}.\n */\n maxBufferBytes?: number;\n /**\n * Called for every chunk of text that survived the guard — exactly\n * what the consumer should treat as the visible delta. May be\n * called many times per `feed()` call, possibly with a single\n * character or with a multi-character flush.\n */\n onSafeDelta: (delta: string) => void;\n /**\n * Called once per envelope the guard successfully classifies as a\n * tool-call recovery. The request carries `recoveredFrom:\n * \"stream-text\"` so downstream consumers can distinguish synthesized\n * calls from real ones.\n */\n onRecoveredCall: (request: ModelToolCallRequest) => void;\n};\n\n/**\n * Per-trip state machine that intercepts streamed text deltas, detects\n * JSON envelopes the model has emitted as plain text (the\n * tool-call-leakage symptom), and synthesizes real `ModelToolCallRequest`\n * entries for them while suppressing the JSON from visible output.\n *\n * **Role.** A `JsonStreamGuard` is the per-trip implementation of the\n * opt-in `streamingToolGuard` config. It sits between the model\n * adapter's `delta` chunks and the agent's `agent.trip.streaming`\n * emit + `content` accumulator — text that survives the guard is what\n * the consumer sees and what the trip records as `output`.\n *\n * **Responsibility.**\n * - Owns: a small character-level state machine (pass-through,\n * brace-buffering, fence-buffering), string-literal-aware brace\n * tracking, fence-opener / fence-closer detection, named-envelope\n * matching against registered tool schemas, buffer-cap enforcement.\n * - Does NOT own: event emission (delegated via callbacks), tool\n * dispatch, `finishReason` normalization, dedupe vs. real tool\n * calls — the agent loop handles all four.\n *\n * **Matcher tier — named envelope only (v1).** A buffer matches when\n * it parses as a JSON object containing both:\n * - a `name` or `tool` key resolving to a registered tool name, AND\n * - an `arguments` or `input` key whose value validates against the\n * resolved tool's `~standard` schema.\n * Bare-object matching (where any registered tool's schema is the\n * sole signal) is deferred until tool input schemas are tight enough\n * to distinguish — `v.record(v.any())` would match everything.\n *\n * **Per-trip lifecycle.** One instance per trip. The agent loop calls\n * `feed(chunk)` for every `delta` chunk and `finalize()` exactly once\n * after the stream's `done` chunk. Mid-stream cancellation: the loop\n * simply stops calling `feed`; any open buffer is discarded with the\n * guard instance.\n *\n * Modeled as a class (see §4.2 of code-style.md — per-call execution\n * state across phases): the machine has 3 states, accumulators for\n * brace depth, string-literal escape tracking, and a synthesized-call\n * counter for stable ids across the trip.\n *\n * @example\n * // Inside the agent's streaming trip body:\n * const guard = new JsonStreamGuard({\n * tools: this.config.tools ?? [],\n * maxBufferBytes: guardConfig.maxBufferBytes,\n * onSafeDelta: (delta) => {\n * content += delta;\n * this.emit(\"agent.trip.streaming\", { delta, tripIndex });\n * },\n * onRecoveredCall: (request) => recoveredCalls.push(request),\n * });\n *\n * for await (const chunk of model.stream(messages, callOptions)) {\n * if (chunk.type === \"delta\") await guard.feed(chunk.content);\n * // ... other chunk types\n * }\n *\n * await guard.finalize();\n */\nexport class JsonStreamGuard {\n private readonly tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n private readonly maxBufferBytes: number;\n private readonly onSafeDelta: (delta: string) => void;\n private readonly onRecoveredCall: (request: ModelToolCallRequest) => void;\n\n private mode: \"passThrough\" | \"bufferingBrace\" | \"bufferingFence\" = \"passThrough\";\n\n /**\n * Characters held back in pass-through mode while we resolve whether\n * a partial fence opener (`` ` ``, `` `` ``, `` ``` ``, `` ```j ``, …)\n * will complete or break. Always a strict prefix of {@link FENCE_OPENER};\n * emptied (and emitted verbatim) the moment a non-matching character\n * arrives.\n */\n private holdback = \"\";\n\n /**\n * Accumulator while `mode === \"bufferingBrace\"` or `\"bufferingFence\"`.\n * In brace mode it carries the JSON including the outermost `{`/`}`.\n * In fence mode it carries everything between the opener and the\n * closer (the opener and closer themselves are NOT in the buffer —\n * they are reconstructed only on a flush-as-text fallback).\n */\n private buffer = \"\";\n\n /**\n * Brace-depth counter for `bufferingBrace` mode. Increments on `{`,\n * decrements on `}` — but only when {@link inString} is false, so a\n * `{` inside a JSON string literal does not skew the depth. Buffer\n * closes when depth returns to zero.\n */\n private braceDepth = 0;\n\n /** True while the scanner is inside a `\"...\"` JSON string literal. */\n private inString = false;\n\n /**\n * True when the previous character inside a string literal was a\n * backslash, so the current character is escaped (`\\\"` does not end\n * the string; `\\\\` resets the flag without escaping anything else).\n */\n private escapeNext = false;\n\n /**\n * Trailing tail of the fence buffer used to detect the closing\n * ```` ``` ```` sequence. Length capped at the closer length; rotated\n * forward as new characters arrive.\n */\n private fenceCloseTail = \"\";\n\n /**\n * Count of envelopes the guard has successfully synthesized this\n * trip. Used to assign deterministic, collision-free ids on\n * recovered `ModelToolCallRequest` entries.\n */\n private recoveredCount = 0;\n\n public constructor(options: JsonStreamGuardOptions) {\n this.tools = options.tools;\n this.maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n this.onSafeDelta = options.onSafeDelta;\n this.onRecoveredCall = options.onRecoveredCall;\n }\n\n /**\n * Feed the next raw delta from the model. Splits the chunk into\n * characters and runs each through the state machine, awaiting\n * envelope classification whenever a buffer closes mid-chunk.\n *\n * The hot path (pass-through prose with no `{` / `` ` ``) is fully\n * synchronous — `await` here only blocks at buffer-close points,\n * which are rare in normal traffic.\n */\n public async feed(chunk: string): Promise<void> {\n for (let i = 0; i < chunk.length; i++) {\n await this.processChar(chunk[i]);\n }\n }\n\n /**\n * Stream ended. Anything still in the holdback was prose\n * misclassified as a partial fence opener — emit it. Anything still\n * in an open buffer never closed — emit it as text too (a leak\n * truncated mid-flight is still text the user partially saw).\n */\n public async finalize(): Promise<void> {\n if (this.holdback.length > 0) {\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n\n if (this.mode === \"bufferingBrace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n this.flushFenceBufferAsText();\n }\n }\n\n /**\n * True when at least one envelope was recovered this trip. The\n * agent loop reads this to override `finishReason` from `\"stop\"` to\n * `\"tool_calls\"` when the model reported a natural stop but the\n * guard found tool calls hiding in the text channel.\n */\n public hasRecoveredCalls(): boolean {\n return this.recoveredCount > 0;\n }\n\n /**\n * Route a single character based on the current mode. The\n * `passThrough` branch handles holdback expansion / flushing\n * iteratively (no recursion) so a character that \"breaks\" a fence\n * opener can be re-evaluated as a fresh pass-through input in the\n * same call.\n */\n private async processChar(char: string): Promise<void> {\n if (this.mode === \"bufferingBrace\") {\n await this.processBraceChar(char);\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n await this.processFenceChar(char);\n return;\n }\n\n let current = char;\n\n while (true) {\n if (this.holdback.length === 0 && current === \"{\") {\n this.openBraceBuffer(current);\n return;\n }\n\n const extended = this.holdback + current;\n\n if (this.isFenceOpenerPrefix(extended)) {\n this.holdback = extended;\n\n if (extended === FENCE_OPENER) {\n this.openFenceBuffer();\n }\n\n return;\n }\n\n if (this.holdback.length === 0) {\n this.onSafeDelta(current);\n return;\n }\n\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n }\n\n /**\n * Recognize any strict prefix of {@link FENCE_OPENER} including the\n * full string. Used to decide whether to keep extending the holdback\n * or flush it as plain text.\n */\n private isFenceOpenerPrefix(candidate: string): boolean {\n return candidate.length <= FENCE_OPENER.length && FENCE_OPENER.startsWith(candidate);\n }\n\n /**\n * Enter `bufferingBrace` mode with the seed `{` as the first buffer\n * character and the initial brace depth set to one. Any holdback at\n * this point was already a non-fence sequence so it stays empty.\n */\n private openBraceBuffer(seed: string): void {\n this.mode = \"bufferingBrace\";\n this.buffer = seed;\n this.braceDepth = 1;\n this.inString = false;\n this.escapeNext = false;\n }\n\n /**\n * Enter `bufferingFence` mode immediately after the opener\n * ```` ```json ```` matched in the holdback. Holdback resets;\n * subsequent characters accumulate into the buffer until the\n * closing fence is seen.\n */\n private openFenceBuffer(): void {\n this.mode = \"bufferingFence\";\n this.buffer = \"\";\n this.fenceCloseTail = \"\";\n this.holdback = \"\";\n }\n\n /**\n * Process one character while accumulating a brace-delimited JSON\n * object. Tracks string-literal context so `{` / `}` inside `\"...\"`\n * do not skew brace depth. Closes (and classifies) on balanced\n * braces; flushes-as-text on cap overflow.\n */\n private async processBraceChar(char: string): Promise<void> {\n this.buffer += char;\n\n if (this.inString) {\n if (this.escapeNext) {\n this.escapeNext = false;\n return;\n }\n\n if (char === \"\\\\\") {\n this.escapeNext = true;\n return;\n }\n\n if (char === '\"') {\n this.inString = false;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === '\"') {\n this.inString = true;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"{\") {\n this.braceDepth++;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"}\") {\n this.braceDepth--;\n\n if (this.braceDepth === 0) {\n await this.closeBraceBuffer();\n return;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n this.guardBufferCap(\"brace\");\n }\n\n /**\n * Process one character while accumulating a fence-delimited JSON\n * block. The closing fence ```` ``` ```` ends the block; the closing\n * characters are NOT included in the classified buffer (they are\n * re-emitted only when the block flushes back to text).\n */\n private async processFenceChar(char: string): Promise<void> {\n this.fenceCloseTail += char;\n\n if (this.fenceCloseTail.length > FENCE_CLOSER.length) {\n this.fenceCloseTail = this.fenceCloseTail.slice(-FENCE_CLOSER.length);\n }\n\n if (this.fenceCloseTail === FENCE_CLOSER) {\n const innerLength = this.buffer.length - (FENCE_CLOSER.length - 1);\n this.buffer = this.buffer.slice(0, Math.max(0, innerLength));\n\n await this.closeFenceBuffer();\n return;\n }\n\n this.buffer += char;\n this.guardBufferCap(\"fence\");\n }\n\n /**\n * Enforce the buffer-byte cap. When the current buffer exceeds the\n * cap, flush it back to the consumer as plain text and reset to\n * pass-through. Acts as a runaway / adversarial-input safety valve.\n */\n private guardBufferCap(source: \"brace\" | \"fence\"): void {\n if (this.buffer.length <= this.maxBufferBytes) {\n return;\n }\n\n if (source === \"brace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n this.flushFenceBufferAsText();\n }\n\n /**\n * Run the envelope matcher against the closed brace buffer. On a\n * match, synthesize a recovered `ModelToolCallRequest`; on no\n * match, flush the buffer back as plain text. Resets state to\n * pass-through either way.\n */\n private async closeBraceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(closed);\n }\n\n /**\n * Run the envelope matcher against the closed fence buffer. On a\n * match, synthesize a recovered call; on no match, flush as text\n * **with** the original opener and closer reconstructed so the\n * customer sees exactly the markdown the model emitted.\n */\n private async closeFenceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(`${FENCE_OPENER}${closed}${FENCE_CLOSER}`);\n }\n\n /**\n * Emit the brace-buffer verbatim as text and reset to pass-through.\n * Used on cap overflow and on `finalize()` for an unclosed buffer.\n */\n private flushBraceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(closed);\n }\n\n /**\n * Emit the fence-buffer verbatim as text, reconstructing the\n * opener and closer so the original markdown structure is\n * preserved for the consumer.\n */\n private flushFenceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(`${FENCE_OPENER}${closed}`);\n }\n\n /**\n * Reset all per-buffer state back to the pass-through baseline.\n * Called whenever a buffer closes — by recovery, by flush, or by\n * cap overflow — so the next character starts a fresh scan.\n */\n private resetToPassThrough(): void {\n this.mode = \"passThrough\";\n this.buffer = \"\";\n this.braceDepth = 0;\n this.inString = false;\n this.escapeNext = false;\n this.fenceCloseTail = \"\";\n }\n\n /**\n * Attempt to classify a closed buffer as a tool-call envelope. On\n * success, invoke `onRecoveredCall` with a synthesized request and\n * return `true`; on failure return `false` so the caller can flush\n * the buffer back as text.\n */\n private async tryMatchEnvelope(raw: string): Promise<boolean> {\n const parsed = safeParseJson(raw);\n\n if (parsed === undefined || typeof parsed !== \"object\" || parsed === null) {\n return false;\n }\n\n const envelope = parsed as Record<string, unknown>;\n const candidateName = readString(envelope, \"name\") ?? readString(envelope, \"tool\");\n const candidateInput = readObject(envelope, \"arguments\") ?? readObject(envelope, \"input\");\n\n if (!candidateName || !candidateInput) {\n return false;\n }\n\n const tool = this.tools.find((entry) => entry.name === candidateName);\n\n if (!tool || !tool.input) {\n return false;\n }\n\n const schema = tool.input as StandardSchemaV1<unknown>;\n\n let validationResult: StandardSchemaV1.Result<unknown>;\n\n try {\n validationResult = await schema[\"~standard\"].validate(candidateInput);\n } catch {\n return false;\n }\n\n if (validationResult.issues) {\n return false;\n }\n\n this.recoveredCount++;\n\n this.onRecoveredCall({\n id: `synth_${candidateName}_${this.recoveredCount}`,\n name: candidateName,\n input: validationResult.value,\n recoveredFrom: \"stream-text\",\n });\n\n return true;\n }\n}\n\n/**\n * Parse a JSON string returning `undefined` on any failure. Local to\n * the guard so it can distinguish \"not JSON\" from a parsed `null`\n * value, which `safeJsonParse` cannot — a parsed `null` is a valid\n * JSON value but not a valid envelope, and we want the difference.\n */\nfunction safeParseJson(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Read a string-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is non-string —\n * the matcher rejects either case.\n */\nfunction readString(envelope: Record<string, unknown>, key: string): string | undefined {\n const value = envelope[key];\n\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/**\n * Read an object-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is not a\n * plain object (rejects arrays, primitives, null) — tool input\n * schemas always validate against an object root.\n */\nfunction readObject(\n envelope: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined {\n const value = envelope[key];\n\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n\n return value as Record<string, unknown>;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signature.mjs","names":[],"sources":["../../../../../../../ai/src/agent/signature.ts"],"sourcesContent":["import type { AgentConfig } from \"./agent-config.type\";\n\n/**\n * Deterministic structural fingerprint of an agent definition.\n * Persisted on every durable snapshot so `agent.resume()` can detect\n * drift between the saved run and the current definition. Covers the\n * fields whose change would make a mid-run resume unsafe — i.e. would\n * make the persisted `messages` / `toolCalls` array inconsistent with\n * what the resumed trip loop would produce:\n *\n * - Model name + provider — a different model invalidates the prior\n * conversation's continuation.\n * - The sorted tool names — adding / removing / renaming a tool changes\n * which dispatches the persisted `toolCalls` could have come from.\n * - `maxTrips` — the loop bound is a semantic shape change.\n * - Whether a default `output` schema is configured — flips the\n * structured-output instruction baked into the system turn.\n * - `version` — dev-curated; a bump is an explicit \"this changed\" signal.\n *\n * Does NOT cover: system-prompt text, middleware, per-event handlers,\n * placeholders, modelOptions — runtime knobs that don't change the\n * shape of a resumable run. Mirrors `supervisor/signature.ts`'s coarse\n * structural philosophy and reuses its FNV-1a `hash`.\n *\n * `tools` here is read off the resolved config (post-normalization), so\n * raw executables dropped into `tools: []` are already adapted to\n * `ToolContract`s carrying a stable `name`.\n */\nexport function computeAgentSignature(config: {\n name?: string;\n version?: AgentConfig[\"version\"];\n model: { name?: string; provider?: string };\n tools?: ReadonlyArray<{ name: string }>;\n maxTrips?: number;\n output?: unknown;\n}): string {\n const toolNames = (config.tools ?? [])\n .map((tool) => tool.name)\n .sort((a, b) => a.localeCompare(b));\n\n const fingerprint = {\n n: config.name ?? null,\n p: config.model?.provider ?? null,\n m: config.model?.name ?? null,\n t: toolNames,\n x: config.maxTrips ?? null,\n o: config.output ? 1 : 0,\n v: config.version ?? null,\n };\n\n return hash(JSON.stringify(fingerprint));\n}\n\n/**\n * FNV-1a 32-bit — the same hash `supervisor/signature.ts` and\n * `workflow/signature.ts` use. Deterministic, no crypto dependency,\n * cheap; signatures are 8-char hex.\n */\nfunction hash(input: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n\n return h.toString(16).padStart(8, \"0\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,sBAAsB,QAO3B;CACT,MAAM,aAAa,OAAO,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"signature.mjs","names":[],"sources":["../../../../../../../ai/src/agent/signature.ts"],"sourcesContent":["import type { AgentConfig } from \"./agent-config.type\";\n\n/**\n * Deterministic structural fingerprint of an agent definition.\n * Persisted on every durable snapshot so `agent.resume()` can detect\n * drift between the saved run and the current definition. Covers the\n * fields whose change would make a mid-run resume unsafe — i.e. would\n * make the persisted `messages` / `toolCalls` array inconsistent with\n * what the resumed trip loop would produce:\n *\n * - Model name + provider — a different model invalidates the prior\n * conversation's continuation.\n * - The sorted tool names — adding / removing / renaming a tool changes\n * which dispatches the persisted `toolCalls` could have come from.\n * - `maxTrips` — the loop bound is a semantic shape change.\n * - Whether a default `output` schema is configured — flips the\n * structured-output instruction baked into the system turn.\n * - `version` — dev-curated; a bump is an explicit \"this changed\" signal.\n *\n * Does NOT cover: system-prompt text, middleware, per-event handlers,\n * placeholders, modelOptions — runtime knobs that don't change the\n * shape of a resumable run. Mirrors `supervisor/signature.ts`'s coarse\n * structural philosophy and reuses its FNV-1a `hash`.\n *\n * `tools` here is read off the resolved config (post-normalization), so\n * raw executables dropped into `tools: []` are already adapted to\n * `ToolContract`s carrying a stable `name`.\n */\nexport function computeAgentSignature(config: {\n name?: string;\n version?: AgentConfig[\"version\"];\n model: { name?: string; provider?: string };\n tools?: ReadonlyArray<{ name: string }>;\n maxTrips?: number;\n output?: unknown;\n}): string {\n const toolNames = (config.tools ?? [])\n .map((tool) => tool.name)\n .sort((a, b) => a.localeCompare(b));\n\n const fingerprint = {\n n: config.name ?? null,\n p: config.model?.provider ?? null,\n m: config.model?.name ?? null,\n t: toolNames,\n x: config.maxTrips ?? null,\n o: config.output ? 1 : 0,\n v: config.version ?? null,\n };\n\n return hash(JSON.stringify(fingerprint));\n}\n\n/**\n * FNV-1a 32-bit — the same hash `supervisor/signature.ts` and\n * `workflow/signature.ts` use. Deterministic, no crypto dependency,\n * cheap; signatures are 8-char hex.\n */\nfunction hash(input: string): string {\n let h = 0x811c9dc5;\n\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n\n return h.toString(16).padStart(8, \"0\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,sBAAsB,QAO3B;CACT,MAAM,aAAa,OAAO,SAAS,CAAC,GACjC,KAAK,SAAS,KAAK,IAAI,EACvB,MAAM,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;CAEpC,MAAM,cAAc;EAClB,GAAG,OAAO,QAAQ;EAClB,GAAG,OAAO,OAAO,YAAY;EAC7B,GAAG,OAAO,OAAO,QAAQ;EACzB,GAAG;EACH,GAAG,OAAO,YAAY;EACtB,GAAG,OAAO,SAAS,IAAI;EACvB,GAAG,OAAO,WAAW;CACvB;CAEA,OAAO,KAAK,KAAK,UAAU,WAAW,CAAC;AACzC;;;;;;AAOA,SAAS,KAAK,OAAuB;CACnC,IAAI,IAAI;CAER,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,KAAK,MAAM,WAAW,CAAC;EACvB,IAAK,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,SAAU;CACxE;CAEA,OAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../../../../../ai/src/agent/snapshot.ts"],"sourcesContent":["import { resolveDefaultSnapshotStore } from \"../config\";\nimport type {\n AgentResumeOptions,\n} from \"../contracts/agent/agent-options.type\";\nimport type {\n AgentSnapshot,\n AgentSnapshotStatus,\n} from \"../contracts/agent/agent-snapshot.type\";\nimport type { SnapshotStore } from \"../contracts/orchestrator/snapshot-store.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport type { LLMTrip } from \"../contracts/result/llm-trip.type\";\nimport type { ToolCall } from \"../contracts/result/tool-call.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AgentDriftError, AgentExecutionError } from \"../errors\";\n\n/**\n * The agent's `durable` config, narrowed to the fields the snapshot\n * helpers read. Kept minimal so this module doesn't depend on the full\n * resolved-config shape.\n */\nexport type AgentDurableConfig = {\n store?: SnapshotStore<AgentSnapshot>;\n deleteOnComplete?: boolean;\n};\n\n/**\n * Resolve the effective {@link SnapshotStore}: the agent's own\n * `durable.store` wins; absent that, fall back to the global default\n * set via `ai.config({ defaultSnapshotStore })`.\n *\n * The global default is typed for the supervisor snapshot shape, but\n * every store impl keys purely by `runId` and round-trips whatever\n * envelope it is handed — so it serves an `AgentSnapshot` just as well.\n * The cast re-tags the shape at this single boundary (Option B); the\n * agent only ever hands it an `AgentSnapshot`.\n */\nfunction resolveSnapshotStore(\n durable: AgentDurableConfig | undefined,\n): SnapshotStore<AgentSnapshot> | undefined {\n return (\n durable?.store ??\n (resolveDefaultSnapshotStore() as SnapshotStore<AgentSnapshot> | undefined)\n );\n}\n\nexport type PersistAgentParams = {\n durable: AgentDurableConfig | undefined;\n runId: string;\n agentName: string;\n signature: string;\n version?: string;\n input: string;\n systemPrompt?: string;\n responseSchema?: Record<string, unknown>;\n promptName?: string;\n promptVersion?: string;\n messages: Message[];\n trips: LLMTrip[];\n toolCalls: ToolCall[];\n usage: Usage;\n status: AgentSnapshotStatus;\n startedAt: string;\n};\n\nexport type PersistOutcome = { ok: true } | { ok: false; error: unknown };\n\n/**\n * Write the current run state to the resolved snapshot store. No-op\n * (returns `{ ok: true }`) when neither `durable.store` nor the global\n * `defaultSnapshotStore` is configured — the common non-durable path.\n * Failures are returned as `{ ok: false }` rather than thrown so the\n * engine can surface them via logs without aborting the run — a failed\n * checkpoint loses resume-ability from that point but never breaks an\n * otherwise-healthy run.\n */\nexport async function persistAgentSnapshot(\n params: PersistAgentParams,\n): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n const snapshot: AgentSnapshot = {\n runId: params.runId,\n agentName: params.agentName,\n signature: params.signature,\n version: params.version,\n input: params.input,\n systemPrompt: params.systemPrompt,\n responseSchema: params.responseSchema,\n promptName: params.promptName,\n promptVersion: params.promptVersion,\n messages: params.messages,\n trips: params.trips,\n toolCalls: params.toolCalls,\n usage: params.usage,\n status: params.status,\n startedAt: params.startedAt,\n savedAt: new Date().toISOString(),\n };\n\n try {\n await store.save(snapshot);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Delete a persisted snapshot — used after a successful run when\n * `durable.deleteOnComplete` is set. Never throws: a failed delete is\n * surfaced as `{ ok: false }` and the engine logs it. No-op (ok) when no\n * store is configured.\n */\nexport async function deleteAgentSnapshot(params: {\n durable: AgentDurableConfig | undefined;\n runId: string;\n}): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n try {\n await store.delete(params.runId);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Load a persisted snapshot for `resume()` and run the drift check.\n * Throws `AgentExecutionError` when no store is configured or when the\n * run is missing; throws `AgentDriftError` when the stored signature\n * doesn't match the current definition (unless `force` is set).\n */\nexport async function loadAgentSnapshotForResume(params: {\n durable: AgentDurableConfig | undefined;\n agentName: string;\n signature: string;\n runId: string;\n options?: AgentResumeOptions<unknown>;\n}): Promise<AgentSnapshot> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n throw new AgentExecutionError(\n `agent \"${params.agentName}\" has no durable store configured — set \\`durable: { store }\\` on the config or call \\`ai.config({ defaultSnapshotStore })\\` at boot before calling resume()`,\n { context: { runId: params.runId } },\n );\n }\n\n const snapshot = (await store.load(params.runId)) ?? null;\n\n if (!snapshot) {\n throw new AgentExecutionError(\n `agent \"${params.agentName}\": no snapshot for runId \"${params.runId}\"`,\n { context: { runId: params.runId } },\n );\n }\n\n if (!params.options?.force && snapshot.signature !== params.signature) {\n throw new AgentDriftError(\n `agent \"${params.agentName}\" signature drift on resume`,\n {\n savedSignature: snapshot.signature,\n currentSignature: params.signature,\n runId: params.runId,\n },\n );\n }\n\n return snapshot;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoCA,SAAS,qBACP,SAC0C;CAC1C,OACE,SAAS,SACR,4BAA4B;AAEjC;;;;;;;;;;AAgCA,eAAsB,qBACpB,QACyB;CACzB,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,OAAO,EAAE,IAAI,KAAK;CAGpB,MAAM,WAA0B;EAC9B,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,cAAc,OAAO;EACrB,gBAAgB,OAAO;EACvB,YAAY,OAAO;EACnB,eAAe,OAAO;EACtB,UAAU,OAAO;EACjB,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,0BAAS,IAAI,KAAK,
|
|
1
|
+
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../../../../../ai/src/agent/snapshot.ts"],"sourcesContent":["import { resolveDefaultSnapshotStore } from \"../config\";\nimport type {\n AgentResumeOptions,\n} from \"../contracts/agent/agent-options.type\";\nimport type {\n AgentSnapshot,\n AgentSnapshotStatus,\n} from \"../contracts/agent/agent-snapshot.type\";\nimport type { SnapshotStore } from \"../contracts/orchestrator/snapshot-store.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport type { LLMTrip } from \"../contracts/result/llm-trip.type\";\nimport type { ToolCall } from \"../contracts/result/tool-call.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AgentDriftError, AgentExecutionError } from \"../errors\";\n\n/**\n * The agent's `durable` config, narrowed to the fields the snapshot\n * helpers read. Kept minimal so this module doesn't depend on the full\n * resolved-config shape.\n */\nexport type AgentDurableConfig = {\n store?: SnapshotStore<AgentSnapshot>;\n deleteOnComplete?: boolean;\n};\n\n/**\n * Resolve the effective {@link SnapshotStore}: the agent's own\n * `durable.store` wins; absent that, fall back to the global default\n * set via `ai.config({ defaultSnapshotStore })`.\n *\n * The global default is typed for the supervisor snapshot shape, but\n * every store impl keys purely by `runId` and round-trips whatever\n * envelope it is handed — so it serves an `AgentSnapshot` just as well.\n * The cast re-tags the shape at this single boundary (Option B); the\n * agent only ever hands it an `AgentSnapshot`.\n */\nfunction resolveSnapshotStore(\n durable: AgentDurableConfig | undefined,\n): SnapshotStore<AgentSnapshot> | undefined {\n return (\n durable?.store ??\n (resolveDefaultSnapshotStore() as SnapshotStore<AgentSnapshot> | undefined)\n );\n}\n\nexport type PersistAgentParams = {\n durable: AgentDurableConfig | undefined;\n runId: string;\n agentName: string;\n signature: string;\n version?: string;\n input: string;\n systemPrompt?: string;\n responseSchema?: Record<string, unknown>;\n promptName?: string;\n promptVersion?: string;\n messages: Message[];\n trips: LLMTrip[];\n toolCalls: ToolCall[];\n usage: Usage;\n status: AgentSnapshotStatus;\n startedAt: string;\n};\n\nexport type PersistOutcome = { ok: true } | { ok: false; error: unknown };\n\n/**\n * Write the current run state to the resolved snapshot store. No-op\n * (returns `{ ok: true }`) when neither `durable.store` nor the global\n * `defaultSnapshotStore` is configured — the common non-durable path.\n * Failures are returned as `{ ok: false }` rather than thrown so the\n * engine can surface them via logs without aborting the run — a failed\n * checkpoint loses resume-ability from that point but never breaks an\n * otherwise-healthy run.\n */\nexport async function persistAgentSnapshot(\n params: PersistAgentParams,\n): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n const snapshot: AgentSnapshot = {\n runId: params.runId,\n agentName: params.agentName,\n signature: params.signature,\n version: params.version,\n input: params.input,\n systemPrompt: params.systemPrompt,\n responseSchema: params.responseSchema,\n promptName: params.promptName,\n promptVersion: params.promptVersion,\n messages: params.messages,\n trips: params.trips,\n toolCalls: params.toolCalls,\n usage: params.usage,\n status: params.status,\n startedAt: params.startedAt,\n savedAt: new Date().toISOString(),\n };\n\n try {\n await store.save(snapshot);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Delete a persisted snapshot — used after a successful run when\n * `durable.deleteOnComplete` is set. Never throws: a failed delete is\n * surfaced as `{ ok: false }` and the engine logs it. No-op (ok) when no\n * store is configured.\n */\nexport async function deleteAgentSnapshot(params: {\n durable: AgentDurableConfig | undefined;\n runId: string;\n}): Promise<PersistOutcome> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n return { ok: true };\n }\n\n try {\n await store.delete(params.runId);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, error };\n }\n}\n\n/**\n * Load a persisted snapshot for `resume()` and run the drift check.\n * Throws `AgentExecutionError` when no store is configured or when the\n * run is missing; throws `AgentDriftError` when the stored signature\n * doesn't match the current definition (unless `force` is set).\n */\nexport async function loadAgentSnapshotForResume(params: {\n durable: AgentDurableConfig | undefined;\n agentName: string;\n signature: string;\n runId: string;\n options?: AgentResumeOptions<unknown>;\n}): Promise<AgentSnapshot> {\n const store = resolveSnapshotStore(params.durable);\n\n if (!store) {\n throw new AgentExecutionError(\n `agent \"${params.agentName}\" has no durable store configured — set \\`durable: { store }\\` on the config or call \\`ai.config({ defaultSnapshotStore })\\` at boot before calling resume()`,\n { context: { runId: params.runId } },\n );\n }\n\n const snapshot = (await store.load(params.runId)) ?? null;\n\n if (!snapshot) {\n throw new AgentExecutionError(\n `agent \"${params.agentName}\": no snapshot for runId \"${params.runId}\"`,\n { context: { runId: params.runId } },\n );\n }\n\n if (!params.options?.force && snapshot.signature !== params.signature) {\n throw new AgentDriftError(\n `agent \"${params.agentName}\" signature drift on resume`,\n {\n savedSignature: snapshot.signature,\n currentSignature: params.signature,\n runId: params.runId,\n },\n );\n }\n\n return snapshot;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoCA,SAAS,qBACP,SAC0C;CAC1C,OACE,SAAS,SACR,4BAA4B;AAEjC;;;;;;;;;;AAgCA,eAAsB,qBACpB,QACyB;CACzB,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,OAAO,EAAE,IAAI,KAAK;CAGpB,MAAM,WAA0B;EAC9B,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,cAAc,OAAO;EACrB,gBAAgB,OAAO;EACvB,YAAY,OAAO;EACnB,eAAe,OAAO;EACtB,UAAU,OAAO;EACjB,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,0BAAS,IAAI,KAAK,GAAE,YAAY;CAClC;CAEA,IAAI;EACF,MAAM,MAAM,KAAK,QAAQ;EAEzB,OAAO,EAAE,IAAI,KAAK;CACpB,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO;EAAM;CAC5B;AACF;;;;;;;AAQA,eAAsB,oBAAoB,QAGd;CAC1B,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,OAAO,EAAE,IAAI,KAAK;CAGpB,IAAI;EACF,MAAM,MAAM,OAAO,OAAO,KAAK;EAE/B,OAAO,EAAE,IAAI,KAAK;CACpB,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO;EAAM;CAC5B;AACF;;;;;;;AAQA,eAAsB,2BAA2B,QAMtB;CACzB,MAAM,QAAQ,qBAAqB,OAAO,OAAO;CAEjD,IAAI,CAAC,OACH,MAAM,IAAI,oBACR,UAAU,OAAO,UAAU,+JAC3B,EAAE,SAAS,EAAE,OAAO,OAAO,MAAM,EAAE,CACrC;CAGF,MAAM,WAAY,MAAM,MAAM,KAAK,OAAO,KAAK,KAAM;CAErD,IAAI,CAAC,UACH,MAAM,IAAI,oBACR,UAAU,OAAO,UAAU,4BAA4B,OAAO,MAAM,IACpE,EAAE,SAAS,EAAE,OAAO,OAAO,MAAM,EAAE,CACrC;CAGF,IAAI,CAAC,OAAO,SAAS,SAAS,SAAS,cAAc,OAAO,WAC1D,MAAM,IAAI,gBACR,UAAU,OAAO,UAAU,8BAC3B;EACE,gBAAgB,SAAS;EACzB,kBAAkB,OAAO;EACzB,OAAO,OAAO;CAChB,CACF;CAGF,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-sub-agent.d.mts","names":[],"sources":["../../../../../../../ai/src/agent/spawn-sub-agent.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;;;;;;;;;KAAY,iBAAA;EAMV,+CAJA,IAAA,UAMe;EAJf,KAAA,EAAO,aAAA,EAMC;EAJR,IAAA,UAaA;EAXA,YAAA,GAAe,oBAAA,WAaf;EAXA,KAAA,GAAQ,cAAA,sBAWkB;EAT1B,QAAA;EAWS;;;AAMA;AAsCX;;EAhDE,MAAA,GAAS,aAAA,EAiDe;EA/CxB,MAAA,GAAS,gBAAA,CAAiB,OAAA,GAgDL;EA9CrB,MAAA,GAAS,WAAA;EA8CR;;;;;EAxCD,SAAA;AAAA;;;;;AAwC4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAFR,aAAA,
|
|
1
|
+
{"version":3,"file":"spawn-sub-agent.d.mts","names":[],"sources":["../../../../../../../ai/src/agent/spawn-sub-agent.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;;;;;;;;;KAAY,iBAAA;EAMV,+CAJA,IAAA,UAMe;EAJf,KAAA,EAAO,aAAA,EAMC;EAJR,IAAA,UAaA;EAXA,YAAA,GAAe,oBAAA,WAaf;EAXA,KAAA,GAAQ,cAAA,sBAWkB;EAT1B,QAAA;EAWS;;;AAMA;AAsCX;;EAhDE,MAAA,GAAS,aAAA,EAiDe;EA/CxB,MAAA,GAAS,gBAAA,CAAiB,OAAA,GAgDL;EA9CrB,MAAA,GAAS,WAAA;EA8CR;;;;;EAxCD,SAAA;AAAA;;;;;AAwC4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAFR,aAAA,mBAAA,CACpB,IAAA,EAAM,iBAAA,CAAkB,OAAA,IACvB,OAAA,CAAQ,WAAA,CAAY,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.d.mts","names":[],"sources":["../../../../../../../ai/src/batch/batch.ts"],"mappings":";;;;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,KAAA,mCAAwC,UAAA,GAAa,aAAA,
|
|
1
|
+
{"version":3,"file":"batch.d.mts","names":[],"sources":["../../../../../../../ai/src/batch/batch.ts"],"mappings":";;;;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAsB,KAAA,mCAAwC,UAAA,GAAa,aAAA,CAAA,CACzE,UAAA,EAAY,kBAAA,CAAmB,MAAA,EAAQ,QAAA,EAAU,OAAA,GACjD,KAAA,WAAgB,MAAA,IAChB,OAAA,GAAS,YAAA,CAAa,OAAA,IACrB,OAAA,CAAQ,WAAA,CAAY,OAAA"}
|
package/esm/batch/batch.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.mjs","names":[],"sources":["../../../../../../../ai/src/batch/batch.ts"],"sourcesContent":["import type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { BaseResult } from \"../contracts/result/base-result.type\";\nimport type { ExecutableContract } from \"../contracts/executable.contract\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { accumulateCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\nimport type {\n BatchItemResult,\n BatchOptions,\n BatchReport,\n BatchResult,\n} from \"./batch.type\";\nimport { runBatchItem } from \"./run-batch-item\";\nimport { runWithConcurrency } from \"./run-with-concurrency\";\n\n/** Batch size above which an unset (unbounded) concurrency warns once (D5). */\nconst BATCH_UNBOUNDED_WARN_THRESHOLD = 50;\n\n/** Process-lifetime flag so the unbounded-batch warning fires at most once. */\nlet warnedUnboundedBatch = false;\n\n/**\n * Run an executable AI primitive (agent, workflow, supervisor, tool,\n * or anything satisfying {@link ExecutableContract}) over a dataset\n * with bounded concurrency and per-item retry, returning per-item\n * outcomes plus rolled-up usage and a walkable report tree.\n *\n * **Role.** The fan-out primitive of `@warlock.js/ai`. Where an agent\n * runs once, `batch` runs the SAME executable N times — once per item\n * — and aggregates the results into the unified {@link ExecuteResult}\n * envelope, so a batch slots into cost dashboards and trace tooling\n * exactly like a single run does.\n *\n * **Isolation.** Items are independent: one item's failure (after its\n * retries are exhausted) never cancels a sibling, and the batch as a\n * whole never rejects — failures live on each {@link BatchItemResult}.\n * Reach for `result.report.failed` / `item.status` to inspect them.\n *\n * **Usage rollup.** `result.usage` and `result.report.usage` sum every\n * item's usage, satisfying the universal rollup invariant (\"own cost\n * + sum of children\"; a batch has zero own cost). Each item's own\n * report is attached under `report.children[]`, in original item\n * order, so a trace walker sees every run.\n *\n * @example\n * const result = await batch(summarizer, articles, {\n * concurrency: 4,\n * retry: { attempts: 3, backoff: \"exponential\" },\n * onItem: (item) => log.info(\"batch\", \"item\", \"settled\", { index: item.index }),\n * });\n *\n * console.log(`${result.report.succeeded}/${result.report.total} ok`);\n * console.log(`${result.usage.total} tokens total`);\n */\nexport async function batch<TInput, TOptions, TResult extends BaseResult = ExecuteResult>(\n executable: ExecutableContract<TInput, TOptions, TResult>,\n items: readonly TInput[],\n options: BatchOptions<TResult> = {},\n): Promise<BatchResult<TResult>> {\n return new BatchRun(executable, items, options).run();\n}\n\n/**\n * Per-call orchestration state for one {@link batch} invocation.\n * Instantiated fresh inside the factory so the mutable accumulators\n * (`results`, `usage`) are never shared across batches. Unexported —\n * callers only ever see the plain {@link BatchResult}.\n */\nclass BatchRun<TInput, TOptions, TResult extends BaseResult> {\n private readonly runId: string;\n private readonly results: BatchItemResult<TResult>[];\n private readonly startedAt = new Date().toISOString();\n private readonly startPerf = performance.now();\n\n public constructor(\n private readonly executable: ExecutableContract<TInput, TOptions, TResult>,\n private readonly items: readonly TInput[],\n private readonly options: BatchOptions<TResult>,\n ) {\n this.runId = generateRunId(\"batch\");\n this.results = new Array<BatchItemResult<TResult>>(items.length);\n }\n\n /**\n * Dispatch every item through the concurrency pool, then assemble\n * the rolled-up {@link BatchResult}. Runs once per `batch()` call.\n */\n public async run(): Promise<BatchResult<TResult>> {\n const concurrency = this.resolveConcurrency();\n\n await runWithConcurrency(this.items.length, concurrency, (index) =>\n this.processItem(index),\n );\n\n return this.buildResult();\n }\n\n /**\n * Resolve the effective concurrency from {@link BatchOptions.concurrency}\n * (D5). An explicit number or `\"unbounded\"` is honored as-is; an omitted\n * value runs unbounded for back-compat but warns once (outside tests)\n * for a large batch so an accidental all-at-once run is visible.\n */\n private resolveConcurrency(): number {\n const configured = this.options.concurrency;\n\n if (configured === \"unbounded\") {\n return this.items.length;\n }\n if (typeof configured === \"number\") {\n return configured;\n }\n\n if (\n this.items.length > BATCH_UNBOUNDED_WARN_THRESHOLD &&\n !warnedUnboundedBatch &&\n !process.env.VITEST &&\n process.env.NODE_ENV !== \"test\"\n ) {\n warnedUnboundedBatch = true;\n console.warn(\n `[warlock-ai] ai.batch() is running ${this.items.length} items with unbounded concurrency (no \\`concurrency\\` set). ` +\n 'Each concurrent item consumes tokens/quota/memory — pass an explicit `concurrency` cap, or `concurrency: \"unbounded\"` to silence this.',\n );\n }\n\n return this.items.length;\n }\n\n /**\n * Run a single item with retry, record it positionally, then fire\n * the `onItem` hook. A throw from the hook is swallowed — a progress\n * callback must never break the batch.\n */\n private async processItem(index: number): Promise<void> {\n const item = await runBatchItem({\n index,\n input: this.items[index] as TInput,\n executable: this.executable,\n retry: this.options.retry,\n signal: this.options.signal,\n });\n\n this.results[index] = item;\n\n if (this.options.onItem) {\n try {\n await this.options.onItem(item);\n } catch {\n // A progress hook must never break the batch — swallow its throw.\n }\n }\n }\n\n /**\n * Fold the per-item outcomes into rolled-up usage, the child report\n * list, and the final {@link BatchResult}, then stamp lineage across\n * the whole subtree so every child shares this batch's root run id.\n */\n private buildResult(): BatchResult<TResult> {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const children: BaseReport[] = [];\n const data: (unknown | undefined)[] = new Array(this.items.length).fill(undefined);\n\n let succeeded = 0;\n let failed = 0;\n let cancelled = 0;\n\n for (const item of this.results) {\n if (item.status === \"completed\") {\n succeeded += 1;\n } else if (item.status === \"failed\") {\n failed += 1;\n } else {\n cancelled += 1;\n }\n\n const itemResult = item.result;\n if (itemResult) {\n this.mergeUsage(usage, itemResult.usage);\n\n if (\"report\" in itemResult && itemResult.report) {\n children.push(itemResult.report as BaseReport);\n }\n\n if (item.status === \"completed\" && \"data\" in itemResult) {\n data[item.index] = (itemResult as { data?: unknown }).data;\n }\n }\n }\n\n const report = this.buildReport(usage, children, { succeeded, failed, cancelled });\n\n stampReportLineage(report, {\n rootRunId: this.runId,\n sessionId: this.options.sessionId,\n });\n\n return {\n type: \"batch\",\n data,\n usage,\n report,\n items: this.results,\n };\n }\n\n /**\n * Add a child's usage into the running batch total. Scalar token\n * channels sum directly; the optional cost breakdown merges via\n * {@link accumulateCost} so a single unpriced child can't erase the\n * cost of priced siblings. Optional token sub-channels\n * (`cachedTokens`, etc.) accumulate only when some child reports\n * them, preserving the \"never reported anywhere\" signal.\n */\n private mergeUsage(target: Usage, child: Usage): void {\n target.input += child.input;\n target.output += child.output;\n target.total += child.total;\n\n if (child.cachedTokens !== undefined) {\n target.cachedTokens = (target.cachedTokens ?? 0) + child.cachedTokens;\n }\n\n if (child.reasoningTokens !== undefined) {\n target.reasoningTokens = (target.reasoningTokens ?? 0) + child.reasoningTokens;\n }\n\n if (child.cacheWriteTokens !== undefined) {\n target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + child.cacheWriteTokens;\n }\n\n const mergedCost = accumulateCost(target.cost, child.cost);\n if (mergedCost !== undefined) {\n target.cost = mergedCost;\n }\n }\n\n /**\n * Build the batch's own {@link BatchReport} node. `parentRunId` /\n * `rootRunId` are placeholders here — {@link stampReportLineage}\n * rewrites them across the whole subtree right after.\n */\n private buildReport(\n usage: Usage,\n children: BaseReport[],\n counts: { succeeded: number; failed: number; cancelled: number },\n ): BatchReport {\n const status = counts.failed > 0 || counts.cancelled > 0 ? \"failed\" : \"completed\";\n\n return {\n runId: this.runId,\n rootRunId: this.runId,\n name: this.options.name ?? \"batch\",\n type: \"batch\",\n status,\n startedAt: this.startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - this.startPerf,\n usage,\n children,\n total: this.items.length,\n succeeded: counts.succeeded,\n failed: counts.failed,\n cancelled: counts.cancelled,\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"batch.mjs","names":[],"sources":["../../../../../../../ai/src/batch/batch.ts"],"sourcesContent":["import type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { BaseResult } from \"../contracts/result/base-result.type\";\nimport type { ExecutableContract } from \"../contracts/executable.contract\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { accumulateCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\nimport type {\n BatchItemResult,\n BatchOptions,\n BatchReport,\n BatchResult,\n} from \"./batch.type\";\nimport { runBatchItem } from \"./run-batch-item\";\nimport { runWithConcurrency } from \"./run-with-concurrency\";\n\n/** Batch size above which an unset (unbounded) concurrency warns once (D5). */\nconst BATCH_UNBOUNDED_WARN_THRESHOLD = 50;\n\n/** Process-lifetime flag so the unbounded-batch warning fires at most once. */\nlet warnedUnboundedBatch = false;\n\n/**\n * Run an executable AI primitive (agent, workflow, supervisor, tool,\n * or anything satisfying {@link ExecutableContract}) over a dataset\n * with bounded concurrency and per-item retry, returning per-item\n * outcomes plus rolled-up usage and a walkable report tree.\n *\n * **Role.** The fan-out primitive of `@warlock.js/ai`. Where an agent\n * runs once, `batch` runs the SAME executable N times — once per item\n * — and aggregates the results into the unified {@link ExecuteResult}\n * envelope, so a batch slots into cost dashboards and trace tooling\n * exactly like a single run does.\n *\n * **Isolation.** Items are independent: one item's failure (after its\n * retries are exhausted) never cancels a sibling, and the batch as a\n * whole never rejects — failures live on each {@link BatchItemResult}.\n * Reach for `result.report.failed` / `item.status` to inspect them.\n *\n * **Usage rollup.** `result.usage` and `result.report.usage` sum every\n * item's usage, satisfying the universal rollup invariant (\"own cost\n * + sum of children\"; a batch has zero own cost). Each item's own\n * report is attached under `report.children[]`, in original item\n * order, so a trace walker sees every run.\n *\n * @example\n * const result = await batch(summarizer, articles, {\n * concurrency: 4,\n * retry: { attempts: 3, backoff: \"exponential\" },\n * onItem: (item) => log.info(\"batch\", \"item\", \"settled\", { index: item.index }),\n * });\n *\n * console.log(`${result.report.succeeded}/${result.report.total} ok`);\n * console.log(`${result.usage.total} tokens total`);\n */\nexport async function batch<TInput, TOptions, TResult extends BaseResult = ExecuteResult>(\n executable: ExecutableContract<TInput, TOptions, TResult>,\n items: readonly TInput[],\n options: BatchOptions<TResult> = {},\n): Promise<BatchResult<TResult>> {\n return new BatchRun(executable, items, options).run();\n}\n\n/**\n * Per-call orchestration state for one {@link batch} invocation.\n * Instantiated fresh inside the factory so the mutable accumulators\n * (`results`, `usage`) are never shared across batches. Unexported —\n * callers only ever see the plain {@link BatchResult}.\n */\nclass BatchRun<TInput, TOptions, TResult extends BaseResult> {\n private readonly runId: string;\n private readonly results: BatchItemResult<TResult>[];\n private readonly startedAt = new Date().toISOString();\n private readonly startPerf = performance.now();\n\n public constructor(\n private readonly executable: ExecutableContract<TInput, TOptions, TResult>,\n private readonly items: readonly TInput[],\n private readonly options: BatchOptions<TResult>,\n ) {\n this.runId = generateRunId(\"batch\");\n this.results = new Array<BatchItemResult<TResult>>(items.length);\n }\n\n /**\n * Dispatch every item through the concurrency pool, then assemble\n * the rolled-up {@link BatchResult}. Runs once per `batch()` call.\n */\n public async run(): Promise<BatchResult<TResult>> {\n const concurrency = this.resolveConcurrency();\n\n await runWithConcurrency(this.items.length, concurrency, (index) =>\n this.processItem(index),\n );\n\n return this.buildResult();\n }\n\n /**\n * Resolve the effective concurrency from {@link BatchOptions.concurrency}\n * (D5). An explicit number or `\"unbounded\"` is honored as-is; an omitted\n * value runs unbounded for back-compat but warns once (outside tests)\n * for a large batch so an accidental all-at-once run is visible.\n */\n private resolveConcurrency(): number {\n const configured = this.options.concurrency;\n\n if (configured === \"unbounded\") {\n return this.items.length;\n }\n if (typeof configured === \"number\") {\n return configured;\n }\n\n if (\n this.items.length > BATCH_UNBOUNDED_WARN_THRESHOLD &&\n !warnedUnboundedBatch &&\n !process.env.VITEST &&\n process.env.NODE_ENV !== \"test\"\n ) {\n warnedUnboundedBatch = true;\n console.warn(\n `[warlock-ai] ai.batch() is running ${this.items.length} items with unbounded concurrency (no \\`concurrency\\` set). ` +\n 'Each concurrent item consumes tokens/quota/memory — pass an explicit `concurrency` cap, or `concurrency: \"unbounded\"` to silence this.',\n );\n }\n\n return this.items.length;\n }\n\n /**\n * Run a single item with retry, record it positionally, then fire\n * the `onItem` hook. A throw from the hook is swallowed — a progress\n * callback must never break the batch.\n */\n private async processItem(index: number): Promise<void> {\n const item = await runBatchItem({\n index,\n input: this.items[index] as TInput,\n executable: this.executable,\n retry: this.options.retry,\n signal: this.options.signal,\n });\n\n this.results[index] = item;\n\n if (this.options.onItem) {\n try {\n await this.options.onItem(item);\n } catch {\n // A progress hook must never break the batch — swallow its throw.\n }\n }\n }\n\n /**\n * Fold the per-item outcomes into rolled-up usage, the child report\n * list, and the final {@link BatchResult}, then stamp lineage across\n * the whole subtree so every child shares this batch's root run id.\n */\n private buildResult(): BatchResult<TResult> {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const children: BaseReport[] = [];\n const data: (unknown | undefined)[] = new Array(this.items.length).fill(undefined);\n\n let succeeded = 0;\n let failed = 0;\n let cancelled = 0;\n\n for (const item of this.results) {\n if (item.status === \"completed\") {\n succeeded += 1;\n } else if (item.status === \"failed\") {\n failed += 1;\n } else {\n cancelled += 1;\n }\n\n const itemResult = item.result;\n if (itemResult) {\n this.mergeUsage(usage, itemResult.usage);\n\n if (\"report\" in itemResult && itemResult.report) {\n children.push(itemResult.report as BaseReport);\n }\n\n if (item.status === \"completed\" && \"data\" in itemResult) {\n data[item.index] = (itemResult as { data?: unknown }).data;\n }\n }\n }\n\n const report = this.buildReport(usage, children, { succeeded, failed, cancelled });\n\n stampReportLineage(report, {\n rootRunId: this.runId,\n sessionId: this.options.sessionId,\n });\n\n return {\n type: \"batch\",\n data,\n usage,\n report,\n items: this.results,\n };\n }\n\n /**\n * Add a child's usage into the running batch total. Scalar token\n * channels sum directly; the optional cost breakdown merges via\n * {@link accumulateCost} so a single unpriced child can't erase the\n * cost of priced siblings. Optional token sub-channels\n * (`cachedTokens`, etc.) accumulate only when some child reports\n * them, preserving the \"never reported anywhere\" signal.\n */\n private mergeUsage(target: Usage, child: Usage): void {\n target.input += child.input;\n target.output += child.output;\n target.total += child.total;\n\n if (child.cachedTokens !== undefined) {\n target.cachedTokens = (target.cachedTokens ?? 0) + child.cachedTokens;\n }\n\n if (child.reasoningTokens !== undefined) {\n target.reasoningTokens = (target.reasoningTokens ?? 0) + child.reasoningTokens;\n }\n\n if (child.cacheWriteTokens !== undefined) {\n target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + child.cacheWriteTokens;\n }\n\n const mergedCost = accumulateCost(target.cost, child.cost);\n if (mergedCost !== undefined) {\n target.cost = mergedCost;\n }\n }\n\n /**\n * Build the batch's own {@link BatchReport} node. `parentRunId` /\n * `rootRunId` are placeholders here — {@link stampReportLineage}\n * rewrites them across the whole subtree right after.\n */\n private buildReport(\n usage: Usage,\n children: BaseReport[],\n counts: { succeeded: number; failed: number; cancelled: number },\n ): BatchReport {\n const status = counts.failed > 0 || counts.cancelled > 0 ? \"failed\" : \"completed\";\n\n return {\n runId: this.runId,\n rootRunId: this.runId,\n name: this.options.name ?? \"batch\",\n type: \"batch\",\n status,\n startedAt: this.startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - this.startPerf,\n usage,\n children,\n total: this.items.length,\n succeeded: counts.succeeded,\n failed: counts.failed,\n cancelled: counts.cancelled,\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/checkpoint/memory.ts"],"mappings":";;;;;AAmKA;;;;AAAyC;;;;;;;;iBAAzB,MAAA,
|
|
1
|
+
{"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/checkpoint/memory.ts"],"mappings":";;;;;AAmKA;;;;AAAyC;;;;;;;;iBAAzB,MAAA,CAAA,GAAU,eAAe"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg.mjs","names":[],"sources":["../../../../../../../ai/src/checkpoint/pg.ts"],"sourcesContent":["import type {\n CheckpointRecord,\n CheckpointStore,\n} from \"../contracts/orchestrator/checkpoint-store.contract\";\nimport type { PgClientLike } from \"../contracts/orchestrator/snapshot-store.contract\";\n\n/**\n * Options for the Postgres {@link CheckpointStore} (orchestrator.md §8.3).\n *\n * The dev owns the connection — `@warlock.js/ai` takes no peer dep on\n * `pg` and never opens or closes the client. A single `pg.Pool` can\n * back both the cache and the orchestrator stores.\n */\nexport type PgCheckpointOptions = {\n /** An already-built `pg.Pool` / `pg.Client` — anything matching {@link PgClientLike}. */\n client: PgClientLike;\n /** Backing table name. Defaults to `warlock_orchestrator_sessions` (§8.6). Must be a safe SQL identifier. */\n table?: string;\n /** Idle-row TTL in seconds. When set, rows older than the TTL are eligible for cleanup on prune. */\n ttl?: number;\n};\n\n/**\n * Default backing table — matches the §8.6 reference DDL verbatim so a\n * stock migration provisions the store with no extra config.\n */\nconst DEFAULT_TABLE = \"warlock_orchestrator_sessions\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). The table\n * name is interpolated into DDL/DML, so anything outside this\n * conservative ASCII subset is rejected — interpolating an arbitrary\n * string would be a SQL-injection footgun (mirrors `PgCacheDriver`).\n */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Coerce a Postgres `INTEGER` column back to a number. `pg` hands back\n * `INTEGER` as a JS number already, but some pool wrappers surface it\n * as a string — normalize defensively so `turn_index` arithmetic and\n * the latest-turn ordering never compare strings.\n */\nfunction toNumber(value: unknown): number {\n return typeof value === \"string\" ? Number(value) : (value as number);\n}\n\n/**\n * Coerce a nullable Postgres integer column to `number | null`.\n */\nfunction toNullableNumber(value: unknown): number | null {\n return value === null || value === undefined ? null : toNumber(value);\n}\n\n/**\n * Coerce a Postgres timestamp/text column to an ISO string. `pg`\n * returns `TIMESTAMPTZ` as a `Date`; normalize to the ISO wire shape\n * the {@link CheckpointRecord} contract declares.\n */\nfunction toIso(value: unknown): string {\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return value as string;\n}\n\n/**\n * Coerce a nullable Postgres timestamp column to `string | null`.\n */\nfunction toNullableIso(value: unknown): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n return toIso(value);\n}\n\n/**\n * Decode the single `last_route` `TEXT` column back to the\n * `string | string[] | null` shape the contract declares. A fan-out\n * array is written JSON-encoded (it starts with `[`), so a leading `[`\n * is the signal to parse; any other value is a single intent stored\n * verbatim. Symmetric with {@link PgCheckpointStore.serializeRoute}.\n */\nfunction deserializeRoute(value: unknown): string | string[] | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n const route = value as string;\n\n if (route.startsWith(\"[\")) {\n return JSON.parse(route) as string[];\n }\n\n return route;\n}\n\n/**\n * Map a raw DB row to a {@link CheckpointRecord}. Column names match\n * the §8.6 DDL 1:1, so this is a typed projection plus the defensive\n * coercions a heterogeneous `pg` client population needs.\n */\nfunction rowToRecord(row: Record<string, unknown>): CheckpointRecord {\n const state =\n typeof row.state === \"string\" ? JSON.parse(row.state) : row.state;\n\n return {\n orchestrator_name: row.orchestrator_name as string,\n session_id: row.session_id as string,\n turn_index: toNumber(row.turn_index),\n state,\n last_route: deserializeRoute(row.last_route),\n signature: row.signature as string,\n version: (row.version as string | null) ?? null,\n summarized_through: toNullableNumber(row.summarized_through),\n lock_acquired_at: toNullableIso(row.lock_acquired_at),\n lock_expires_at: toNullableIso(row.lock_expires_at),\n saved_at: toIso(row.saved_at),\n };\n}\n\n/**\n * Postgres-backed {@link CheckpointStore} (orchestrator.md §8.2, §8.6).\n *\n * Owns: append-only checkpoint rows keyed by\n * `(orchestrator_name, session_id, turn_index)`, the \"latest turn wins\"\n * load, the §8.6 DDL via {@link PgCheckpointStore.schema}, and the\n * §4-Phase-6 retention prune. Does NOT own: the connection lifecycle\n * (the dev passes a client and keeps it), schema migration (the dev\n * runs `schema()` through their own tool — never auto-migrated, §8.5),\n * or the `keepSnapshots` policy itself (that lives on the orchestrator\n * config; the orchestrator passes the resolved bound into\n * {@link PgCheckpointStore.prune}).\n *\n * Front it with the {@link pg} factory — callers never `new` it.\n */\nclass PgCheckpointStore implements CheckpointStore {\n /** The dev-supplied `pg.Pool` / `pg.Client`. Never closed by the store. */\n private readonly client: PgClientLike;\n\n /** Validated backing table name, safe to interpolate into SQL. */\n private readonly table: string;\n\n /** Idle-row TTL in seconds, or `undefined` for no expiry. */\n private ttl?: number;\n\n public constructor(options: PgCheckpointOptions) {\n if (!options || typeof options.client?.query !== \"function\") {\n throw new TypeError(\n \"ai.checkpoint.pg requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? DEFAULT_TABLE;\n\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new TypeError(\n `ai.checkpoint.pg: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n this.client = options.client;\n this.table = table;\n this.ttl = options.ttl;\n }\n\n /**\n * Return the latest checkpoint (highest `turn_index`) for a session,\n * or `undefined` when the store has never seen it. The `(name,\n * session_id, turn_index DESC)` lookup index keeps this O(1) on the\n * latest row (§8.6).\n */\n public async load(\n orchestratorName: string,\n sessionId: string,\n ): Promise<CheckpointRecord | undefined> {\n const { rows } = await this.client.query(\n `SELECT * FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2\n ORDER BY turn_index DESC\n LIMIT 1`,\n [orchestratorName, sessionId],\n );\n\n if (rows.length === 0) {\n return undefined;\n }\n\n return rowToRecord(rows[0] as Record<string, unknown>);\n }\n\n /**\n * Persist a fresh checkpoint row. Append-only — an existing\n * `turn_index` is never overwritten; the PK collision surfaces as a\n * Postgres error rather than a silent clobber (§4 Phase 6, Q15).\n */\n public async save(record: CheckpointRecord): Promise<void> {\n await this.client.query(\n `INSERT INTO ${this.table} (\n orchestrator_name, session_id, turn_index, state, last_route,\n signature, version, summarized_through, lock_acquired_at,\n lock_expires_at, saved_at\n )\n VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9, $10, $11)`,\n [\n record.orchestrator_name,\n record.session_id,\n record.turn_index,\n JSON.stringify(record.state),\n this.serializeRoute(record.last_route),\n record.signature,\n record.version,\n record.summarized_through,\n record.lock_acquired_at,\n record.lock_expires_at,\n record.saved_at,\n ],\n );\n }\n\n /**\n * Delete every checkpoint row for a session, ending it.\n */\n public async delete(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n await this.client.query(\n `DELETE FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2`,\n [orchestratorName, sessionId],\n );\n }\n\n /**\n * List the distinct session ids known for an orchestrator, optionally\n * filtered by a session-id prefix. Used by the production boot-drain\n * loop (§9.3). The prefix is matched with `LIKE`, escaping the SQL\n * wildcards so a literal `_` or `%` in the prefix is not treated as a\n * pattern.\n */\n public async list(\n orchestratorName: string,\n prefix?: string,\n ): Promise<string[]> {\n if (prefix === undefined) {\n const { rows } = await this.client.query(\n `SELECT DISTINCT session_id FROM ${this.table}\n WHERE orchestrator_name = $1`,\n [orchestratorName],\n );\n\n return rows.map((row) => (row as Record<string, unknown>).session_id as string);\n }\n\n const escaped = prefix\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/_/g, \"\\\\_\")\n .replace(/%/g, \"\\\\%\");\n\n const { rows } = await this.client.query(\n `SELECT DISTINCT session_id FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id LIKE $2 ESCAPE '\\\\'`,\n [orchestratorName, `${escaped}%`],\n );\n\n return rows.map((row) => (row as Record<string, unknown>).session_id as string);\n }\n\n /**\n * Prune retained turns for a session down to the most recent\n * `keepSnapshots` rows (orchestrator.md §4 Phase 6 / §15.2). Deletes\n * every row with `turn_index < (max_turn_index - keepSnapshots)`. The\n * orchestrator calls this synchronously after a successful\n * {@link save} when `keepSnapshots` is a finite number; `\"all\"`\n * retention skips the call entirely. Additive to the\n * {@link CheckpointStore} contract — the contract carries no prune\n * hook, so the policy stays on the orchestrator and the store only\n * executes the bounded delete.\n */\n public async prune(\n orchestratorName: string,\n sessionId: string,\n keepSnapshots: number,\n ): Promise<void> {\n if (!Number.isFinite(keepSnapshots) || keepSnapshots < 0) {\n return;\n }\n\n await this.client.query(\n `DELETE FROM ${this.table}\n WHERE orchestrator_name = $1\n AND session_id = $2\n AND turn_index < (\n SELECT max(turn_index) - $3\n FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2\n )`,\n [orchestratorName, sessionId, keepSnapshots],\n );\n }\n\n /**\n * Return the §8.6 reference DDL for this store's backing table,\n * interpolating the configured table name. The dev runs it through\n * their migration tool — the framework never auto-migrates (§8.5).\n *\n * @example\n * await pool.query(store.schema());\n */\n public schema(): string {\n return [\n `CREATE TABLE IF NOT EXISTS ${this.table} (`,\n ` orchestrator_name TEXT NOT NULL,`,\n ` session_id TEXT NOT NULL,`,\n ` turn_index INTEGER NOT NULL,`,\n ` state JSONB NOT NULL,`,\n ` last_route TEXT,`,\n ` signature TEXT NOT NULL,`,\n ` version TEXT,`,\n ` summarized_through INTEGER,`,\n ` lock_acquired_at TIMESTAMPTZ,`,\n ` lock_expires_at TIMESTAMPTZ,`,\n ` saved_at TIMESTAMPTZ NOT NULL DEFAULT now(),`,\n ` PRIMARY KEY (orchestrator_name, session_id, turn_index)`,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_saved_at`,\n ` ON ${this.table} (saved_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_lookup`,\n ` ON ${this.table} (orchestrator_name, session_id, turn_index DESC);`,\n ].join(\"\\n\");\n }\n\n /**\n * Set the idle-row TTL (§8.2). Stored for prune-time cleanup; the\n * store never opens a background timer.\n */\n public setOptions(options: { ttl?: number }): void {\n this.ttl = options.ttl;\n }\n\n /**\n * `last_route` rides a single `TEXT` column. A fan-out array is\n * JSON-encoded so it round-trips through one column without a schema\n * change; a single intent (an identifier — never starts with `[`) is\n * stored verbatim. {@link deserializeRoute} reverses this on load.\n */\n private serializeRoute(route: string | string[] | null): string | null {\n if (route === null) {\n return null;\n }\n\n if (Array.isArray(route)) {\n return JSON.stringify(route);\n }\n\n return route;\n }\n}\n\n/**\n * Create a Postgres-backed {@link CheckpointStore} (orchestrator.md\n * §8.3). The dev installs `pg` and passes a `pg.Pool` / `pg.Client` —\n * `@warlock.js/ai` never imports `pg`. Run {@link CheckpointStore.schema}\n * through your migration tool once before use; the store never\n * auto-migrates.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * const store = ai.checkpoint.pg({ client: pool });\n *\n * // Once, via your migration tooling:\n * // await pool.query(store.schema());\n */\nexport function pg(options: PgCheckpointOptions): CheckpointStore {\n return new PgCheckpointStore(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pg.mjs","names":[],"sources":["../../../../../../../ai/src/checkpoint/pg.ts"],"sourcesContent":["import type {\n CheckpointRecord,\n CheckpointStore,\n} from \"../contracts/orchestrator/checkpoint-store.contract\";\nimport type { PgClientLike } from \"../contracts/orchestrator/snapshot-store.contract\";\n\n/**\n * Options for the Postgres {@link CheckpointStore} (orchestrator.md §8.3).\n *\n * The dev owns the connection — `@warlock.js/ai` takes no peer dep on\n * `pg` and never opens or closes the client. A single `pg.Pool` can\n * back both the cache and the orchestrator stores.\n */\nexport type PgCheckpointOptions = {\n /** An already-built `pg.Pool` / `pg.Client` — anything matching {@link PgClientLike}. */\n client: PgClientLike;\n /** Backing table name. Defaults to `warlock_orchestrator_sessions` (§8.6). Must be a safe SQL identifier. */\n table?: string;\n /** Idle-row TTL in seconds. When set, rows older than the TTL are eligible for cleanup on prune. */\n ttl?: number;\n};\n\n/**\n * Default backing table — matches the §8.6 reference DDL verbatim so a\n * stock migration provisions the store with no extra config.\n */\nconst DEFAULT_TABLE = \"warlock_orchestrator_sessions\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). The table\n * name is interpolated into DDL/DML, so anything outside this\n * conservative ASCII subset is rejected — interpolating an arbitrary\n * string would be a SQL-injection footgun (mirrors `PgCacheDriver`).\n */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Coerce a Postgres `INTEGER` column back to a number. `pg` hands back\n * `INTEGER` as a JS number already, but some pool wrappers surface it\n * as a string — normalize defensively so `turn_index` arithmetic and\n * the latest-turn ordering never compare strings.\n */\nfunction toNumber(value: unknown): number {\n return typeof value === \"string\" ? Number(value) : (value as number);\n}\n\n/**\n * Coerce a nullable Postgres integer column to `number | null`.\n */\nfunction toNullableNumber(value: unknown): number | null {\n return value === null || value === undefined ? null : toNumber(value);\n}\n\n/**\n * Coerce a Postgres timestamp/text column to an ISO string. `pg`\n * returns `TIMESTAMPTZ` as a `Date`; normalize to the ISO wire shape\n * the {@link CheckpointRecord} contract declares.\n */\nfunction toIso(value: unknown): string {\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return value as string;\n}\n\n/**\n * Coerce a nullable Postgres timestamp column to `string | null`.\n */\nfunction toNullableIso(value: unknown): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n return toIso(value);\n}\n\n/**\n * Decode the single `last_route` `TEXT` column back to the\n * `string | string[] | null` shape the contract declares. A fan-out\n * array is written JSON-encoded (it starts with `[`), so a leading `[`\n * is the signal to parse; any other value is a single intent stored\n * verbatim. Symmetric with {@link PgCheckpointStore.serializeRoute}.\n */\nfunction deserializeRoute(value: unknown): string | string[] | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n const route = value as string;\n\n if (route.startsWith(\"[\")) {\n return JSON.parse(route) as string[];\n }\n\n return route;\n}\n\n/**\n * Map a raw DB row to a {@link CheckpointRecord}. Column names match\n * the §8.6 DDL 1:1, so this is a typed projection plus the defensive\n * coercions a heterogeneous `pg` client population needs.\n */\nfunction rowToRecord(row: Record<string, unknown>): CheckpointRecord {\n const state =\n typeof row.state === \"string\" ? JSON.parse(row.state) : row.state;\n\n return {\n orchestrator_name: row.orchestrator_name as string,\n session_id: row.session_id as string,\n turn_index: toNumber(row.turn_index),\n state,\n last_route: deserializeRoute(row.last_route),\n signature: row.signature as string,\n version: (row.version as string | null) ?? null,\n summarized_through: toNullableNumber(row.summarized_through),\n lock_acquired_at: toNullableIso(row.lock_acquired_at),\n lock_expires_at: toNullableIso(row.lock_expires_at),\n saved_at: toIso(row.saved_at),\n };\n}\n\n/**\n * Postgres-backed {@link CheckpointStore} (orchestrator.md §8.2, §8.6).\n *\n * Owns: append-only checkpoint rows keyed by\n * `(orchestrator_name, session_id, turn_index)`, the \"latest turn wins\"\n * load, the §8.6 DDL via {@link PgCheckpointStore.schema}, and the\n * §4-Phase-6 retention prune. Does NOT own: the connection lifecycle\n * (the dev passes a client and keeps it), schema migration (the dev\n * runs `schema()` through their own tool — never auto-migrated, §8.5),\n * or the `keepSnapshots` policy itself (that lives on the orchestrator\n * config; the orchestrator passes the resolved bound into\n * {@link PgCheckpointStore.prune}).\n *\n * Front it with the {@link pg} factory — callers never `new` it.\n */\nclass PgCheckpointStore implements CheckpointStore {\n /** The dev-supplied `pg.Pool` / `pg.Client`. Never closed by the store. */\n private readonly client: PgClientLike;\n\n /** Validated backing table name, safe to interpolate into SQL. */\n private readonly table: string;\n\n /** Idle-row TTL in seconds, or `undefined` for no expiry. */\n private ttl?: number;\n\n public constructor(options: PgCheckpointOptions) {\n if (!options || typeof options.client?.query !== \"function\") {\n throw new TypeError(\n \"ai.checkpoint.pg requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? DEFAULT_TABLE;\n\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new TypeError(\n `ai.checkpoint.pg: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n this.client = options.client;\n this.table = table;\n this.ttl = options.ttl;\n }\n\n /**\n * Return the latest checkpoint (highest `turn_index`) for a session,\n * or `undefined` when the store has never seen it. The `(name,\n * session_id, turn_index DESC)` lookup index keeps this O(1) on the\n * latest row (§8.6).\n */\n public async load(\n orchestratorName: string,\n sessionId: string,\n ): Promise<CheckpointRecord | undefined> {\n const { rows } = await this.client.query(\n `SELECT * FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2\n ORDER BY turn_index DESC\n LIMIT 1`,\n [orchestratorName, sessionId],\n );\n\n if (rows.length === 0) {\n return undefined;\n }\n\n return rowToRecord(rows[0] as Record<string, unknown>);\n }\n\n /**\n * Persist a fresh checkpoint row. Append-only — an existing\n * `turn_index` is never overwritten; the PK collision surfaces as a\n * Postgres error rather than a silent clobber (§4 Phase 6, Q15).\n */\n public async save(record: CheckpointRecord): Promise<void> {\n await this.client.query(\n `INSERT INTO ${this.table} (\n orchestrator_name, session_id, turn_index, state, last_route,\n signature, version, summarized_through, lock_acquired_at,\n lock_expires_at, saved_at\n )\n VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9, $10, $11)`,\n [\n record.orchestrator_name,\n record.session_id,\n record.turn_index,\n JSON.stringify(record.state),\n this.serializeRoute(record.last_route),\n record.signature,\n record.version,\n record.summarized_through,\n record.lock_acquired_at,\n record.lock_expires_at,\n record.saved_at,\n ],\n );\n }\n\n /**\n * Delete every checkpoint row for a session, ending it.\n */\n public async delete(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n await this.client.query(\n `DELETE FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2`,\n [orchestratorName, sessionId],\n );\n }\n\n /**\n * List the distinct session ids known for an orchestrator, optionally\n * filtered by a session-id prefix. Used by the production boot-drain\n * loop (§9.3). The prefix is matched with `LIKE`, escaping the SQL\n * wildcards so a literal `_` or `%` in the prefix is not treated as a\n * pattern.\n */\n public async list(\n orchestratorName: string,\n prefix?: string,\n ): Promise<string[]> {\n if (prefix === undefined) {\n const { rows } = await this.client.query(\n `SELECT DISTINCT session_id FROM ${this.table}\n WHERE orchestrator_name = $1`,\n [orchestratorName],\n );\n\n return rows.map((row) => (row as Record<string, unknown>).session_id as string);\n }\n\n const escaped = prefix\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/_/g, \"\\\\_\")\n .replace(/%/g, \"\\\\%\");\n\n const { rows } = await this.client.query(\n `SELECT DISTINCT session_id FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id LIKE $2 ESCAPE '\\\\'`,\n [orchestratorName, `${escaped}%`],\n );\n\n return rows.map((row) => (row as Record<string, unknown>).session_id as string);\n }\n\n /**\n * Prune retained turns for a session down to the most recent\n * `keepSnapshots` rows (orchestrator.md §4 Phase 6 / §15.2). Deletes\n * every row with `turn_index < (max_turn_index - keepSnapshots)`. The\n * orchestrator calls this synchronously after a successful\n * {@link save} when `keepSnapshots` is a finite number; `\"all\"`\n * retention skips the call entirely. Additive to the\n * {@link CheckpointStore} contract — the contract carries no prune\n * hook, so the policy stays on the orchestrator and the store only\n * executes the bounded delete.\n */\n public async prune(\n orchestratorName: string,\n sessionId: string,\n keepSnapshots: number,\n ): Promise<void> {\n if (!Number.isFinite(keepSnapshots) || keepSnapshots < 0) {\n return;\n }\n\n await this.client.query(\n `DELETE FROM ${this.table}\n WHERE orchestrator_name = $1\n AND session_id = $2\n AND turn_index < (\n SELECT max(turn_index) - $3\n FROM ${this.table}\n WHERE orchestrator_name = $1 AND session_id = $2\n )`,\n [orchestratorName, sessionId, keepSnapshots],\n );\n }\n\n /**\n * Return the §8.6 reference DDL for this store's backing table,\n * interpolating the configured table name. The dev runs it through\n * their migration tool — the framework never auto-migrates (§8.5).\n *\n * @example\n * await pool.query(store.schema());\n */\n public schema(): string {\n return [\n `CREATE TABLE IF NOT EXISTS ${this.table} (`,\n ` orchestrator_name TEXT NOT NULL,`,\n ` session_id TEXT NOT NULL,`,\n ` turn_index INTEGER NOT NULL,`,\n ` state JSONB NOT NULL,`,\n ` last_route TEXT,`,\n ` signature TEXT NOT NULL,`,\n ` version TEXT,`,\n ` summarized_through INTEGER,`,\n ` lock_acquired_at TIMESTAMPTZ,`,\n ` lock_expires_at TIMESTAMPTZ,`,\n ` saved_at TIMESTAMPTZ NOT NULL DEFAULT now(),`,\n ` PRIMARY KEY (orchestrator_name, session_id, turn_index)`,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_saved_at`,\n ` ON ${this.table} (saved_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_lookup`,\n ` ON ${this.table} (orchestrator_name, session_id, turn_index DESC);`,\n ].join(\"\\n\");\n }\n\n /**\n * Set the idle-row TTL (§8.2). Stored for prune-time cleanup; the\n * store never opens a background timer.\n */\n public setOptions(options: { ttl?: number }): void {\n this.ttl = options.ttl;\n }\n\n /**\n * `last_route` rides a single `TEXT` column. A fan-out array is\n * JSON-encoded so it round-trips through one column without a schema\n * change; a single intent (an identifier — never starts with `[`) is\n * stored verbatim. {@link deserializeRoute} reverses this on load.\n */\n private serializeRoute(route: string | string[] | null): string | null {\n if (route === null) {\n return null;\n }\n\n if (Array.isArray(route)) {\n return JSON.stringify(route);\n }\n\n return route;\n }\n}\n\n/**\n * Create a Postgres-backed {@link CheckpointStore} (orchestrator.md\n * §8.3). The dev installs `pg` and passes a `pg.Pool` / `pg.Client` —\n * `@warlock.js/ai` never imports `pg`. Run {@link CheckpointStore.schema}\n * through your migration tool once before use; the store never\n * auto-migrates.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * const store = ai.checkpoint.pg({ client: pool });\n *\n * // Once, via your migration tooling:\n * // await pool.query(store.schema());\n */\nexport function pg(options: PgCheckpointOptions): CheckpointStore {\n return new PgCheckpointStore(options);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.mjs","names":[],"sources":["../../../../../../../ai/src/checkpoint/redis.ts"],"sourcesContent":["import type {\n CheckpointRecord,\n CheckpointStore,\n} from \"../contracts/orchestrator/checkpoint-store.contract\";\nimport type { RedisClientLike } from \"../contracts/orchestrator/snapshot-store.contract\";\n\n/**\n * Options for the Redis {@link CheckpointStore} (orchestrator.md §8.3).\n *\n * The dev owns the connection — `@warlock.js/ai` takes no peer dep on\n * `redis` and never opens or closes the client.\n */\nexport type RedisCheckpointOptions = {\n /** An already-connected `redis` client — anything matching {@link RedisClientLike}. */\n client: RedisClientLike;\n /**\n * Key prefix for every key this store writes. Lets one Redis database\n * back multiple stores without collision. Defaults to\n * `warlock:orchestrator`.\n */\n prefix?: string;\n /** Idle-key TTL in seconds. When set, every written key expires after the TTL. */\n ttl?: number;\n};\n\n/**\n * Default key prefix — namespaces the store's keys inside a shared\n * Redis database.\n */\nconst DEFAULT_PREFIX = \"warlock:orchestrator\";\n\n/**\n * Per-session document persisted under one Redis key: the append-only\n * list of {@link CheckpointRecord} rows in turn order, mirroring the\n * Postgres append-only PK shape (§8.6) inside a single JSON value so\n * the store needs only `get`/`set`/`del` from {@link RedisClientLike}.\n */\ntype SessionDocument = {\n rows: CheckpointRecord[];\n};\n\n/**\n * Per-orchestrator index document: the set of live session ids. Kept as\n * a JSON array because {@link RedisClientLike} exposes no `keys` / `scan`\n * — enumeration for the §9.3 boot drain must be self-maintained.\n */\ntype IndexDocument = {\n sessionIds: string[];\n};\n\n/**\n * Redis-backed {@link CheckpointStore} (orchestrator.md §8.2).\n *\n * Owns: the per-session append-only document, a per-orchestrator\n * session-id index (so {@link RedisCheckpointStore.list} works without\n * `KEYS`/`SCAN`), the \"latest turn wins\" load, and the §4-Phase-6\n * retention prune. Does NOT own: durability guarantees beyond Redis's\n * own, the connection lifecycle (the dev passes a client), or the\n * `keepSnapshots` policy (that lives on the orchestrator config).\n *\n * Because {@link RedisClientLike} is intentionally minimal (`get` /\n * `set` / `del` only — §8.4), the store models a session as a single\n * JSON document rather than one Redis key per turn. This keeps every\n * operation a single round-trip and avoids depending on key scanning,\n * at the cost of read-modify-write on `save`. Callers must serialize\n * traffic per `sessionId` anyway (§17 \"two turns racing\"), so the\n * read-modify-write is safe under that contract.\n *\n * Front it with the {@link redis} factory — callers never `new` it.\n */\nclass RedisCheckpointStore implements CheckpointStore {\n /** The dev-supplied redis client. Never disconnected by the store. */\n private readonly client: RedisClientLike;\n\n /** Key prefix namespacing every key this store writes. */\n private readonly prefix: string;\n\n /** Idle-key TTL in seconds, or `undefined` for no expiry. */\n private ttl?: number;\n\n public constructor(options: RedisCheckpointOptions) {\n if (\n !options ||\n typeof options.client?.get !== \"function\" ||\n typeof options.client?.set !== \"function\" ||\n typeof options.client?.del !== \"function\"\n ) {\n throw new TypeError(\n \"ai.checkpoint.redis requires a 'client' option implementing { get, set, del } — pass a connected redis client.\",\n );\n }\n\n this.client = options.client;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n this.ttl = options.ttl;\n }\n\n /**\n * Return the latest checkpoint (highest `turn_index`) for a session,\n * or `undefined` when the session has no document. Rows are appended\n * in turn order, so the last element is the latest.\n */\n public async load(\n orchestratorName: string,\n sessionId: string,\n ): Promise<CheckpointRecord | undefined> {\n const document = await this.readSession(orchestratorName, sessionId);\n\n if (!document || document.rows.length === 0) {\n return undefined;\n }\n\n return document.rows[document.rows.length - 1];\n }\n\n /**\n * Append a checkpoint row to its session document, creating the\n * document and indexing the session id on first write. Append-only —\n * an existing `turn_index` is never overwritten; a fresh row is\n * pushed (§4 Phase 6, Q15).\n */\n public async save(record: CheckpointRecord): Promise<void> {\n const { orchestrator_name, session_id } = record;\n\n const document =\n (await this.readSession(orchestrator_name, session_id)) ?? { rows: [] };\n\n document.rows.push(record);\n\n await this.writeSession(orchestrator_name, session_id, document);\n await this.indexSession(orchestrator_name, session_id);\n }\n\n /**\n * Drop a session document and de-index its session id.\n */\n public async delete(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n await this.client.del(this.sessionKey(orchestratorName, sessionId));\n await this.deindexSession(orchestratorName, sessionId);\n }\n\n /**\n * List the session ids known for an orchestrator, optionally filtered\n * by a session-id prefix. Reads the self-maintained index document\n * (§9.3 boot drain).\n */\n public async list(\n orchestratorName: string,\n prefix?: string,\n ): Promise<string[]> {\n const index = await this.readIndex(orchestratorName);\n\n if (prefix === undefined) {\n return [...index.sessionIds];\n }\n\n return index.sessionIds.filter((sessionId) =>\n sessionId.startsWith(prefix),\n );\n }\n\n /**\n * Prune retained turns for a session down to the most recent\n * `keepSnapshots` rows (orchestrator.md §4 Phase 6 / §15.2). Drops\n * every row whose `turn_index` is below `(max_turn_index -\n * keepSnapshots)`. The orchestrator calls this synchronously after a\n * successful {@link save} when `keepSnapshots` is a finite number;\n * `\"all\"` retention skips the call. Additive to the\n * {@link CheckpointStore} contract — the policy stays on the\n * orchestrator and the store only executes the bounded trim.\n */\n public async prune(\n orchestratorName: string,\n sessionId: string,\n keepSnapshots: number,\n ): Promise<void> {\n if (!Number.isFinite(keepSnapshots) || keepSnapshots < 0) {\n return;\n }\n\n const document = await this.readSession(orchestratorName, sessionId);\n\n if (!document || document.rows.length === 0) {\n return;\n }\n\n const maxTurnIndex = document.rows[document.rows.length - 1].turn_index;\n const threshold = maxTurnIndex - keepSnapshots;\n\n const kept = document.rows.filter((row) => row.turn_index >= threshold);\n\n if (kept.length === document.rows.length) {\n return;\n }\n\n await this.writeSession(orchestratorName, sessionId, { rows: kept });\n }\n\n /**\n * The Redis store has no relational table — there is nothing to\n * migrate. Returns an empty string so callers can treat `schema()`\n * uniformly across drivers (mirrors the memory store).\n */\n public schema(): string {\n return \"\";\n }\n\n /**\n * Set the idle-key TTL (§8.2). Applied on every subsequent write; the\n * store never opens a background timer.\n */\n public setOptions(options: { ttl?: number }): void {\n this.ttl = options.ttl;\n }\n\n /**\n * Read and parse a session document, or `undefined` when the key is\n * absent.\n */\n private async readSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<SessionDocument | undefined> {\n const raw = await this.client.get(\n this.sessionKey(orchestratorName, sessionId),\n );\n\n if (raw === null) {\n return undefined;\n }\n\n return JSON.parse(raw) as SessionDocument;\n }\n\n /**\n * Serialize and persist a session document, honoring the configured\n * idle TTL when set.\n */\n private async writeSession(\n orchestratorName: string,\n sessionId: string,\n document: SessionDocument,\n ): Promise<void> {\n await this.write(\n this.sessionKey(orchestratorName, sessionId),\n JSON.stringify(document),\n );\n }\n\n /**\n * Read and parse the per-orchestrator index document, defaulting to an\n * empty index when absent.\n */\n private async readIndex(orchestratorName: string): Promise<IndexDocument> {\n const raw = await this.client.get(this.indexKey(orchestratorName));\n\n if (raw === null) {\n return { sessionIds: [] };\n }\n\n return JSON.parse(raw) as IndexDocument;\n }\n\n /**\n * Add a session id to the per-orchestrator index, no-op when already\n * present.\n */\n private async indexSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n const index = await this.readIndex(orchestratorName);\n\n if (index.sessionIds.includes(sessionId)) {\n return;\n }\n\n index.sessionIds.push(sessionId);\n\n await this.write(this.indexKey(orchestratorName), JSON.stringify(index));\n }\n\n /**\n * Remove a session id from the per-orchestrator index, no-op when\n * absent.\n */\n private async deindexSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n const index = await this.readIndex(orchestratorName);\n const next = index.sessionIds.filter((id) => id !== sessionId);\n\n if (next.length === index.sessionIds.length) {\n return;\n }\n\n await this.write(\n this.indexKey(orchestratorName),\n JSON.stringify({ sessionIds: next }),\n );\n }\n\n /**\n * Write a key, attaching the `EX` expiry option when an idle TTL is\n * configured. The TTL flows through {@link RedisClientLike.set}'s\n * variadic args as node-redis's `{ EX }` option object.\n */\n private async write(key: string, value: string): Promise<void> {\n if (this.ttl !== undefined && this.ttl > 0) {\n await this.client.set(key, value, { EX: this.ttl });\n\n return;\n }\n\n await this.client.set(key, value);\n }\n\n /**\n * Key for a session document — `<prefix>:session:<name>:<sessionId>`.\n */\n private sessionKey(orchestratorName: string, sessionId: string): string {\n return `${this.prefix}:session:${orchestratorName}:${sessionId}`;\n }\n\n /**\n * Key for a per-orchestrator session-id index —\n * `<prefix>:index:<name>`.\n */\n private indexKey(orchestratorName: string): string {\n return `${this.prefix}:index:${orchestratorName}`;\n }\n}\n\n/**\n * Create a Redis-backed {@link CheckpointStore} (orchestrator.md §8.3).\n * The dev installs `redis` and passes a connected client —\n * `@warlock.js/ai` never imports `redis`. {@link CheckpointStore.schema}\n * returns an empty string; Redis needs no migration.\n *\n * @example\n * import { createClient } from \"redis\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const client = createClient();\n * await client.connect();\n *\n * const store = ai.checkpoint.redis({ client });\n */\nexport function redis(options: RedisCheckpointOptions): CheckpointStore {\n return new RedisCheckpointStore(options);\n}\n"],"mappings":";;;;;AA6BA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;AAyCvB,IAAM,uBAAN,MAAsD;CAUpD,AAAO,YAAY,SAAiC;EAClD,IACE,CAAC,WACD,OAAO,QAAQ,QAAQ,QAAQ,cAC/B,OAAO,QAAQ,QAAQ,QAAQ,cAC/B,OAAO,QAAQ,QAAQ,QAAQ,YAE/B,MAAM,IAAI,UACR,gHACF;EAGF,KAAK,SAAS,QAAQ;EACtB,KAAK,SAAS,QAAQ,UAAU;EAChC,KAAK,MAAM,QAAQ;CACrB;;;;;;CAOA,MAAa,KACX,kBACA,WACuC;EACvC,MAAM,WAAW,MAAM,KAAK,YAAY,kBAAkB,SAAS;EAEnE,IAAI,CAAC,YAAY,SAAS,KAAK,WAAW,GACxC;EAGF,OAAO,SAAS,KAAK,SAAS,KAAK,SAAS;CAC9C;;;;;;;CAQA,MAAa,KAAK,QAAyC;EACzD,MAAM,EAAE,mBAAmB,eAAe;EAE1C,MAAM,WACH,MAAM,KAAK,YAAY,mBAAmB,UAAU,KAAM,EAAE,MAAM,CAAC,EAAE;EAExE,SAAS,KAAK,KAAK,MAAM;EAEzB,MAAM,KAAK,aAAa,mBAAmB,YAAY,QAAQ;EAC/D,MAAM,KAAK,aAAa,mBAAmB,UAAU;CACvD;;;;CAKA,MAAa,OACX,kBACA,WACe;EACf,MAAM,KAAK,OAAO,IAAI,KAAK,WAAW,kBAAkB,SAAS,CAAC;EAClE,MAAM,KAAK,eAAe,kBAAkB,SAAS;CACvD;;;;;;CAOA,MAAa,KACX,kBACA,QACmB;EACnB,MAAM,QAAQ,MAAM,KAAK,UAAU,gBAAgB;EAEnD,IAAI,WAAW,QACb,OAAO,CAAC,GAAG,MAAM,UAAU;EAG7B,OAAO,MAAM,WAAW,QAAQ,cAC9B,UAAU,WAAW,MAAM,CAC7B;CACF;;;;;;;;;;;CAYA,MAAa,MACX,kBACA,WACA,eACe;EACf,IAAI,CAAC,OAAO,SAAS,aAAa,KAAK,gBAAgB,GACrD;EAGF,MAAM,WAAW,MAAM,KAAK,YAAY,kBAAkB,SAAS;EAEnE,IAAI,CAAC,YAAY,SAAS,KAAK,WAAW,GACxC;EAIF,MAAM,YADe,SAAS,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,aAC5B;EAEjC,MAAM,OAAO,SAAS,KAAK,QAAQ,QAAQ,IAAI,cAAc,SAAS;EAEtE,IAAI,KAAK,WAAW,SAAS,KAAK,QAChC;EAGF,MAAM,KAAK,aAAa,kBAAkB,WAAW,EAAE,MAAM,KAAK,CAAC;CACrE;;;;;;CAOA,AAAO,SAAiB;EACtB,OAAO;CACT;;;;;CAMA,AAAO,WAAW,SAAiC;EACjD,KAAK,MAAM,QAAQ;CACrB;;;;;CAMA,MAAc,YACZ,kBACA,WACsC;EACtC,MAAM,MAAM,MAAM,KAAK,OAAO,IAC5B,KAAK,WAAW,kBAAkB,SAAS,CAC7C;EAEA,IAAI,QAAQ,MACV;EAGF,OAAO,KAAK,MAAM,GAAG;CACvB;;;;;CAMA,MAAc,aACZ,kBACA,WACA,UACe;EACf,MAAM,KAAK,MACT,KAAK,WAAW,kBAAkB,SAAS,GAC3C,KAAK,UAAU,QAAQ,CACzB;CACF;;;;;CAMA,MAAc,UAAU,kBAAkD;EACxE,MAAM,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,gBAAgB,CAAC;EAEjE,IAAI,QAAQ,MACV,OAAO,EAAE,YAAY,CAAC,EAAE;EAG1B,OAAO,KAAK,MAAM,GAAG;CACvB;;;;;CAMA,MAAc,aACZ,kBACA,WACe;EACf,MAAM,QAAQ,MAAM,KAAK,UAAU,gBAAgB;EAEnD,IAAI,MAAM,WAAW,SAAS,SAAS,GACrC;EAGF,MAAM,WAAW,KAAK,SAAS;EAE/B,MAAM,KAAK,MAAM,KAAK,SAAS,gBAAgB,GAAG,KAAK,UAAU,KAAK,CAAC;CACzE;;;;;CAMA,MAAc,eACZ,kBACA,WACe;EACf,MAAM,QAAQ,MAAM,KAAK,UAAU,gBAAgB;EACnD,MAAM,OAAO,MAAM,WAAW,QAAQ,OAAO,OAAO,SAAS;EAE7D,IAAI,KAAK,WAAW,MAAM,WAAW,QACnC;EAGF,MAAM,KAAK,MACT,KAAK,SAAS,gBAAgB,GAC9B,KAAK,UAAU,EAAE,YAAY,KAAK,CAAC,CACrC;CACF;;;;;;CAOA,MAAc,MAAM,KAAa,OAA8B;EAC7D,IAAI,KAAK,QAAQ,UAAa,KAAK,MAAM,GAAG;GAC1C,MAAM,KAAK,OAAO,IAAI,KAAK,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;GAElD;EACF;EAEA,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK;CAClC;;;;CAKA,AAAQ,WAAW,kBAA0B,WAA2B;EACtE,OAAO,GAAG,KAAK,OAAO,WAAW,iBAAiB,GAAG;CACvD;;;;;CAMA,AAAQ,SAAS,kBAAkC;EACjD,OAAO,GAAG,KAAK,OAAO,SAAS;CACjC;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,MAAM,SAAkD;CACtE,OAAO,IAAI,qBAAqB,OAAO;AACzC"}
|
|
1
|
+
{"version":3,"file":"redis.mjs","names":[],"sources":["../../../../../../../ai/src/checkpoint/redis.ts"],"sourcesContent":["import type {\n CheckpointRecord,\n CheckpointStore,\n} from \"../contracts/orchestrator/checkpoint-store.contract\";\nimport type { RedisClientLike } from \"../contracts/orchestrator/snapshot-store.contract\";\n\n/**\n * Options for the Redis {@link CheckpointStore} (orchestrator.md §8.3).\n *\n * The dev owns the connection — `@warlock.js/ai` takes no peer dep on\n * `redis` and never opens or closes the client.\n */\nexport type RedisCheckpointOptions = {\n /** An already-connected `redis` client — anything matching {@link RedisClientLike}. */\n client: RedisClientLike;\n /**\n * Key prefix for every key this store writes. Lets one Redis database\n * back multiple stores without collision. Defaults to\n * `warlock:orchestrator`.\n */\n prefix?: string;\n /** Idle-key TTL in seconds. When set, every written key expires after the TTL. */\n ttl?: number;\n};\n\n/**\n * Default key prefix — namespaces the store's keys inside a shared\n * Redis database.\n */\nconst DEFAULT_PREFIX = \"warlock:orchestrator\";\n\n/**\n * Per-session document persisted under one Redis key: the append-only\n * list of {@link CheckpointRecord} rows in turn order, mirroring the\n * Postgres append-only PK shape (§8.6) inside a single JSON value so\n * the store needs only `get`/`set`/`del` from {@link RedisClientLike}.\n */\ntype SessionDocument = {\n rows: CheckpointRecord[];\n};\n\n/**\n * Per-orchestrator index document: the set of live session ids. Kept as\n * a JSON array because {@link RedisClientLike} exposes no `keys` / `scan`\n * — enumeration for the §9.3 boot drain must be self-maintained.\n */\ntype IndexDocument = {\n sessionIds: string[];\n};\n\n/**\n * Redis-backed {@link CheckpointStore} (orchestrator.md §8.2).\n *\n * Owns: the per-session append-only document, a per-orchestrator\n * session-id index (so {@link RedisCheckpointStore.list} works without\n * `KEYS`/`SCAN`), the \"latest turn wins\" load, and the §4-Phase-6\n * retention prune. Does NOT own: durability guarantees beyond Redis's\n * own, the connection lifecycle (the dev passes a client), or the\n * `keepSnapshots` policy (that lives on the orchestrator config).\n *\n * Because {@link RedisClientLike} is intentionally minimal (`get` /\n * `set` / `del` only — §8.4), the store models a session as a single\n * JSON document rather than one Redis key per turn. This keeps every\n * operation a single round-trip and avoids depending on key scanning,\n * at the cost of read-modify-write on `save`. Callers must serialize\n * traffic per `sessionId` anyway (§17 \"two turns racing\"), so the\n * read-modify-write is safe under that contract.\n *\n * Front it with the {@link redis} factory — callers never `new` it.\n */\nclass RedisCheckpointStore implements CheckpointStore {\n /** The dev-supplied redis client. Never disconnected by the store. */\n private readonly client: RedisClientLike;\n\n /** Key prefix namespacing every key this store writes. */\n private readonly prefix: string;\n\n /** Idle-key TTL in seconds, or `undefined` for no expiry. */\n private ttl?: number;\n\n public constructor(options: RedisCheckpointOptions) {\n if (\n !options ||\n typeof options.client?.get !== \"function\" ||\n typeof options.client?.set !== \"function\" ||\n typeof options.client?.del !== \"function\"\n ) {\n throw new TypeError(\n \"ai.checkpoint.redis requires a 'client' option implementing { get, set, del } — pass a connected redis client.\",\n );\n }\n\n this.client = options.client;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n this.ttl = options.ttl;\n }\n\n /**\n * Return the latest checkpoint (highest `turn_index`) for a session,\n * or `undefined` when the session has no document. Rows are appended\n * in turn order, so the last element is the latest.\n */\n public async load(\n orchestratorName: string,\n sessionId: string,\n ): Promise<CheckpointRecord | undefined> {\n const document = await this.readSession(orchestratorName, sessionId);\n\n if (!document || document.rows.length === 0) {\n return undefined;\n }\n\n return document.rows[document.rows.length - 1];\n }\n\n /**\n * Append a checkpoint row to its session document, creating the\n * document and indexing the session id on first write. Append-only —\n * an existing `turn_index` is never overwritten; a fresh row is\n * pushed (§4 Phase 6, Q15).\n */\n public async save(record: CheckpointRecord): Promise<void> {\n const { orchestrator_name, session_id } = record;\n\n const document =\n (await this.readSession(orchestrator_name, session_id)) ?? { rows: [] };\n\n document.rows.push(record);\n\n await this.writeSession(orchestrator_name, session_id, document);\n await this.indexSession(orchestrator_name, session_id);\n }\n\n /**\n * Drop a session document and de-index its session id.\n */\n public async delete(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n await this.client.del(this.sessionKey(orchestratorName, sessionId));\n await this.deindexSession(orchestratorName, sessionId);\n }\n\n /**\n * List the session ids known for an orchestrator, optionally filtered\n * by a session-id prefix. Reads the self-maintained index document\n * (§9.3 boot drain).\n */\n public async list(\n orchestratorName: string,\n prefix?: string,\n ): Promise<string[]> {\n const index = await this.readIndex(orchestratorName);\n\n if (prefix === undefined) {\n return [...index.sessionIds];\n }\n\n return index.sessionIds.filter((sessionId) =>\n sessionId.startsWith(prefix),\n );\n }\n\n /**\n * Prune retained turns for a session down to the most recent\n * `keepSnapshots` rows (orchestrator.md §4 Phase 6 / §15.2). Drops\n * every row whose `turn_index` is below `(max_turn_index -\n * keepSnapshots)`. The orchestrator calls this synchronously after a\n * successful {@link save} when `keepSnapshots` is a finite number;\n * `\"all\"` retention skips the call. Additive to the\n * {@link CheckpointStore} contract — the policy stays on the\n * orchestrator and the store only executes the bounded trim.\n */\n public async prune(\n orchestratorName: string,\n sessionId: string,\n keepSnapshots: number,\n ): Promise<void> {\n if (!Number.isFinite(keepSnapshots) || keepSnapshots < 0) {\n return;\n }\n\n const document = await this.readSession(orchestratorName, sessionId);\n\n if (!document || document.rows.length === 0) {\n return;\n }\n\n const maxTurnIndex = document.rows[document.rows.length - 1].turn_index;\n const threshold = maxTurnIndex - keepSnapshots;\n\n const kept = document.rows.filter((row) => row.turn_index >= threshold);\n\n if (kept.length === document.rows.length) {\n return;\n }\n\n await this.writeSession(orchestratorName, sessionId, { rows: kept });\n }\n\n /**\n * The Redis store has no relational table — there is nothing to\n * migrate. Returns an empty string so callers can treat `schema()`\n * uniformly across drivers (mirrors the memory store).\n */\n public schema(): string {\n return \"\";\n }\n\n /**\n * Set the idle-key TTL (§8.2). Applied on every subsequent write; the\n * store never opens a background timer.\n */\n public setOptions(options: { ttl?: number }): void {\n this.ttl = options.ttl;\n }\n\n /**\n * Read and parse a session document, or `undefined` when the key is\n * absent.\n */\n private async readSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<SessionDocument | undefined> {\n const raw = await this.client.get(\n this.sessionKey(orchestratorName, sessionId),\n );\n\n if (raw === null) {\n return undefined;\n }\n\n return JSON.parse(raw) as SessionDocument;\n }\n\n /**\n * Serialize and persist a session document, honoring the configured\n * idle TTL when set.\n */\n private async writeSession(\n orchestratorName: string,\n sessionId: string,\n document: SessionDocument,\n ): Promise<void> {\n await this.write(\n this.sessionKey(orchestratorName, sessionId),\n JSON.stringify(document),\n );\n }\n\n /**\n * Read and parse the per-orchestrator index document, defaulting to an\n * empty index when absent.\n */\n private async readIndex(orchestratorName: string): Promise<IndexDocument> {\n const raw = await this.client.get(this.indexKey(orchestratorName));\n\n if (raw === null) {\n return { sessionIds: [] };\n }\n\n return JSON.parse(raw) as IndexDocument;\n }\n\n /**\n * Add a session id to the per-orchestrator index, no-op when already\n * present.\n */\n private async indexSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n const index = await this.readIndex(orchestratorName);\n\n if (index.sessionIds.includes(sessionId)) {\n return;\n }\n\n index.sessionIds.push(sessionId);\n\n await this.write(this.indexKey(orchestratorName), JSON.stringify(index));\n }\n\n /**\n * Remove a session id from the per-orchestrator index, no-op when\n * absent.\n */\n private async deindexSession(\n orchestratorName: string,\n sessionId: string,\n ): Promise<void> {\n const index = await this.readIndex(orchestratorName);\n const next = index.sessionIds.filter((id) => id !== sessionId);\n\n if (next.length === index.sessionIds.length) {\n return;\n }\n\n await this.write(\n this.indexKey(orchestratorName),\n JSON.stringify({ sessionIds: next }),\n );\n }\n\n /**\n * Write a key, attaching the `EX` expiry option when an idle TTL is\n * configured. The TTL flows through {@link RedisClientLike.set}'s\n * variadic args as node-redis's `{ EX }` option object.\n */\n private async write(key: string, value: string): Promise<void> {\n if (this.ttl !== undefined && this.ttl > 0) {\n await this.client.set(key, value, { EX: this.ttl });\n\n return;\n }\n\n await this.client.set(key, value);\n }\n\n /**\n * Key for a session document — `<prefix>:session:<name>:<sessionId>`.\n */\n private sessionKey(orchestratorName: string, sessionId: string): string {\n return `${this.prefix}:session:${orchestratorName}:${sessionId}`;\n }\n\n /**\n * Key for a per-orchestrator session-id index —\n * `<prefix>:index:<name>`.\n */\n private indexKey(orchestratorName: string): string {\n return `${this.prefix}:index:${orchestratorName}`;\n }\n}\n\n/**\n * Create a Redis-backed {@link CheckpointStore} (orchestrator.md §8.3).\n * The dev installs `redis` and passes a connected client —\n * `@warlock.js/ai` never imports `redis`. {@link CheckpointStore.schema}\n * returns an empty string; Redis needs no migration.\n *\n * @example\n * import { createClient } from \"redis\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const client = createClient();\n * await client.connect();\n *\n * const store = ai.checkpoint.redis({ client });\n */\nexport function redis(options: RedisCheckpointOptions): CheckpointStore {\n return new RedisCheckpointStore(options);\n}\n"],"mappings":"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"}
|
package/esm/config.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.mts","names":[],"sources":["../../../../../../ai/src/config.ts"],"mappings":";;;;;;;AA2CA;;;;;;;;;;;;;;;AAoDsC;AACrC;;;;AAKsC;AAoBvC;;;;AAAwD;AAmBxD;;;;;;;;;;UAjGiB,QAAA;EAiGwC;;AAAQ;AAuBjE;;;;AAAuC;AAYvC;;;;AAAkD;AAUlD;;;;AAAgE;AAUhE;;;;AAA4D;EAhI1D,YAAA,GAAe,WAAA;;;;;;;;;;;;;EAcf,sBAAA,GAAyB,eAAA;;;;;;;;;;;;;EAczB,oBAAA,GAAuB,aAAA;AAAA;;KAMpB,cAAA,IAAkB,MAAgB,EAAR,QAAQ;;;;;;;;;;;;;;;;;iBAoBvB,eAAA,CAAgB,QAAwB,EAAd,cAAc;;;;;;;;;;;;;;;;iBAmBxC,WAAA,CAAY,OAAA,EAAS,OAAA,CAAQ,QAAA,IAAY,QAAA;;;;;;iBAuBzC,WAAA,
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../../../../../../ai/src/config.ts"],"mappings":";;;;;;;AA2CA;;;;;;;;;;;;;;;AAoDsC;AACrC;;;;AAKsC;AAoBvC;;;;AAAwD;AAmBxD;;;;;;;;;;UAjGiB,QAAA;EAiGwC;;AAAQ;AAuBjE;;;;AAAuC;AAYvC;;;;AAAkD;AAUlD;;;;AAAgE;AAUhE;;;;AAA4D;EAhI1D,YAAA,GAAe,WAAA;;;;;;;;;;;;;EAcf,sBAAA,GAAyB,eAAA;;;;;;;;;;;;;EAczB,oBAAA,GAAuB,aAAA;AAAA;;KAMpB,cAAA,IAAkB,MAAgB,EAAR,QAAQ;;;;;;;;;;;;;;;;;iBAoBvB,eAAA,CAAgB,QAAwB,EAAd,cAAc;;;;;;;;;;;;;;;;iBAmBxC,WAAA,CAAY,OAAA,EAAS,OAAA,CAAQ,QAAA,IAAY,QAAA;;;;;;iBAuBzC,WAAA,CAAA,GAAe,QAAQ;;;;;;;;;iBAYvB,mBAAA,CAAA,GAAuB,WAAW;;;;;;;iBAUlC,6BAAA,CAAA,GAAiC,eAAe;;;;;;;iBAUhD,2BAAA,CAAA,GAA+B,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/dataset.ts"],"mappings":";;;;;AAkIA;;;;;;;;;;;;;;;;AAE0B;;;iBAFV,OAAA,
|
|
1
|
+
{"version":3,"file":"dataset.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/dataset.ts"],"mappings":";;;;;AAkIA;;;;;;;;;;;;;;;;AAE0B;;;iBAFV,OAAA,mBAAA,CACd,OAAA,EAAS,cAAA,CAAe,OAAA,IACvB,eAAA,CAAgB,OAAA"}
|
package/esm/eval/dataset.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset.mjs","names":[],"sources":["../../../../../../../ai/src/eval/dataset.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { DatasetContract, DatasetEntry, DatasetOptions } from \"./dataset.type\";\nimport { InvalidRequestError } from \"../errors\";\n\n/**\n * Parse a JSONL file's contents into {@link DatasetEntry} rows. Blank\n * lines (and trailing whitespace-only lines) are skipped; every other\n * line must be a JSON object. A malformed line throws an\n * `InvalidRequestError` naming the 1-based line number — failing loud at\n * construction, like `SystemPrompt.fromFile`.\n */\nfunction parseJsonl<TOutput>(path: string, contents: string): DatasetEntry<TOutput>[] {\n const entries: DatasetEntry<TOutput>[] = [];\n const lines = contents.split(/\\r?\\n/);\n\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index].trim();\n\n if (line === \"\") {\n continue;\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(line);\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to parse dataset file \"${path}\" — line ${index + 1} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path, line: index + 1 }, cause: error },\n );\n }\n\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidRequestError(\n `Failed to parse dataset file \"${path}\" — line ${index + 1} is not a JSON object`,\n { context: { path, line: index + 1 } },\n );\n }\n\n entries.push(parsed as DatasetEntry<TOutput>);\n }\n\n return entries;\n}\n\n/**\n * Read a JSONL dataset file once, synchronously, at construction.\n * Mirrors `SystemPrompt.fromFile`: a read failure (missing path,\n * permission denied) throws an `InvalidRequestError` surfacing the\n * underlying cause.\n */\nfunction readDatasetFile<TOutput>(path: string): DatasetEntry<TOutput>[] {\n let contents: string;\n\n try {\n contents = readFileSync(path, \"utf8\");\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to read dataset file \"${path}\" — ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path }, cause: error },\n );\n }\n\n return parseJsonl<TOutput>(path, contents);\n}\n\n/**\n * Build an immutable {@link DatasetContract} over the given entries.\n * Shared by the {@link dataset} factory and by `filter` / `shard`, which\n * each return a fresh dataset built from a derived case list.\n */\nfunction makeDataset<TOutput>(\n name: string,\n cases: DatasetEntry<TOutput>[],\n): DatasetContract<TOutput> {\n return {\n name,\n cases,\n filter(predicate) {\n return makeDataset(name, cases.filter(predicate));\n },\n shard(index, total) {\n if (!Number.isInteger(total) || total <= 0) {\n throw new InvalidRequestError(\n `dataset.shard: \"total\" must be a positive integer, received ${total}`,\n { context: { name, total } },\n );\n }\n\n if (!Number.isInteger(index) || index < 0 || index >= total) {\n throw new InvalidRequestError(\n `dataset.shard: \"index\" must be an integer in [0, ${total}), received ${index}`,\n { context: { name, index, total } },\n );\n }\n\n return makeDataset(\n name,\n cases.filter((_, position) => position % total === index),\n );\n },\n };\n}\n\n/**\n * Create an immutable evaluation dataset that feeds `agent.eval({ cases })`\n * directly.\n *\n * **Role.** A taggable, filterable, shardable wrapper around a list of\n * {@link DatasetEntry} rows. `agent.eval` accepts a `DatasetContract` in\n * place of a raw `EvalCase[]`, reading `.cases` off it.\n *\n * Sources (combinable — file entries append after inline `cases`):\n * - `cases` → inline entries.\n * - `fromFile` → a JSONL file read once, synchronously, at construction\n * (one JSON object per line). A malformed line throws an\n * `InvalidRequestError` naming the 1-based line number.\n *\n * @example\n * const ds = dataset({ name: \"support\", fromFile: \"./eval/support.jsonl\" });\n * const smoke = ds.filter((entry) => entry.tags?.includes(\"smoke\"));\n * const shard = ds.shard(0, 4); // first of four parallel CI shards\n *\n * const report = await agent.eval({ cases: ds, scorers: [contains()] });\n */\nexport function dataset<TOutput = unknown>(\n options: DatasetOptions<TOutput>,\n): DatasetContract<TOutput> {\n const cases: DatasetEntry<TOutput>[] = [...(options.cases ?? [])];\n\n if (options.fromFile !== undefined) {\n cases.push(...readDatasetFile<TOutput>(options.fromFile));\n }\n\n return makeDataset(options.name, cases);\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAS,WAAoB,MAAc,UAA2C;CACpF,MAAM,UAAmC,CAAC;CAC1C,MAAM,QAAQ,SAAS,MAAM,OAAO;CAEpC,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;EACjD,MAAM,OAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"dataset.mjs","names":[],"sources":["../../../../../../../ai/src/eval/dataset.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport type { DatasetContract, DatasetEntry, DatasetOptions } from \"./dataset.type\";\nimport { InvalidRequestError } from \"../errors\";\n\n/**\n * Parse a JSONL file's contents into {@link DatasetEntry} rows. Blank\n * lines (and trailing whitespace-only lines) are skipped; every other\n * line must be a JSON object. A malformed line throws an\n * `InvalidRequestError` naming the 1-based line number — failing loud at\n * construction, like `SystemPrompt.fromFile`.\n */\nfunction parseJsonl<TOutput>(path: string, contents: string): DatasetEntry<TOutput>[] {\n const entries: DatasetEntry<TOutput>[] = [];\n const lines = contents.split(/\\r?\\n/);\n\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index].trim();\n\n if (line === \"\") {\n continue;\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(line);\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to parse dataset file \"${path}\" — line ${index + 1} is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path, line: index + 1 }, cause: error },\n );\n }\n\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidRequestError(\n `Failed to parse dataset file \"${path}\" — line ${index + 1} is not a JSON object`,\n { context: { path, line: index + 1 } },\n );\n }\n\n entries.push(parsed as DatasetEntry<TOutput>);\n }\n\n return entries;\n}\n\n/**\n * Read a JSONL dataset file once, synchronously, at construction.\n * Mirrors `SystemPrompt.fromFile`: a read failure (missing path,\n * permission denied) throws an `InvalidRequestError` surfacing the\n * underlying cause.\n */\nfunction readDatasetFile<TOutput>(path: string): DatasetEntry<TOutput>[] {\n let contents: string;\n\n try {\n contents = readFileSync(path, \"utf8\");\n } catch (error) {\n throw new InvalidRequestError(\n `Failed to read dataset file \"${path}\" — ${\n error instanceof Error ? error.message : String(error)\n }`,\n { context: { path }, cause: error },\n );\n }\n\n return parseJsonl<TOutput>(path, contents);\n}\n\n/**\n * Build an immutable {@link DatasetContract} over the given entries.\n * Shared by the {@link dataset} factory and by `filter` / `shard`, which\n * each return a fresh dataset built from a derived case list.\n */\nfunction makeDataset<TOutput>(\n name: string,\n cases: DatasetEntry<TOutput>[],\n): DatasetContract<TOutput> {\n return {\n name,\n cases,\n filter(predicate) {\n return makeDataset(name, cases.filter(predicate));\n },\n shard(index, total) {\n if (!Number.isInteger(total) || total <= 0) {\n throw new InvalidRequestError(\n `dataset.shard: \"total\" must be a positive integer, received ${total}`,\n { context: { name, total } },\n );\n }\n\n if (!Number.isInteger(index) || index < 0 || index >= total) {\n throw new InvalidRequestError(\n `dataset.shard: \"index\" must be an integer in [0, ${total}), received ${index}`,\n { context: { name, index, total } },\n );\n }\n\n return makeDataset(\n name,\n cases.filter((_, position) => position % total === index),\n );\n },\n };\n}\n\n/**\n * Create an immutable evaluation dataset that feeds `agent.eval({ cases })`\n * directly.\n *\n * **Role.** A taggable, filterable, shardable wrapper around a list of\n * {@link DatasetEntry} rows. `agent.eval` accepts a `DatasetContract` in\n * place of a raw `EvalCase[]`, reading `.cases` off it.\n *\n * Sources (combinable — file entries append after inline `cases`):\n * - `cases` → inline entries.\n * - `fromFile` → a JSONL file read once, synchronously, at construction\n * (one JSON object per line). A malformed line throws an\n * `InvalidRequestError` naming the 1-based line number.\n *\n * @example\n * const ds = dataset({ name: \"support\", fromFile: \"./eval/support.jsonl\" });\n * const smoke = ds.filter((entry) => entry.tags?.includes(\"smoke\"));\n * const shard = ds.shard(0, 4); // first of four parallel CI shards\n *\n * const report = await agent.eval({ cases: ds, scorers: [contains()] });\n */\nexport function dataset<TOutput = unknown>(\n options: DatasetOptions<TOutput>,\n): DatasetContract<TOutput> {\n const cases: DatasetEntry<TOutput>[] = [...(options.cases ?? [])];\n\n if (options.fromFile !== undefined) {\n cases.push(...readDatasetFile<TOutput>(options.fromFile));\n }\n\n return makeDataset(options.name, cases);\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAS,WAAoB,MAAc,UAA2C;CACpF,MAAM,UAAmC,CAAC;CAC1C,MAAM,QAAQ,SAAS,MAAM,OAAO;CAEpC,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;EACjD,MAAM,OAAO,MAAM,OAAO,KAAK;EAE/B,IAAI,SAAS,IACX;EAGF,IAAI;EAEJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI;EAC1B,SAAS,OAAO;GACd,MAAM,IAAI,oBACR,iCAAiC,KAAK,WAAW,QAAQ,EAAE,sBACzD,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEvD;IAAE,SAAS;KAAE;KAAM,MAAM,QAAQ;IAAE;IAAG,OAAO;GAAM,CACrD;EACF;EAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,oBACR,iCAAiC,KAAK,WAAW,QAAQ,EAAE,wBAC3D,EAAE,SAAS;GAAE;GAAM,MAAM,QAAQ;EAAE,EAAE,CACvC;EAGF,QAAQ,KAAK,MAA+B;CAC9C;CAEA,OAAO;AACT;;;;;;;AAQA,SAAS,gBAAyB,MAAuC;CACvE,IAAI;CAEJ,IAAI;EACF,WAAW,aAAa,MAAM,MAAM;CACtC,SAAS,OAAO;EACd,MAAM,IAAI,oBACR,gCAAgC,KAAK,MACnC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEvD;GAAE,SAAS,EAAE,KAAK;GAAG,OAAO;EAAM,CACpC;CACF;CAEA,OAAO,WAAoB,MAAM,QAAQ;AAC3C;;;;;;AAOA,SAAS,YACP,MACA,OAC0B;CAC1B,OAAO;EACL;EACA;EACA,OAAO,WAAW;GAChB,OAAO,YAAY,MAAM,MAAM,OAAO,SAAS,CAAC;EAClD;EACA,MAAM,OAAO,OAAO;GAClB,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,SAAS,GACvC,MAAM,IAAI,oBACR,+DAA+D,SAC/D,EAAE,SAAS;IAAE;IAAM;GAAM,EAAE,CAC7B;GAGF,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,SAAS,OACpD,MAAM,IAAI,oBACR,oDAAoD,MAAM,cAAc,SACxE,EAAE,SAAS;IAAE;IAAM;IAAO;GAAM,EAAE,CACpC;GAGF,OAAO,YACL,MACA,MAAM,QAAQ,GAAG,aAAa,WAAW,UAAU,KAAK,CAC1D;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,QACd,SAC0B;CAC1B,MAAM,QAAiC,CAAC,GAAI,QAAQ,SAAS,CAAC,CAAE;CAEhE,IAAI,QAAQ,aAAa,QACvB,MAAM,KAAK,GAAG,gBAAyB,QAAQ,QAAQ,CAAC;CAG1D,OAAO,YAAY,QAAQ,MAAM,KAAK;AACxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-runner.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/eval-runner.ts"],"mappings":";;;;;;AAqJA;;;;;;;iBAAsB,OAAA,
|
|
1
|
+
{"version":3,"file":"eval-runner.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/eval-runner.ts"],"mappings":";;;;;;AAqJA;;;;;;;iBAAsB,OAAA,SAAA,CACpB,KAAA,EAAO,aAAA,CAAc,OAAA,GACrB,OAAA,EAAS,WAAA,CAAY,OAAA,IACpB,OAAA,CAAQ,UAAA,CAAW,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-runner.mjs","names":["judgeScorer"],"sources":["../../../../../../../ai/src/eval/eval-runner.ts"],"sourcesContent":["import type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { AgentExecuteOptions } from \"../contracts/agent/agent-options.type\";\nimport type {\n EvalCase,\n EvalCaseResult,\n EvalOptions,\n EvalReport,\n EvalScore,\n EvalScorer,\n EvalScorerContext,\n} from \"../contracts/agent/eval.type\";\nimport type { EvalCase as EvalCaseType } from \"../contracts/agent/eval.type\";\nimport { AgentExecutionError } from \"../errors\";\nimport { log } from \"@warlock.js/logger\";\nimport { judge as judgeScorer } from \"./judge-scorer\";\nimport { diff } from \"./regression\";\n\n/**\n * Narrow `EvalOptions.cases` to the underlying `EvalCase[]`. A\n * `DatasetContract` is identified structurally by its `cases` property\n * (an array carried alongside `name` / `filter` / `shard`); a raw\n * `EvalCase[]` is used as-is.\n */\nfunction resolveCases<TOutput>(\n cases: EvalOptions<TOutput>[\"cases\"],\n): EvalCaseType<TOutput>[] {\n if (Array.isArray(cases)) {\n return cases;\n }\n\n return cases.cases;\n}\n\nconst LOG_MODULE = \"ai.eval\";\nconst DEFAULT_PASS_THRESHOLD = 0.5;\n\n/**\n * Resolve the scorer list for a single case. Precedence: the case's\n * own `scorers` → the suite `scorers` → a synthesized judge scorer\n * when `judge` is configured. Throws an authoring-time\n * `AgentExecutionError` when a case can resolve none — an eval suite\n * with no way to score a case is a config bug worth surfacing at the\n * call site, not a silent pass.\n */\nfunction resolveScorers<TOutput>(\n evalCase: EvalCase<TOutput>,\n options: EvalOptions<TOutput>,\n passThreshold: number,\n): EvalScorer<TOutput>[] {\n if (evalCase.scorers && evalCase.scorers.length > 0) {\n return evalCase.scorers;\n }\n\n if (options.scorers && options.scorers.length > 0) {\n return options.scorers;\n }\n\n if (options.judge) {\n return [judgeScorer<TOutput>(options.judge, passThreshold)];\n }\n\n throw new AgentExecutionError(\n `eval case \"${evalCase.name}\" has no scorer — supply per-case \"scorers\", suite \"scorers\", or a \"judge\"`,\n { context: { authoring: true, case: evalCase.name } },\n );\n}\n\n/**\n * Decide a single scorer verdict's pass/fail. Honors an explicit\n * `passed` from the scorer; otherwise derives it from\n * `score >= passThreshold`.\n */\nfunction isScorePassing(score: EvalScore, passThreshold: number): boolean {\n if (typeof score.passed === \"boolean\") {\n return score.passed;\n }\n\n return score.score >= passThreshold;\n}\n\n/**\n * Merge suite-level execute options with the case's own override.\n * Per-case wins on conflict (shallow merge).\n */\nfunction mergeOptions<TOutput>(\n suite: AgentExecuteOptions<TOutput> | undefined,\n perCase: AgentExecuteOptions<TOutput> | undefined,\n): AgentExecuteOptions<TOutput> | undefined {\n if (!suite) return perCase;\n if (!perCase) return suite;\n return { ...suite, ...perCase };\n}\n\n/**\n * Run one case end-to-end: execute the agent, run every resolved\n * scorer, aggregate into an {@link EvalCaseResult}. A case passes only\n * when the agent did not error AND every scorer passed.\n */\nasync function runCase<TOutput>(\n agent: AgentContract<TOutput>,\n evalCase: EvalCase<TOutput>,\n options: EvalOptions<TOutput>,\n passThreshold: number,\n): Promise<EvalCaseResult<TOutput>> {\n const scorers = resolveScorers(evalCase, options, passThreshold);\n const executeOptions = mergeOptions(options.executeOptions, evalCase.options);\n\n const start = performance.now();\n const result = await agent.execute(evalCase.input, executeOptions);\n const duration = performance.now() - start;\n\n const context: EvalScorerContext<TOutput> = {\n case: evalCase,\n result,\n output: result.data,\n text: result.text,\n };\n\n const scores: EvalScore[] = [];\n\n for (const scorer of scorers) {\n scores.push(await scorer(context));\n }\n\n const meanScore =\n scores.length > 0 ? scores.reduce((sum, score) => sum + score.score, 0) / scores.length : 0;\n\n const allScorersPassed = scores.every((score) => isScorePassing(score, passThreshold));\n const passed = result.error === undefined && allScorersPassed;\n\n return {\n case: evalCase,\n result,\n scores,\n score: meanScore,\n passed,\n duration,\n };\n}\n\n/**\n * Core implementation of `agent.eval`. Runs every case sequentially\n * (cases share the agent and may carry side effects — ordering must be\n * deterministic), scores each, fires `onFailure` for failed cases, and\n * assembles the aggregate {@link EvalReport}.\n *\n * Never throws on a case-level failure; the only throw is the\n * authoring-time \"no scorer\" guard from {@link resolveScorers}.\n */\nexport async function runEval<TOutput>(\n agent: AgentContract<TOutput>,\n options: EvalOptions<TOutput>,\n): Promise<EvalReport<TOutput>> {\n const passThreshold = options.passThreshold ?? DEFAULT_PASS_THRESHOLD;\n const start = performance.now();\n\n const suiteCases = resolveCases(options.cases);\n const cases: EvalCaseResult<TOutput>[] = [];\n\n for (const evalCase of suiteCases) {\n const caseResult = await runCase(agent, evalCase, options, passThreshold);\n\n cases.push(caseResult);\n\n if (!caseResult.passed && options.onFailure) {\n try {\n await options.onFailure(caseResult);\n } catch (error) {\n log.warn(LOG_MODULE, \"onFailure.hook.error\", \"eval onFailure handler threw\", {\n agent: agent.name,\n case: evalCase.name,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n const passedCount = cases.filter((entry) => entry.passed).length;\n const total = cases.length;\n const meanScore =\n total > 0 ? cases.reduce((sum, entry) => sum + entry.score, 0) / total : 0;\n\n const report: EvalReport<TOutput> = {\n agentName: agent.name,\n total,\n passedCount,\n failedCount: total - passedCount,\n passRate: total > 0 ? passedCount / total : 0,\n meanScore,\n passed: total > 0 && passedCount === total,\n cases,\n duration: performance.now() - start,\n };\n\n if (options.baseline) {\n report.regression = diff(report, options.baseline, options.tolerance);\n }\n\n return report;\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAS,aACP,OACyB;CACzB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;CAGT,OAAO,MAAM;AACf;AAEA,MAAM,aAAa;AACnB,MAAM,yBAAyB;;;;;;;;;AAU/B,SAAS,eACP,UACA,SACA,eACuB;CACvB,IAAI,SAAS,WAAW,SAAS,QAAQ,SAAS,GAChD,OAAO,SAAS;CAGlB,IAAI,QAAQ,WAAW,QAAQ,QAAQ,SAAS,GAC9C,OAAO,QAAQ;CAGjB,IAAI,QAAQ,OACV,OAAO,CAACA,MAAqB,QAAQ,OAAO,aAAa,CAAC;CAG5D,MAAM,IAAI,oBACR,cAAc,SAAS,KAAK,6EAC5B,EAAE,SAAS;EAAE,WAAW;EAAM,MAAM,SAAS;CAAK,EAAE,CACtD;AACF;;;;;;AAOA,SAAS,eAAe,OAAkB,eAAgC;CACxE,IAAI,OAAO,MAAM,WAAW,WAC1B,OAAO,MAAM;CAGf,OAAO,MAAM,SAAS;AACxB;;;;;AAMA,SAAS,aACP,OACA,SAC0C;CAC1C,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO;EAAE,GAAG;EAAO,GAAG;CAAQ;AAChC;;;;;;AAOA,eAAe,QACb,OACA,UACA,SACA,eACkC;CAClC,MAAM,UAAU,eAAe,UAAU,SAAS,aAAa;CAC/D,MAAM,iBAAiB,aAAa,QAAQ,gBAAgB,SAAS,OAAO;CAE5E,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,SAAS,MAAM,MAAM,QAAQ,SAAS,OAAO,cAAc;CACjE,MAAM,WAAW,YAAY,IAAI,IAAI;CAErC,MAAM,UAAsC;EAC1C,MAAM;EACN;EACA,QAAQ,OAAO;EACf,MAAM,OAAO;CACf;CAEA,MAAM,SAAsB,CAAC;CAE7B,KAAK,MAAM,UAAU,SACnB,OAAO,KAAK,MAAM,OAAO,OAAO,CAAC;CAGnC,MAAM,YACJ,OAAO,SAAS,IAAI,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAM,OAAO,CAAC,IAAI,OAAO,SAAS;CAE5F,MAAM,mBAAmB,OAAO,OAAO,UAAU,eAAe,OAAO,aAAa,CAAC;CAGrF,OAAO;EACL,MAAM;EACN;EACA;EACA,OAAO;EACP,QAPa,OAAO,UAAU,UAAa;EAQ3C;CACF;AACF;;;;;;;;;;AAWA,eAAsB,QACpB,OACA,SAC8B;CAC9B,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,QAAQ,YAAY,IAAI;CAE9B,MAAM,aAAa,aAAa,QAAQ,KAAK;CAC7C,MAAM,QAAmC,CAAC;CAE1C,KAAK,MAAM,YAAY,YAAY;EACjC,MAAM,aAAa,MAAM,QAAQ,OAAO,UAAU,SAAS,aAAa;EAExE,MAAM,KAAK,UAAU;EAErB,IAAI,CAAC,WAAW,UAAU,QAAQ,WAChC,IAAI;GACF,MAAM,QAAQ,UAAU,UAAU;EACpC,SAAS,OAAO;GACd,IAAI,KAAK,YAAY,wBAAwB,gCAAgC;IAC3E,OAAO,MAAM;IACb,MAAM,SAAS;IACf,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;EACH;CAEJ;CAEA,MAAM,cAAc,MAAM,QAAQ,UAAU,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"eval-runner.mjs","names":["judgeScorer"],"sources":["../../../../../../../ai/src/eval/eval-runner.ts"],"sourcesContent":["import type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { AgentExecuteOptions } from \"../contracts/agent/agent-options.type\";\nimport type {\n EvalCase,\n EvalCaseResult,\n EvalOptions,\n EvalReport,\n EvalScore,\n EvalScorer,\n EvalScorerContext,\n} from \"../contracts/agent/eval.type\";\nimport type { EvalCase as EvalCaseType } from \"../contracts/agent/eval.type\";\nimport { AgentExecutionError } from \"../errors\";\nimport { log } from \"@warlock.js/logger\";\nimport { judge as judgeScorer } from \"./judge-scorer\";\nimport { diff } from \"./regression\";\n\n/**\n * Narrow `EvalOptions.cases` to the underlying `EvalCase[]`. A\n * `DatasetContract` is identified structurally by its `cases` property\n * (an array carried alongside `name` / `filter` / `shard`); a raw\n * `EvalCase[]` is used as-is.\n */\nfunction resolveCases<TOutput>(\n cases: EvalOptions<TOutput>[\"cases\"],\n): EvalCaseType<TOutput>[] {\n if (Array.isArray(cases)) {\n return cases;\n }\n\n return cases.cases;\n}\n\nconst LOG_MODULE = \"ai.eval\";\nconst DEFAULT_PASS_THRESHOLD = 0.5;\n\n/**\n * Resolve the scorer list for a single case. Precedence: the case's\n * own `scorers` → the suite `scorers` → a synthesized judge scorer\n * when `judge` is configured. Throws an authoring-time\n * `AgentExecutionError` when a case can resolve none — an eval suite\n * with no way to score a case is a config bug worth surfacing at the\n * call site, not a silent pass.\n */\nfunction resolveScorers<TOutput>(\n evalCase: EvalCase<TOutput>,\n options: EvalOptions<TOutput>,\n passThreshold: number,\n): EvalScorer<TOutput>[] {\n if (evalCase.scorers && evalCase.scorers.length > 0) {\n return evalCase.scorers;\n }\n\n if (options.scorers && options.scorers.length > 0) {\n return options.scorers;\n }\n\n if (options.judge) {\n return [judgeScorer<TOutput>(options.judge, passThreshold)];\n }\n\n throw new AgentExecutionError(\n `eval case \"${evalCase.name}\" has no scorer — supply per-case \"scorers\", suite \"scorers\", or a \"judge\"`,\n { context: { authoring: true, case: evalCase.name } },\n );\n}\n\n/**\n * Decide a single scorer verdict's pass/fail. Honors an explicit\n * `passed` from the scorer; otherwise derives it from\n * `score >= passThreshold`.\n */\nfunction isScorePassing(score: EvalScore, passThreshold: number): boolean {\n if (typeof score.passed === \"boolean\") {\n return score.passed;\n }\n\n return score.score >= passThreshold;\n}\n\n/**\n * Merge suite-level execute options with the case's own override.\n * Per-case wins on conflict (shallow merge).\n */\nfunction mergeOptions<TOutput>(\n suite: AgentExecuteOptions<TOutput> | undefined,\n perCase: AgentExecuteOptions<TOutput> | undefined,\n): AgentExecuteOptions<TOutput> | undefined {\n if (!suite) return perCase;\n if (!perCase) return suite;\n return { ...suite, ...perCase };\n}\n\n/**\n * Run one case end-to-end: execute the agent, run every resolved\n * scorer, aggregate into an {@link EvalCaseResult}. A case passes only\n * when the agent did not error AND every scorer passed.\n */\nasync function runCase<TOutput>(\n agent: AgentContract<TOutput>,\n evalCase: EvalCase<TOutput>,\n options: EvalOptions<TOutput>,\n passThreshold: number,\n): Promise<EvalCaseResult<TOutput>> {\n const scorers = resolveScorers(evalCase, options, passThreshold);\n const executeOptions = mergeOptions(options.executeOptions, evalCase.options);\n\n const start = performance.now();\n const result = await agent.execute(evalCase.input, executeOptions);\n const duration = performance.now() - start;\n\n const context: EvalScorerContext<TOutput> = {\n case: evalCase,\n result,\n output: result.data,\n text: result.text,\n };\n\n const scores: EvalScore[] = [];\n\n for (const scorer of scorers) {\n scores.push(await scorer(context));\n }\n\n const meanScore =\n scores.length > 0 ? scores.reduce((sum, score) => sum + score.score, 0) / scores.length : 0;\n\n const allScorersPassed = scores.every((score) => isScorePassing(score, passThreshold));\n const passed = result.error === undefined && allScorersPassed;\n\n return {\n case: evalCase,\n result,\n scores,\n score: meanScore,\n passed,\n duration,\n };\n}\n\n/**\n * Core implementation of `agent.eval`. Runs every case sequentially\n * (cases share the agent and may carry side effects — ordering must be\n * deterministic), scores each, fires `onFailure` for failed cases, and\n * assembles the aggregate {@link EvalReport}.\n *\n * Never throws on a case-level failure; the only throw is the\n * authoring-time \"no scorer\" guard from {@link resolveScorers}.\n */\nexport async function runEval<TOutput>(\n agent: AgentContract<TOutput>,\n options: EvalOptions<TOutput>,\n): Promise<EvalReport<TOutput>> {\n const passThreshold = options.passThreshold ?? DEFAULT_PASS_THRESHOLD;\n const start = performance.now();\n\n const suiteCases = resolveCases(options.cases);\n const cases: EvalCaseResult<TOutput>[] = [];\n\n for (const evalCase of suiteCases) {\n const caseResult = await runCase(agent, evalCase, options, passThreshold);\n\n cases.push(caseResult);\n\n if (!caseResult.passed && options.onFailure) {\n try {\n await options.onFailure(caseResult);\n } catch (error) {\n log.warn(LOG_MODULE, \"onFailure.hook.error\", \"eval onFailure handler threw\", {\n agent: agent.name,\n case: evalCase.name,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n const passedCount = cases.filter((entry) => entry.passed).length;\n const total = cases.length;\n const meanScore =\n total > 0 ? cases.reduce((sum, entry) => sum + entry.score, 0) / total : 0;\n\n const report: EvalReport<TOutput> = {\n agentName: agent.name,\n total,\n passedCount,\n failedCount: total - passedCount,\n passRate: total > 0 ? passedCount / total : 0,\n meanScore,\n passed: total > 0 && passedCount === total,\n cases,\n duration: performance.now() - start,\n };\n\n if (options.baseline) {\n report.regression = diff(report, options.baseline, options.tolerance);\n }\n\n return report;\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAS,aACP,OACyB;CACzB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;CAGT,OAAO,MAAM;AACf;AAEA,MAAM,aAAa;AACnB,MAAM,yBAAyB;;;;;;;;;AAU/B,SAAS,eACP,UACA,SACA,eACuB;CACvB,IAAI,SAAS,WAAW,SAAS,QAAQ,SAAS,GAChD,OAAO,SAAS;CAGlB,IAAI,QAAQ,WAAW,QAAQ,QAAQ,SAAS,GAC9C,OAAO,QAAQ;CAGjB,IAAI,QAAQ,OACV,OAAO,CAACA,MAAqB,QAAQ,OAAO,aAAa,CAAC;CAG5D,MAAM,IAAI,oBACR,cAAc,SAAS,KAAK,6EAC5B,EAAE,SAAS;EAAE,WAAW;EAAM,MAAM,SAAS;CAAK,EAAE,CACtD;AACF;;;;;;AAOA,SAAS,eAAe,OAAkB,eAAgC;CACxE,IAAI,OAAO,MAAM,WAAW,WAC1B,OAAO,MAAM;CAGf,OAAO,MAAM,SAAS;AACxB;;;;;AAMA,SAAS,aACP,OACA,SAC0C;CAC1C,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO;EAAE,GAAG;EAAO,GAAG;CAAQ;AAChC;;;;;;AAOA,eAAe,QACb,OACA,UACA,SACA,eACkC;CAClC,MAAM,UAAU,eAAe,UAAU,SAAS,aAAa;CAC/D,MAAM,iBAAiB,aAAa,QAAQ,gBAAgB,SAAS,OAAO;CAE5E,MAAM,QAAQ,YAAY,IAAI;CAC9B,MAAM,SAAS,MAAM,MAAM,QAAQ,SAAS,OAAO,cAAc;CACjE,MAAM,WAAW,YAAY,IAAI,IAAI;CAErC,MAAM,UAAsC;EAC1C,MAAM;EACN;EACA,QAAQ,OAAO;EACf,MAAM,OAAO;CACf;CAEA,MAAM,SAAsB,CAAC;CAE7B,KAAK,MAAM,UAAU,SACnB,OAAO,KAAK,MAAM,OAAO,OAAO,CAAC;CAGnC,MAAM,YACJ,OAAO,SAAS,IAAI,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAM,OAAO,CAAC,IAAI,OAAO,SAAS;CAE5F,MAAM,mBAAmB,OAAO,OAAO,UAAU,eAAe,OAAO,aAAa,CAAC;CAGrF,OAAO;EACL,MAAM;EACN;EACA;EACA,OAAO;EACP,QAPa,OAAO,UAAU,UAAa;EAQ3C;CACF;AACF;;;;;;;;;;AAWA,eAAsB,QACpB,OACA,SAC8B;CAC9B,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,QAAQ,YAAY,IAAI;CAE9B,MAAM,aAAa,aAAa,QAAQ,KAAK;CAC7C,MAAM,QAAmC,CAAC;CAE1C,KAAK,MAAM,YAAY,YAAY;EACjC,MAAM,aAAa,MAAM,QAAQ,OAAO,UAAU,SAAS,aAAa;EAExE,MAAM,KAAK,UAAU;EAErB,IAAI,CAAC,WAAW,UAAU,QAAQ,WAChC,IAAI;GACF,MAAM,QAAQ,UAAU,UAAU;EACpC,SAAS,OAAO;GACd,IAAI,KAAK,YAAY,wBAAwB,gCAAgC;IAC3E,OAAO,MAAM;IACb,MAAM,SAAS;IACf,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;EACH;CAEJ;CAEA,MAAM,cAAc,MAAM,QAAQ,UAAU,MAAM,MAAM,EAAE;CAC1D,MAAM,QAAQ,MAAM;CACpB,MAAM,YACJ,QAAQ,IAAI,MAAM,QAAQ,KAAK,UAAU,MAAM,MAAM,OAAO,CAAC,IAAI,QAAQ;CAE3E,MAAM,SAA8B;EAClC,WAAW,MAAM;EACjB;EACA;EACA,aAAa,QAAQ;EACrB,UAAU,QAAQ,IAAI,cAAc,QAAQ;EAC5C;EACA,QAAQ,QAAQ,KAAK,gBAAgB;EACrC;EACA,UAAU,YAAY,IAAI,IAAI;CAChC;CAEA,IAAI,QAAQ,UACV,OAAO,aAAa,KAAK,QAAQ,QAAQ,UAAU,QAAQ,SAAS;CAGtE,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"judge-scorer.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/judge-scorer.ts"],"mappings":";;;;;AA4FA;;;;;;;;;;;;iBAAgB,KAAA,
|
|
1
|
+
{"version":3,"file":"judge-scorer.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/judge-scorer.ts"],"mappings":";;;;;AA4FA;;;;;;;;;;;;iBAAgB,KAAA,mBAAA,CACd,MAAA,EAAQ,SAAA,EACR,aAAA,YACC,UAAA,CAAW,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regression.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/regression.ts"],"mappings":";;;;;AAyBA;;;;;;;;;;;;;;;;;;;;AAIiB;iBAJD,IAAA,
|
|
1
|
+
{"version":3,"file":"regression.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/regression.ts"],"mappings":";;;;;AAyBA;;;;;;;;;;;;;;;;;;;;AAIiB;iBAJD,IAAA,mBAAA,CACd,MAAA,EAAQ,UAAA,CAAW,OAAA,GACnB,QAAA,EAAU,UAAA,CAAW,OAAA,GACrB,SAAA,YACC,cAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regression.mjs","names":[],"sources":["../../../../../../../ai/src/eval/regression.ts"],"sourcesContent":["import type { EvalRegression, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Diff a fresh {@link EvalReport} against a `baseline`, joining cases by\n * name, to produce an {@link EvalRegression} verdict.\n *\n * A case **regresses** when its new aggregate `score` is more than\n * `tolerance` below its baseline score (`before - after > tolerance`).\n * Cases that improved, held steady, or moved within `tolerance` are not\n * flagged. Cases present in only one of the two reports are surfaced\n * under `added` / `removed` rather than treated as regressions, so adding\n * or dropping a case never fails the gate by itself.\n *\n * Pure — depends only on the two reports and the tolerance; attaches no\n * state and mutates neither input.\n *\n * @param report - The newly produced report.\n * @param baseline - A prior report to compare against.\n * @param tolerance - Max allowed score drop before a case counts as a\n * regression. Defaults to `0` (any drop regresses).\n *\n * @example\n * const regression = diff(report, baseline, 0.05);\n * expect(regression.passed).toBe(true);\n */\nexport function diff<TOutput = unknown>(\n report: EvalReport<TOutput>,\n baseline: EvalReport<TOutput>,\n tolerance = 0,\n): EvalRegression {\n const baselineScores = new Map<string, number>();\n\n for (const entry of baseline.cases) {\n baselineScores.set(entry.case.name, entry.score);\n }\n\n const currentNames = new Set<string>();\n const regressed: EvalRegression[\"regressed\"] = [];\n\n for (const entry of report.cases) {\n const name = entry.case.name;\n currentNames.add(name);\n\n const before = baselineScores.get(name);\n\n if (before === undefined) {\n continue;\n }\n\n if (before - entry.score > tolerance) {\n regressed.push({ name, before, after: entry.score });\n }\n }\n\n const removed = baseline.cases\n .map((entry) => entry.case.name)\n .filter((name) => !currentNames.has(name));\n\n const added = report.cases\n .map((entry) => entry.case.name)\n .filter((name) => !baselineScores.has(name));\n\n return {\n regressed,\n removed,\n added,\n passed: regressed.length === 0,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,KACd,QACA,UACA,YAAY,GACI;CAChB,MAAM,iCAAiB,IAAI,IAAoB;CAE/C,KAAK,MAAM,SAAS,SAAS,OAC3B,eAAe,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK;CAGjD,MAAM,+BAAe,IAAI,IAAY;CACrC,MAAM,YAAyC,CAAC;CAEhD,KAAK,MAAM,SAAS,OAAO,OAAO;EAChC,MAAM,OAAO,MAAM,KAAK;EACxB,aAAa,IAAI,IAAI;EAErB,MAAM,SAAS,eAAe,IAAI,IAAI;EAEtC,IAAI,WAAW,QACb;EAGF,IAAI,SAAS,MAAM,QAAQ,WACzB,UAAU,KAAK;GAAE;GAAM;GAAQ,OAAO,MAAM;EAAM,CAAC;CAEvD;CAUA,OAAO;EACL;EACA,SAVc,SAAS,MACtB,KAAK,UAAU,MAAM,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"regression.mjs","names":[],"sources":["../../../../../../../ai/src/eval/regression.ts"],"sourcesContent":["import type { EvalRegression, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Diff a fresh {@link EvalReport} against a `baseline`, joining cases by\n * name, to produce an {@link EvalRegression} verdict.\n *\n * A case **regresses** when its new aggregate `score` is more than\n * `tolerance` below its baseline score (`before - after > tolerance`).\n * Cases that improved, held steady, or moved within `tolerance` are not\n * flagged. Cases present in only one of the two reports are surfaced\n * under `added` / `removed` rather than treated as regressions, so adding\n * or dropping a case never fails the gate by itself.\n *\n * Pure — depends only on the two reports and the tolerance; attaches no\n * state and mutates neither input.\n *\n * @param report - The newly produced report.\n * @param baseline - A prior report to compare against.\n * @param tolerance - Max allowed score drop before a case counts as a\n * regression. Defaults to `0` (any drop regresses).\n *\n * @example\n * const regression = diff(report, baseline, 0.05);\n * expect(regression.passed).toBe(true);\n */\nexport function diff<TOutput = unknown>(\n report: EvalReport<TOutput>,\n baseline: EvalReport<TOutput>,\n tolerance = 0,\n): EvalRegression {\n const baselineScores = new Map<string, number>();\n\n for (const entry of baseline.cases) {\n baselineScores.set(entry.case.name, entry.score);\n }\n\n const currentNames = new Set<string>();\n const regressed: EvalRegression[\"regressed\"] = [];\n\n for (const entry of report.cases) {\n const name = entry.case.name;\n currentNames.add(name);\n\n const before = baselineScores.get(name);\n\n if (before === undefined) {\n continue;\n }\n\n if (before - entry.score > tolerance) {\n regressed.push({ name, before, after: entry.score });\n }\n }\n\n const removed = baseline.cases\n .map((entry) => entry.case.name)\n .filter((name) => !currentNames.has(name));\n\n const added = report.cases\n .map((entry) => entry.case.name)\n .filter((name) => !baselineScores.has(name));\n\n return {\n regressed,\n removed,\n added,\n passed: regressed.length === 0,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,KACd,QACA,UACA,YAAY,GACI;CAChB,MAAM,iCAAiB,IAAI,IAAoB;CAE/C,KAAK,MAAM,SAAS,SAAS,OAC3B,eAAe,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK;CAGjD,MAAM,+BAAe,IAAI,IAAY;CACrC,MAAM,YAAyC,CAAC;CAEhD,KAAK,MAAM,SAAS,OAAO,OAAO;EAChC,MAAM,OAAO,MAAM,KAAK;EACxB,aAAa,IAAI,IAAI;EAErB,MAAM,SAAS,eAAe,IAAI,IAAI;EAEtC,IAAI,WAAW,QACb;EAGF,IAAI,SAAS,MAAM,QAAQ,WACzB,UAAU,KAAK;GAAE;GAAM;GAAQ,OAAO,MAAM;EAAM,CAAC;CAEvD;CAUA,OAAO;EACL;EACA,SAVc,SAAS,MACtB,KAAK,UAAU,MAAM,KAAK,IAAI,EAC9B,QAAQ,SAAS,CAAC,aAAa,IAAI,IAAI,CAQlC;EACN,OAPY,OAAO,MAClB,KAAK,UAAU,MAAM,KAAK,IAAI,EAC9B,QAAQ,SAAS,CAAC,eAAe,IAAI,IAAI,CAKtC;EACJ,QAAQ,UAAU,WAAW;CAC/B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-json.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/report-json.ts"],"mappings":";;;;;AAeA;;;;AAAyC;AAazC;;;;;;iBAbgB,MAAA,CAAO,MAAkB,EAAV,UAAU;;;AAa0C;;;;;;;iBAAnE,QAAA,
|
|
1
|
+
{"version":3,"file":"report-json.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/report-json.ts"],"mappings":";;;;;AAeA;;;;AAAyC;AAazC;;;;;;iBAbgB,MAAA,CAAO,MAAkB,EAAV,UAAU;;;AAa0C;;;;;;;iBAAnE,QAAA,mBAAA,CAA4B,UAAA,WAAqB,UAAU,CAAC,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-junit.mjs","names":[],"sources":["../../../../../../../ai/src/eval/report-junit.ts"],"sourcesContent":["import type { EvalCaseResult, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Escape the five XML predefined entities so arbitrary text (case names,\n * failure reasons, agent names) is safe inside an attribute value or\n * element body. Covers `&`, `<`, `>`, `\"`, and `'`.\n */\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Build the `<failure>` body for a failed case: the joined reasons of\n * every non-passing scorer, falling back to a generic message when a\n * scorer offered no reason (or the failure was an agent error).\n */\nfunction failureMessage(entry: EvalCaseResult): string {\n if (entry.result.error) {\n return `agent error: ${entry.result.error.message}`;\n }\n\n const reasons = entry.scores\n .filter((score) => score.passed === false)\n .map((score) => score.reason)\n .filter((reason): reason is string => typeof reason === \"string\" && reason !== \"\");\n\n if (reasons.length > 0) {\n return reasons.join(\"; \");\n }\n\n return \"case did not pass\";\n}\n\n/**\n * Serialize an {@link EvalReport} to a JUnit-XML string for CI ingestion.\n *\n * **Role.** A pure, runner-decoupled reporter: one `<testsuite>` whose\n * name is the agent, one `<testcase>` per eval case, a `<failure>` child\n * on each case that did not pass (with the joined scorer reasons), and a\n * `time` attribute carrying the case / suite duration in **seconds**\n * (JUnit's unit; the report stores milliseconds).\n *\n * XML is hand-emitted (no `xml` dependency) and every dynamic value is\n * entity-escaped via {@link escapeXml}.\n *\n * @example\n * await writeFile(\"./report.junit.xml\", toJUnit(report));\n */\nexport function toJUnit(report: EvalReport): string {\n const suiteName = escapeXml(report.agentName);\n const suiteTime = (report.duration / 1000).toFixed(3);\n\n const lines: string[] = [];\n\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n `<testsuite name=\"${suiteName}\" tests=\"${report.total}\" failures=\"${report.failedCount}\" time=\"${suiteTime}\">`,\n );\n\n for (const entry of report.cases) {\n const caseName = escapeXml(entry.case.name);\n const caseTime = (entry.duration / 1000).toFixed(3);\n\n if (entry.passed) {\n lines.push(\n ` <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\"/>`,\n );\n\n continue;\n }\n\n const message = failureMessage(entry);\n lines.push(\n ` <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\">`,\n );\n lines.push(\n ` <failure message=\"${escapeXml(message)}\">${escapeXml(message)}</failure>`,\n );\n lines.push(\" </testcase>\");\n }\n\n lines.push(\"</testsuite>\");\n\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;;AAOA,SAAS,UAAU,OAAuB;CACxC,OAAO,MACJ,QAAQ,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"report-junit.mjs","names":[],"sources":["../../../../../../../ai/src/eval/report-junit.ts"],"sourcesContent":["import type { EvalCaseResult, EvalReport } from \"../contracts/agent/eval.type\";\n\n/**\n * Escape the five XML predefined entities so arbitrary text (case names,\n * failure reasons, agent names) is safe inside an attribute value or\n * element body. Covers `&`, `<`, `>`, `\"`, and `'`.\n */\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Build the `<failure>` body for a failed case: the joined reasons of\n * every non-passing scorer, falling back to a generic message when a\n * scorer offered no reason (or the failure was an agent error).\n */\nfunction failureMessage(entry: EvalCaseResult): string {\n if (entry.result.error) {\n return `agent error: ${entry.result.error.message}`;\n }\n\n const reasons = entry.scores\n .filter((score) => score.passed === false)\n .map((score) => score.reason)\n .filter((reason): reason is string => typeof reason === \"string\" && reason !== \"\");\n\n if (reasons.length > 0) {\n return reasons.join(\"; \");\n }\n\n return \"case did not pass\";\n}\n\n/**\n * Serialize an {@link EvalReport} to a JUnit-XML string for CI ingestion.\n *\n * **Role.** A pure, runner-decoupled reporter: one `<testsuite>` whose\n * name is the agent, one `<testcase>` per eval case, a `<failure>` child\n * on each case that did not pass (with the joined scorer reasons), and a\n * `time` attribute carrying the case / suite duration in **seconds**\n * (JUnit's unit; the report stores milliseconds).\n *\n * XML is hand-emitted (no `xml` dependency) and every dynamic value is\n * entity-escaped via {@link escapeXml}.\n *\n * @example\n * await writeFile(\"./report.junit.xml\", toJUnit(report));\n */\nexport function toJUnit(report: EvalReport): string {\n const suiteName = escapeXml(report.agentName);\n const suiteTime = (report.duration / 1000).toFixed(3);\n\n const lines: string[] = [];\n\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n `<testsuite name=\"${suiteName}\" tests=\"${report.total}\" failures=\"${report.failedCount}\" time=\"${suiteTime}\">`,\n );\n\n for (const entry of report.cases) {\n const caseName = escapeXml(entry.case.name);\n const caseTime = (entry.duration / 1000).toFixed(3);\n\n if (entry.passed) {\n lines.push(\n ` <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\"/>`,\n );\n\n continue;\n }\n\n const message = failureMessage(entry);\n lines.push(\n ` <testcase name=\"${caseName}\" classname=\"${suiteName}\" time=\"${caseTime}\">`,\n );\n lines.push(\n ` <failure message=\"${escapeXml(message)}\">${escapeXml(message)}</failure>`,\n );\n lines.push(\" </testcase>\");\n }\n\n lines.push(\"</testsuite>\");\n\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;;AAOA,SAAS,UAAU,OAAuB;CACxC,OAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;;;;;;AAOA,SAAS,eAAe,OAA+B;CACrD,IAAI,MAAM,OAAO,OACf,OAAO,gBAAgB,MAAM,OAAO,MAAM;CAG5C,MAAM,UAAU,MAAM,OACnB,QAAQ,UAAU,MAAM,WAAW,KAAK,EACxC,KAAK,UAAU,MAAM,MAAM,EAC3B,QAAQ,WAA6B,OAAO,WAAW,YAAY,WAAW,EAAE;CAEnF,IAAI,QAAQ,SAAS,GACnB,OAAO,QAAQ,KAAK,IAAI;CAG1B,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAgB,QAAQ,QAA4B;CAClD,MAAM,YAAY,UAAU,OAAO,SAAS;CAC5C,MAAM,aAAa,OAAO,WAAW,KAAM,QAAQ,CAAC;CAEpD,MAAM,QAAkB,CAAC;CAEzB,MAAM,KAAK,4CAAwC;CACnD,MAAM,KACJ,oBAAoB,UAAU,WAAW,OAAO,MAAM,cAAc,OAAO,YAAY,UAAU,UAAU,GAC7G;CAEA,KAAK,MAAM,SAAS,OAAO,OAAO;EAChC,MAAM,WAAW,UAAU,MAAM,KAAK,IAAI;EAC1C,MAAM,YAAY,MAAM,WAAW,KAAM,QAAQ,CAAC;EAElD,IAAI,MAAM,QAAQ;GAChB,MAAM,KACJ,qBAAqB,SAAS,eAAe,UAAU,UAAU,SAAS,IAC5E;GAEA;EACF;EAEA,MAAM,UAAU,eAAe,KAAK;EACpC,MAAM,KACJ,qBAAqB,SAAS,eAAe,UAAU,UAAU,SAAS,GAC5E;EACA,MAAM,KACJ,yBAAyB,UAAU,OAAO,EAAE,IAAI,UAAU,OAAO,EAAE,WACrE;EACA,MAAM,KAAK,eAAe;CAC5B;CAEA,MAAM,KAAK,cAAc;CAEzB,OAAO,MAAM,KAAK,IAAI;AACxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scorers.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/scorers.ts"],"mappings":";;;;;AAWA;;;KAAY,aAAA,uBACV,OAAA,EAAS,iBAAA,CAAkB,OAAA,gBACd,OAAA;;;;;;;;;;;AAAO;AA8BtB;;;;;;iBAAgB,KAAA,
|
|
1
|
+
{"version":3,"file":"scorers.d.mts","names":[],"sources":["../../../../../../../ai/src/eval/scorers.ts"],"mappings":";;;;;AAWA;;;KAAY,aAAA,uBACV,OAAA,EAAS,iBAAA,CAAkB,OAAA,gBACd,OAAA;;;;;;;;;;;AAAO;AA8BtB;;;;;;iBAAgB,KAAA,mBAAA,CAAA,GAA4B,UAAU,CAAC,OAAA;;AAAO;AAmC9D;;;;;;iBAAgB,QAAA,mBAAA,CAAA,GAA+B,UAAU,CAAC,OAAA;;AAAO;AAsCjE;;;;;;;;iBAAgB,SAAA,mBAAA,CACd,EAAA,EAAI,aAAA,CAAc,OAAA,IACjB,UAAA,CAAW,OAAA"}
|