@warlock.js/ai 4.15.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +183 -158
  2. package/cjs/index.cjs +637 -104
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/index.d.mts +2 -2
  5. package/esm/contracts/memory/index.d.mts +1 -1
  6. package/esm/contracts/memory/memory-config.type.d.mts +29 -3
  7. package/esm/contracts/memory/memory-config.type.d.mts.map +1 -1
  8. package/esm/contracts/memory/memory-item.type.d.mts +15 -1
  9. package/esm/contracts/memory/memory-item.type.d.mts.map +1 -1
  10. package/esm/contracts/memory/memory.contract.d.mts +15 -2
  11. package/esm/contracts/memory/memory.contract.d.mts.map +1 -1
  12. package/esm/contracts/memory/recall-options.type.d.mts +12 -0
  13. package/esm/contracts/memory/recall-options.type.d.mts.map +1 -1
  14. package/esm/contracts/orchestrator/index.d.mts +1 -1
  15. package/esm/contracts/orchestrator/orchestrator-config.type.d.mts +38 -1
  16. package/esm/contracts/orchestrator/orchestrator-config.type.d.mts.map +1 -1
  17. package/esm/contracts/orchestrator/orchestrator.contract.d.mts +67 -3
  18. package/esm/contracts/orchestrator/orchestrator.contract.d.mts.map +1 -1
  19. package/esm/contracts/supervisor/supervisor-config.type.d.mts +23 -0
  20. package/esm/contracts/supervisor/supervisor-config.type.d.mts.map +1 -1
  21. package/esm/index.d.mts +6 -5
  22. package/esm/index.mjs +3 -2
  23. package/esm/memory/episodic-memory.mjs +14 -6
  24. package/esm/memory/episodic-memory.mjs.map +1 -1
  25. package/esm/memory/index.d.mts +1 -1
  26. package/esm/memory/memory.d.mts +13 -1
  27. package/esm/memory/memory.d.mts.map +1 -1
  28. package/esm/memory/memory.mjs +41 -7
  29. package/esm/memory/memory.mjs.map +1 -1
  30. package/esm/memory/procedural-memory.mjs +20 -7
  31. package/esm/memory/procedural-memory.mjs.map +1 -1
  32. package/esm/memory/semantic-memory.mjs +27 -10
  33. package/esm/memory/semantic-memory.mjs.map +1 -1
  34. package/esm/memory/working-memory.mjs +70 -13
  35. package/esm/memory/working-memory.mjs.map +1 -1
  36. package/esm/middleware/builtins/semantic-cache.d.mts +46 -1
  37. package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -1
  38. package/esm/middleware/builtins/semantic-cache.mjs +60 -15
  39. package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
  40. package/esm/middleware/index.d.mts +1 -1
  41. package/esm/orchestrator/as-tool.d.mts +35 -9
  42. package/esm/orchestrator/as-tool.d.mts.map +1 -1
  43. package/esm/orchestrator/as-tool.mjs +67 -19
  44. package/esm/orchestrator/as-tool.mjs.map +1 -1
  45. package/esm/orchestrator/execution.d.mts.map +1 -1
  46. package/esm/orchestrator/execution.mjs +2 -2
  47. package/esm/orchestrator/execution.mjs.map +1 -1
  48. package/esm/orchestrator/index.d.mts +1 -1
  49. package/esm/orchestrator/index.mjs +1 -1
  50. package/esm/orchestrator/memory.d.mts +41 -5
  51. package/esm/orchestrator/memory.d.mts.map +1 -1
  52. package/esm/orchestrator/memory.mjs +53 -5
  53. package/esm/orchestrator/memory.mjs.map +1 -1
  54. package/esm/planner/plan-schema.d.mts +3 -3
  55. package/esm/planner/plan-schema.d.mts.map +1 -1
  56. package/esm/planner/plan-schema.mjs +30 -0
  57. package/esm/planner/plan-schema.mjs.map +1 -1
  58. package/esm/security/index.mjs +1 -0
  59. package/esm/security/outbound-policy.d.mts +9 -0
  60. package/esm/security/outbound-policy.d.mts.map +1 -1
  61. package/esm/security/outbound-policy.mjs +79 -5
  62. package/esm/security/outbound-policy.mjs.map +1 -1
  63. package/esm/security/outbound-policy.type.d.mts +8 -0
  64. package/esm/security/outbound-policy.type.d.mts.map +1 -1
  65. package/esm/security/safe-merge.d.mts +52 -0
  66. package/esm/security/safe-merge.d.mts.map +1 -0
  67. package/esm/security/safe-merge.mjs +68 -0
  68. package/esm/security/safe-merge.mjs.map +1 -0
  69. package/esm/supervisor/decide.mjs +52 -5
  70. package/esm/supervisor/decide.mjs.map +1 -1
  71. package/esm/supervisor/execution.d.mts +22 -0
  72. package/esm/supervisor/execution.d.mts.map +1 -1
  73. package/esm/supervisor/execution.mjs +46 -9
  74. package/esm/supervisor/execution.mjs.map +1 -1
  75. package/esm/supervisor/supervisor.mjs +4 -0
  76. package/esm/supervisor/supervisor.mjs.map +1 -1
  77. package/llms-full.txt +174 -10
  78. package/llms.txt +4 -3
  79. package/package.json +4 -4
  80. package/skills/README.md +5 -1
  81. package/skills/attach-ai-middleware/SKILL.md +17 -1
  82. package/skills/rag-loaders-and-stores/SKILL.md +3 -0
  83. package/skills/run-ai-agent/SKILL.md +3 -0
  84. package/skills/run-orchestrator/SKILL.md +6 -1
  85. package/skills/run-planner/SKILL.md +7 -3
  86. package/skills/run-supervisor/SKILL.md +11 -1
  87. package/skills/secure-outbound-requests/SKILL.md +85 -0
  88. package/skills/use-ai-memory/SKILL.md +36 -3
  89. package/skills/use-runtime-skills/SKILL.md +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"execution.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/execution.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type { OrchestratorCommands } from \"../contracts/orchestrator/orchestrator-commands.type\";\nimport type { OrchestratorConfig } from \"../contracts/orchestrator/orchestrator-config.type\";\nimport type {\n OrchestratorEvent,\n OrchestratorEventHandlers,\n OrchestratorEventMap,\n OrchestratorEventName,\n} from \"../contracts/orchestrator/orchestrator-event.type\";\nimport type {\n OrchestratorExecuteOptions,\n OrchestratorResumeOptions,\n} from \"../contracts/orchestrator/orchestrator-execute-options.type\";\nimport type {\n CompactionResult,\n OrchestratorReport,\n OrchestratorReportStatus,\n OrchestratorResult,\n TurnSnapshot,\n} from \"../contracts/result/orchestrator-result.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport type { EventIdentity } from \"../contracts/events/event-identity.type\";\nimport {\n resolveDefaultCheckpointStore,\n resolveDefaultSnapshotStore,\n} from \"../config\";\nimport type { AIError } from \"../errors/ai-error\";\nimport { OrchestratorConfigError, OrchestratorDriftError } from \"../errors\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport type { ResolvedIntentEntry } from \"../supervisor/entries\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { persistCheckpoint, summarizeRoute } from \"./checkpoint\";\nimport { runCompaction, runManualCompaction, shouldCompact } from \"./compaction\";\nimport { deriveRunId, dispatchTurn } from \"./dispatch\";\nimport type { OrchestratorEmitter } from \"./emitter\";\nimport type { OrchestratorEmitterLike } from \"./emitter-port.type\";\nimport type { OrchestratorEngineContext } from \"./engine-context.type\";\nimport { acquireLock } from \"./lock\";\nimport { loadSession } from \"./load\";\nimport {\n injectMemories,\n outcomeTextFromTurn,\n recallForTurn,\n rememberTurnOutcome,\n resolveOrchestratorMemory,\n} from \"./memory\";\nimport type { OrchestratorStreamController } from \"./orchestrator-stream\";\nimport { resolveResume } from \"./resume\";\nimport { windowHistory } from \"./window\";\n\n/** Empty rolled-up usage for turns that never dispatched (drift/seed). */\nconst ZERO_USAGE: Usage = { input: 0, output: 0, total: 0 };\n\n/**\n * Constructor params the C1 factory passes when building an\n * {@link OrchestratorExecution} per call. The factory owns author-time\n * validation, intent-entry resolution, and signature computation; it\n * hands the engine the validated `config`, the resolved `entries`, the\n * computed `signature`, and the shared three-tier `emitter`. The\n * per-call inputs vary by entry point:\n *\n * - `execute` / `stream` — `input` + `options` (and `streamController`\n * for `stream`).\n * - `resume` — `resumeSessionId` + `resumeOptions`.\n * - `command(\"compact\")` — neither; `compact(args)` carries its own.\n */\nexport type OrchestratorExecutionParams<TOutput, TState> = {\n config: OrchestratorConfig<TOutput, TState>;\n /** Resolved intent entries (validated by C1; the engine delegates dispatch to the supervisor). */\n entries?: Map<string, ResolvedIntentEntry>;\n signature: string;\n emitter: OrchestratorEmitter;\n input?: SupervisorInput;\n options?: OrchestratorExecuteOptions<TState>;\n streamController?: OrchestratorStreamController<OrchestratorResult<TOutput>>;\n resumeSessionId?: string;\n resumeOptions?: OrchestratorResumeOptions;\n};\n\n/**\n * Per-call lifecycle engine — the single object the C1 factory\n * constructs and drives. Owns the 7-phase lifecycle (orchestrator.md §3:\n * load → drift → lock → window → dispatch → persist → compaction),\n * resolving the durable stores (own config field → `ai.config` default)\n * and adapting C1's three-tier {@link OrchestratorEmitter} to the\n * {@link OrchestratorEmitterLike} port the phase modules call.\n *\n * The factory creates a fresh instance per `execute` / `stream` /\n * `resume` / `command` call (single-call lifecycle invariant — §18.8);\n * the heavy lifting lives in the standalone phase functions\n * ({@link runTurn} / {@link runResume}) which this class delegates to.\n *\n * @example\n * const execution = new OrchestratorExecution({\n * config, entries, signature, emitter, input, options,\n * });\n * const result = await execution.run();\n */\nexport class OrchestratorExecution<TOutput, TState> {\n private readonly params: OrchestratorExecutionParams<TOutput, TState>;\n private readonly ctx: OrchestratorEngineContext<TOutput, TState>;\n private readonly streamController?: OrchestratorStreamController<\n OrchestratorResult<TOutput>\n >;\n\n public constructor(params: OrchestratorExecutionParams<TOutput, TState>) {\n this.params = params;\n this.streamController = params.streamController;\n this.ctx = {\n config: params.config,\n signature: params.signature,\n checkpointStore: resolveCheckpointStore(params.config),\n snapshotStore: resolveSnapshotStore(params.config),\n emitter: adaptEmitter(\n params.emitter,\n generateRunId(\"orchestrator\"),\n this.streamController as\n | OrchestratorStreamController<unknown>\n | undefined,\n ),\n memory: resolveOrchestratorMemory(params.config.memory),\n };\n }\n\n /**\n * `execute()` / `stream()` entry — run one turn through the 7-phase\n * lifecycle. When a `streamController` was supplied, the adapter mirrors\n * every emitted event into the stream and the controller is settled\n * (`end` / `fail`) once the result resolves.\n */\n public async run(): Promise<OrchestratorResult<TOutput>> {\n if (this.params.input === undefined || !this.params.options) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${this.params.config.name}\"): internal — run() invoked without input/options`,\n );\n }\n\n try {\n const result = await runTurn(\n this.ctx,\n this.params.input,\n this.params.options,\n );\n\n // Route the orchestrator's report to observers (per-flow `observe` +\n // the global observe-all gate) — parity with agent/workflow/supervisor,\n // so a durable session root no longer needs a manual observe.collect().\n await notifyObservers(this.ctx.config.observe, result.report);\n\n this.streamController?.end(result);\n\n return result;\n } catch (error) {\n this.streamController?.fail(error as Error);\n\n throw error;\n }\n }\n\n /**\n * `resume()` entry — drain an interrupted `iterate: true` turn (§9).\n * Returns `null` when nothing is in flight.\n */\n public async resume(): Promise<OrchestratorResult<TOutput> | null> {\n if (!this.params.resumeSessionId) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${this.params.config.name}\"): internal — resume() invoked without a sessionId`,\n );\n }\n\n return runResume(this.ctx, this.params.resumeSessionId, this.params.resumeOptions);\n }\n\n /**\n * `command(\"compact\")` entry — run a manual compaction on demand (§11 /\n * §12.1). Reuses the post-turn compaction code path against the\n * caller-supplied history and returns the raw {@link CompactionResult}.\n */\n public async compact(\n args: OrchestratorCommands[\"compact\"][\"args\"],\n ): Promise<OrchestratorCommands[\"compact\"][\"result\"]> {\n return runManualCompaction(\n this.ctx as OrchestratorEngineContext<unknown, TState>,\n args.history,\n );\n }\n}\n\n/**\n * Resolve the durable checkpoint store: the config's own field, falling\n * back to `ai.config({ defaultCheckpointStore })`. Throws\n * {@link OrchestratorConfigError} when neither resolves — persistence is\n * always on (§8.1), so a turn can never run without a checkpoint store.\n */\nfunction resolveCheckpointStore<TOutput, TState>(\n config: OrchestratorConfig<TOutput, TState>,\n) {\n const store = config.checkpointStore ?? resolveDefaultCheckpointStore();\n\n if (!store) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${config.name}\"): a \\`checkpointStore\\` is required ` +\n `(set one on the config or via \\`ai.config({ defaultCheckpointStore })\\`)`,\n );\n }\n\n return store;\n}\n\n/**\n * Resolve the internal-supervisor snapshot store for `iterate: true`\n * turns: the config's own field, falling back to\n * `ai.config({ defaultSnapshotStore })`. Returns `undefined` for\n * `iterate: false` orchestrators (no mid-turn resume — nothing to\n * snapshot). The factory already guarantees presence when\n * `iterate: true`, so the engine never asserts here.\n */\nfunction resolveSnapshotStore<TOutput, TState>(\n config: OrchestratorConfig<TOutput, TState>,\n) {\n if (config.iterate !== true) {\n return undefined;\n }\n\n return config.snapshotStore ?? resolveDefaultSnapshotStore();\n}\n\n/**\n * Adapt C1's three-tier {@link OrchestratorEmitter} (whose `emit` takes\n * `event, payload, identity, perCallHandlers?`) to the\n * {@link OrchestratorEmitterLike} port the phase modules call (a 2-arg\n * `emit(event, payload)` plus `bindPerCall`).\n *\n * The adapter injects the run identity centrally and, when a stream\n * controller is present, mirrors every fully-stamped event into the\n * stream pipe (§14.1 — the orchestrator's own events surface on the\n * stream alongside the bubbled child events). `bindPerCall` registers\n * the per-call `options.on` bag for the turn's duration and returns a\n * disposer that clears it.\n */\nfunction adaptEmitter(\n emitter: OrchestratorEmitter,\n runId: string,\n streamController: OrchestratorStreamController<unknown> | undefined,\n): OrchestratorEmitterLike {\n // `rootRunId === runId` for a standalone run; nested propagation lands\n // in a follow-up (see `EventIdentity`).\n const fullIdentity: EventIdentity = { runId, rootRunId: runId };\n\n let perCall: OrchestratorEventHandlers | undefined;\n\n return {\n emit<K extends OrchestratorEventName>(\n event: K,\n payload: OrchestratorEventMap[K],\n ): void {\n const fullPayload = emitter.emit(event, payload, fullIdentity, perCall);\n\n // The discriminated-union correlation between `type` and the\n // matching payload variant can't be expressed structurally — the\n // cast mirrors the supervisor stream's established pattern.\n streamController?.push({ type: event, ...fullPayload } as OrchestratorEvent);\n },\n bindPerCall(handlers: OrchestratorEventHandlers | undefined): () => void {\n perCall = handlers;\n\n return () => {\n perCall = undefined;\n };\n },\n };\n}\n\n/**\n * Phase 2 — drift check (orchestrator.md §3 / §4 Phase 2). Compares the\n * loaded checkpoint's `signature` against the current definition's.\n * Mismatch throws `OrchestratorDriftError` synchronously unless\n * `force` is set. Emits `orchestrator.drift.checked` either way. A new\n * session (no loaded signature) never drifts.\n */\nfunction assertNoDrift(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n loadedSignature: string | undefined,\n force: boolean | undefined,\n): void {\n const drifted =\n loadedSignature !== undefined && loadedSignature !== ctx.signature;\n\n ctx.emitter.emit(\"orchestrator.drift.checked\", {\n sessionId,\n signature: ctx.signature,\n drifted,\n });\n\n if (drifted && !force) {\n throw new OrchestratorDriftError(\n `orchestrator \"${ctx.config.name}\": signature drift on session \"${sessionId}\" — ` +\n `the definition changed since this session was last persisted. ` +\n `Pass { force: true } only after reviewing the change, or discard / migrate the session.`,\n {\n savedSignature: loadedSignature as string,\n currentSignature: ctx.signature,\n sessionId,\n },\n );\n }\n}\n\n/**\n * Shallow-merge the per-call `state` patch (§5 — partial state\n * override) over the loaded session-state seed. The merged value\n * becomes the supervisor's seed for this turn.\n */\nfunction applyStatePatch<TState>(\n seed: TState,\n patch: Partial<TState> | undefined,\n): TState {\n if (!patch) {\n return seed;\n }\n\n return { ...seed, ...patch } as TState;\n}\n\n/**\n * Assemble the orchestrator-scope {@link OrchestratorReport} from the\n * dispatched turn's child report and the turn snapshot. Wraps the\n * child supervisor/agent report tree as `children[0]` (§15.6 —\n * `children[]` carries only the CURRENT turn's dispatched primitive\n * reports) while the per-turn forensic record lives on `turns[]`.\n */\nfunction buildReport(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n turnIndex: number,\n status: OrchestratorReportStatus,\n turnSnapshot: TurnSnapshot | undefined,\n childReport: BaseReport | undefined,\n error?: AIError,\n): OrchestratorReport {\n const now = new Date().toISOString();\n const usage = turnSnapshot?.usage ?? childReport?.usage ?? ZERO_USAGE;\n\n return {\n runId: deriveRunId(sessionId, ctx.config.version, turnIndex),\n rootRunId: deriveRunId(sessionId, ctx.config.version, turnIndex),\n name: ctx.config.name,\n version: ctx.config.version,\n sessionId,\n type: \"orchestrator\",\n status,\n // Stamp the terminal error so the observe path surfaces it on the\n // orchestrator span (an observer never sees the result envelope).\n // Absent on a clean turn.\n ...(error ? { error } : {}),\n startedAt: turnSnapshot?.startedAt ?? now,\n endedAt: turnSnapshot?.endedAt ?? now,\n duration: turnSnapshot?.duration ?? 0,\n usage,\n children: childReport ? [childReport] : [],\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n turnIndex,\n signature: ctx.signature,\n turns: turnSnapshot ? [turnSnapshot] : [],\n };\n}\n\n/**\n * Map the dispatched supervisor result's report status onto the\n * orchestrator's status surface (§15.6). A clean completion that is\n * still mid-conversation reports `\"awaiting-input\"` (the session\n * continues) rather than `\"completed\"`; failures and cancellations\n * pass through.\n */\nfunction deriveStatus(childStatus: BaseReport[\"status\"]): OrchestratorReportStatus {\n if (childStatus === \"completed\") {\n return \"awaiting-input\";\n }\n\n return childStatus;\n}\n\n/**\n * Emit the terminal turn event matching the report status (§14.1).\n */\nfunction emitTerminal(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n turnIndex: number,\n status: OrchestratorReportStatus,\n): void {\n if (status === \"cancelled\") {\n ctx.emitter.emit(\"orchestrator.turn.cancelled\", { sessionId, turnIndex });\n\n return;\n }\n\n if (status === \"failed\" || status === \"max-iterations\") {\n ctx.emitter.emit(\"orchestrator.turn.failed\", { sessionId, turnIndex });\n\n return;\n }\n\n if (status === \"awaiting-input\") {\n ctx.emitter.emit(\"orchestrator.turn.awaiting-input\", {\n sessionId,\n turnIndex,\n });\n\n return;\n }\n\n ctx.emitter.emit(\"orchestrator.turn.completed\", { sessionId, turnIndex });\n}\n\n/**\n * Run one turn end-to-end through the 7-phase lifecycle (orchestrator\n * .md §3). The single entry the C1 factory's `execute()` delegates to.\n *\n * Phase order is the diagram's contract: load → drift → lock → window\n * → dispatch → persist → compaction. Drift / config misuse throw;\n * every other failure surfaces on `result.error` (the contract: the\n * orchestrator never throws on runtime failure). Cancellation and\n * failure do NOT persist a fresh checkpoint (§17 — state reverts to the\n * pre-turn checkpoint).\n */\nexport async function runTurn<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n input: SupervisorInput,\n options: OrchestratorExecuteOptions<TState>,\n): Promise<OrchestratorResult<TOutput>> {\n const sessionId = options.sessionId;\n const disposePerCall = ctx.emitter.bindPerCall(options.on);\n\n try {\n // Phase 1 — load session.\n const loaded = await loadSession(ctx, sessionId);\n\n ctx.emitter.emit(\"orchestrator.turn.starting\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n });\n\n ctx.emitter.emit(\"orchestrator.session.loaded\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n found: loaded.found,\n });\n\n // Phase 2 — drift check.\n assertNoDrift(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.record?.signature,\n options.force,\n );\n\n // Phase 3 — lock check (cooperative, fail-open).\n await acquireLock(ctx, sessionId, loaded.record);\n\n // Phase 4 — window history.\n const windowed = windowHistory(\n ctx as OrchestratorEngineContext,\n sessionId,\n options.history,\n );\n\n // Phase 5 — dispatch. When memory is configured, recall the\n // turn-relevant memories and inject them into the request-scoped\n // context bag so every route / router / evaluate / dispatch callback\n // surfaces them at `ctx.context[injectKey]` before routing runs.\n const seedState = applyStatePatch(loaded.state, options.state);\n\n let turnContext = options.context;\n\n if (ctx.memory) {\n const recalled = await recallForTurn(ctx.memory, input);\n turnContext = injectMemories(turnContext, ctx.memory, recalled);\n }\n\n const { result, state, turnSnapshot } = await dispatchTurn<TOutput, TState>({\n ctx,\n sessionId,\n input,\n seedState,\n turnIndex: loaded.turnIndex,\n history: windowed.agents,\n context: turnContext,\n signal: options.signal,\n });\n\n ctx.emitter.emit(\"orchestrator.turn.routed\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n source: turnSnapshot.decision.source,\n raw: turnSnapshot.decision.raw,\n });\n\n const status = result.error\n ? deriveStatus(result.report.status)\n : \"awaiting-input\";\n\n // Cancelled / failed turns revert: no fresh checkpoint, no compaction.\n if (result.error) {\n const report = buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.turnIndex,\n status,\n turnSnapshot,\n result.report,\n result.error,\n );\n\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, loaded.turnIndex, status);\n\n return {\n data: result.data,\n error: result.error,\n usage: result.usage,\n report,\n sessionId,\n turnIndex: loaded.turnIndex,\n };\n }\n\n // Phase 6 — persist checkpoint.\n await persistCheckpoint({\n ctx,\n sessionId,\n turnIndex: loaded.turnIndex,\n state,\n lastRoute: summarizeRoute(turnSnapshot.decision.raw as never),\n summarizedThrough: loaded.record?.summarized_through ?? null,\n });\n\n // Memory write-back (memory core M2). The turn settled cleanly (the\n // `result.error` branch above already returned for cancelled /\n // failed turns, which revert and never remember — §17), so remember\n // the input + its outcome for later recall.\n if (ctx.memory) {\n await rememberTurnOutcome(\n ctx.memory,\n input,\n outcomeTextFromTurn(result.data, turnSnapshot),\n );\n }\n\n // Phase 7 — post-turn compaction (only when triggered).\n let compaction: CompactionResult | undefined;\n\n if (shouldCompact(ctx as OrchestratorEngineContext, loaded.turnIndex)) {\n const outcome = await runCompaction(\n ctx as OrchestratorEngineContext<unknown, TState>,\n sessionId,\n options.history,\n );\n\n if (outcome) {\n compaction = outcome.compaction;\n\n if (outcome.applied) {\n await advanceSummarizedThrough(\n ctx as OrchestratorEngineContext<unknown, TState>,\n sessionId,\n outcome.compaction.replacesToIndex,\n );\n }\n }\n }\n\n const report = buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.turnIndex,\n \"awaiting-input\",\n turnSnapshot,\n result.report,\n );\n\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, loaded.turnIndex, \"awaiting-input\");\n\n return {\n data: result.data,\n error: undefined,\n usage: result.usage,\n report,\n sessionId,\n turnIndex: loaded.turnIndex,\n compaction,\n };\n } finally {\n disposePerCall();\n }\n}\n\n/**\n * After a framework-applied compaction (`onCompact` succeeded), advance\n * the persisted `summarized_through` to the compaction's\n * `replacesToIndex` (§12.2 step 4). Re-saves the latest row with the\n * updated marker (append-only stores keep the prior row).\n */\nasync function advanceSummarizedThrough<TState>(\n ctx: OrchestratorEngineContext<unknown, TState>,\n sessionId: string,\n replacesToIndex: number,\n): Promise<void> {\n const latest = await ctx.checkpointStore.load(ctx.config.name, sessionId);\n\n if (!latest) {\n return;\n }\n\n await ctx.checkpointStore.save({\n ...latest,\n summarized_through: replacesToIndex,\n saved_at: new Date().toISOString(),\n });\n}\n\n/**\n * §9 resume protocol entry the C1 factory's `resume()` delegates to.\n * Returns `null` when no in-flight `iterate: true` turn is detected;\n * otherwise drains the interrupted supervisor run, persists a fresh\n * checkpoint for the resumed turn, and returns the completed result.\n *\n * Runs the same Phase 2 drift check as `runTurn` (§9.4). The heavy\n * lifting lives in `resume.ts`; this wrapper threads the engine\n * context.\n */\nexport async function runResume<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n sessionId: string,\n options?: OrchestratorResumeOptions,\n): Promise<OrchestratorResult<TOutput> | null> {\n const disposePerCall = ctx.emitter.bindPerCall(options?.on);\n\n try {\n return await resolveResume(ctx, sessionId, options, {\n assertNoDrift: (loadedSignature) =>\n assertNoDrift(\n ctx as OrchestratorEngineContext,\n sessionId,\n loadedSignature,\n options?.force,\n ),\n buildReport: (turnIndex, status, turnSnapshot, childReport) =>\n buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n turnIndex,\n status,\n turnSnapshot,\n childReport,\n ),\n deriveStatus,\n emitTerminal: (turnIndex, status) =>\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, turnIndex, status),\n persist: (turnIndex, state, lastRoute, summarizedThrough) =>\n persistCheckpoint({\n ctx,\n sessionId,\n turnIndex,\n state,\n lastRoute,\n summarizedThrough,\n }),\n });\n } finally {\n disposePerCall();\n }\n}\n\n/**\n * The `stream()` entry. The orchestrator's streaming surface bubbles\n * child agent/supervisor events under their own namespace (§14.2); the\n * C1 stream controller owns the `StreamContract` wiring. This engine\n * entry runs the same lifecycle as `runTurn` — the C1 factory passes a\n * per-call `on` bag wired to the stream controller, so the engine needs\n * no streaming-specific branch. Exposed as a distinct name for the\n * factory to call, returning the same `OrchestratorResult` the stream's\n * `.result` resolves to.\n */\nexport async function streamTurn<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n input: SupervisorInput,\n options: OrchestratorExecuteOptions<TState>,\n): Promise<OrchestratorResult<TOutput>> {\n return runTurn(ctx, input, options);\n}\n\nexport type { OrchestratorEngineContext } from \"./engine-context.type\";\nexport type { Message };\n"],"mappings":"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"}
1
+ {"version":3,"file":"execution.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/execution.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type { OrchestratorCommands } from \"../contracts/orchestrator/orchestrator-commands.type\";\nimport type { OrchestratorConfig } from \"../contracts/orchestrator/orchestrator-config.type\";\nimport type {\n OrchestratorEvent,\n OrchestratorEventHandlers,\n OrchestratorEventMap,\n OrchestratorEventName,\n} from \"../contracts/orchestrator/orchestrator-event.type\";\nimport type {\n OrchestratorExecuteOptions,\n OrchestratorResumeOptions,\n} from \"../contracts/orchestrator/orchestrator-execute-options.type\";\nimport type {\n CompactionResult,\n OrchestratorReport,\n OrchestratorReportStatus,\n OrchestratorResult,\n TurnSnapshot,\n} from \"../contracts/result/orchestrator-result.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport type { EventIdentity } from \"../contracts/events/event-identity.type\";\nimport {\n resolveDefaultCheckpointStore,\n resolveDefaultSnapshotStore,\n} from \"../config\";\nimport type { AIError } from \"../errors/ai-error\";\nimport { OrchestratorConfigError, OrchestratorDriftError } from \"../errors\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport type { ResolvedIntentEntry } from \"../supervisor/entries\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { persistCheckpoint, summarizeRoute } from \"./checkpoint\";\nimport { runCompaction, runManualCompaction, shouldCompact } from \"./compaction\";\nimport { deriveRunId, dispatchTurn } from \"./dispatch\";\nimport type { OrchestratorEmitter } from \"./emitter\";\nimport type { OrchestratorEmitterLike } from \"./emitter-port.type\";\nimport type { OrchestratorEngineContext } from \"./engine-context.type\";\nimport { acquireLock } from \"./lock\";\nimport { loadSession } from \"./load\";\nimport {\n injectMemories,\n outcomeTextFromTurn,\n recallForTurn,\n rememberTurnOutcome,\n resolveOrchestratorMemory,\n} from \"./memory\";\nimport type { OrchestratorStreamController } from \"./orchestrator-stream\";\nimport { resolveResume } from \"./resume\";\nimport { windowHistory } from \"./window\";\n\n/** Empty rolled-up usage for turns that never dispatched (drift/seed). */\nconst ZERO_USAGE: Usage = { input: 0, output: 0, total: 0 };\n\n/**\n * Constructor params the C1 factory passes when building an\n * {@link OrchestratorExecution} per call. The factory owns author-time\n * validation, intent-entry resolution, and signature computation; it\n * hands the engine the validated `config`, the resolved `entries`, the\n * computed `signature`, and the shared three-tier `emitter`. The\n * per-call inputs vary by entry point:\n *\n * - `execute` / `stream` — `input` + `options` (and `streamController`\n * for `stream`).\n * - `resume` — `resumeSessionId` + `resumeOptions`.\n * - `command(\"compact\")` — neither; `compact(args)` carries its own.\n */\nexport type OrchestratorExecutionParams<TOutput, TState> = {\n config: OrchestratorConfig<TOutput, TState>;\n /** Resolved intent entries (validated by C1; the engine delegates dispatch to the supervisor). */\n entries?: Map<string, ResolvedIntentEntry>;\n signature: string;\n emitter: OrchestratorEmitter;\n input?: SupervisorInput;\n options?: OrchestratorExecuteOptions<TState>;\n streamController?: OrchestratorStreamController<OrchestratorResult<TOutput>>;\n resumeSessionId?: string;\n resumeOptions?: OrchestratorResumeOptions;\n};\n\n/**\n * Per-call lifecycle engine — the single object the C1 factory\n * constructs and drives. Owns the 7-phase lifecycle (orchestrator.md §3:\n * load → drift → lock → window → dispatch → persist → compaction),\n * resolving the durable stores (own config field → `ai.config` default)\n * and adapting C1's three-tier {@link OrchestratorEmitter} to the\n * {@link OrchestratorEmitterLike} port the phase modules call.\n *\n * The factory creates a fresh instance per `execute` / `stream` /\n * `resume` / `command` call (single-call lifecycle invariant — §18.8);\n * the heavy lifting lives in the standalone phase functions\n * ({@link runTurn} / {@link runResume}) which this class delegates to.\n *\n * @example\n * const execution = new OrchestratorExecution({\n * config, entries, signature, emitter, input, options,\n * });\n * const result = await execution.run();\n */\nexport class OrchestratorExecution<TOutput, TState> {\n private readonly params: OrchestratorExecutionParams<TOutput, TState>;\n private readonly ctx: OrchestratorEngineContext<TOutput, TState>;\n private readonly streamController?: OrchestratorStreamController<\n OrchestratorResult<TOutput>\n >;\n\n public constructor(params: OrchestratorExecutionParams<TOutput, TState>) {\n this.params = params;\n this.streamController = params.streamController;\n this.ctx = {\n config: params.config,\n signature: params.signature,\n checkpointStore: resolveCheckpointStore(params.config),\n snapshotStore: resolveSnapshotStore(params.config),\n emitter: adaptEmitter(\n params.emitter,\n generateRunId(\"orchestrator\"),\n this.streamController as\n | OrchestratorStreamController<unknown>\n | undefined,\n ),\n memory: resolveOrchestratorMemory(params.config.memory),\n };\n }\n\n /**\n * `execute()` / `stream()` entry — run one turn through the 7-phase\n * lifecycle. When a `streamController` was supplied, the adapter mirrors\n * every emitted event into the stream and the controller is settled\n * (`end` / `fail`) once the result resolves.\n */\n public async run(): Promise<OrchestratorResult<TOutput>> {\n if (this.params.input === undefined || !this.params.options) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${this.params.config.name}\"): internal — run() invoked without input/options`,\n );\n }\n\n try {\n const result = await runTurn(\n this.ctx,\n this.params.input,\n this.params.options,\n );\n\n // Route the orchestrator's report to observers (per-flow `observe` +\n // the global observe-all gate) — parity with agent/workflow/supervisor,\n // so a durable session root no longer needs a manual observe.collect().\n await notifyObservers(this.ctx.config.observe, result.report);\n\n this.streamController?.end(result);\n\n return result;\n } catch (error) {\n this.streamController?.fail(error as Error);\n\n throw error;\n }\n }\n\n /**\n * `resume()` entry — drain an interrupted `iterate: true` turn (§9).\n * Returns `null` when nothing is in flight.\n */\n public async resume(): Promise<OrchestratorResult<TOutput> | null> {\n if (!this.params.resumeSessionId) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${this.params.config.name}\"): internal — resume() invoked without a sessionId`,\n );\n }\n\n return runResume(this.ctx, this.params.resumeSessionId, this.params.resumeOptions);\n }\n\n /**\n * `command(\"compact\")` entry — run a manual compaction on demand (§11 /\n * §12.1). Reuses the post-turn compaction code path against the\n * caller-supplied history and returns the raw {@link CompactionResult}.\n */\n public async compact(\n args: OrchestratorCommands[\"compact\"][\"args\"],\n ): Promise<OrchestratorCommands[\"compact\"][\"result\"]> {\n return runManualCompaction(\n this.ctx as OrchestratorEngineContext<unknown, TState>,\n args.history,\n );\n }\n}\n\n/**\n * Resolve the durable checkpoint store: the config's own field, falling\n * back to `ai.config({ defaultCheckpointStore })`. Throws\n * {@link OrchestratorConfigError} when neither resolves — persistence is\n * always on (§8.1), so a turn can never run without a checkpoint store.\n */\nfunction resolveCheckpointStore<TOutput, TState>(\n config: OrchestratorConfig<TOutput, TState>,\n) {\n const store = config.checkpointStore ?? resolveDefaultCheckpointStore();\n\n if (!store) {\n throw new OrchestratorConfigError(\n `ai.orchestrator(\"${config.name}\"): a \\`checkpointStore\\` is required ` +\n `(set one on the config or via \\`ai.config({ defaultCheckpointStore })\\`)`,\n );\n }\n\n return store;\n}\n\n/**\n * Resolve the internal-supervisor snapshot store for `iterate: true`\n * turns: the config's own field, falling back to\n * `ai.config({ defaultSnapshotStore })`. Returns `undefined` for\n * `iterate: false` orchestrators (no mid-turn resume — nothing to\n * snapshot). The factory already guarantees presence when\n * `iterate: true`, so the engine never asserts here.\n */\nfunction resolveSnapshotStore<TOutput, TState>(\n config: OrchestratorConfig<TOutput, TState>,\n) {\n if (config.iterate !== true) {\n return undefined;\n }\n\n return config.snapshotStore ?? resolveDefaultSnapshotStore();\n}\n\n/**\n * Adapt C1's three-tier {@link OrchestratorEmitter} (whose `emit` takes\n * `event, payload, identity, perCallHandlers?`) to the\n * {@link OrchestratorEmitterLike} port the phase modules call (a 2-arg\n * `emit(event, payload)` plus `bindPerCall`).\n *\n * The adapter injects the run identity centrally and, when a stream\n * controller is present, mirrors every fully-stamped event into the\n * stream pipe (§14.1 — the orchestrator's own events surface on the\n * stream alongside the bubbled child events). `bindPerCall` registers\n * the per-call `options.on` bag for the turn's duration and returns a\n * disposer that clears it.\n */\nfunction adaptEmitter(\n emitter: OrchestratorEmitter,\n runId: string,\n streamController: OrchestratorStreamController<unknown> | undefined,\n): OrchestratorEmitterLike {\n // `rootRunId === runId` for a standalone run; nested propagation lands\n // in a follow-up (see `EventIdentity`).\n const fullIdentity: EventIdentity = { runId, rootRunId: runId };\n\n let perCall: OrchestratorEventHandlers | undefined;\n\n return {\n emit<K extends OrchestratorEventName>(\n event: K,\n payload: OrchestratorEventMap[K],\n ): void {\n const fullPayload = emitter.emit(event, payload, fullIdentity, perCall);\n\n // The discriminated-union correlation between `type` and the\n // matching payload variant can't be expressed structurally — the\n // cast mirrors the supervisor stream's established pattern.\n streamController?.push({ type: event, ...fullPayload } as OrchestratorEvent);\n },\n bindPerCall(handlers: OrchestratorEventHandlers | undefined): () => void {\n perCall = handlers;\n\n return () => {\n perCall = undefined;\n };\n },\n };\n}\n\n/**\n * Phase 2 — drift check (orchestrator.md §3 / §4 Phase 2). Compares the\n * loaded checkpoint's `signature` against the current definition's.\n * Mismatch throws `OrchestratorDriftError` synchronously unless\n * `force` is set. Emits `orchestrator.drift.checked` either way. A new\n * session (no loaded signature) never drifts.\n */\nfunction assertNoDrift(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n loadedSignature: string | undefined,\n force: boolean | undefined,\n): void {\n const drifted =\n loadedSignature !== undefined && loadedSignature !== ctx.signature;\n\n ctx.emitter.emit(\"orchestrator.drift.checked\", {\n sessionId,\n signature: ctx.signature,\n drifted,\n });\n\n if (drifted && !force) {\n throw new OrchestratorDriftError(\n `orchestrator \"${ctx.config.name}\": signature drift on session \"${sessionId}\" — ` +\n `the definition changed since this session was last persisted. ` +\n `Pass { force: true } only after reviewing the change, or discard / migrate the session.`,\n {\n savedSignature: loadedSignature as string,\n currentSignature: ctx.signature,\n sessionId,\n },\n );\n }\n}\n\n/**\n * Shallow-merge the per-call `state` patch (§5 — partial state\n * override) over the loaded session-state seed. The merged value\n * becomes the supervisor's seed for this turn.\n */\nfunction applyStatePatch<TState>(\n seed: TState,\n patch: Partial<TState> | undefined,\n): TState {\n if (!patch) {\n return seed;\n }\n\n return { ...seed, ...patch } as TState;\n}\n\n/**\n * Assemble the orchestrator-scope {@link OrchestratorReport} from the\n * dispatched turn's child report and the turn snapshot. Wraps the\n * child supervisor/agent report tree as `children[0]` (§15.6 —\n * `children[]` carries only the CURRENT turn's dispatched primitive\n * reports) while the per-turn forensic record lives on `turns[]`.\n */\nfunction buildReport(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n turnIndex: number,\n status: OrchestratorReportStatus,\n turnSnapshot: TurnSnapshot | undefined,\n childReport: BaseReport | undefined,\n error?: AIError,\n): OrchestratorReport {\n const now = new Date().toISOString();\n const usage = turnSnapshot?.usage ?? childReport?.usage ?? ZERO_USAGE;\n\n return {\n runId: deriveRunId(sessionId, ctx.config.version, turnIndex),\n rootRunId: deriveRunId(sessionId, ctx.config.version, turnIndex),\n name: ctx.config.name,\n version: ctx.config.version,\n sessionId,\n type: \"orchestrator\",\n status,\n // Stamp the terminal error so the observe path surfaces it on the\n // orchestrator span (an observer never sees the result envelope).\n // Absent on a clean turn.\n ...(error ? { error } : {}),\n startedAt: turnSnapshot?.startedAt ?? now,\n endedAt: turnSnapshot?.endedAt ?? now,\n duration: turnSnapshot?.duration ?? 0,\n usage,\n children: childReport ? [childReport] : [],\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n turnIndex,\n signature: ctx.signature,\n turns: turnSnapshot ? [turnSnapshot] : [],\n };\n}\n\n/**\n * Map the dispatched supervisor result's report status onto the\n * orchestrator's status surface (§15.6). A clean completion that is\n * still mid-conversation reports `\"awaiting-input\"` (the session\n * continues) rather than `\"completed\"`; failures and cancellations\n * pass through.\n */\nfunction deriveStatus(childStatus: BaseReport[\"status\"]): OrchestratorReportStatus {\n if (childStatus === \"completed\") {\n return \"awaiting-input\";\n }\n\n return childStatus;\n}\n\n/**\n * Emit the terminal turn event matching the report status (§14.1).\n */\nfunction emitTerminal(\n ctx: OrchestratorEngineContext,\n sessionId: string,\n turnIndex: number,\n status: OrchestratorReportStatus,\n): void {\n if (status === \"cancelled\") {\n ctx.emitter.emit(\"orchestrator.turn.cancelled\", { sessionId, turnIndex });\n\n return;\n }\n\n if (status === \"failed\" || status === \"max-iterations\") {\n ctx.emitter.emit(\"orchestrator.turn.failed\", { sessionId, turnIndex });\n\n return;\n }\n\n if (status === \"awaiting-input\") {\n ctx.emitter.emit(\"orchestrator.turn.awaiting-input\", {\n sessionId,\n turnIndex,\n });\n\n return;\n }\n\n ctx.emitter.emit(\"orchestrator.turn.completed\", { sessionId, turnIndex });\n}\n\n/**\n * Run one turn end-to-end through the 7-phase lifecycle (orchestrator\n * .md §3). The single entry the C1 factory's `execute()` delegates to.\n *\n * Phase order is the diagram's contract: load → drift → lock → window\n * → dispatch → persist → compaction. Drift / config misuse throw;\n * every other failure surfaces on `result.error` (the contract: the\n * orchestrator never throws on runtime failure). Cancellation and\n * failure do NOT persist a fresh checkpoint (§17 — state reverts to the\n * pre-turn checkpoint).\n */\nexport async function runTurn<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n input: SupervisorInput,\n options: OrchestratorExecuteOptions<TState>,\n): Promise<OrchestratorResult<TOutput>> {\n const sessionId = options.sessionId;\n const disposePerCall = ctx.emitter.bindPerCall(options.on);\n\n try {\n // Phase 1 — load session.\n const loaded = await loadSession(ctx, sessionId);\n\n ctx.emitter.emit(\"orchestrator.turn.starting\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n });\n\n ctx.emitter.emit(\"orchestrator.session.loaded\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n found: loaded.found,\n });\n\n // Phase 2 — drift check.\n assertNoDrift(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.record?.signature,\n options.force,\n );\n\n // Phase 3 — lock check (cooperative, fail-open).\n await acquireLock(ctx, sessionId, loaded.record);\n\n // Phase 4 — window history.\n const windowed = windowHistory(\n ctx as OrchestratorEngineContext,\n sessionId,\n options.history,\n );\n\n // Phase 5 — dispatch. When memory is configured, recall the\n // turn-relevant memories and inject them into the request-scoped\n // context bag so every route / router / evaluate / dispatch callback\n // surfaces them at `ctx.context[injectKey]` before routing runs.\n const seedState = applyStatePatch(loaded.state, options.state);\n\n let turnContext = options.context;\n\n // Recall is scoped to THIS session (`memory.scope`, default\n // `\"session\"`): the store is shared by every session of this\n // orchestrator instance, so the scope — not the store — is what keeps\n // another session's remembered turns out of this turn's context.\n if (ctx.memory) {\n const recalled = await recallForTurn(ctx.memory, input, sessionId);\n turnContext = injectMemories(turnContext, ctx.memory, recalled);\n }\n\n const { result, state, turnSnapshot } = await dispatchTurn<TOutput, TState>({\n ctx,\n sessionId,\n input,\n seedState,\n turnIndex: loaded.turnIndex,\n history: windowed.agents,\n context: turnContext,\n signal: options.signal,\n });\n\n ctx.emitter.emit(\"orchestrator.turn.routed\", {\n sessionId,\n turnIndex: loaded.turnIndex,\n source: turnSnapshot.decision.source,\n raw: turnSnapshot.decision.raw,\n });\n\n const status = result.error\n ? deriveStatus(result.report.status)\n : \"awaiting-input\";\n\n // Cancelled / failed turns revert: no fresh checkpoint, no compaction.\n if (result.error) {\n const report = buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.turnIndex,\n status,\n turnSnapshot,\n result.report,\n result.error,\n );\n\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, loaded.turnIndex, status);\n\n return {\n data: result.data,\n error: result.error,\n usage: result.usage,\n report,\n sessionId,\n turnIndex: loaded.turnIndex,\n };\n }\n\n // Phase 6 — persist checkpoint.\n await persistCheckpoint({\n ctx,\n sessionId,\n turnIndex: loaded.turnIndex,\n state,\n lastRoute: summarizeRoute(turnSnapshot.decision.raw as never),\n summarizedThrough: loaded.record?.summarized_through ?? null,\n });\n\n // Memory write-back (memory core M2). The turn settled cleanly (the\n // `result.error` branch above already returned for cancelled /\n // failed turns, which revert and never remember — §17), so remember\n // the input + its outcome for later recall.\n if (ctx.memory) {\n await rememberTurnOutcome(\n ctx.memory,\n input,\n outcomeTextFromTurn(result.data, turnSnapshot),\n sessionId,\n );\n }\n\n // Phase 7 — post-turn compaction (only when triggered).\n let compaction: CompactionResult | undefined;\n\n if (shouldCompact(ctx as OrchestratorEngineContext, loaded.turnIndex)) {\n const outcome = await runCompaction(\n ctx as OrchestratorEngineContext<unknown, TState>,\n sessionId,\n options.history,\n );\n\n if (outcome) {\n compaction = outcome.compaction;\n\n if (outcome.applied) {\n await advanceSummarizedThrough(\n ctx as OrchestratorEngineContext<unknown, TState>,\n sessionId,\n outcome.compaction.replacesToIndex,\n );\n }\n }\n }\n\n const report = buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n loaded.turnIndex,\n \"awaiting-input\",\n turnSnapshot,\n result.report,\n );\n\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, loaded.turnIndex, \"awaiting-input\");\n\n return {\n data: result.data,\n error: undefined,\n usage: result.usage,\n report,\n sessionId,\n turnIndex: loaded.turnIndex,\n compaction,\n };\n } finally {\n disposePerCall();\n }\n}\n\n/**\n * After a framework-applied compaction (`onCompact` succeeded), advance\n * the persisted `summarized_through` to the compaction's\n * `replacesToIndex` (§12.2 step 4). Re-saves the latest row with the\n * updated marker (append-only stores keep the prior row).\n */\nasync function advanceSummarizedThrough<TState>(\n ctx: OrchestratorEngineContext<unknown, TState>,\n sessionId: string,\n replacesToIndex: number,\n): Promise<void> {\n const latest = await ctx.checkpointStore.load(ctx.config.name, sessionId);\n\n if (!latest) {\n return;\n }\n\n await ctx.checkpointStore.save({\n ...latest,\n summarized_through: replacesToIndex,\n saved_at: new Date().toISOString(),\n });\n}\n\n/**\n * §9 resume protocol entry the C1 factory's `resume()` delegates to.\n * Returns `null` when no in-flight `iterate: true` turn is detected;\n * otherwise drains the interrupted supervisor run, persists a fresh\n * checkpoint for the resumed turn, and returns the completed result.\n *\n * Runs the same Phase 2 drift check as `runTurn` (§9.4). The heavy\n * lifting lives in `resume.ts`; this wrapper threads the engine\n * context.\n */\nexport async function runResume<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n sessionId: string,\n options?: OrchestratorResumeOptions,\n): Promise<OrchestratorResult<TOutput> | null> {\n const disposePerCall = ctx.emitter.bindPerCall(options?.on);\n\n try {\n return await resolveResume(ctx, sessionId, options, {\n assertNoDrift: (loadedSignature) =>\n assertNoDrift(\n ctx as OrchestratorEngineContext,\n sessionId,\n loadedSignature,\n options?.force,\n ),\n buildReport: (turnIndex, status, turnSnapshot, childReport) =>\n buildReport(\n ctx as OrchestratorEngineContext,\n sessionId,\n turnIndex,\n status,\n turnSnapshot,\n childReport,\n ),\n deriveStatus,\n emitTerminal: (turnIndex, status) =>\n emitTerminal(ctx as OrchestratorEngineContext, sessionId, turnIndex, status),\n persist: (turnIndex, state, lastRoute, summarizedThrough) =>\n persistCheckpoint({\n ctx,\n sessionId,\n turnIndex,\n state,\n lastRoute,\n summarizedThrough,\n }),\n });\n } finally {\n disposePerCall();\n }\n}\n\n/**\n * The `stream()` entry. The orchestrator's streaming surface bubbles\n * child agent/supervisor events under their own namespace (§14.2); the\n * C1 stream controller owns the `StreamContract` wiring. This engine\n * entry runs the same lifecycle as `runTurn` — the C1 factory passes a\n * per-call `on` bag wired to the stream controller, so the engine needs\n * no streaming-specific branch. Exposed as a distinct name for the\n * factory to call, returning the same `OrchestratorResult` the stream's\n * `.result` resolves to.\n */\nexport async function streamTurn<TOutput, TState>(\n ctx: OrchestratorEngineContext<TOutput, TState>,\n input: SupervisorInput,\n options: OrchestratorExecuteOptions<TState>,\n): Promise<OrchestratorResult<TOutput>> {\n return runTurn(ctx, input, options);\n}\n\nexport type { OrchestratorEngineContext } from \"./engine-context.type\";\nexport type { Message };\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { asTool } from "./as-tool.mjs";
2
2
  import { OrchestratorCommandHandlers, createCommandDispatcher } from "./commands.mjs";
3
3
  import { OrchestratorEmitter } from "./emitter.mjs";
4
- import { ResolvedOrchestratorMemory, injectMemories, memoryQueryFromInput, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory } from "./memory.mjs";
4
+ import { ResolvedOrchestratorMemory, injectMemories, memoryQueryFromInput, memoryScopeFor, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory, sessionMemoryScope } from "./memory.mjs";
5
5
  import { OrchestratorStreamController, createOrchestratorStream } from "./orchestrator-stream.mjs";
6
6
  import { OrchestratorExecution, OrchestratorExecutionParams, runResume, runTurn, streamTurn } from "./execution.mjs";
7
7
  import { orchestrator } from "./orchestrator.mjs";
@@ -1,7 +1,7 @@
1
1
  import { asTool } from "./as-tool.mjs";
2
2
  import { createCommandDispatcher } from "./commands.mjs";
3
3
  import { OrchestratorEmitter } from "./emitter.mjs";
4
- import { injectMemories, memoryQueryFromInput, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory } from "./memory.mjs";
4
+ import { injectMemories, memoryQueryFromInput, memoryScopeFor, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory, sessionMemoryScope } from "./memory.mjs";
5
5
  import { OrchestratorExecution, runResume, runTurn, streamTurn } from "./execution.mjs";
6
6
  import { createOrchestratorStream } from "./orchestrator-stream.mjs";
7
7
  import { inProcessSessionLock, noopSessionLock } from "./session-lock.mjs";
@@ -2,7 +2,7 @@ import { SupervisorInput } from "../contracts/supervisor/supervisor-input.type.m
2
2
  import { TurnSnapshot } from "../contracts/result/orchestrator-result.type.mjs";
3
3
  import { RecalledMemory } from "../contracts/memory/memory-item.type.mjs";
4
4
  import { MemoryContract } from "../contracts/memory/memory.contract.mjs";
5
- import { OrchestratorMemoryConfig } from "../contracts/orchestrator/orchestrator-config.type.mjs";
5
+ import { OrchestratorMemoryConfig, OrchestratorMemoryScope } from "../contracts/orchestrator/orchestrator-config.type.mjs";
6
6
 
7
7
  //#region ../ai/src/orchestrator/memory.d.ts
8
8
  /**
@@ -18,7 +18,13 @@ type ResolvedOrchestratorMemory = {
18
18
  threshold?: number; /** Single-tier recall restriction. */
19
19
  tier?: ResolvedTier; /** Whether a clean turn writes its outcome back. Default `true`. */
20
20
  remember: boolean; /** Tier the remembered outcome lands in. Omit for the memory's `defaultTier`. */
21
- rememberTier?: ResolvedTier; /** Context-bag key the recalled memories are injected under. */
21
+ rememberTier?: ResolvedTier;
22
+ /**
23
+ * Isolation boundary for recall + write-back. Default `"session"` —
24
+ * the turn's `sessionId` keys every read and write, so one session
25
+ * cannot recall another's memories out of the shared store.
26
+ */
27
+ scope: OrchestratorMemoryScope; /** Context-bag key the recalled memories are injected under. */
22
28
  injectKey: string;
23
29
  };
24
30
  type ResolvedTier = NonNullable<OrchestratorMemoryConfig["recall"]>["tier"];
@@ -29,6 +35,28 @@ type ResolvedTier = NonNullable<OrchestratorMemoryConfig["recall"]>["tier"];
29
35
  * shape and the lifecycle phase stays branch-free.
30
36
  */
31
37
  declare function resolveOrchestratorMemory(memory: MemoryContract | OrchestratorMemoryConfig | undefined): ResolvedOrchestratorMemory | undefined;
38
+ /**
39
+ * Resolve the isolation key a turn reads and writes memories under
40
+ * (4.15.0 — security fix for cross-session recall).
41
+ *
42
+ * The memory store is resolved once per orchestrator instance and reused
43
+ * by every session, so this — not the store — is what keeps one session's
44
+ * remembered turns out of another's recall. It is derived from the
45
+ * execute-time `sessionId` by the engine and handed to every tier as an
46
+ * exact-match filter; the model, the tool payload, and the per-call
47
+ * `context` bag have no say in it.
48
+ *
49
+ * `"shared"` resolves to `undefined`, i.e. the store's unscoped pool —
50
+ * the explicit opt-in back to pre-4.15.0 cross-session behavior, which
51
+ * also keeps memories written before this release readable.
52
+ */
53
+ declare function memoryScopeFor(memory: ResolvedOrchestratorMemory, sessionId: string): string | undefined;
54
+ /**
55
+ * The default `"session"` scope key: the session id under a reserved
56
+ * prefix, so a custom `scope` callback returning a bare tenant id can
57
+ * never accidentally collide with a session-scoped pool.
58
+ */
59
+ declare function sessionMemoryScope(sessionId: string): string;
32
60
  /**
33
61
  * Coerce a turn's {@link SupervisorInput} (string or structured object)
34
62
  * into the natural-language query the memory store recalls / embeds
@@ -44,8 +72,12 @@ declare function memoryQueryFromInput(input: SupervisorInput): string;
44
72
  * `memory.injectKey`. Returns an empty array — never throws on "no hits"
45
73
  * — and short-circuits when `k === 0` (recall disabled / write-only
46
74
  * memory) so a write-only config never round-trips the embedder.
75
+ *
76
+ * The recall is confined to the calling session's scope (see
77
+ * {@link memoryScopeFor}) — `sessionId` is required, not optional, so a
78
+ * new call site cannot silently recall across every session.
47
79
  */
48
- declare function recallForTurn(memory: ResolvedOrchestratorMemory, input: SupervisorInput): Promise<RecalledMemory[]>;
80
+ declare function recallForTurn(memory: ResolvedOrchestratorMemory, input: SupervisorInput, sessionId: string): Promise<RecalledMemory[]>;
49
81
  /**
50
82
  * Merge the recalled memories into a fresh per-turn context bag under
51
83
  * `memory.injectKey` (memory core M2 — the injection half). Never
@@ -68,8 +100,12 @@ declare function injectMemories(context: Record<string, unknown> | undefined, me
68
100
  * The remembered text is the turn input followed by the model's textual
69
101
  * outcome when one is available, so a later `recall` keyed on a similar
70
102
  * input surfaces both the prior question and its answer.
103
+ *
104
+ * The write is tagged with the calling session's scope (see
105
+ * {@link memoryScopeFor}) so only that session recalls it later —
106
+ * turn text routinely contains one user's private content.
71
107
  */
72
- declare function rememberTurnOutcome(memory: ResolvedOrchestratorMemory, input: SupervisorInput, outcomeText: string | undefined): Promise<void>;
108
+ declare function rememberTurnOutcome(memory: ResolvedOrchestratorMemory, input: SupervisorInput, outcomeText: string | undefined, sessionId: string): Promise<void>;
73
109
  /**
74
110
  * Derive a turn's textual outcome for remembering (memory core M2).
75
111
  * Prefers the validated `result.data` (an `output` schema reshaped it);
@@ -80,5 +116,5 @@ declare function rememberTurnOutcome(memory: ResolvedOrchestratorMemory, input:
80
116
  */
81
117
  declare function outcomeTextFromTurn(data: unknown, turnSnapshot: TurnSnapshot): string | undefined;
82
118
  //#endregion
83
- export { ResolvedOrchestratorMemory, injectMemories, memoryQueryFromInput, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory };
119
+ export { ResolvedOrchestratorMemory, injectMemories, memoryQueryFromInput, memoryScopeFor, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory, sessionMemoryScope };
84
120
  //# sourceMappingURL=memory.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/orchestrator/memory.ts"],"mappings":";;;;;;;;;AAmBA;;;;;KAAY,0BAAA;EAYiB,yEAV3B,KAAA,EAAO,cAAA,EAAP;EAEA,CAAA;EAEA,SAAA,WAEA;EAAA,IAAA,GAAO,YAAA,EAEP;EAAA,QAAA,WAEe;EAAf,YAAA,GAAe,YAAA,EAEN;EAAT,SAAA;AAAA;AAAA,KAGG,YAAA,GAAe,WAAW,CAAC,wBAAA;;;AAAwB;AAoBxD;;;iBAAgB,yBAAA,CACd,MAAA,EAAQ,cAAA,GAAiB,wBAAA,eACxB,0BAAA;;;;;;;;iBA+Ba,oBAAA,CAAqB,KAAsB,EAAf,eAAe;;AA/B9B;AA+B7B;;;;AAA2D;AAY3D;iBAAsB,aAAA,CACpB,MAAA,EAAQ,0BAAA,EACR,KAAA,EAAO,eAAA,GACN,OAAA,CAAQ,cAAA;;;;;;;;;;;;;iBAwBK,cAAA,CACd,OAAA,EAAS,MAAA,+BACT,MAAA,EAAQ,0BAAA,EACR,QAAA,EAAU,cAAA,KACT,MAAA;;AA5BsB;AAwBzB;;;;;;;;iBAsBsB,mBAAA,CACpB,MAAA,EAAQ,0BAAA,EACR,KAAA,EAAO,eAAA,EACP,WAAA,uBACC,OAAA;;;;;;;;;iBA4Ca,mBAAA,CACd,IAAA,WACA,YAAA,EAAc,YAAY"}
1
+ {"version":3,"file":"memory.d.mts","names":[],"sources":["../../../../../../../ai/src/orchestrator/memory.ts"],"mappings":";;;;;;;;;AA+BA;;;;;KAAY,0BAAA;EAkBH,yEAhBP,KAAA,EAAO,cAAA,EAgBuB;EAd9B,CAAA,WAFO;EAIP,SAAA;EAEA,IAAA,GAAO,YAAA,EAAA;EAEP,QAAA,WAEA;EAAA,YAAA,GAAe,YAAA;EAMf;;;;AAES;EAFT,KAAA,EAAO,uBAAA,EAKQ;EAHf,SAAA;AAAA;AAAA,KAGG,YAAA,GAAe,WAAW,CAAC,wBAAA;AAoBhC;;;;;;AAAA,iBAAgB,yBAAA,CACd,MAAA,EAAQ,cAAA,GAAiB,wBAAA,eACxB,0BAAA;;;;;;;AAA0B;AAyC7B;;;;;;;;iBAAgB,cAAA,CACd,MAAA,EAAQ,0BAA0B,EAClC,SAAA;AAkBF;;;;AAAoD;AAApD,iBAAgB,kBAAA,CAAmB,SAAiB;;;;AAWO;AAgB3D;;;iBAhBgB,oBAAA,CAAqB,KAAsB,EAAf,eAAe;;;;;;;;;;;;;iBAgBrC,aAAA,CACpB,MAAA,EAAQ,0BAAA,EACR,KAAA,EAAO,eAAA,EACP,SAAA,WACC,OAAA,CAAQ,cAAA;AAAc;AAyBzB;;;;;;;;;;;AAzByB,iBAyBT,cAAA,CACd,OAAA,EAAS,MAAA,+BACT,MAAA,EAAQ,0BAAA,EACR,QAAA,EAAU,cAAA,KACT,MAAA;;;;;;AAAM;AAsBT;;;;;;;;iBAAsB,mBAAA,CACpB,MAAA,EAAQ,0BAAA,EACR,KAAA,EAAO,eAAA,EACP,WAAA,sBACA,SAAA,WACC,OAAA;;;;;;;;AAAO;iBAgDM,mBAAA,CACd,IAAA,WACA,YAAA,EAAc,YAAY"}
@@ -2,6 +2,14 @@
2
2
  /** Default key the recalled memories are injected under in the context bag. */
3
3
  const DEFAULT_INJECT_KEY = "memories";
4
4
  /**
5
+ * Default isolation boundary: a turn recalls only what its own session
6
+ * remembered. Cross-session pooling is opt-in (`scope: "shared"`) — the
7
+ * default must not leak one user's remembered turns into another's
8
+ * context, since one memory store backs every session of an
9
+ * orchestrator instance.
10
+ */
11
+ const DEFAULT_SCOPE = "session";
12
+ /**
5
13
  * A `MemoryContract` is the bare-store form; anything carrying a `store`
6
14
  * is the {@link OrchestratorMemoryConfig} wrapper. Distinguished by the
7
15
  * presence of `recall` — a method on the contract, absent on the config
@@ -21,6 +29,7 @@ function resolveOrchestratorMemory(memory) {
21
29
  if (isBareMemory(memory)) return {
22
30
  store: memory,
23
31
  remember: true,
32
+ scope: DEFAULT_SCOPE,
24
33
  injectKey: DEFAULT_INJECT_KEY
25
34
  };
26
35
  return {
@@ -30,10 +39,39 @@ function resolveOrchestratorMemory(memory) {
30
39
  tier: memory.recall?.tier,
31
40
  remember: memory.remember ?? true,
32
41
  rememberTier: memory.rememberTier,
42
+ scope: memory.scope ?? DEFAULT_SCOPE,
33
43
  injectKey: memory.injectKey ?? DEFAULT_INJECT_KEY
34
44
  };
35
45
  }
36
46
  /**
47
+ * Resolve the isolation key a turn reads and writes memories under
48
+ * (4.15.0 — security fix for cross-session recall).
49
+ *
50
+ * The memory store is resolved once per orchestrator instance and reused
51
+ * by every session, so this — not the store — is what keeps one session's
52
+ * remembered turns out of another's recall. It is derived from the
53
+ * execute-time `sessionId` by the engine and handed to every tier as an
54
+ * exact-match filter; the model, the tool payload, and the per-call
55
+ * `context` bag have no say in it.
56
+ *
57
+ * `"shared"` resolves to `undefined`, i.e. the store's unscoped pool —
58
+ * the explicit opt-in back to pre-4.15.0 cross-session behavior, which
59
+ * also keeps memories written before this release readable.
60
+ */
61
+ function memoryScopeFor(memory, sessionId) {
62
+ if (memory.scope === "shared") return;
63
+ if (typeof memory.scope === "function") return memory.scope(sessionId);
64
+ return sessionMemoryScope(sessionId);
65
+ }
66
+ /**
67
+ * The default `"session"` scope key: the session id under a reserved
68
+ * prefix, so a custom `scope` callback returning a bare tenant id can
69
+ * never accidentally collide with a session-scoped pool.
70
+ */
71
+ function sessionMemoryScope(sessionId) {
72
+ return `session:${sessionId}`;
73
+ }
74
+ /**
37
75
  * Coerce a turn's {@link SupervisorInput} (string or structured object)
38
76
  * into the natural-language query the memory store recalls / embeds
39
77
  * against. Strings pass through; objects are JSON-serialized — the same
@@ -50,13 +88,18 @@ function memoryQueryFromInput(input) {
50
88
  * `memory.injectKey`. Returns an empty array — never throws on "no hits"
51
89
  * — and short-circuits when `k === 0` (recall disabled / write-only
52
90
  * memory) so a write-only config never round-trips the embedder.
91
+ *
92
+ * The recall is confined to the calling session's scope (see
93
+ * {@link memoryScopeFor}) — `sessionId` is required, not optional, so a
94
+ * new call site cannot silently recall across every session.
53
95
  */
54
- async function recallForTurn(memory, input) {
96
+ async function recallForTurn(memory, input, sessionId) {
55
97
  if (memory.k === 0) return [];
56
98
  return memory.store.recall(memoryQueryFromInput(input), {
57
99
  k: memory.k,
58
100
  threshold: memory.threshold,
59
- tier: memory.tier
101
+ tier: memory.tier,
102
+ scope: memoryScopeFor(memory, sessionId)
60
103
  });
61
104
  }
62
105
  /**
@@ -87,14 +130,19 @@ function injectMemories(context, memory, recalled) {
87
130
  * The remembered text is the turn input followed by the model's textual
88
131
  * outcome when one is available, so a later `recall` keyed on a similar
89
132
  * input surfaces both the prior question and its answer.
133
+ *
134
+ * The write is tagged with the calling session's scope (see
135
+ * {@link memoryScopeFor}) so only that session recalls it later —
136
+ * turn text routinely contains one user's private content.
90
137
  */
91
- async function rememberTurnOutcome(memory, input, outcomeText) {
138
+ async function rememberTurnOutcome(memory, input, outcomeText, sessionId) {
92
139
  if (!memory.remember) return;
93
140
  const text = buildOutcomeText(input, outcomeText);
94
141
  if (!text) return;
95
142
  const item = {
96
143
  text,
97
- tier: memory.rememberTier
144
+ tier: memory.rememberTier,
145
+ scope: memoryScopeFor(memory, sessionId)
98
146
  };
99
147
  await memory.store.remember(item);
100
148
  }
@@ -137,5 +185,5 @@ function stringifyOutcome(value) {
137
185
  }
138
186
 
139
187
  //#endregion
140
- export { injectMemories, memoryQueryFromInput, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory };
188
+ export { injectMemories, memoryQueryFromInput, memoryScopeFor, outcomeTextFromTurn, recallForTurn, rememberTurnOutcome, resolveOrchestratorMemory, sessionMemoryScope };
141
189
  //# sourceMappingURL=memory.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/memory.ts"],"sourcesContent":["import type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type { OrchestratorMemoryConfig } from \"../contracts/orchestrator/orchestrator-config.type\";\nimport type { TurnSnapshot } from \"../contracts/result/orchestrator-result.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\n\n/** Default key the recalled memories are injected under in the context bag. */\nconst DEFAULT_INJECT_KEY = \"memories\";\n\n/**\n * Memory wiring resolved once per turn from `OrchestratorConfig.memory`\n * (memory core M2). Normalizes the two accepted config shapes — a bare\n * {@link MemoryContract} or the richer {@link OrchestratorMemoryConfig} —\n * into a single flat record the lifecycle phase reads, so `runTurn` never\n * branches on which form the dev supplied.\n */\nexport type ResolvedOrchestratorMemory = {\n /** The store recalled-from before dispatch and remembered-into after. */\n store: MemoryContract;\n /** Recall count cap; `0` disables recall (write-only memory). */\n k?: number;\n /** Semantic-similarity floor for recall. */\n threshold?: number;\n /** Single-tier recall restriction. */\n tier?: ResolvedTier;\n /** Whether a clean turn writes its outcome back. Default `true`. */\n remember: boolean;\n /** Tier the remembered outcome lands in. Omit for the memory's `defaultTier`. */\n rememberTier?: ResolvedTier;\n /** Context-bag key the recalled memories are injected under. */\n injectKey: string;\n};\n\ntype ResolvedTier = NonNullable<OrchestratorMemoryConfig[\"recall\"]>[\"tier\"];\n\n/**\n * A `MemoryContract` is the bare-store form; anything carrying a `store`\n * is the {@link OrchestratorMemoryConfig} wrapper. Distinguished by the\n * presence of `recall` — a method on the contract, absent on the config\n * (whose own `recall` is a plain options object, never a function).\n */\nfunction isBareMemory(\n value: MemoryContract | OrchestratorMemoryConfig,\n): value is MemoryContract {\n return typeof (value as MemoryContract).recall === \"function\";\n}\n\n/**\n * Normalize `OrchestratorConfig.memory` into {@link ResolvedOrchestratorMemory},\n * or `undefined` when no memory is configured. Centralizes the\n * bare-store-vs-config distinction so the engine context carries one\n * shape and the lifecycle phase stays branch-free.\n */\nexport function resolveOrchestratorMemory(\n memory: MemoryContract | OrchestratorMemoryConfig | undefined,\n): ResolvedOrchestratorMemory | undefined {\n if (!memory) {\n return undefined;\n }\n\n if (isBareMemory(memory)) {\n return {\n store: memory,\n remember: true,\n injectKey: DEFAULT_INJECT_KEY,\n };\n }\n\n return {\n store: memory.store,\n k: memory.recall?.k,\n threshold: memory.recall?.threshold,\n tier: memory.recall?.tier,\n remember: memory.remember ?? true,\n rememberTier: memory.rememberTier,\n injectKey: memory.injectKey ?? DEFAULT_INJECT_KEY,\n };\n}\n\n/**\n * Coerce a turn's {@link SupervisorInput} (string or structured object)\n * into the natural-language query the memory store recalls / embeds\n * against. Strings pass through; objects are JSON-serialized — the same\n * coercion the supervisor applies when forwarding an object input to a\n * child agent without an explicit `input(ctx)` override.\n */\nexport function memoryQueryFromInput(input: SupervisorInput): string {\n return typeof input === \"string\" ? input : JSON.stringify(input);\n}\n\n/**\n * Recall the memories relevant to a turn's input (memory core M2 — the\n * pre-dispatch half). Returns the scored {@link RecalledMemory}[] the\n * lifecycle injects into the turn's `context` bag under\n * `memory.injectKey`. Returns an empty array — never throws on \"no hits\"\n * — and short-circuits when `k === 0` (recall disabled / write-only\n * memory) so a write-only config never round-trips the embedder.\n */\nexport async function recallForTurn(\n memory: ResolvedOrchestratorMemory,\n input: SupervisorInput,\n): Promise<RecalledMemory[]> {\n if (memory.k === 0) {\n return [];\n }\n\n return memory.store.recall(memoryQueryFromInput(input), {\n k: memory.k,\n threshold: memory.threshold,\n tier: memory.tier,\n });\n}\n\n/**\n * Merge the recalled memories into a fresh per-turn context bag under\n * `memory.injectKey` (memory core M2 — the injection half). Never\n * mutates the caller's `context` object — returns a new bag (or the\n * original when there is nothing to inject) so the request-scoped input\n * stays immutable, and the supervisor's intake (which freezes a\n * shallow copy) sees the recalled set on every `ctx.context[injectKey]`.\n *\n * A pre-existing value at `injectKey` is preserved when recall produced\n * nothing, and overwritten with the recalled set otherwise — the\n * orchestrator owns that key once memory is configured.\n */\nexport function injectMemories(\n context: Record<string, unknown> | undefined,\n memory: ResolvedOrchestratorMemory,\n recalled: RecalledMemory[],\n): Record<string, unknown> | undefined {\n if (recalled.length === 0) {\n return context;\n }\n\n return { ...(context ?? {}), [memory.injectKey]: recalled };\n}\n\n/**\n * Remember a settled turn's outcome (memory core M2 — the post-dispatch\n * half). Called only after a clean turn (cancelled / failed turns revert\n * and never remember — §17). No-ops when `remember` is `false`\n * (read-only memory) or when the produced text is empty.\n *\n * The remembered text is the turn input followed by the model's textual\n * outcome when one is available, so a later `recall` keyed on a similar\n * input surfaces both the prior question and its answer.\n */\nexport async function rememberTurnOutcome(\n memory: ResolvedOrchestratorMemory,\n input: SupervisorInput,\n outcomeText: string | undefined,\n): Promise<void> {\n if (!memory.remember) {\n return;\n }\n\n const text = buildOutcomeText(input, outcomeText);\n\n if (!text) {\n return;\n }\n\n const item: MemoryItem = { text, tier: memory.rememberTier };\n\n await memory.store.remember(item);\n}\n\n/**\n * Compose the text written to memory for a turn: the input query, plus\n * the outcome text on a following line when the dispatch produced one.\n * Returns `undefined` when neither side carries content so an empty turn\n * never pollutes the store.\n */\nfunction buildOutcomeText(\n input: SupervisorInput,\n outcomeText: string | undefined,\n): string | undefined {\n const query = memoryQueryFromInput(input).trim();\n const outcome = outcomeText?.trim();\n\n if (query && outcome) {\n return `${query}\\n${outcome}`;\n }\n\n return query || outcome || undefined;\n}\n\n/**\n * Derive a turn's textual outcome for remembering (memory core M2).\n * Prefers the validated `result.data` (an `output` schema reshaped it);\n * otherwise stringifies the dispatched intents' branch outputs from the\n * turn snapshot, joined newline-wise so a multi-branch fan-out\n * contributes every output. Returns `undefined` when the turn produced\n * no usable text — the caller then remembers the input alone.\n */\nexport function outcomeTextFromTurn(\n data: unknown,\n turnSnapshot: TurnSnapshot,\n): string | undefined {\n const fromData = stringifyOutcome(data);\n\n if (fromData) {\n return fromData;\n }\n\n const outputs = Object.values(turnSnapshot.result)\n .map((branch) => stringifyOutcome(branch.output))\n .filter((text): text is string => Boolean(text));\n\n return outputs.length > 0 ? outputs.join(\"\\n\") : undefined;\n}\n\n/**\n * Coerce one outcome value to text: strings pass through; everything\n * else (objects, numbers) is JSON-serialized. `undefined` / `null` and\n * empty strings collapse to `undefined` so they don't masquerade as\n * content.\n */\nfunction stringifyOutcome(value: unknown): string | undefined {\n if (value === undefined || value === null) {\n return undefined;\n }\n\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n\n return text.trim() ? text : undefined;\n}\n"],"mappings":";;AAUA,MAAM,qBAAqB;;;;;;;AAkC3B,SAAS,aACP,OACyB;CACzB,OAAO,OAAQ,MAAyB,WAAW;AACrD;;;;;;;AAQA,SAAgB,0BACd,QACwC;CACxC,IAAI,CAAC,QACH;CAGF,IAAI,aAAa,MAAM,GACrB,OAAO;EACL,OAAO;EACP,UAAU;EACV,WAAW;CACb;CAGF,OAAO;EACL,OAAO,OAAO;EACd,GAAG,OAAO,QAAQ;EAClB,WAAW,OAAO,QAAQ;EAC1B,MAAM,OAAO,QAAQ;EACrB,UAAU,OAAO,YAAY;EAC7B,cAAc,OAAO;EACrB,WAAW,OAAO,aAAa;CACjC;AACF;;;;;;;;AASA,SAAgB,qBAAqB,OAAgC;CACnE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;;;;;;;;;AAUA,eAAsB,cACpB,QACA,OAC2B;CAC3B,IAAI,OAAO,MAAM,GACf,OAAO,CAAC;CAGV,OAAO,OAAO,MAAM,OAAO,qBAAqB,KAAK,GAAG;EACtD,GAAG,OAAO;EACV,WAAW,OAAO;EAClB,MAAM,OAAO;CACf,CAAC;AACH;;;;;;;;;;;;;AAcA,SAAgB,eACd,SACA,QACA,UACqC;CACrC,IAAI,SAAS,WAAW,GACtB,OAAO;CAGT,OAAO;EAAE,GAAI,WAAW,CAAC;GAAK,OAAO,YAAY;CAAS;AAC5D;;;;;;;;;;;AAYA,eAAsB,oBACpB,QACA,OACA,aACe;CACf,IAAI,CAAC,OAAO,UACV;CAGF,MAAM,OAAO,iBAAiB,OAAO,WAAW;CAEhD,IAAI,CAAC,MACH;CAGF,MAAM,OAAmB;EAAE;EAAM,MAAM,OAAO;CAAa;CAE3D,MAAM,OAAO,MAAM,SAAS,IAAI;AAClC;;;;;;;AAQA,SAAS,iBACP,OACA,aACoB;CACpB,MAAM,QAAQ,qBAAqB,KAAK,CAAC,CAAC,KAAK;CAC/C,MAAM,UAAU,aAAa,KAAK;CAElC,IAAI,SAAS,SACX,OAAO,GAAG,MAAM,IAAI;CAGtB,OAAO,SAAS,WAAW;AAC7B;;;;;;;;;AAUA,SAAgB,oBACd,MACA,cACoB;CACpB,MAAM,WAAW,iBAAiB,IAAI;CAEtC,IAAI,UACF,OAAO;CAGT,MAAM,UAAU,OAAO,OAAO,aAAa,MAAM,CAAC,CAC/C,KAAK,WAAW,iBAAiB,OAAO,MAAM,CAAC,CAAC,CAChD,QAAQ,SAAyB,QAAQ,IAAI,CAAC;CAEjD,OAAO,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AACnD;;;;;;;AAQA,SAAS,iBAAiB,OAAoC;CAC5D,IAAI,UAAU,UAAa,UAAU,MACnC;CAGF,MAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;CAErE,OAAO,KAAK,KAAK,IAAI,OAAO;AAC9B"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/memory.ts"],"sourcesContent":["import type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type {\n OrchestratorMemoryConfig,\n OrchestratorMemoryScope,\n} from \"../contracts/orchestrator/orchestrator-config.type\";\nimport type { TurnSnapshot } from \"../contracts/result/orchestrator-result.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\n\n/** Default key the recalled memories are injected under in the context bag. */\nconst DEFAULT_INJECT_KEY = \"memories\";\n\n/**\n * Default isolation boundary: a turn recalls only what its own session\n * remembered. Cross-session pooling is opt-in (`scope: \"shared\"`) — the\n * default must not leak one user's remembered turns into another's\n * context, since one memory store backs every session of an\n * orchestrator instance.\n */\nconst DEFAULT_SCOPE = \"session\" as const;\n\n/**\n * Memory wiring resolved once per turn from `OrchestratorConfig.memory`\n * (memory core M2). Normalizes the two accepted config shapes — a bare\n * {@link MemoryContract} or the richer {@link OrchestratorMemoryConfig} —\n * into a single flat record the lifecycle phase reads, so `runTurn` never\n * branches on which form the dev supplied.\n */\nexport type ResolvedOrchestratorMemory = {\n /** The store recalled-from before dispatch and remembered-into after. */\n store: MemoryContract;\n /** Recall count cap; `0` disables recall (write-only memory). */\n k?: number;\n /** Semantic-similarity floor for recall. */\n threshold?: number;\n /** Single-tier recall restriction. */\n tier?: ResolvedTier;\n /** Whether a clean turn writes its outcome back. Default `true`. */\n remember: boolean;\n /** Tier the remembered outcome lands in. Omit for the memory's `defaultTier`. */\n rememberTier?: ResolvedTier;\n /**\n * Isolation boundary for recall + write-back. Default `\"session\"` —\n * the turn's `sessionId` keys every read and write, so one session\n * cannot recall another's memories out of the shared store.\n */\n scope: OrchestratorMemoryScope;\n /** Context-bag key the recalled memories are injected under. */\n injectKey: string;\n};\n\ntype ResolvedTier = NonNullable<OrchestratorMemoryConfig[\"recall\"]>[\"tier\"];\n\n/**\n * A `MemoryContract` is the bare-store form; anything carrying a `store`\n * is the {@link OrchestratorMemoryConfig} wrapper. Distinguished by the\n * presence of `recall` — a method on the contract, absent on the config\n * (whose own `recall` is a plain options object, never a function).\n */\nfunction isBareMemory(\n value: MemoryContract | OrchestratorMemoryConfig,\n): value is MemoryContract {\n return typeof (value as MemoryContract).recall === \"function\";\n}\n\n/**\n * Normalize `OrchestratorConfig.memory` into {@link ResolvedOrchestratorMemory},\n * or `undefined` when no memory is configured. Centralizes the\n * bare-store-vs-config distinction so the engine context carries one\n * shape and the lifecycle phase stays branch-free.\n */\nexport function resolveOrchestratorMemory(\n memory: MemoryContract | OrchestratorMemoryConfig | undefined,\n): ResolvedOrchestratorMemory | undefined {\n if (!memory) {\n return undefined;\n }\n\n if (isBareMemory(memory)) {\n return {\n store: memory,\n remember: true,\n scope: DEFAULT_SCOPE,\n injectKey: DEFAULT_INJECT_KEY,\n };\n }\n\n return {\n store: memory.store,\n k: memory.recall?.k,\n threshold: memory.recall?.threshold,\n tier: memory.recall?.tier,\n remember: memory.remember ?? true,\n rememberTier: memory.rememberTier,\n scope: memory.scope ?? DEFAULT_SCOPE,\n injectKey: memory.injectKey ?? DEFAULT_INJECT_KEY,\n };\n}\n\n/**\n * Resolve the isolation key a turn reads and writes memories under\n * (4.15.0 — security fix for cross-session recall).\n *\n * The memory store is resolved once per orchestrator instance and reused\n * by every session, so this — not the store — is what keeps one session's\n * remembered turns out of another's recall. It is derived from the\n * execute-time `sessionId` by the engine and handed to every tier as an\n * exact-match filter; the model, the tool payload, and the per-call\n * `context` bag have no say in it.\n *\n * `\"shared\"` resolves to `undefined`, i.e. the store's unscoped pool —\n * the explicit opt-in back to pre-4.15.0 cross-session behavior, which\n * also keeps memories written before this release readable.\n */\nexport function memoryScopeFor(\n memory: ResolvedOrchestratorMemory,\n sessionId: string,\n): string | undefined {\n if (memory.scope === \"shared\") {\n return undefined;\n }\n\n if (typeof memory.scope === \"function\") {\n return memory.scope(sessionId);\n }\n\n return sessionMemoryScope(sessionId);\n}\n\n/**\n * The default `\"session\"` scope key: the session id under a reserved\n * prefix, so a custom `scope` callback returning a bare tenant id can\n * never accidentally collide with a session-scoped pool.\n */\nexport function sessionMemoryScope(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\n/**\n * Coerce a turn's {@link SupervisorInput} (string or structured object)\n * into the natural-language query the memory store recalls / embeds\n * against. Strings pass through; objects are JSON-serialized — the same\n * coercion the supervisor applies when forwarding an object input to a\n * child agent without an explicit `input(ctx)` override.\n */\nexport function memoryQueryFromInput(input: SupervisorInput): string {\n return typeof input === \"string\" ? input : JSON.stringify(input);\n}\n\n/**\n * Recall the memories relevant to a turn's input (memory core M2 — the\n * pre-dispatch half). Returns the scored {@link RecalledMemory}[] the\n * lifecycle injects into the turn's `context` bag under\n * `memory.injectKey`. Returns an empty array — never throws on \"no hits\"\n * — and short-circuits when `k === 0` (recall disabled / write-only\n * memory) so a write-only config never round-trips the embedder.\n *\n * The recall is confined to the calling session's scope (see\n * {@link memoryScopeFor}) — `sessionId` is required, not optional, so a\n * new call site cannot silently recall across every session.\n */\nexport async function recallForTurn(\n memory: ResolvedOrchestratorMemory,\n input: SupervisorInput,\n sessionId: string,\n): Promise<RecalledMemory[]> {\n if (memory.k === 0) {\n return [];\n }\n\n return memory.store.recall(memoryQueryFromInput(input), {\n k: memory.k,\n threshold: memory.threshold,\n tier: memory.tier,\n scope: memoryScopeFor(memory, sessionId),\n });\n}\n\n/**\n * Merge the recalled memories into a fresh per-turn context bag under\n * `memory.injectKey` (memory core M2 — the injection half). Never\n * mutates the caller's `context` object — returns a new bag (or the\n * original when there is nothing to inject) so the request-scoped input\n * stays immutable, and the supervisor's intake (which freezes a\n * shallow copy) sees the recalled set on every `ctx.context[injectKey]`.\n *\n * A pre-existing value at `injectKey` is preserved when recall produced\n * nothing, and overwritten with the recalled set otherwise — the\n * orchestrator owns that key once memory is configured.\n */\nexport function injectMemories(\n context: Record<string, unknown> | undefined,\n memory: ResolvedOrchestratorMemory,\n recalled: RecalledMemory[],\n): Record<string, unknown> | undefined {\n if (recalled.length === 0) {\n return context;\n }\n\n return { ...(context ?? {}), [memory.injectKey]: recalled };\n}\n\n/**\n * Remember a settled turn's outcome (memory core M2 — the post-dispatch\n * half). Called only after a clean turn (cancelled / failed turns revert\n * and never remember — §17). No-ops when `remember` is `false`\n * (read-only memory) or when the produced text is empty.\n *\n * The remembered text is the turn input followed by the model's textual\n * outcome when one is available, so a later `recall` keyed on a similar\n * input surfaces both the prior question and its answer.\n *\n * The write is tagged with the calling session's scope (see\n * {@link memoryScopeFor}) so only that session recalls it later —\n * turn text routinely contains one user's private content.\n */\nexport async function rememberTurnOutcome(\n memory: ResolvedOrchestratorMemory,\n input: SupervisorInput,\n outcomeText: string | undefined,\n sessionId: string,\n): Promise<void> {\n if (!memory.remember) {\n return;\n }\n\n const text = buildOutcomeText(input, outcomeText);\n\n if (!text) {\n return;\n }\n\n const item: MemoryItem = {\n text,\n tier: memory.rememberTier,\n scope: memoryScopeFor(memory, sessionId),\n };\n\n await memory.store.remember(item);\n}\n\n/**\n * Compose the text written to memory for a turn: the input query, plus\n * the outcome text on a following line when the dispatch produced one.\n * Returns `undefined` when neither side carries content so an empty turn\n * never pollutes the store.\n */\nfunction buildOutcomeText(\n input: SupervisorInput,\n outcomeText: string | undefined,\n): string | undefined {\n const query = memoryQueryFromInput(input).trim();\n const outcome = outcomeText?.trim();\n\n if (query && outcome) {\n return `${query}\\n${outcome}`;\n }\n\n return query || outcome || undefined;\n}\n\n/**\n * Derive a turn's textual outcome for remembering (memory core M2).\n * Prefers the validated `result.data` (an `output` schema reshaped it);\n * otherwise stringifies the dispatched intents' branch outputs from the\n * turn snapshot, joined newline-wise so a multi-branch fan-out\n * contributes every output. Returns `undefined` when the turn produced\n * no usable text — the caller then remembers the input alone.\n */\nexport function outcomeTextFromTurn(\n data: unknown,\n turnSnapshot: TurnSnapshot,\n): string | undefined {\n const fromData = stringifyOutcome(data);\n\n if (fromData) {\n return fromData;\n }\n\n const outputs = Object.values(turnSnapshot.result)\n .map((branch) => stringifyOutcome(branch.output))\n .filter((text): text is string => Boolean(text));\n\n return outputs.length > 0 ? outputs.join(\"\\n\") : undefined;\n}\n\n/**\n * Coerce one outcome value to text: strings pass through; everything\n * else (objects, numbers) is JSON-serialized. `undefined` / `null` and\n * empty strings collapse to `undefined` so they don't masquerade as\n * content.\n */\nfunction stringifyOutcome(value: unknown): string | undefined {\n if (value === undefined || value === null) {\n return undefined;\n }\n\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n\n return text.trim() ? text : undefined;\n}\n"],"mappings":";;AAaA,MAAM,qBAAqB;;;;;;;;AAS3B,MAAM,gBAAgB;;;;;;;AAwCtB,SAAS,aACP,OACyB;CACzB,OAAO,OAAQ,MAAyB,WAAW;AACrD;;;;;;;AAQA,SAAgB,0BACd,QACwC;CACxC,IAAI,CAAC,QACH;CAGF,IAAI,aAAa,MAAM,GACrB,OAAO;EACL,OAAO;EACP,UAAU;EACV,OAAO;EACP,WAAW;CACb;CAGF,OAAO;EACL,OAAO,OAAO;EACd,GAAG,OAAO,QAAQ;EAClB,WAAW,OAAO,QAAQ;EAC1B,MAAM,OAAO,QAAQ;EACrB,UAAU,OAAO,YAAY;EAC7B,cAAc,OAAO;EACrB,OAAO,OAAO,SAAS;EACvB,WAAW,OAAO,aAAa;CACjC;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,eACd,QACA,WACoB;CACpB,IAAI,OAAO,UAAU,UACnB;CAGF,IAAI,OAAO,OAAO,UAAU,YAC1B,OAAO,OAAO,MAAM,SAAS;CAG/B,OAAO,mBAAmB,SAAS;AACrC;;;;;;AAOA,SAAgB,mBAAmB,WAA2B;CAC5D,OAAO,WAAW;AACpB;;;;;;;;AASA,SAAgB,qBAAqB,OAAgC;CACnE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;;;;;;;;;;;;;AAcA,eAAsB,cACpB,QACA,OACA,WAC2B;CAC3B,IAAI,OAAO,MAAM,GACf,OAAO,CAAC;CAGV,OAAO,OAAO,MAAM,OAAO,qBAAqB,KAAK,GAAG;EACtD,GAAG,OAAO;EACV,WAAW,OAAO;EAClB,MAAM,OAAO;EACb,OAAO,eAAe,QAAQ,SAAS;CACzC,CAAC;AACH;;;;;;;;;;;;;AAcA,SAAgB,eACd,SACA,QACA,UACqC;CACrC,IAAI,SAAS,WAAW,GACtB,OAAO;CAGT,OAAO;EAAE,GAAI,WAAW,CAAC;GAAK,OAAO,YAAY;CAAS;AAC5D;;;;;;;;;;;;;;;AAgBA,eAAsB,oBACpB,QACA,OACA,aACA,WACe;CACf,IAAI,CAAC,OAAO,UACV;CAGF,MAAM,OAAO,iBAAiB,OAAO,WAAW;CAEhD,IAAI,CAAC,MACH;CAGF,MAAM,OAAmB;EACvB;EACA,MAAM,OAAO;EACb,OAAO,eAAe,QAAQ,SAAS;CACzC;CAEA,MAAM,OAAO,MAAM,SAAS,IAAI;AAClC;;;;;;;AAQA,SAAS,iBACP,OACA,aACoB;CACpB,MAAM,QAAQ,qBAAqB,KAAK,CAAC,CAAC,KAAK;CAC/C,MAAM,UAAU,aAAa,KAAK;CAElC,IAAI,SAAS,SACX,OAAO,GAAG,MAAM,IAAI;CAGtB,OAAO,SAAS,WAAW;AAC7B;;;;;;;;;AAUA,SAAgB,oBACd,MACA,cACoB;CACpB,MAAM,WAAW,iBAAiB,IAAI;CAEtC,IAAI,UACF,OAAO;CAGT,MAAM,UAAU,OAAO,OAAO,aAAa,MAAM,CAAC,CAC/C,KAAK,WAAW,iBAAiB,OAAO,MAAM,CAAC,CAAC,CAChD,QAAQ,SAAyB,QAAQ,IAAI,CAAC;CAEjD,OAAO,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AACnD;;;;;;;AAQA,SAAS,iBAAiB,OAAoC;CAC5D,IAAI,UAAU,UAAa,UAAU,MACnC;CAGF,MAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;CAErE,OAAO,KAAK,KAAK,IAAI,OAAO;AAC9B"}
@@ -17,9 +17,9 @@ import { StandardSchemaV1 } from "@standard-schema/spec";
17
17
  * `PlannerPlanInvalidError`, with the full forensic context, rather
18
18
  * than as an opaque schema issue here.
19
19
  *
20
- * `maxSteps`, when provided, is emitted as the `steps` array's
21
- * `maxItems` so capable providers refuse to over-produce up front
22
- * (the planner still truncates the tail to `skipped` defensively).
20
+ * `maxSteps` cannot be expressed on the wire (strict mode rejects
21
+ * `maxItems`), so `validate()` enforces a hard parse-time ceiling
22
+ * derived from it see {@link parsedStepCeiling}.
23
23
  */
24
24
  type PlanSchema = StandardSchemaV1<PlannerPlan> & {
25
25
  "~standard": {
@@ -1 +1 @@
1
- {"version":3,"file":"plan-schema.d.mts","names":[],"sources":["../../../../../../../ai/src/planner/plan-schema.ts"],"mappings":";;;;;;AAsBA;;;;;;;;;;;;;;;AAMqC;AAIrC;KAVY,UAAA,GAAa,gBAAA,CAAiB,WAAA;EACxC,WAAA;IASkF;;;;IAJhF,UAAA;MAAc,KAAA,QAAa,MAAA;IAAA;EAAA;AAAA;AAAA,iBAIf,UAAA,CAAW,eAAA,YAA2B,QAAA,YAAoB,UAAU"}
1
+ {"version":3,"file":"plan-schema.d.mts","names":[],"sources":["../../../../../../../ai/src/planner/plan-schema.ts"],"mappings":";;;;;;AAsBA;;;;;;;;;;;;;;;AAMqC;AAsCrC;KA5CY,UAAA,GAAa,gBAAA,CAAiB,WAAA;EACxC,WAAA;IA2CkF;;;;IAtChF,UAAA;MAAc,KAAA,QAAa,MAAA;IAAA;EAAA;AAAA;AAAA,iBAsCf,UAAA,CAAW,eAAA,YAA2B,QAAA,YAAoB,UAAU"}
@@ -1,5 +1,34 @@
1
1
  //#region ../ai/src/planner/plan-schema.ts
2
+ /**
3
+ * Slack allowed over `maxSteps` before a returned plan is rejected
4
+ * outright. A model that overshoots the prompt's "at most N steps" by a
5
+ * little is normal and the runtime truncates the tail to `skipped`;
6
+ * one that returns several times the budget is malfunctioning (or the
7
+ * provider/proxy is not the one we think it is), and parsing it is
8
+ * unbounded work on attacker-adjacent input.
9
+ */
10
+ const STEP_CEILING_FACTOR = 4;
11
+ /**
12
+ * Ceiling used when `planSchema` is built without a `maxSteps` — direct
13
+ * callers outside `PlannerRun`, which has no runtime truncation of its
14
+ * own to fall back on.
15
+ */
16
+ const DEFAULT_STEP_CEILING = 100;
17
+ /**
18
+ * Hard upper bound on the number of steps `validate()` will parse.
19
+ *
20
+ * Strict-mode JSON Schema can't carry `maxItems`, so nothing on the wire
21
+ * stops a provider from returning an arbitrarily long `steps[]`; before
22
+ * 4.15.0 the whole array was parsed, normalized and stored, and only the
23
+ * execution loop truncated it. This is the parse-time backstop that
24
+ * makes the bound hold regardless of what the provider honors.
25
+ */
26
+ function parsedStepCeiling(maxSteps) {
27
+ if (maxSteps === void 0) return DEFAULT_STEP_CEILING;
28
+ return Math.max(1, Math.ceil(maxSteps)) * STEP_CEILING_FACTOR;
29
+ }
2
30
  function planSchema(capabilityNames, maxSteps) {
31
+ const stepCeiling = parsedStepCeiling(maxSteps);
3
32
  const jsonSchema = {
4
33
  type: "object",
5
34
  properties: {
@@ -24,6 +53,7 @@ function planSchema(capabilityNames, maxSteps) {
24
53
  if (!value || typeof value !== "object") return { issues: [{ message: "plan must be an object" }] };
25
54
  const record = value;
26
55
  if (!Array.isArray(record.steps) || record.steps.length === 0) return { issues: [{ message: "plan `steps` must be a non-empty array" }] };
56
+ if (record.steps.length > stepCeiling) return { issues: [{ message: `plan \`steps\` must not exceed ${stepCeiling} entries (received ${record.steps.length})` }] };
27
57
  const steps = [];
28
58
  for (const raw of record.steps) {
29
59
  const normalized = normalizeStep(raw);
@@ -1 +1 @@
1
- {"version":3,"file":"plan-schema.mjs","names":[],"sources":["../../../../../../../ai/src/planner/plan-schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { PlannerPlan, PlannerStep } from \"../contracts/planner/planner-plan.type\";\n\n/**\n * Build the Standard Schema the planning agent emits — an ordered\n * `{ steps: [...], summary? }` plan whose every step references one of\n * `capabilityNames` via the `capability` field.\n *\n * Mirrors the router's hand-built schema approach\n * (`supervisor/router-factory.ts`): the JSON Schema extension carries\n * the capability names as an `enum` so capable providers enforce the\n * choice natively, while `validate()` still accepts the shape softly so\n * providers without native structured output can pass a parsed object\n * through. Validation is intentionally lenient on `capability` — an\n * unknown name is surfaced later by the planner as a typed\n * `PlannerPlanInvalidError`, with the full forensic context, rather\n * than as an opaque schema issue here.\n *\n * `maxSteps`, when provided, is emitted as the `steps` array's\n * `maxItems` so capable providers refuse to over-produce up front\n * (the planner still truncates the tail to `skipped` defensively).\n */\nexport type PlanSchema = StandardSchemaV1<PlannerPlan> & {\n \"~standard\": {\n /**\n * JSON Schema extension read by the native structured-output path.\n * Part of the declared type so callers don't have to re-assert it.\n */\n jsonSchema: { input: () => Record<string, unknown> };\n };\n};\n\nexport function planSchema(capabilityNames: string[], maxSteps?: number): PlanSchema {\n // OpenAI strict `json_schema` mode (and other native structured-output\n // providers) require EVERY property to appear in `required` — with truly\n // optional fields expressed as nullable — and reject array `minItems` /\n // `maxItems`. So the schema is strict-shaped: all keys required, the\n // optional ones nullable, no item-count bounds. A non-empty plan is\n // enforced in `validate()`, and `maxSteps` by the runtime's tail\n // truncation, so neither bound is needed on the wire.\n void maxSteps;\n\n const jsonSchema = {\n type: \"object\",\n properties: {\n summary: {\n type: [\"string\", \"null\"],\n description: \"One-line summary of the overall strategy.\",\n },\n steps: {\n type: \"array\",\n description: \"Ordered steps to execute, one capability dispatch each.\",\n items: stepItemsSchema(capabilityNames),\n },\n },\n required: [\"summary\", \"steps\"],\n additionalProperties: false,\n };\n\n return {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-planner\",\n jsonSchema: {\n input: () => jsonSchema,\n },\n validate(value: unknown): StandardSchemaV1.Result<PlannerPlan> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"plan must be an object\" }] };\n }\n\n const record = value as { steps?: unknown; summary?: unknown };\n\n if (!Array.isArray(record.steps) || record.steps.length === 0) {\n return { issues: [{ message: \"plan `steps` must be a non-empty array\" }] };\n }\n\n const steps: PlannerStep[] = [];\n\n for (const raw of record.steps) {\n const normalized = normalizeStep(raw);\n\n if (!normalized) {\n return {\n issues: [{ message: \"each plan step must carry a string `capability` and `input`\" }],\n };\n }\n\n steps.push(normalized);\n }\n\n const summary = typeof record.summary === \"string\" ? record.summary : undefined;\n\n return { value: summary !== undefined ? { steps, summary } : { steps } };\n },\n } as StandardSchemaV1<PlannerPlan>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n };\n}\n\n/** Per-step JSON Schema object — one capability dispatch. */\nfunction stepItemsSchema(capabilityNames: string[]): Record<string, unknown> {\n return {\n type: \"object\",\n properties: {\n id: {\n type: [\"string\", \"null\"],\n description: \"Stable step id, referenced by dependsOn.\",\n },\n capability: {\n type: \"string\",\n enum: capabilityNames,\n description: \"Name of the capability to dispatch for this step.\",\n },\n input: {\n type: \"string\",\n description: \"Concrete input passed to the capability's execute().\",\n },\n reason: { type: [\"string\", \"null\"], description: \"Why this step exists.\" },\n dependsOn: {\n type: [\"array\", \"null\"],\n items: { type: \"string\" },\n description: \"Ids of steps this one conceptually follows.\",\n },\n },\n // Strict mode: every property required; the genuinely-optional ones\n // (id / reason / dependsOn) are nullable. `validate()` treats null and\n // missing identically, so a model emitting `null` round-trips fine.\n required: [\"id\", \"capability\", \"input\", \"reason\", \"dependsOn\"],\n additionalProperties: false,\n };\n}\n\n/**\n * Coerce one raw step object into a {@link PlannerStep}, returning\n * `undefined` when the mandatory `capability` / `input` strings are\n * missing. Optional fields are copied only when well-typed.\n */\nfunction normalizeStep(raw: unknown): PlannerStep | undefined {\n if (!raw || typeof raw !== \"object\") {\n return undefined;\n }\n\n const record = raw as {\n id?: unknown;\n capability?: unknown;\n input?: unknown;\n reason?: unknown;\n dependsOn?: unknown;\n };\n\n if (typeof record.capability !== \"string\" || record.capability.length === 0) {\n return undefined;\n }\n\n if (typeof record.input !== \"string\") {\n return undefined;\n }\n\n const step: PlannerStep = {\n capability: record.capability,\n input: record.input,\n };\n\n if (typeof record.id === \"string\") {\n step.id = record.id;\n }\n\n if (typeof record.reason === \"string\") {\n step.reason = record.reason;\n }\n\n if (Array.isArray(record.dependsOn) && record.dependsOn.every((entry) => typeof entry === \"string\")) {\n step.dependsOn = record.dependsOn as string[];\n }\n\n return step;\n}\n"],"mappings":";AAgCA,SAAgB,WAAW,iBAA2B,UAA+B;CAUnF,MAAM,aAAa;EACjB,MAAM;EACN,YAAY;GACV,SAAS;IACP,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;IACb,OAAO,gBAAgB,eAAe;GACxC;EACF;EACA,UAAU,CAAC,WAAW,OAAO;EAC7B,sBAAsB;CACxB;CAEA,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,YAAY,EACV,aAAa,WACf;EACA,SAAS,OAAsD;GAC7D,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yBAAyB,CAAC,EAAE;GAG3D,MAAM,SAAS;GAEf,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,KAAK,OAAO,MAAM,WAAW,GAC1D,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yCAAyC,CAAC,EAAE;GAG3E,MAAM,QAAuB,CAAC;GAE9B,KAAK,MAAM,OAAO,OAAO,OAAO;IAC9B,MAAM,aAAa,cAAc,GAAG;IAEpC,IAAI,CAAC,YACH,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,8DAA8D,CAAC,EACrF;IAGF,MAAM,KAAK,UAAU;GACvB;GAEA,MAAM,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;GAEtE,OAAO,EAAE,OAAO,YAAY,SAAY;IAAE;IAAO;GAAQ,IAAI,EAAE,MAAM,EAAE;EACzE;CACF,EAGF;AACF;;AAGA,SAAS,gBAAgB,iBAAoD;CAC3E,OAAO;EACL,MAAM;EACN,YAAY;GACV,IAAI;IACF,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,YAAY;IACV,MAAM;IACN,MAAM;IACN,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;GACf;GACA,QAAQ;IAAE,MAAM,CAAC,UAAU,MAAM;IAAG,aAAa;GAAwB;GACzE,WAAW;IACT,MAAM,CAAC,SAAS,MAAM;IACtB,OAAO,EAAE,MAAM,SAAS;IACxB,aAAa;GACf;EACF;EAIA,UAAU;GAAC;GAAM;GAAc;GAAS;GAAU;EAAW;EAC7D,sBAAsB;CACxB;AACF;;;;;;AAOA,SAAS,cAAc,KAAuC;CAC5D,IAAI,CAAC,OAAO,OAAO,QAAQ,UACzB;CAGF,MAAM,SAAS;CAQf,IAAI,OAAO,OAAO,eAAe,YAAY,OAAO,WAAW,WAAW,GACxE;CAGF,IAAI,OAAO,OAAO,UAAU,UAC1B;CAGF,MAAM,OAAoB;EACxB,YAAY,OAAO;EACnB,OAAO,OAAO;CAChB;CAEA,IAAI,OAAO,OAAO,OAAO,UACvB,KAAK,KAAK,OAAO;CAGnB,IAAI,OAAO,OAAO,WAAW,UAC3B,KAAK,SAAS,OAAO;CAGvB,IAAI,MAAM,QAAQ,OAAO,SAAS,KAAK,OAAO,UAAU,OAAO,UAAU,OAAO,UAAU,QAAQ,GAChG,KAAK,YAAY,OAAO;CAG1B,OAAO;AACT"}
1
+ {"version":3,"file":"plan-schema.mjs","names":[],"sources":["../../../../../../../ai/src/planner/plan-schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { PlannerPlan, PlannerStep } from \"../contracts/planner/planner-plan.type\";\n\n/**\n * Build the Standard Schema the planning agent emits — an ordered\n * `{ steps: [...], summary? }` plan whose every step references one of\n * `capabilityNames` via the `capability` field.\n *\n * Mirrors the router's hand-built schema approach\n * (`supervisor/router-factory.ts`): the JSON Schema extension carries\n * the capability names as an `enum` so capable providers enforce the\n * choice natively, while `validate()` still accepts the shape softly so\n * providers without native structured output can pass a parsed object\n * through. Validation is intentionally lenient on `capability` — an\n * unknown name is surfaced later by the planner as a typed\n * `PlannerPlanInvalidError`, with the full forensic context, rather\n * than as an opaque schema issue here.\n *\n * `maxSteps` cannot be expressed on the wire (strict mode rejects\n * `maxItems`), so `validate()` enforces a hard parse-time ceiling\n * derived from it — see {@link parsedStepCeiling}.\n */\nexport type PlanSchema = StandardSchemaV1<PlannerPlan> & {\n \"~standard\": {\n /**\n * JSON Schema extension read by the native structured-output path.\n * Part of the declared type so callers don't have to re-assert it.\n */\n jsonSchema: { input: () => Record<string, unknown> };\n };\n};\n\n/**\n * Slack allowed over `maxSteps` before a returned plan is rejected\n * outright. A model that overshoots the prompt's \"at most N steps\" by a\n * little is normal and the runtime truncates the tail to `skipped`;\n * one that returns several times the budget is malfunctioning (or the\n * provider/proxy is not the one we think it is), and parsing it is\n * unbounded work on attacker-adjacent input.\n */\nconst STEP_CEILING_FACTOR = 4;\n\n/**\n * Ceiling used when `planSchema` is built without a `maxSteps` — direct\n * callers outside `PlannerRun`, which has no runtime truncation of its\n * own to fall back on.\n */\nconst DEFAULT_STEP_CEILING = 100;\n\n/**\n * Hard upper bound on the number of steps `validate()` will parse.\n *\n * Strict-mode JSON Schema can't carry `maxItems`, so nothing on the wire\n * stops a provider from returning an arbitrarily long `steps[]`; before\n * 4.15.0 the whole array was parsed, normalized and stored, and only the\n * execution loop truncated it. This is the parse-time backstop that\n * makes the bound hold regardless of what the provider honors.\n */\nexport function parsedStepCeiling(maxSteps?: number): number {\n if (maxSteps === undefined) {\n return DEFAULT_STEP_CEILING;\n }\n\n return Math.max(1, Math.ceil(maxSteps)) * STEP_CEILING_FACTOR;\n}\n\nexport function planSchema(capabilityNames: string[], maxSteps?: number): PlanSchema {\n // OpenAI strict `json_schema` mode (and other native structured-output\n // providers) require EVERY property to appear in `required` — with truly\n // optional fields expressed as nullable — and reject array `minItems` /\n // `maxItems`. So the schema is strict-shaped: all keys required, the\n // optional ones nullable, no item-count bounds on the wire. Both bounds\n // live in `validate()` instead: non-empty below, and the over-long\n // ceiling that `maxItems` would have expressed.\n const stepCeiling = parsedStepCeiling(maxSteps);\n\n const jsonSchema = {\n type: \"object\",\n properties: {\n summary: {\n type: [\"string\", \"null\"],\n description: \"One-line summary of the overall strategy.\",\n },\n steps: {\n type: \"array\",\n description: \"Ordered steps to execute, one capability dispatch each.\",\n items: stepItemsSchema(capabilityNames),\n },\n },\n required: [\"summary\", \"steps\"],\n additionalProperties: false,\n };\n\n return {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-planner\",\n jsonSchema: {\n input: () => jsonSchema,\n },\n validate(value: unknown): StandardSchemaV1.Result<PlannerPlan> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"plan must be an object\" }] };\n }\n\n const record = value as { steps?: unknown; summary?: unknown };\n\n if (!Array.isArray(record.steps) || record.steps.length === 0) {\n return { issues: [{ message: \"plan `steps` must be a non-empty array\" }] };\n }\n\n // Reject an over-long plan HERE, before a single step is\n // normalized — the runtime's tail truncation runs after the whole\n // array has been parsed and stored, so it bounds execution but\n // not the parsing cost of a pathological response. Rejecting\n // rather than truncating is deliberate: a plan several times its\n // budget is a malfunction worth surfacing as\n // `PlannerPlanInvalidError`, not something to silently trim into\n // a plausible-looking prefix.\n if (record.steps.length > stepCeiling) {\n return {\n issues: [\n {\n message: `plan \\`steps\\` must not exceed ${stepCeiling} entries (received ${record.steps.length})`,\n },\n ],\n };\n }\n\n const steps: PlannerStep[] = [];\n\n for (const raw of record.steps) {\n const normalized = normalizeStep(raw);\n\n if (!normalized) {\n return {\n issues: [{ message: \"each plan step must carry a string `capability` and `input`\" }],\n };\n }\n\n steps.push(normalized);\n }\n\n const summary = typeof record.summary === \"string\" ? record.summary : undefined;\n\n return { value: summary !== undefined ? { steps, summary } : { steps } };\n },\n } as StandardSchemaV1<PlannerPlan>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n };\n}\n\n/** Per-step JSON Schema object — one capability dispatch. */\nfunction stepItemsSchema(capabilityNames: string[]): Record<string, unknown> {\n return {\n type: \"object\",\n properties: {\n id: {\n type: [\"string\", \"null\"],\n description: \"Stable step id, referenced by dependsOn.\",\n },\n capability: {\n type: \"string\",\n enum: capabilityNames,\n description: \"Name of the capability to dispatch for this step.\",\n },\n input: {\n type: \"string\",\n description: \"Concrete input passed to the capability's execute().\",\n },\n reason: { type: [\"string\", \"null\"], description: \"Why this step exists.\" },\n dependsOn: {\n type: [\"array\", \"null\"],\n items: { type: \"string\" },\n description: \"Ids of steps this one conceptually follows.\",\n },\n },\n // Strict mode: every property required; the genuinely-optional ones\n // (id / reason / dependsOn) are nullable. `validate()` treats null and\n // missing identically, so a model emitting `null` round-trips fine.\n required: [\"id\", \"capability\", \"input\", \"reason\", \"dependsOn\"],\n additionalProperties: false,\n };\n}\n\n/**\n * Coerce one raw step object into a {@link PlannerStep}, returning\n * `undefined` when the mandatory `capability` / `input` strings are\n * missing. Optional fields are copied only when well-typed.\n */\nfunction normalizeStep(raw: unknown): PlannerStep | undefined {\n if (!raw || typeof raw !== \"object\") {\n return undefined;\n }\n\n const record = raw as {\n id?: unknown;\n capability?: unknown;\n input?: unknown;\n reason?: unknown;\n dependsOn?: unknown;\n };\n\n if (typeof record.capability !== \"string\" || record.capability.length === 0) {\n return undefined;\n }\n\n if (typeof record.input !== \"string\") {\n return undefined;\n }\n\n const step: PlannerStep = {\n capability: record.capability,\n input: record.input,\n };\n\n if (typeof record.id === \"string\") {\n step.id = record.id;\n }\n\n if (typeof record.reason === \"string\") {\n step.reason = record.reason;\n }\n\n if (Array.isArray(record.dependsOn) && record.dependsOn.every((entry) => typeof entry === \"string\")) {\n step.dependsOn = record.dependsOn as string[];\n }\n\n return step;\n}\n"],"mappings":";;;;;;;;;AAwCA,MAAM,sBAAsB;;;;;;AAO5B,MAAM,uBAAuB;;;;;;;;;;AAW7B,SAAgB,kBAAkB,UAA2B;CAC3D,IAAI,aAAa,QACf,OAAO;CAGT,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,CAAC,IAAI;AAC5C;AAEA,SAAgB,WAAW,iBAA2B,UAA+B;CAQnF,MAAM,cAAc,kBAAkB,QAAQ;CAE9C,MAAM,aAAa;EACjB,MAAM;EACN,YAAY;GACV,SAAS;IACP,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;IACb,OAAO,gBAAgB,eAAe;GACxC;EACF;EACA,UAAU,CAAC,WAAW,OAAO;EAC7B,sBAAsB;CACxB;CAEA,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,YAAY,EACV,aAAa,WACf;EACA,SAAS,OAAsD;GAC7D,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yBAAyB,CAAC,EAAE;GAG3D,MAAM,SAAS;GAEf,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,KAAK,OAAO,MAAM,WAAW,GAC1D,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yCAAyC,CAAC,EAAE;GAW3E,IAAI,OAAO,MAAM,SAAS,aACxB,OAAO,EACL,QAAQ,CACN,EACE,SAAS,kCAAkC,YAAY,qBAAqB,OAAO,MAAM,OAAO,GAClG,CACF,EACF;GAGF,MAAM,QAAuB,CAAC;GAE9B,KAAK,MAAM,OAAO,OAAO,OAAO;IAC9B,MAAM,aAAa,cAAc,GAAG;IAEpC,IAAI,CAAC,YACH,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,8DAA8D,CAAC,EACrF;IAGF,MAAM,KAAK,UAAU;GACvB;GAEA,MAAM,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;GAEtE,OAAO,EAAE,OAAO,YAAY,SAAY;IAAE;IAAO;GAAQ,IAAI,EAAE,MAAM,EAAE;EACzE;CACF,EAGF;AACF;;AAGA,SAAS,gBAAgB,iBAAoD;CAC3E,OAAO;EACL,MAAM;EACN,YAAY;GACV,IAAI;IACF,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,YAAY;IACV,MAAM;IACN,MAAM;IACN,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;GACf;GACA,QAAQ;IAAE,MAAM,CAAC,UAAU,MAAM;IAAG,aAAa;GAAwB;GACzE,WAAW;IACT,MAAM,CAAC,SAAS,MAAM;IACtB,OAAO,EAAE,MAAM,SAAS;IACxB,aAAa;GACf;EACF;EAIA,UAAU;GAAC;GAAM;GAAc;GAAS;GAAU;EAAW;EAC7D,sBAAsB;CACxB;AACF;;;;;;AAOA,SAAS,cAAc,KAAuC;CAC5D,IAAI,CAAC,OAAO,OAAO,QAAQ,UACzB;CAGF,MAAM,SAAS;CAQf,IAAI,OAAO,OAAO,eAAe,YAAY,OAAO,WAAW,WAAW,GACxE;CAGF,IAAI,OAAO,OAAO,UAAU,UAC1B;CAGF,MAAM,OAAoB;EACxB,YAAY,OAAO;EACnB,OAAO,OAAO;CAChB;CAEA,IAAI,OAAO,OAAO,OAAO,UACvB,KAAK,KAAK,OAAO;CAGnB,IAAI,OAAO,OAAO,WAAW,UAC3B,KAAK,SAAS,OAAO;CAGvB,IAAI,MAAM,QAAQ,OAAO,SAAS,KAAK,OAAO,UAAU,OAAO,UAAU,OAAO,UAAU,QAAQ,GAChG,KAAK,YAAY,OAAO;CAG1B,OAAO;AACT"}
@@ -1,5 +1,6 @@
1
1
  import { isPrivateOrReservedIp } from "./private-ip.mjs";
2
2
  import { assertUrlAllowed, fetchTextWithPolicy, guardedFetch, readTextCapped, resolveOutboundPolicy } from "./outbound-policy.mjs";
3
+ import { UNSAFE_MERGE_KEYS, assignSafeKey, isUnsafeMergeKey, mergeSafely } from "./safe-merge.mjs";
3
4
  import { DEFAULT_SENSITIVE_KEYS, SENSITIVE_HEADERS, redact, redactError, redactHeaders, scrubSecrets } from "./redact.mjs";
4
5
 
5
6
  export { };
@@ -21,6 +21,15 @@ declare function assertUrlAllowed(rawUrl: string, policy: ResolvedOutboundPolicy
21
21
  * caller signal merged. Returns the raw `Response` — read its body via
22
22
  * {@link readTextCapped} to enforce `maxBytes`. Throws
23
23
  * {@link OutboundPolicyError} on a policy violation or timeout.
24
+ *
25
+ * Redirects are NEVER delegated to the platform: every hop is issued
26
+ * with `redirect: "manual"` and its `Location` is re-run through
27
+ * {@link assertUrlAllowed} before being followed (capped at
28
+ * `maxRedirects`), so a 3xx from an allowed host cannot smuggle the
29
+ * request to a private / metadata / off-allowlist target. Credential
30
+ * headers are stripped when a hop crosses an origin boundary. Pass
31
+ * `init.redirect: "manual"` to receive the raw 3xx, or `"error"` to
32
+ * reject on any redirect.
24
33
  */
25
34
  declare function guardedFetch(rawUrl: string, policyInput: OutboundPolicy, init?: RequestInit): Promise<Response>;
26
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-policy.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"mappings":";;;;;AAmBA;;;iBAAgB,qBAAA,CACd,MAAA,GAAQ,cAAA,GACP,sBAAsB;;;;;AAAA;AAiCzB;;iBAAsB,gBAAA,CACpB,MAAA,UACA,MAAA,EAAQ,sBAAA,GACP,OAAA,CAAQ,GAAA;;;;;;;;iBAiGW,YAAA,CACpB,MAAA,UACA,WAAA,EAAa,cAAA,EACb,IAAA,GAAO,WAAA,GACN,OAAA,CAAQ,QAAA;;;;AArGG;AAiGd;;iBAkCsB,cAAA,CACpB,QAAA,EAAU,QAAA,EACV,QAAA,WACC,OAAO;;;;;;iBAgDY,mBAAA,CACpB,MAAA,UACA,WAAA,EAAa,cAAA,EACb,IAAA,GAAO,WAAA,GACN,OAAA;EAAU,EAAA;EAAa,MAAA;EAAgB,UAAA;EAAoB,IAAA;AAAA"}
1
+ {"version":3,"file":"outbound-policy.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"mappings":";;;;;AA+BA;;;iBAAgB,qBAAA,CACd,MAAA,GAAQ,cAAA,GACP,sBAAsB;;;;;AAAA;AAkCzB;;iBAAsB,gBAAA,CACpB,MAAA,UACA,MAAA,EAAQ,sBAAA,GACP,OAAA,CAAQ,GAAA;;;;;;;;;;;;AAAG;AAqHd;;;;iBAAsB,YAAA,CACpB,MAAA,UACA,WAAA,EAAa,cAAA,EACb,IAAA,GAAO,WAAA,GACN,OAAA,CAAQ,QAAA;;;;;;;iBAwGW,cAAA,CACpB,QAAA,EAAU,QAAA,EACV,QAAA,WACC,OAAO;;;;;;iBAgDY,mBAAA,CACpB,MAAA,UACA,WAAA,EAAa,cAAA,EACb,IAAA,GAAO,WAAA,GACN,OAAA;EAAU,EAAA;EAAa,MAAA;EAAgB,UAAA;EAAoB,IAAA;AAAA"}