@warlock.js/ai 4.14.0 → 4.16.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 (104) hide show
  1. package/CHANGELOG.md +182 -141
  2. package/cjs/index.cjs +640 -139
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/index.d.mts +3 -3
  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/contracts/team/team-config.type.d.mts +9 -6
  22. package/esm/contracts/team/team-config.type.d.mts.map +1 -1
  23. package/esm/contracts/tool.contract.d.mts +35 -3
  24. package/esm/contracts/tool.contract.d.mts.map +1 -1
  25. package/esm/index.d.mts +8 -7
  26. package/esm/index.mjs +3 -2
  27. package/esm/memory/episodic-memory.mjs +14 -6
  28. package/esm/memory/episodic-memory.mjs.map +1 -1
  29. package/esm/memory/index.d.mts +1 -1
  30. package/esm/memory/memory.d.mts +13 -1
  31. package/esm/memory/memory.d.mts.map +1 -1
  32. package/esm/memory/memory.mjs +41 -7
  33. package/esm/memory/memory.mjs.map +1 -1
  34. package/esm/memory/procedural-memory.mjs +20 -7
  35. package/esm/memory/procedural-memory.mjs.map +1 -1
  36. package/esm/memory/semantic-memory.mjs +27 -10
  37. package/esm/memory/semantic-memory.mjs.map +1 -1
  38. package/esm/memory/working-memory.mjs +70 -13
  39. package/esm/memory/working-memory.mjs.map +1 -1
  40. package/esm/middleware/builtins/semantic-cache.d.mts +46 -1
  41. package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -1
  42. package/esm/middleware/builtins/semantic-cache.mjs +60 -15
  43. package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
  44. package/esm/middleware/index.d.mts +1 -1
  45. package/esm/mock/index.d.mts +1 -1
  46. package/esm/mock/mock-config.type.d.mts +33 -4
  47. package/esm/mock/mock-config.type.d.mts.map +1 -1
  48. package/esm/mock/mock-model.d.mts +2 -1
  49. package/esm/mock/mock-model.d.mts.map +1 -1
  50. package/esm/mock/mock-model.mjs +5 -4
  51. package/esm/mock/mock-model.mjs.map +1 -1
  52. package/esm/mock/mock-sdk.d.mts +11 -3
  53. package/esm/mock/mock-sdk.d.mts.map +1 -1
  54. package/esm/mock/mock-sdk.mjs.map +1 -1
  55. package/esm/orchestrator/as-tool.d.mts +35 -9
  56. package/esm/orchestrator/as-tool.d.mts.map +1 -1
  57. package/esm/orchestrator/as-tool.mjs +67 -19
  58. package/esm/orchestrator/as-tool.mjs.map +1 -1
  59. package/esm/orchestrator/execution.d.mts.map +1 -1
  60. package/esm/orchestrator/execution.mjs +2 -2
  61. package/esm/orchestrator/execution.mjs.map +1 -1
  62. package/esm/orchestrator/index.d.mts +1 -1
  63. package/esm/orchestrator/index.mjs +1 -1
  64. package/esm/orchestrator/memory.d.mts +41 -5
  65. package/esm/orchestrator/memory.d.mts.map +1 -1
  66. package/esm/orchestrator/memory.mjs +53 -5
  67. package/esm/orchestrator/memory.mjs.map +1 -1
  68. package/esm/planner/plan-schema.d.mts +15 -4
  69. package/esm/planner/plan-schema.d.mts.map +1 -1
  70. package/esm/planner/plan-schema.mjs +27 -16
  71. package/esm/planner/plan-schema.mjs.map +1 -1
  72. package/esm/security/index.mjs +1 -0
  73. package/esm/security/outbound-policy.d.mts +9 -0
  74. package/esm/security/outbound-policy.d.mts.map +1 -1
  75. package/esm/security/outbound-policy.mjs +79 -5
  76. package/esm/security/outbound-policy.mjs.map +1 -1
  77. package/esm/security/outbound-policy.type.d.mts +8 -0
  78. package/esm/security/outbound-policy.type.d.mts.map +1 -1
  79. package/esm/security/safe-merge.d.mts +52 -0
  80. package/esm/security/safe-merge.d.mts.map +1 -0
  81. package/esm/security/safe-merge.mjs +68 -0
  82. package/esm/security/safe-merge.mjs.map +1 -0
  83. package/esm/supervisor/decide.mjs +52 -5
  84. package/esm/supervisor/decide.mjs.map +1 -1
  85. package/esm/supervisor/execution.d.mts +22 -12
  86. package/esm/supervisor/execution.d.mts.map +1 -1
  87. package/esm/supervisor/execution.mjs +47 -24
  88. package/esm/supervisor/execution.mjs.map +1 -1
  89. package/esm/supervisor/supervisor.mjs +4 -0
  90. package/esm/supervisor/supervisor.mjs.map +1 -1
  91. package/llms-full.txt +185 -14
  92. package/llms.txt +5 -4
  93. package/package.json +24 -4
  94. package/skills/README.md +6 -2
  95. package/skills/attach-ai-middleware/SKILL.md +17 -1
  96. package/skills/generate-images/SKILL.md +11 -4
  97. package/skills/rag-loaders-and-stores/SKILL.md +3 -0
  98. package/skills/run-ai-agent/SKILL.md +3 -0
  99. package/skills/run-orchestrator/SKILL.md +6 -1
  100. package/skills/run-planner/SKILL.md +7 -3
  101. package/skills/run-supervisor/SKILL.md +11 -1
  102. package/skills/secure-outbound-requests/SKILL.md +85 -0
  103. package/skills/use-ai-memory/SKILL.md +36 -3
  104. package/skills/use-runtime-skills/SKILL.md +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-policy.mjs","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { OutboundPolicyError } from \"../errors\";\nimport { isPrivateOrReservedIp } from \"./private-ip\";\nimport type {\n OutboundPolicy,\n ResolvedOutboundPolicy,\n} from \"./outbound-policy.type\";\n\n/** 5 MiB — default cap on an outbound response body. */\nconst DEFAULT_MAX_BYTES = 5 * 1024 * 1024;\n/** 10s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Fill an {@link OutboundPolicy} with strict defaults: https-only,\n * private-IP deny on, 10s timeout, 5 MiB cap, global `fetch`. Idempotent\n * — resolving an already-resolved policy yields the same shape.\n */\nexport function resolveOutboundPolicy(\n policy: OutboundPolicy = {},\n): ResolvedOutboundPolicy {\n return {\n allowedSchemes: policy.allowedSchemes ?? [\"https\"],\n hostAllowlist: policy.hostAllowlist,\n denyPrivateIPsAfterDNS: policy.denyPrivateIPsAfterDNS ?? true,\n maxBytes: policy.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n signal: policy.signal,\n fetch: policy.fetch ?? globalThis.fetch,\n };\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/** Whether `host` equals or is a subdomain of any allowlist entry. */\nfunction hostAllowed(host: string, allowlist: string[]): boolean {\n const lower = host.toLowerCase();\n return allowlist.some(entry => {\n const e = entry.toLowerCase();\n return lower === e || lower.endsWith(`.${e}`);\n });\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist, host allowlist, and (when enabled) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws\n * {@link OutboundPolicyError} otherwise.\n */\nexport async function assertUrlAllowed(\n rawUrl: string,\n policy: ResolvedOutboundPolicy,\n): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new OutboundPolicyError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some(s => s.toLowerCase() === scheme)) {\n throw new OutboundPolicyError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n const host = stripBrackets(url.hostname);\n\n if (policy.hostAllowlist && !hostAllowed(host, policy.hostAllowlist)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" is not in the allowlist`,\n { context: { url: rawUrl, host } },\n );\n }\n\n if (policy.denyPrivateIPsAfterDNS) {\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and\n * every returned address is checked (a public name pointing inward is\n * caught). A resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new OutboundPolicyError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new OutboundPolicyError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/** Merge the internal timeout signal with an optional caller signal. */\nfunction mergeSignals(\n timeout: AbortSignal,\n external?: AbortSignal,\n): AbortSignal {\n if (!external) return timeout;\n\n const controller = new AbortController();\n const abort = (from: AbortSignal) => controller.abort(from.reason);\n\n if (timeout.aborted) abort(timeout);\n else timeout.addEventListener(\"abort\", () => abort(timeout), { once: true });\n\n if (external.aborted) abort(external);\n else external.addEventListener(\"abort\", () => abort(external), { once: true });\n\n return controller.signal;\n}\n\n/**\n * Policy-guarded `fetch`: validates the URL ({@link assertUrlAllowed}),\n * then performs the request with the policy's timeout and (optional)\n * caller signal merged. Returns the raw `Response` — read its body via\n * {@link readTextCapped} to enforce `maxBytes`. Throws\n * {@link OutboundPolicyError} on a policy violation or timeout.\n */\nexport async function guardedFetch(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<Response> {\n const policy = resolveOutboundPolicy(policyInput);\n const url = await assertUrlAllowed(rawUrl, policy);\n\n const timeoutController = new AbortController();\n const timer = setTimeout(() => {\n timeoutController.abort(\n new OutboundPolicyError(\n `outbound request timed out after ${policy.timeoutMs}ms`,\n { context: { url: rawUrl, timeoutMs: policy.timeoutMs } },\n ),\n );\n }, policy.timeoutMs);\n\n try {\n return await policy.fetch(url, {\n ...init,\n signal: mergeSignals(timeoutController.signal, policy.signal),\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body as UTF-8 text with a hard byte cap. A declared\n * `content-length` over the cap fails fast; otherwise the stream is read\n * chunk-by-chunk and aborted the moment the running total exceeds\n * `maxBytes`. Throws {@link OutboundPolicyError} on overflow.\n */\nexport async function readTextCapped(\n response: Response,\n maxBytes: number,\n): Promise<string> {\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${maxBytes}-byte cap`,\n { context: { declared, maxBytes } },\n );\n }\n\n if (!response.body) {\n const text = await response.text();\n if (Buffer.byteLength(text) > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n return text;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\n/**\n * Convenience: {@link guardedFetch} + {@link readTextCapped}. Returns the\n * response status alongside the (capped) body text so callers can shape\n * their own not-OK error. The body is only read when the response is OK.\n */\nexport async function fetchTextWithPolicy(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<{ ok: boolean; status: number; statusText: string; text: string }> {\n const policy = resolveOutboundPolicy(policyInput);\n const response = await guardedFetch(rawUrl, policy, init);\n\n return {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n text: response.ok ? await readTextCapped(response, policy.maxBytes) : \"\",\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"outbound-policy.mjs","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { OutboundPolicyError } from \"../errors\";\nimport { isPrivateOrReservedIp } from \"./private-ip\";\nimport type {\n OutboundPolicy,\n ResolvedOutboundPolicy,\n} from \"./outbound-policy.type\";\n\n/** 5 MiB — default cap on an outbound response body. */\nconst DEFAULT_MAX_BYTES = 5 * 1024 * 1024;\n/** 10s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n/** Default cap on the number of policy-validated redirect hops. */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Credential headers that must not survive a cross-origin redirect. */\nconst CROSS_ORIGIN_STRIP_HEADERS = [\n \"authorization\",\n \"cookie\",\n \"proxy-authorization\",\n];\n\n/**\n * Fill an {@link OutboundPolicy} with strict defaults: https-only,\n * private-IP deny on, 10s timeout, 5 MiB cap, global `fetch`. Idempotent\n * — resolving an already-resolved policy yields the same shape.\n */\nexport function resolveOutboundPolicy(\n policy: OutboundPolicy = {},\n): ResolvedOutboundPolicy {\n return {\n allowedSchemes: policy.allowedSchemes ?? [\"https\"],\n hostAllowlist: policy.hostAllowlist,\n denyPrivateIPsAfterDNS: policy.denyPrivateIPsAfterDNS ?? true,\n maxBytes: policy.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n maxRedirects: policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS,\n signal: policy.signal,\n fetch: policy.fetch ?? globalThis.fetch,\n };\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/** Whether `host` equals or is a subdomain of any allowlist entry. */\nfunction hostAllowed(host: string, allowlist: string[]): boolean {\n const lower = host.toLowerCase();\n return allowlist.some(entry => {\n const e = entry.toLowerCase();\n return lower === e || lower.endsWith(`.${e}`);\n });\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist, host allowlist, and (when enabled) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws\n * {@link OutboundPolicyError} otherwise.\n */\nexport async function assertUrlAllowed(\n rawUrl: string,\n policy: ResolvedOutboundPolicy,\n): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new OutboundPolicyError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some(s => s.toLowerCase() === scheme)) {\n throw new OutboundPolicyError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n const host = stripBrackets(url.hostname);\n\n if (policy.hostAllowlist && !hostAllowed(host, policy.hostAllowlist)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" is not in the allowlist`,\n { context: { url: rawUrl, host } },\n );\n }\n\n if (policy.denyPrivateIPsAfterDNS) {\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and\n * every returned address is checked (a public name pointing inward is\n * caught). A resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new OutboundPolicyError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new OutboundPolicyError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/** Merge the internal timeout signal with an optional caller signal. */\nfunction mergeSignals(\n timeout: AbortSignal,\n external?: AbortSignal,\n): AbortSignal {\n if (!external) return timeout;\n\n const controller = new AbortController();\n const abort = (from: AbortSignal) => controller.abort(from.reason);\n\n if (timeout.aborted) abort(timeout);\n else timeout.addEventListener(\"abort\", () => abort(timeout), { once: true });\n\n if (external.aborted) abort(external);\n else external.addEventListener(\"abort\", () => abort(external), { once: true });\n\n return controller.signal;\n}\n\n/** Flatten a headers init into a mutable lower-cased-key record. */\nfunction headersToRecord(\n headersInit?: RequestInit[\"headers\"],\n): Record<string, string> {\n const record: Record<string, string> = {};\n new Headers(headersInit).forEach((value, key) => {\n record[key] = value;\n });\n return record;\n}\n\n/**\n * Policy-guarded `fetch`: validates the URL ({@link assertUrlAllowed}),\n * then performs the request with the policy's timeout and (optional)\n * caller signal merged. Returns the raw `Response` — read its body via\n * {@link readTextCapped} to enforce `maxBytes`. Throws\n * {@link OutboundPolicyError} on a policy violation or timeout.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued\n * with `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} before being followed (capped at\n * `maxRedirects`), so a 3xx from an allowed host cannot smuggle the\n * request to a private / metadata / off-allowlist target. Credential\n * headers are stripped when a hop crosses an origin boundary. Pass\n * `init.redirect: \"manual\"` to receive the raw 3xx, or `\"error\"` to\n * reject on any redirect.\n */\nexport async function guardedFetch(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<Response> {\n const policy = resolveOutboundPolicy(policyInput);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const timeoutController = new AbortController();\n const timer = setTimeout(() => {\n timeoutController.abort(\n new OutboundPolicyError(\n `outbound request timed out after ${policy.timeoutMs}ms`,\n { context: { url: rawUrl, timeoutMs: policy.timeoutMs } },\n ),\n );\n }, policy.timeoutMs);\n\n const signal = mergeSignals(timeoutController.signal, policy.signal);\n const redirectMode = init?.redirect ?? \"follow\";\n const headers = headersToRecord(init?.headers);\n let method = init?.method ?? \"GET\";\n let body = init?.body ?? undefined;\n\n try {\n for (let hop = 0; ; hop++) {\n const response = await policy.fetch(url, {\n ...init,\n method,\n headers: { ...headers },\n body,\n redirect: \"manual\",\n signal,\n });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n\n if (redirectMode === \"manual\") {\n return response;\n }\n\n if (redirectMode === \"error\") {\n throw new OutboundPolicyError(\n `outbound request blocked — redirect received with redirect: \"error\" (${response.status} → ${location})`,\n { context: { url: url.toString(), location, status: response.status } },\n );\n }\n\n if (hop >= policy.maxRedirects) {\n throw new OutboundPolicyError(\n `outbound request blocked — more than ${policy.maxRedirects} redirects`,\n { context: { url: rawUrl, maxRedirects: policy.maxRedirects } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new OutboundPolicyError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n { context: { url: url.toString(), location } },\n );\n }\n\n // The redirect target gets the SAME scheme / allowlist / private-IP\n // validation as the original URL.\n const next = await assertUrlAllowed(target.toString(), policy);\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n if (next.origin !== url.origin) {\n for (const name of CROSS_ORIGIN_STRIP_HEADERS) {\n delete headers[name];\n }\n }\n\n // 303 — and the legacy 301/302-on-a-non-GET convention — re-issue\n // as a bodyless GET, matching platform follow semantics.\n if (\n response.status === 303 ||\n ((response.status === 301 || response.status === 302) &&\n method !== \"GET\" &&\n method !== \"HEAD\")\n ) {\n method = \"GET\";\n body = undefined;\n }\n\n url = next;\n }\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body as UTF-8 text with a hard byte cap. A declared\n * `content-length` over the cap fails fast; otherwise the stream is read\n * chunk-by-chunk and aborted the moment the running total exceeds\n * `maxBytes`. Throws {@link OutboundPolicyError} on overflow.\n */\nexport async function readTextCapped(\n response: Response,\n maxBytes: number,\n): Promise<string> {\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${maxBytes}-byte cap`,\n { context: { declared, maxBytes } },\n );\n }\n\n if (!response.body) {\n const text = await response.text();\n if (Buffer.byteLength(text) > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n return text;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\n/**\n * Convenience: {@link guardedFetch} + {@link readTextCapped}. Returns the\n * response status alongside the (capped) body text so callers can shape\n * their own not-OK error. The body is only read when the response is OK.\n */\nexport async function fetchTextWithPolicy(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<{ ok: boolean; status: number; statusText: string; text: string }> {\n const policy = resolveOutboundPolicy(policyInput);\n const response = await guardedFetch(rawUrl, policy, init);\n\n return {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n text: response.ok ? await readTextCapped(response, policy.maxBytes) : \"\",\n };\n}\n"],"mappings":"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"}
@@ -47,6 +47,13 @@ type OutboundPolicy = {
47
47
  */
48
48
  maxBytes?: number; /** Per-request timeout in milliseconds. Default `10_000`. */
49
49
  timeoutMs?: number;
50
+ /**
51
+ * Maximum number of redirect hops `guardedFetch` will follow. Every
52
+ * hop's `Location` is re-validated against this policy (scheme, host
53
+ * allowlist, private-IP guard) before it is followed — a redirect can
54
+ * never reach a URL the original request could not have. Default `5`.
55
+ */
56
+ maxRedirects?: number;
50
57
  /**
51
58
  * Caller `AbortSignal`, merged with the internal timeout — whichever
52
59
  * fires first aborts the request.
@@ -66,6 +73,7 @@ type ResolvedOutboundPolicy = {
66
73
  denyPrivateIPsAfterDNS: boolean;
67
74
  maxBytes: number;
68
75
  timeoutMs: number;
76
+ maxRedirects: number;
69
77
  signal?: AbortSignal;
70
78
  fetch: typeof fetch;
71
79
  };
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-policy.type.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.type.ts"],"mappings":";;AAoBA;;;;;;;;;;;;;;AAuCsB;AAItB;;;;KA3CY,cAAA;EA6CV;;;;;EAvCA,cAAA;EA4CA;;;AAAmB;;;EArCnB,aAAA;;;;;;;EAOA,sBAAA;;;;;;EAMA,QAAA;EAEA,SAAA;;;;;EAKA,MAAA,GAAS,WAAA;;;;;;EAMT,KAAA,UAAe,KAAK;AAAA;;KAIV,sBAAA;EACV,cAAA;EACA,aAAA;EACA,sBAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA,GAAS,WAAA;EACT,KAAA,SAAc,KAAK;AAAA"}
1
+ {"version":3,"file":"outbound-policy.type.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.type.ts"],"mappings":";;AAoBA;;;;;;;;;;;;;;;AA8CsB;AAItB;;;KAlDY,cAAA;EAmDV;;;;;EA7CA,cAAA;EAmDA;;;;;AACmB;EA7CnB,aAAA;;;;;;;EAOA,sBAAA;;;;;;EAMA,QAAA;EAEA,SAAA;;;;;;;EAOA,YAAA;;;;;EAKA,MAAA,GAAS,WAAA;;;;;;EAMT,KAAA,UAAe,KAAK;AAAA;;KAIV,sBAAA;EACV,cAAA;EACA,aAAA;EACA,sBAAA;EACA,QAAA;EACA,SAAA;EACA,YAAA;EACA,MAAA,GAAS,WAAA;EACT,KAAA,SAAc,KAAK;AAAA"}
@@ -0,0 +1,52 @@
1
+ //#region ../ai/src/security/safe-merge.d.ts
2
+ /**
3
+ * Prototype-key guard for merges of model-influenced data into plain
4
+ * objects (supervisor `state`, artifact bags, refine slices, …).
5
+ *
6
+ * Any `target[key] = value` where `key` came from an LLM, a tool
7
+ * result, or a permissively-schema'd agent output is a prototype-
8
+ * tampering primitive: `state["__proto__"] = {...}` repoints that
9
+ * object's prototype, and `state["constructor"]` shadows its
10
+ * constructor. On a plain object literal the blast radius is contained
11
+ * (the write lands on the one object, not on `Object.prototype`), but
12
+ * it becomes real prototype pollution the moment anything downstream
13
+ * uses `in`, `hasOwnProperty`, or a recursive deep-merge on the
14
+ * tainted object — which is exactly the kind of change that gets added
15
+ * later without re-auditing the merge sites.
16
+ *
17
+ * So: one shared guard, applied at every merge boundary, dropping the
18
+ * dangerous keys instead of assigning them. Dropping (not throwing) is
19
+ * deliberate — these keys are never legitimate state fields, and a
20
+ * merge boundary in the middle of a settled iteration is the wrong
21
+ * place to fail a run. Callers get the dropped keys back so they can
22
+ * log the anomaly.
23
+ */
24
+ /**
25
+ * Keys that must never be written through a dynamic-key assignment.
26
+ * `__proto__` repoints the prototype; `constructor` / `prototype`
27
+ * are the standard escalation path from there.
28
+ */
29
+ declare const UNSAFE_MERGE_KEYS: ReadonlyArray<string>;
30
+ /**
31
+ * True when `key` must not be assigned onto an object built from
32
+ * untrusted (model/tool-influenced) data.
33
+ */
34
+ declare function isUnsafeMergeKey(key: string): boolean;
35
+ /**
36
+ * Assign one key onto `target`, skipping prototype-tampering keys.
37
+ * Returns `true` when the value was written, `false` when the key was
38
+ * refused.
39
+ */
40
+ declare function assignSafeKey(target: Record<string, unknown>, key: string, value: unknown): boolean;
41
+ /**
42
+ * Shallow-merge every own enumerable key of `source` into `target`,
43
+ * skipping prototype-tampering keys. Mutates `target` in place (call
44
+ * sites rely on external references to the merged object staying
45
+ * coherent) and returns the list of refused keys — empty in the
46
+ * overwhelmingly common case, non-empty only when something upstream
47
+ * tried to smuggle `__proto__`/`constructor`/`prototype` through.
48
+ */
49
+ declare function mergeSafely(target: Record<string, unknown>, source: Record<string, unknown>): string[];
50
+ //#endregion
51
+ export { UNSAFE_MERGE_KEYS, assignSafeKey, isUnsafeMergeKey, mergeSafely };
52
+ //# sourceMappingURL=safe-merge.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-merge.d.mts","names":[],"sources":["../../../../../../../ai/src/security/safe-merge.ts"],"mappings":";;AA4BA;;;;AAA6C;AAQ7C;;;;AAA4C;AAS5C;;;;;;;;;AAGgB;AAmBhB;;;;;;cAvCa,iBAAA,EAAmB,aAAa;;;AAyCZ;;iBAjCjB,gBAAA,CAAiB,GAAW;;;;;;iBAS5B,aAAA,CACd,MAAA,EAAQ,MAAM,mBACd,GAAA,UACA,KAAA;;;;;;;;;iBAmBc,WAAA,CACd,MAAA,EAAQ,MAAA,mBACR,MAAA,EAAQ,MAAM"}
@@ -0,0 +1,68 @@
1
+ //#region ../ai/src/security/safe-merge.ts
2
+ /**
3
+ * Prototype-key guard for merges of model-influenced data into plain
4
+ * objects (supervisor `state`, artifact bags, refine slices, …).
5
+ *
6
+ * Any `target[key] = value` where `key` came from an LLM, a tool
7
+ * result, or a permissively-schema'd agent output is a prototype-
8
+ * tampering primitive: `state["__proto__"] = {...}` repoints that
9
+ * object's prototype, and `state["constructor"]` shadows its
10
+ * constructor. On a plain object literal the blast radius is contained
11
+ * (the write lands on the one object, not on `Object.prototype`), but
12
+ * it becomes real prototype pollution the moment anything downstream
13
+ * uses `in`, `hasOwnProperty`, or a recursive deep-merge on the
14
+ * tainted object — which is exactly the kind of change that gets added
15
+ * later without re-auditing the merge sites.
16
+ *
17
+ * So: one shared guard, applied at every merge boundary, dropping the
18
+ * dangerous keys instead of assigning them. Dropping (not throwing) is
19
+ * deliberate — these keys are never legitimate state fields, and a
20
+ * merge boundary in the middle of a settled iteration is the wrong
21
+ * place to fail a run. Callers get the dropped keys back so they can
22
+ * log the anomaly.
23
+ */
24
+ /**
25
+ * Keys that must never be written through a dynamic-key assignment.
26
+ * `__proto__` repoints the prototype; `constructor` / `prototype`
27
+ * are the standard escalation path from there.
28
+ */
29
+ const UNSAFE_MERGE_KEYS = [
30
+ "__proto__",
31
+ "constructor",
32
+ "prototype"
33
+ ];
34
+ const UNSAFE_MERGE_KEY_SET = new Set(UNSAFE_MERGE_KEYS);
35
+ /**
36
+ * True when `key` must not be assigned onto an object built from
37
+ * untrusted (model/tool-influenced) data.
38
+ */
39
+ function isUnsafeMergeKey(key) {
40
+ return UNSAFE_MERGE_KEY_SET.has(key);
41
+ }
42
+ /**
43
+ * Assign one key onto `target`, skipping prototype-tampering keys.
44
+ * Returns `true` when the value was written, `false` when the key was
45
+ * refused.
46
+ */
47
+ function assignSafeKey(target, key, value) {
48
+ if (isUnsafeMergeKey(key)) return false;
49
+ target[key] = value;
50
+ return true;
51
+ }
52
+ /**
53
+ * Shallow-merge every own enumerable key of `source` into `target`,
54
+ * skipping prototype-tampering keys. Mutates `target` in place (call
55
+ * sites rely on external references to the merged object staying
56
+ * coherent) and returns the list of refused keys — empty in the
57
+ * overwhelmingly common case, non-empty only when something upstream
58
+ * tried to smuggle `__proto__`/`constructor`/`prototype` through.
59
+ */
60
+ function mergeSafely(target, source) {
61
+ const skipped = [];
62
+ for (const [key, value] of Object.entries(source)) if (!assignSafeKey(target, key, value)) skipped.push(key);
63
+ return skipped;
64
+ }
65
+
66
+ //#endregion
67
+ export { UNSAFE_MERGE_KEYS, assignSafeKey, isUnsafeMergeKey, mergeSafely };
68
+ //# sourceMappingURL=safe-merge.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-merge.mjs","names":[],"sources":["../../../../../../../ai/src/security/safe-merge.ts"],"sourcesContent":["/**\n * Prototype-key guard for merges of model-influenced data into plain\n * objects (supervisor `state`, artifact bags, refine slices, …).\n *\n * Any `target[key] = value` where `key` came from an LLM, a tool\n * result, or a permissively-schema'd agent output is a prototype-\n * tampering primitive: `state[\"__proto__\"] = {...}` repoints that\n * object's prototype, and `state[\"constructor\"]` shadows its\n * constructor. On a plain object literal the blast radius is contained\n * (the write lands on the one object, not on `Object.prototype`), but\n * it becomes real prototype pollution the moment anything downstream\n * uses `in`, `hasOwnProperty`, or a recursive deep-merge on the\n * tainted object — which is exactly the kind of change that gets added\n * later without re-auditing the merge sites.\n *\n * So: one shared guard, applied at every merge boundary, dropping the\n * dangerous keys instead of assigning them. Dropping (not throwing) is\n * deliberate — these keys are never legitimate state fields, and a\n * merge boundary in the middle of a settled iteration is the wrong\n * place to fail a run. Callers get the dropped keys back so they can\n * log the anomaly.\n */\n\n/**\n * Keys that must never be written through a dynamic-key assignment.\n * `__proto__` repoints the prototype; `constructor` / `prototype`\n * are the standard escalation path from there.\n */\nexport const UNSAFE_MERGE_KEYS: ReadonlyArray<string> = [\"__proto__\", \"constructor\", \"prototype\"];\n\nconst UNSAFE_MERGE_KEY_SET = new Set(UNSAFE_MERGE_KEYS);\n\n/**\n * True when `key` must not be assigned onto an object built from\n * untrusted (model/tool-influenced) data.\n */\nexport function isUnsafeMergeKey(key: string): boolean {\n return UNSAFE_MERGE_KEY_SET.has(key);\n}\n\n/**\n * Assign one key onto `target`, skipping prototype-tampering keys.\n * Returns `true` when the value was written, `false` when the key was\n * refused.\n */\nexport function assignSafeKey(\n target: Record<string, unknown>,\n key: string,\n value: unknown,\n): boolean {\n if (isUnsafeMergeKey(key)) {\n return false;\n }\n\n target[key] = value;\n\n return true;\n}\n\n/**\n * Shallow-merge every own enumerable key of `source` into `target`,\n * skipping prototype-tampering keys. Mutates `target` in place (call\n * sites rely on external references to the merged object staying\n * coherent) and returns the list of refused keys — empty in the\n * overwhelmingly common case, non-empty only when something upstream\n * tried to smuggle `__proto__`/`constructor`/`prototype` through.\n */\nexport function mergeSafely(\n target: Record<string, unknown>,\n source: Record<string, unknown>,\n): string[] {\n const skipped: string[] = [];\n\n for (const [key, value] of Object.entries(source)) {\n if (!assignSafeKey(target, key, value)) {\n skipped.push(key);\n }\n }\n\n return skipped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,oBAA2C;CAAC;CAAa;CAAe;AAAW;AAEhG,MAAM,uBAAuB,IAAI,IAAI,iBAAiB;;;;;AAMtD,SAAgB,iBAAiB,KAAsB;CACrD,OAAO,qBAAqB,IAAI,GAAG;AACrC;;;;;;AAOA,SAAgB,cACd,QACA,KACA,OACS;CACT,IAAI,iBAAiB,GAAG,GACtB,OAAO;CAGT,OAAO,OAAO;CAEd,OAAO;AACT;;;;;;;;;AAUA,SAAgB,YACd,QACA,QACU;CACV,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,cAAc,QAAQ,KAAK,KAAK,GACnC,QAAQ,KAAK,GAAG;CAIpB,OAAO;AACT"}
@@ -50,7 +50,7 @@ async function decideViaCallback(params) {
50
50
  throw wrapRouteError(params.config.name, thrown);
51
51
  }
52
52
  const durationMs = performance.now() - started;
53
- return normalize(raw, params.entries, "route", durationMs);
53
+ return normalize(raw, params.entries, "route", durationMs, resolveMaxFanOut(params.config));
54
54
  }
55
55
  async function decideViaRouter(params) {
56
56
  const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(params.config.router);
@@ -99,7 +99,7 @@ async function decideViaRouter(params) {
99
99
  availableKeys: [...params.entries.keys()]
100
100
  });
101
101
  return {
102
- ...normalize(rawNext, params.entries, "router", durationMs),
102
+ ...normalize(rawNext, params.entries, "router", durationMs, resolveMaxFanOut(params.config)),
103
103
  reasoning: typeof reasoning === "string" ? reasoning : void 0,
104
104
  usage: routerResult.usage,
105
105
  routerReport: routerResult.report
@@ -139,7 +139,7 @@ function resolveSupervisorPromptText(config) {
139
139
  * `next` field) into a canonical `DispatchDecision`, validating every
140
140
  * named intent against the supervisor's `intents` map.
141
141
  */
142
- function normalize(raw, entries, source, durationMs) {
142
+ function normalize(raw, entries, source, durationMs, maxFanOut) {
143
143
  if (isEnd(raw)) return {
144
144
  kind: "end",
145
145
  source,
@@ -170,7 +170,7 @@ function normalize(raw, entries, source, durationMs) {
170
170
  }
171
171
  return {
172
172
  kind: "dispatch",
173
- intents: raw,
173
+ intents: capFanOut(raw, entries, maxFanOut),
174
174
  source,
175
175
  raw,
176
176
  durationMs
@@ -181,6 +181,53 @@ function normalize(raw, entries, source, durationMs) {
181
181
  availableKeys: [...entries.keys()]
182
182
  });
183
183
  }
184
+ /**
185
+ * Default fan-out WIDTH ceiling — how many intents one dispatch
186
+ * decision may run in parallel. `maxIterations` bounds depth; this
187
+ * bounds width, so total work per run is bounded by the product
188
+ * instead of by iterations alone.
189
+ */
190
+ const DEFAULT_MAX_FAN_OUT = 10;
191
+ /**
192
+ * Resolve the configured width ceiling. Factory validation
193
+ * (`supervisor.ts`) rejects non-integer / `< 1` values at authoring
194
+ * time, so this only has to apply the default.
195
+ */
196
+ function resolveMaxFanOut(config) {
197
+ return config.maxFanOut ?? 10;
198
+ }
199
+ /**
200
+ * Dedupe + width-cap a fan-out intent list before it reaches
201
+ * `Promise.all(...dispatchOne)`.
202
+ *
203
+ * Duplicates are collapsed silently: running the same intent twice in
204
+ * one decision is pure wasted spend (branch results are indexed by
205
+ * intent downstream, so the extras can't change the outcome), and a
206
+ * router that repeats itself is sloppy rather than hostile.
207
+ *
208
+ * Exceeding the cap *after* dedupe THROWS rather than truncating.
209
+ * Truncation would silently hand an attacker-chosen subset of the
210
+ * decision to the executor and hide the anomaly from the operator;
211
+ * every other routing violation in this file (unknown key, empty
212
+ * array, non-string element) already fails loudly as
213
+ * `SupervisorRoutingError`, so a width violation surfaces in the same
214
+ * place, with the same code, carrying the offending array.
215
+ *
216
+ * Threat model: the router's prompt embeds supervisor `state` and
217
+ * prior branch outputs, both of which can carry attacker-controlled
218
+ * text from tool results. Without a width bound, one injected
219
+ * "always return this 200-element `next` array" turns a single
220
+ * iteration into 200 real agent/workflow executions — no unknown
221
+ * intent name required, so the existing allowlist check never fires.
222
+ */
223
+ function capFanOut(intents, entries, maxFanOut) {
224
+ const unique = [...new Set(intents)];
225
+ if (unique.length > maxFanOut) throw new SupervisorRoutingError(`routing decision fanned out to ${unique.length} intents — exceeds maxFanOut=${maxFanOut}. Raise \`maxFanOut\` if this width is intended.`, {
226
+ returned: intents,
227
+ availableKeys: [...entries.keys()]
228
+ });
229
+ return unique;
230
+ }
184
231
  function validateKey(intent, entries) {
185
232
  if (!entries.has(intent)) throw new SupervisorRoutingError(`router returned unknown agent key "${intent}"`, {
186
233
  returned: intent,
@@ -268,5 +315,5 @@ function wrapRouteError(supervisorName, thrown) {
268
315
  }
269
316
 
270
317
  //#endregion
271
- export { decide };
318
+ export { capFanOut, decide, resolveMaxFanOut };
272
319
  //# sourceMappingURL=decide.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"decide.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/decide.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport { END, type EndSentinel } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { IterationSnapshot } from \"../contracts/supervisor/iteration-snapshot.type\";\nimport type { Next } from \"../contracts/supervisor/next.type\";\nimport type { RouteContext } from \"../contracts/supervisor/route-context.type\";\nimport type { RouterEntry } from \"../contracts/supervisor/router-entry.type\";\nimport type { SupervisorConfig } from \"../contracts/supervisor/supervisor-config.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport { AIError, SupervisorFailedError, SupervisorRoutingError } from \"../errors\";\nimport type { ResolvedIntentEntry } from \"./entries\";\nimport { buildRouterContextMessage } from \"./router-prompt\";\n\n/**\n * Outcome of one dispatch decision — what the iteration loop needs to\n * act on. `kind: \"end\"` signals termination; `kind: \"dispatch\"` carries\n * the resolved intents (always an array; single-agent dispatch has\n * length 1). `source` records which path made the call so the\n * iteration snapshot can surface it to debuggers.\n */\nexport type DispatchDecision =\n | {\n kind: \"end\";\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n }\n | {\n kind: \"dispatch\";\n intents: string[];\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n };\n\nexport type DecideParams = {\n config: SupervisorConfig<unknown>;\n entries: Map<string, ResolvedIntentEntry>;\n iteration: number;\n maxIterations: number;\n iterations: IterationSnapshot[];\n input: SupervisorInput;\n /**\n * Per-execute state accumulator at the start of this iteration.\n * Threaded into `RouteContext` for the route callback and\n * rendered into the router prompt so routing decisions can be\n * state-aware (Q14).\n */\n state: Record<string, unknown>;\n /**\n * Frozen request-scoped bag from the `execute({ context })` call —\n * surfaced on `RouteContext.context` for both `route` callbacks\n * and `RouterEntry.placeholders` / `RouterEntry.input` resolvers.\n */\n context: Readonly<Record<string, unknown>>;\n /**\n * Frozen prior-conversation history from `execute({ history })` —\n * surfaced on `RouteContext.history` and forwarded to the router\n * agent as `agent.execute(input, { history })` so router decisions\n * are conversation-aware.\n */\n history: ReadonlyArray<Message>;\n /**\n * Resolved natural-language objective from `SupervisorConfig.goal`\n * (materialized to plain text at supervisor construction). Surfaced\n * on `RouteContext.goal` for `route` / `RouterEntry` resolvers, and\n * injected into the router agent's per-turn user message via\n * `buildRouterContextMessage`. `undefined` when no goal was set.\n */\n goal: string | undefined;\n evaluateFeedback?: RouteContext[\"evaluateFeedback\"];\n /**\n * Forensic record of the iter-0 classifier (Phase 7). Threaded into\n * `RouteContext.classifier` so route callbacks and router-agent\n * input composers can read the classification trail without\n * re-parsing state.\n */\n classifier?: RouteContext[\"classifier\"];\n signal?: AbortSignal;\n /**\n * Override for the very first iteration — when `initialAgent` is\n * set, the first turn skips `route`/`router` and dispatches the\n * named intent directly. `runIteration` passes `true` only on turn\n * 0 when the config has `initialAgent`.\n */\n useInitialAgent?: boolean;\n};\n\n/**\n * Unified dispatch decision entry — calls either the `route` callback\n * or the `router` agent based on the supervisor's configured mode and\n * normalizes the result into a `DispatchDecision`. Runtime validates\n * every routing value against the configured agent keys; unknown keys\n * surface as `SupervisorRoutingError`.\n */\nexport async function decide(params: DecideParams): Promise<DispatchDecision> {\n if (params.useInitialAgent && params.config.initialAgent) {\n const intent = params.config.initialAgent;\n validateKey(intent, params.entries);\n\n return {\n kind: \"dispatch\",\n intents: [intent],\n source: \"initialAgent\",\n raw: intent,\n durationMs: 0,\n };\n }\n\n if (params.config.route) {\n return decideViaCallback(params);\n }\n\n if (params.config.router) {\n return decideViaRouter(params);\n }\n\n throw new SupervisorFailedError(\n `ai.supervisor(\"${params.config.name}\"): neither \\`route\\` nor \\`router\\` is configured — factory validation should have prevented this`,\n { context: { authoring: true } },\n );\n}\n\nasync function decideViaCallback(params: DecideParams): Promise<DispatchDecision> {\n const started = performance.now();\n const ctx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n classifier: params.classifier,\n };\n\n let raw: Next;\n\n try {\n raw = await params.config.route!(ctx);\n } catch (thrown) {\n throw wrapRouteError(params.config.name, thrown);\n }\n\n const durationMs = performance.now() - started;\n\n return normalize(raw, params.entries, \"route\", durationMs);\n}\n\nasync function decideViaRouter(params: DecideParams): Promise<DispatchDecision> {\n const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(\n params.config.router!,\n );\n const started = performance.now();\n\n const routeCtx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n };\n\n const userMessage =\n inputOverride?.(routeCtx) ??\n buildRouterContextMessage({\n entries: params.entries,\n iteration: params.iteration,\n maxIterations: params.maxIterations,\n iterations: params.iterations,\n input: params.input,\n state: params.state,\n feedback: routeCtx.feedback,\n supervisorPrompt: resolveSupervisorPromptText(params.config),\n goal: params.goal,\n });\n\n const resolvedPlaceholders = placeholders?.(routeCtx);\n\n // Inject the canonical router output schema so the supervisor gets\n // a predictable `{ next, reasoning? }` shape regardless of what the\n // user scripted on the router agent. Lets the router stay a plain\n // agent — no supervisor-specific config needed at construction.\n const routerHistory = resolveRouterHistory(\n historySlicer,\n routeCtx,\n params.history,\n params.config.historyWindow?.router,\n );\n\n const routerResult = await agent.execute(userMessage, {\n signal: params.signal,\n output: ROUTER_OUTPUT_SCHEMA as unknown as StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n }>,\n ...(resolvedPlaceholders ? { placeholders: resolvedPlaceholders } : {}),\n ...(routerHistory.length > 0 ? { history: routerHistory } : {}),\n });\n\n const durationMs = performance.now() - started;\n\n if (routerResult.error) {\n throw routerResult.error instanceof AIError\n ? routerResult.error\n : new SupervisorFailedError(`router agent failed`, {\n cause: routerResult.error,\n });\n }\n\n const data = routerResult.data;\n\n if (!data || typeof data !== \"object\") {\n throw new SupervisorRoutingError(\n `router agent returned no structured \\`next\\` — did its output schema include { next, reasoning? }?`,\n { returned: data, availableKeys: [...params.entries.keys()] },\n );\n }\n\n const rawNext = (data as { next?: unknown }).next;\n const reasoning = (data as { reasoning?: unknown }).reasoning;\n\n if (rawNext === undefined) {\n throw new SupervisorRoutingError(`router agent output missing \\`next\\` field`, {\n returned: data,\n availableKeys: [...params.entries.keys()],\n });\n }\n\n const decision = normalize(rawNext as Next, params.entries, \"router\", durationMs);\n\n return {\n ...decision,\n reasoning: typeof reasoning === \"string\" ? reasoning : undefined,\n usage: routerResult.usage,\n routerReport: routerResult.report,\n };\n}\n\n/**\n * Normalize the `router` config field — accepts either a bare\n * `AgentContract` (shorthand) or a full `RouterEntry` — into a\n * uniform `{ agent, placeholders?, inputOverride? }` triple. Centralized\n * so the dispatch path doesn't branch on shape.\n */\nfunction resolveRouterEntry(router: AgentContract<unknown> | RouterEntry): {\n agent: AgentContract<unknown>;\n placeholders?: RouterEntry[\"placeholders\"];\n inputOverride?: RouterEntry[\"input\"];\n historySlicer?: RouterEntry[\"history\"];\n} {\n if (typeof (router as { execute?: unknown }).execute === \"function\") {\n return { agent: router as AgentContract<unknown> };\n }\n\n const entry = router as RouterEntry;\n\n return {\n agent: entry.agent,\n placeholders: entry.placeholders,\n inputOverride: entry.input,\n historySlicer: entry.history,\n };\n}\n\n/**\n * Resolve the supervisor's own `systemPrompt` (string or contract)\n * into plain text. Returns `undefined` when the supervisor didn't\n * configure one. The resolved text is surfaced in the per-turn\n * router user message so the router sees team/domain context without\n * disturbing the router agent's own factory-level system prompt —\n * functionally equivalent to prepending, without requiring an API\n * expansion on `AgentContract` to read the router's system prompt.\n */\nfunction resolveSupervisorPromptText(config: SupervisorConfig<unknown>): string | undefined {\n if (!config.systemPrompt) {\n return undefined;\n }\n\n return typeof config.systemPrompt === \"string\"\n ? config.systemPrompt\n : config.systemPrompt.resolve();\n}\n\n/**\n * Convert the raw routing value (callback return OR router agent\n * `next` field) into a canonical `DispatchDecision`, validating every\n * named intent against the supervisor's `intents` map.\n */\nfunction normalize(\n raw: Next,\n entries: Map<string, ResolvedIntentEntry>,\n source: \"route\" | \"router\",\n durationMs: number,\n): DispatchDecision {\n if (isEnd(raw)) {\n return { kind: \"end\", source, raw, durationMs };\n }\n\n if (typeof raw === \"string\") {\n validateKey(raw, entries);\n\n return {\n kind: \"dispatch\",\n intents: [raw],\n source,\n raw,\n durationMs,\n };\n }\n\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new SupervisorRoutingError(\n `router returned an empty array — must be a non-empty list of agent intents`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n }\n\n for (const intent of raw) {\n if (typeof intent !== \"string\") {\n throw new SupervisorRoutingError(`router returned a non-string inside its fan-out array`, {\n returned: raw,\n availableKeys: [...entries.keys()],\n });\n }\n\n validateKey(intent, entries);\n }\n\n return {\n kind: \"dispatch\",\n intents: raw,\n source,\n raw,\n durationMs,\n };\n }\n\n throw new SupervisorRoutingError(\n `router returned an unsupported value — expected a string, string[], or END`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n}\n\nfunction validateKey(intent: string, entries: Map<string, ResolvedIntentEntry>): void {\n if (!entries.has(intent)) {\n throw new SupervisorRoutingError(`router returned unknown agent key \"${intent}\"`, {\n returned: intent,\n availableKeys: [...entries.keys()],\n });\n }\n}\n\nfunction isEnd(value: unknown): value is EndSentinel {\n return value === END;\n}\n\n/**\n * Resolve the history slice forwarded to the router agent. Mirrors\n * `SupervisorExecution.resolveHistoryFor(\"router\", ...)` — duplicated\n * here so the standalone `decide()` function stays callable without\n * threading the execution instance through. Precedence is identical:\n * entry slicer > `historyWindow.router` > full history.\n */\nfunction resolveRouterHistory(\n slicer: RouterEntry[\"history\"] | undefined,\n routeCtx: RouteContext,\n full: ReadonlyArray<Message>,\n window: number | undefined,\n): Message[] {\n if (slicer) {\n const sliced = slicer(routeCtx);\n return sliced ? [...sliced] : [];\n }\n\n if (window === undefined || window < 0) {\n return [...full];\n }\n\n if (window === 0) {\n return [];\n }\n\n return full.slice(-window);\n}\n\n/**\n * JSON Schema form of the canonical router output shape. Surfaced via\n * the Standard JSON Schema V1 extension path (`[\"~standard\"].jsonSchema.input`)\n * so `extractJsonSchema()` can pull it for native structured-output\n * enforcement on capable providers (OpenAI strict json_schema mode,\n * Anthropic tool-use shape, etc.). Without this, the model is told to\n * emit JSON only via soft system-prompt instruction — fragile, and\n * skipped entirely when the model advertises `structuredOutput: true`.\n *\n * `next` is intentionally `string` (not a union with arrays) because\n * OpenAI strict mode rejects polymorphic root types — fan-out via\n * `string[]` is still validated at the framework layer; the model\n * just emits a single intent name (or the END sentinel) and the\n * supervisor's own normalizer handles the rest.\n */\nconst ROUTER_OUTPUT_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n next: {\n type: \"string\",\n description: \"Name of the agent to dispatch next, or the END sentinel to terminate the run.\",\n },\n reasoning: {\n type: \"string\",\n description: \"One-sentence justification for the routing choice.\",\n },\n },\n required: [\"next\", \"reasoning\"],\n additionalProperties: false,\n};\n\n/**\n * Canonical Standard Schema the supervisor injects when calling the\n * router agent. Pragmatic — accepts any `next` shape the router can\n * plausibly emit (`string`, `string[]`, or the `END` literal) plus an\n * optional `reasoning` field. Rejects anything else so a broken\n * router output surfaces cleanly via the agent's own validation path.\n *\n * Exposes `[\"~standard\"].jsonSchema.input()` (Standard JSON Schema V1)\n * so capable providers enforce the shape natively rather than relying\n * on prompt-side coaching.\n */\nconst ROUTER_OUTPUT_SCHEMA: StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n}> = {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-supervisor\",\n jsonSchema: {\n input: () => ROUTER_OUTPUT_JSON_SCHEMA,\n },\n validate(value: unknown): StandardSchemaV1.Result<{ next: Next; reasoning?: string }> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"router output must be an object\" }] };\n }\n\n const record = value as { next?: unknown; reasoning?: unknown };\n const rawNext = record.next;\n\n const nextIsValid =\n typeof rawNext === \"string\" ||\n (Array.isArray(rawNext) && rawNext.every((element) => typeof element === \"string\"));\n\n if (!nextIsValid) {\n return {\n issues: [\n {\n message: \"router output `next` must be a string, string[], or the END sentinel\",\n },\n ],\n };\n }\n\n const reasoning = typeof record.reasoning === \"string\" ? record.reasoning : undefined;\n\n return {\n value: { next: rawNext as Next, reasoning },\n };\n },\n } as StandardSchemaV1<{ next: Next; reasoning?: string }>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n};\n\nfunction wrapRouteError(supervisorName: string, thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return new SupervisorFailedError(\n `\\`route\\` callback threw in supervisor \"${supervisorName}\": ${message}`,\n { cause: thrown },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"decide.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/decide.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport { END, type EndSentinel } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { IterationSnapshot } from \"../contracts/supervisor/iteration-snapshot.type\";\nimport type { Next } from \"../contracts/supervisor/next.type\";\nimport type { RouteContext } from \"../contracts/supervisor/route-context.type\";\nimport type { RouterEntry } from \"../contracts/supervisor/router-entry.type\";\nimport type { SupervisorConfig } from \"../contracts/supervisor/supervisor-config.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport { AIError, SupervisorFailedError, SupervisorRoutingError } from \"../errors\";\nimport type { ResolvedIntentEntry } from \"./entries\";\nimport { buildRouterContextMessage } from \"./router-prompt\";\n\n/**\n * Outcome of one dispatch decision — what the iteration loop needs to\n * act on. `kind: \"end\"` signals termination; `kind: \"dispatch\"` carries\n * the resolved intents (always an array; single-agent dispatch has\n * length 1). `source` records which path made the call so the\n * iteration snapshot can surface it to debuggers.\n */\nexport type DispatchDecision =\n | {\n kind: \"end\";\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n }\n | {\n kind: \"dispatch\";\n intents: string[];\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n };\n\nexport type DecideParams = {\n config: SupervisorConfig<unknown>;\n entries: Map<string, ResolvedIntentEntry>;\n iteration: number;\n maxIterations: number;\n iterations: IterationSnapshot[];\n input: SupervisorInput;\n /**\n * Per-execute state accumulator at the start of this iteration.\n * Threaded into `RouteContext` for the route callback and\n * rendered into the router prompt so routing decisions can be\n * state-aware (Q14).\n */\n state: Record<string, unknown>;\n /**\n * Frozen request-scoped bag from the `execute({ context })` call —\n * surfaced on `RouteContext.context` for both `route` callbacks\n * and `RouterEntry.placeholders` / `RouterEntry.input` resolvers.\n */\n context: Readonly<Record<string, unknown>>;\n /**\n * Frozen prior-conversation history from `execute({ history })` —\n * surfaced on `RouteContext.history` and forwarded to the router\n * agent as `agent.execute(input, { history })` so router decisions\n * are conversation-aware.\n */\n history: ReadonlyArray<Message>;\n /**\n * Resolved natural-language objective from `SupervisorConfig.goal`\n * (materialized to plain text at supervisor construction). Surfaced\n * on `RouteContext.goal` for `route` / `RouterEntry` resolvers, and\n * injected into the router agent's per-turn user message via\n * `buildRouterContextMessage`. `undefined` when no goal was set.\n */\n goal: string | undefined;\n evaluateFeedback?: RouteContext[\"evaluateFeedback\"];\n /**\n * Forensic record of the iter-0 classifier (Phase 7). Threaded into\n * `RouteContext.classifier` so route callbacks and router-agent\n * input composers can read the classification trail without\n * re-parsing state.\n */\n classifier?: RouteContext[\"classifier\"];\n signal?: AbortSignal;\n /**\n * Override for the very first iteration — when `initialAgent` is\n * set, the first turn skips `route`/`router` and dispatches the\n * named intent directly. `runIteration` passes `true` only on turn\n * 0 when the config has `initialAgent`.\n */\n useInitialAgent?: boolean;\n};\n\n/**\n * Unified dispatch decision entry — calls either the `route` callback\n * or the `router` agent based on the supervisor's configured mode and\n * normalizes the result into a `DispatchDecision`. Runtime validates\n * every routing value against the configured agent keys; unknown keys\n * surface as `SupervisorRoutingError`.\n */\nexport async function decide(params: DecideParams): Promise<DispatchDecision> {\n if (params.useInitialAgent && params.config.initialAgent) {\n const intent = params.config.initialAgent;\n validateKey(intent, params.entries);\n\n return {\n kind: \"dispatch\",\n intents: [intent],\n source: \"initialAgent\",\n raw: intent,\n durationMs: 0,\n };\n }\n\n if (params.config.route) {\n return decideViaCallback(params);\n }\n\n if (params.config.router) {\n return decideViaRouter(params);\n }\n\n throw new SupervisorFailedError(\n `ai.supervisor(\"${params.config.name}\"): neither \\`route\\` nor \\`router\\` is configured — factory validation should have prevented this`,\n { context: { authoring: true } },\n );\n}\n\nasync function decideViaCallback(params: DecideParams): Promise<DispatchDecision> {\n const started = performance.now();\n const ctx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n classifier: params.classifier,\n };\n\n let raw: Next;\n\n try {\n raw = await params.config.route!(ctx);\n } catch (thrown) {\n throw wrapRouteError(params.config.name, thrown);\n }\n\n const durationMs = performance.now() - started;\n\n return normalize(raw, params.entries, \"route\", durationMs, resolveMaxFanOut(params.config));\n}\n\nasync function decideViaRouter(params: DecideParams): Promise<DispatchDecision> {\n const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(\n params.config.router!,\n );\n const started = performance.now();\n\n const routeCtx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n };\n\n const userMessage =\n inputOverride?.(routeCtx) ??\n buildRouterContextMessage({\n entries: params.entries,\n iteration: params.iteration,\n maxIterations: params.maxIterations,\n iterations: params.iterations,\n input: params.input,\n state: params.state,\n feedback: routeCtx.feedback,\n supervisorPrompt: resolveSupervisorPromptText(params.config),\n goal: params.goal,\n });\n\n const resolvedPlaceholders = placeholders?.(routeCtx);\n\n // Inject the canonical router output schema so the supervisor gets\n // a predictable `{ next, reasoning? }` shape regardless of what the\n // user scripted on the router agent. Lets the router stay a plain\n // agent — no supervisor-specific config needed at construction.\n const routerHistory = resolveRouterHistory(\n historySlicer,\n routeCtx,\n params.history,\n params.config.historyWindow?.router,\n );\n\n const routerResult = await agent.execute(userMessage, {\n signal: params.signal,\n output: ROUTER_OUTPUT_SCHEMA as unknown as StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n }>,\n ...(resolvedPlaceholders ? { placeholders: resolvedPlaceholders } : {}),\n ...(routerHistory.length > 0 ? { history: routerHistory } : {}),\n });\n\n const durationMs = performance.now() - started;\n\n if (routerResult.error) {\n throw routerResult.error instanceof AIError\n ? routerResult.error\n : new SupervisorFailedError(`router agent failed`, {\n cause: routerResult.error,\n });\n }\n\n const data = routerResult.data;\n\n if (!data || typeof data !== \"object\") {\n throw new SupervisorRoutingError(\n `router agent returned no structured \\`next\\` — did its output schema include { next, reasoning? }?`,\n { returned: data, availableKeys: [...params.entries.keys()] },\n );\n }\n\n const rawNext = (data as { next?: unknown }).next;\n const reasoning = (data as { reasoning?: unknown }).reasoning;\n\n if (rawNext === undefined) {\n throw new SupervisorRoutingError(`router agent output missing \\`next\\` field`, {\n returned: data,\n availableKeys: [...params.entries.keys()],\n });\n }\n\n const decision = normalize(\n rawNext as Next,\n params.entries,\n \"router\",\n durationMs,\n resolveMaxFanOut(params.config),\n );\n\n return {\n ...decision,\n reasoning: typeof reasoning === \"string\" ? reasoning : undefined,\n usage: routerResult.usage,\n routerReport: routerResult.report,\n };\n}\n\n/**\n * Normalize the `router` config field — accepts either a bare\n * `AgentContract` (shorthand) or a full `RouterEntry` — into a\n * uniform `{ agent, placeholders?, inputOverride? }` triple. Centralized\n * so the dispatch path doesn't branch on shape.\n */\nfunction resolveRouterEntry(router: AgentContract<unknown> | RouterEntry): {\n agent: AgentContract<unknown>;\n placeholders?: RouterEntry[\"placeholders\"];\n inputOverride?: RouterEntry[\"input\"];\n historySlicer?: RouterEntry[\"history\"];\n} {\n if (typeof (router as { execute?: unknown }).execute === \"function\") {\n return { agent: router as AgentContract<unknown> };\n }\n\n const entry = router as RouterEntry;\n\n return {\n agent: entry.agent,\n placeholders: entry.placeholders,\n inputOverride: entry.input,\n historySlicer: entry.history,\n };\n}\n\n/**\n * Resolve the supervisor's own `systemPrompt` (string or contract)\n * into plain text. Returns `undefined` when the supervisor didn't\n * configure one. The resolved text is surfaced in the per-turn\n * router user message so the router sees team/domain context without\n * disturbing the router agent's own factory-level system prompt —\n * functionally equivalent to prepending, without requiring an API\n * expansion on `AgentContract` to read the router's system prompt.\n */\nfunction resolveSupervisorPromptText(config: SupervisorConfig<unknown>): string | undefined {\n if (!config.systemPrompt) {\n return undefined;\n }\n\n return typeof config.systemPrompt === \"string\"\n ? config.systemPrompt\n : config.systemPrompt.resolve();\n}\n\n/**\n * Convert the raw routing value (callback return OR router agent\n * `next` field) into a canonical `DispatchDecision`, validating every\n * named intent against the supervisor's `intents` map.\n */\nfunction normalize(\n raw: Next,\n entries: Map<string, ResolvedIntentEntry>,\n source: \"route\" | \"router\",\n durationMs: number,\n maxFanOut: number,\n): DispatchDecision {\n if (isEnd(raw)) {\n return { kind: \"end\", source, raw, durationMs };\n }\n\n if (typeof raw === \"string\") {\n validateKey(raw, entries);\n\n return {\n kind: \"dispatch\",\n intents: [raw],\n source,\n raw,\n durationMs,\n };\n }\n\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new SupervisorRoutingError(\n `router returned an empty array — must be a non-empty list of agent intents`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n }\n\n for (const intent of raw) {\n if (typeof intent !== \"string\") {\n throw new SupervisorRoutingError(`router returned a non-string inside its fan-out array`, {\n returned: raw,\n availableKeys: [...entries.keys()],\n });\n }\n\n validateKey(intent, entries);\n }\n\n return {\n kind: \"dispatch\",\n intents: capFanOut(raw as string[], entries, maxFanOut),\n source,\n raw,\n durationMs,\n };\n }\n\n throw new SupervisorRoutingError(\n `router returned an unsupported value — expected a string, string[], or END`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n}\n\n/**\n * Default fan-out WIDTH ceiling — how many intents one dispatch\n * decision may run in parallel. `maxIterations` bounds depth; this\n * bounds width, so total work per run is bounded by the product\n * instead of by iterations alone.\n */\nexport const DEFAULT_MAX_FAN_OUT = 10;\n\n/**\n * Resolve the configured width ceiling. Factory validation\n * (`supervisor.ts`) rejects non-integer / `< 1` values at authoring\n * time, so this only has to apply the default.\n */\nexport function resolveMaxFanOut(config: Pick<SupervisorConfig<never>, \"maxFanOut\">): number {\n return config.maxFanOut ?? DEFAULT_MAX_FAN_OUT;\n}\n\n/**\n * Dedupe + width-cap a fan-out intent list before it reaches\n * `Promise.all(...dispatchOne)`.\n *\n * Duplicates are collapsed silently: running the same intent twice in\n * one decision is pure wasted spend (branch results are indexed by\n * intent downstream, so the extras can't change the outcome), and a\n * router that repeats itself is sloppy rather than hostile.\n *\n * Exceeding the cap *after* dedupe THROWS rather than truncating.\n * Truncation would silently hand an attacker-chosen subset of the\n * decision to the executor and hide the anomaly from the operator;\n * every other routing violation in this file (unknown key, empty\n * array, non-string element) already fails loudly as\n * `SupervisorRoutingError`, so a width violation surfaces in the same\n * place, with the same code, carrying the offending array.\n *\n * Threat model: the router's prompt embeds supervisor `state` and\n * prior branch outputs, both of which can carry attacker-controlled\n * text from tool results. Without a width bound, one injected\n * \"always return this 200-element `next` array\" turns a single\n * iteration into 200 real agent/workflow executions — no unknown\n * intent name required, so the existing allowlist check never fires.\n */\nexport function capFanOut(\n intents: string[],\n entries: Map<string, ResolvedIntentEntry>,\n maxFanOut: number,\n): string[] {\n const unique = [...new Set(intents)];\n\n if (unique.length > maxFanOut) {\n throw new SupervisorRoutingError(\n `routing decision fanned out to ${unique.length} intents — exceeds maxFanOut=${maxFanOut}. Raise \\`maxFanOut\\` if this width is intended.`,\n { returned: intents, availableKeys: [...entries.keys()] },\n );\n }\n\n return unique;\n}\n\nfunction validateKey(intent: string, entries: Map<string, ResolvedIntentEntry>): void {\n if (!entries.has(intent)) {\n throw new SupervisorRoutingError(`router returned unknown agent key \"${intent}\"`, {\n returned: intent,\n availableKeys: [...entries.keys()],\n });\n }\n}\n\nfunction isEnd(value: unknown): value is EndSentinel {\n return value === END;\n}\n\n/**\n * Resolve the history slice forwarded to the router agent. Mirrors\n * `SupervisorExecution.resolveHistoryFor(\"router\", ...)` — duplicated\n * here so the standalone `decide()` function stays callable without\n * threading the execution instance through. Precedence is identical:\n * entry slicer > `historyWindow.router` > full history.\n */\nfunction resolveRouterHistory(\n slicer: RouterEntry[\"history\"] | undefined,\n routeCtx: RouteContext,\n full: ReadonlyArray<Message>,\n window: number | undefined,\n): Message[] {\n if (slicer) {\n const sliced = slicer(routeCtx);\n return sliced ? [...sliced] : [];\n }\n\n if (window === undefined || window < 0) {\n return [...full];\n }\n\n if (window === 0) {\n return [];\n }\n\n return full.slice(-window);\n}\n\n/**\n * JSON Schema form of the canonical router output shape. Surfaced via\n * the Standard JSON Schema V1 extension path (`[\"~standard\"].jsonSchema.input`)\n * so `extractJsonSchema()` can pull it for native structured-output\n * enforcement on capable providers (OpenAI strict json_schema mode,\n * Anthropic tool-use shape, etc.). Without this, the model is told to\n * emit JSON only via soft system-prompt instruction — fragile, and\n * skipped entirely when the model advertises `structuredOutput: true`.\n *\n * `next` is intentionally `string` (not a union with arrays) because\n * OpenAI strict mode rejects polymorphic root types — fan-out via\n * `string[]` is still validated at the framework layer; the model\n * just emits a single intent name (or the END sentinel) and the\n * supervisor's own normalizer handles the rest.\n */\nconst ROUTER_OUTPUT_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n next: {\n type: \"string\",\n description: \"Name of the agent to dispatch next, or the END sentinel to terminate the run.\",\n },\n reasoning: {\n type: \"string\",\n description: \"One-sentence justification for the routing choice.\",\n },\n },\n required: [\"next\", \"reasoning\"],\n additionalProperties: false,\n};\n\n/**\n * Canonical Standard Schema the supervisor injects when calling the\n * router agent. Pragmatic — accepts any `next` shape the router can\n * plausibly emit (`string`, `string[]`, or the `END` literal) plus an\n * optional `reasoning` field. Rejects anything else so a broken\n * router output surfaces cleanly via the agent's own validation path.\n *\n * Exposes `[\"~standard\"].jsonSchema.input()` (Standard JSON Schema V1)\n * so capable providers enforce the shape natively rather than relying\n * on prompt-side coaching.\n */\nconst ROUTER_OUTPUT_SCHEMA: StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n}> = {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-supervisor\",\n jsonSchema: {\n input: () => ROUTER_OUTPUT_JSON_SCHEMA,\n },\n validate(value: unknown): StandardSchemaV1.Result<{ next: Next; reasoning?: string }> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"router output must be an object\" }] };\n }\n\n const record = value as { next?: unknown; reasoning?: unknown };\n const rawNext = record.next;\n\n const nextIsValid =\n typeof rawNext === \"string\" ||\n (Array.isArray(rawNext) && rawNext.every((element) => typeof element === \"string\"));\n\n if (!nextIsValid) {\n return {\n issues: [\n {\n message: \"router output `next` must be a string, string[], or the END sentinel\",\n },\n ],\n };\n }\n\n const reasoning = typeof record.reasoning === \"string\" ? record.reasoning : undefined;\n\n return {\n value: { next: rawNext as Next, reasoning },\n };\n },\n } as StandardSchemaV1<{ next: Next; reasoning?: string }>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n};\n\nfunction wrapRouteError(supervisorName: string, thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return new SupervisorFailedError(\n `\\`route\\` callback threw in supervisor \"${supervisorName}\": ${message}`,\n { cause: thrown },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyGA,eAAsB,OAAO,QAAiD;CAC5E,IAAI,OAAO,mBAAmB,OAAO,OAAO,cAAc;EACxD,MAAM,SAAS,OAAO,OAAO;EAC7B,YAAY,QAAQ,OAAO,OAAO;EAElC,OAAO;GACL,MAAM;GACN,SAAS,CAAC,MAAM;GAChB,QAAQ;GACR,KAAK;GACL,YAAY;EACd;CACF;CAEA,IAAI,OAAO,OAAO,OAChB,OAAO,kBAAkB,MAAM;CAGjC,IAAI,OAAO,OAAO,QAChB,OAAO,gBAAgB,MAAM;CAG/B,MAAM,IAAI,sBACR,kBAAkB,OAAO,OAAO,KAAK,qGACrC,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;AACF;AAEA,eAAe,kBAAkB,QAAiD;CAChF,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,MAAoB;EACxB,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,OAAO,OAAO;EACd,YAAY,OAAO;EACnB,UACE,OAAO,OAAO,kBAAkB,aAAa,WACzC,OAAO,iBAAiB,WACxB;EACN,kBAAkB,OAAO;EACzB,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,YAAY,OAAO;CACrB;CAEA,IAAI;CAEJ,IAAI;EACF,MAAM,MAAM,OAAO,OAAO,MAAO,GAAG;CACtC,SAAS,QAAQ;EACf,MAAM,eAAe,OAAO,OAAO,MAAM,MAAM;CACjD;CAEA,MAAM,aAAa,YAAY,IAAI,IAAI;CAEvC,OAAO,UAAU,KAAK,OAAO,SAAS,SAAS,YAAY,iBAAiB,OAAO,MAAM,CAAC;AAC5F;AAEA,eAAe,gBAAgB,QAAiD;CAC9E,MAAM,EAAE,OAAO,cAAc,eAAe,kBAAkB,mBAC5D,OAAO,OAAO,MAChB;CACA,MAAM,UAAU,YAAY,IAAI;CAEhC,MAAM,WAAyB;EAC7B,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,OAAO,OAAO;EACd,YAAY,OAAO;EACnB,UACE,OAAO,OAAO,kBAAkB,aAAa,WACzC,OAAO,iBAAiB,WACxB;EACN,kBAAkB,OAAO;EACzB,SAAS,OAAO;EAChB,SAAS,OAAO;EAChB,MAAM,OAAO;CACf;CAEA,MAAM,cACJ,gBAAgB,QAAQ,KACxB,0BAA0B;EACxB,SAAS,OAAO;EAChB,WAAW,OAAO;EAClB,eAAe,OAAO;EACtB,YAAY,OAAO;EACnB,OAAO,OAAO;EACd,OAAO,OAAO;EACd,UAAU,SAAS;EACnB,kBAAkB,4BAA4B,OAAO,MAAM;EAC3D,MAAM,OAAO;CACf,CAAC;CAEH,MAAM,uBAAuB,eAAe,QAAQ;CAMpD,MAAM,gBAAgB,qBACpB,eACA,UACA,OAAO,SACP,OAAO,OAAO,eAAe,MAC/B;CAEA,MAAM,eAAe,MAAM,MAAM,QAAQ,aAAa;EACpD,QAAQ,OAAO;EACf,QAAQ;EAIR,GAAI,uBAAuB,EAAE,cAAc,qBAAqB,IAAI,CAAC;EACrE,GAAI,cAAc,SAAS,IAAI,EAAE,SAAS,cAAc,IAAI,CAAC;CAC/D,CAAC;CAED,MAAM,aAAa,YAAY,IAAI,IAAI;CAEvC,IAAI,aAAa,OACf,MAAM,aAAa,iBAAiB,UAChC,aAAa,QACb,IAAI,sBAAsB,uBAAuB,EAC/C,OAAO,aAAa,MACtB,CAAC;CAGP,MAAM,OAAO,aAAa;CAE1B,IAAI,CAAC,QAAQ,OAAO,SAAS,UAC3B,MAAM,IAAI,uBACR,sGACA;EAAE,UAAU;EAAM,eAAe,CAAC,GAAG,OAAO,QAAQ,KAAK,CAAC;CAAE,CAC9D;CAGF,MAAM,UAAW,KAA4B;CAC7C,MAAM,YAAa,KAAiC;CAEpD,IAAI,YAAY,QACd,MAAM,IAAI,uBAAuB,8CAA8C;EAC7E,UAAU;EACV,eAAe,CAAC,GAAG,OAAO,QAAQ,KAAK,CAAC;CAC1C,CAAC;CAWH,OAAO;EACL,GATe,UACf,SACA,OAAO,SACP,UACA,YACA,iBAAiB,OAAO,MAAM,CAIpB;EACV,WAAW,OAAO,cAAc,WAAW,YAAY;EACvD,OAAO,aAAa;EACpB,cAAc,aAAa;CAC7B;AACF;;;;;;;AAQA,SAAS,mBAAmB,QAK1B;CACA,IAAI,OAAQ,OAAiC,YAAY,YACvD,OAAO,EAAE,OAAO,OAAiC;CAGnD,MAAM,QAAQ;CAEd,OAAO;EACL,OAAO,MAAM;EACb,cAAc,MAAM;EACpB,eAAe,MAAM;EACrB,eAAe,MAAM;CACvB;AACF;;;;;;;;;;AAWA,SAAS,4BAA4B,QAAuD;CAC1F,IAAI,CAAC,OAAO,cACV;CAGF,OAAO,OAAO,OAAO,iBAAiB,WAClC,OAAO,eACP,OAAO,aAAa,QAAQ;AAClC;;;;;;AAOA,SAAS,UACP,KACA,SACA,QACA,YACA,WACkB;CAClB,IAAI,MAAM,GAAG,GACX,OAAO;EAAE,MAAM;EAAO;EAAQ;EAAK;CAAW;CAGhD,IAAI,OAAO,QAAQ,UAAU;EAC3B,YAAY,KAAK,OAAO;EAExB,OAAO;GACL,MAAM;GACN,SAAS,CAAC,GAAG;GACb;GACA;GACA;EACF;CACF;CAEA,IAAI,MAAM,QAAQ,GAAG,GAAG;EACtB,IAAI,IAAI,WAAW,GACjB,MAAM,IAAI,uBACR,8EACA;GAAE,UAAU;GAAK,eAAe,CAAC,GAAG,QAAQ,KAAK,CAAC;EAAE,CACtD;EAGF,KAAK,MAAM,UAAU,KAAK;GACxB,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,uBAAuB,yDAAyD;IACxF,UAAU;IACV,eAAe,CAAC,GAAG,QAAQ,KAAK,CAAC;GACnC,CAAC;GAGH,YAAY,QAAQ,OAAO;EAC7B;EAEA,OAAO;GACL,MAAM;GACN,SAAS,UAAU,KAAiB,SAAS,SAAS;GACtD;GACA;GACA;EACF;CACF;CAEA,MAAM,IAAI,uBACR,8EACA;EAAE,UAAU;EAAK,eAAe,CAAC,GAAG,QAAQ,KAAK,CAAC;CAAE,CACtD;AACF;;;;;;;AAQA,MAAa,sBAAsB;;;;;;AAOnC,SAAgB,iBAAiB,QAA4D;CAC3F,OAAO,OAAO;AAChB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,UACd,SACA,SACA,WACU;CACV,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;CAEnC,IAAI,OAAO,SAAS,WAClB,MAAM,IAAI,uBACR,kCAAkC,OAAO,OAAO,+BAA+B,UAAU,mDACzF;EAAE,UAAU;EAAS,eAAe,CAAC,GAAG,QAAQ,KAAK,CAAC;CAAE,CAC1D;CAGF,OAAO;AACT;AAEA,SAAS,YAAY,QAAgB,SAAiD;CACpF,IAAI,CAAC,QAAQ,IAAI,MAAM,GACrB,MAAM,IAAI,uBAAuB,sCAAsC,OAAO,IAAI;EAChF,UAAU;EACV,eAAe,CAAC,GAAG,QAAQ,KAAK,CAAC;CACnC,CAAC;AAEL;AAEA,SAAS,MAAM,OAAsC;CACnD,OAAO,UAAU;AACnB;;;;;;;;AASA,SAAS,qBACP,QACA,UACA,MACA,QACW;CACX,IAAI,QAAQ;EACV,MAAM,SAAS,OAAO,QAAQ;EAC9B,OAAO,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;CACjC;CAEA,IAAI,WAAW,UAAa,SAAS,GACnC,OAAO,CAAC,GAAG,IAAI;CAGjB,IAAI,WAAW,GACb,OAAO,CAAC;CAGV,OAAO,KAAK,MAAM,CAAC,MAAM;AAC3B;;;;;;;;;;;;;;;;AAiBA,MAAM,4BAA4B;CAChC,MAAM;CACN,YAAY;EACV,MAAM;GACJ,MAAM;GACN,aAAa;EACf;EACA,WAAW;GACT,MAAM;GACN,aAAa;EACf;CACF;CACA,UAAU,CAAC,QAAQ,WAAW;CAC9B,sBAAsB;AACxB;;;;;;;;;;;;AAaA,MAAM,uBAGD,EACH,aAAa;CACX,SAAS;CACT,QAAQ;CACR,YAAY,EACV,aAAa,0BACf;CACA,SAAS,OAA6E;EACpF,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,kCAAkC,CAAC,EAAE;EAGpE,MAAM,SAAS;EACf,MAAM,UAAU,OAAO;EAMvB,IAAI,EAHF,OAAO,YAAY,YAClB,MAAM,QAAQ,OAAO,KAAK,QAAQ,OAAO,YAAY,OAAO,YAAY,QAAQ,IAGjF,OAAO,EACL,QAAQ,CACN,EACE,SAAS,uEACX,CACF,EACF;EAKF,OAAO,EACL,OAAO;GAAE,MAAM;GAAiB,WAHhB,OAAO,OAAO,cAAc,WAAW,OAAO,YAAY;EAGhC,EAC5C;CACF;AACF,EAGF;AAEA,SAAS,eAAe,gBAAwB,QAA0B;CACxE,IAAI,kBAAkB,SACpB,OAAO;CAKT,OAAO,IAAI,sBACT,2CAA2C,eAAe,KAH5C,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,KAItE,EAAE,OAAO,OAAO,CAClB;AACF"}
@@ -251,6 +251,13 @@ declare class SupervisorExecution<TOutput> {
251
251
  * branch errors don't abort siblings — they're recorded on the
252
252
  * branch snapshot and let evaluate (or default termination logic)
253
253
  * decide the response.
254
+ *
255
+ * `capFanOut` runs here as well as in `decide.ts` — this is the one
256
+ * chokepoint every dispatch source funnels through (router/route
257
+ * decisions, `evaluate.reassignTo`, classifier picks, per-intent
258
+ * `next` unions), so the width bound holds even for the paths that
259
+ * build a `DispatchDecision` without going through `normalize()`.
260
+ * Idempotent for already-normalized decisions.
254
261
  */
255
262
  private dispatchBranches;
256
263
  /**
@@ -373,18 +380,6 @@ declare class SupervisorExecution<TOutput> {
373
380
  * through unchanged so structured inputs work.
374
381
  */
375
382
  private coerceInlineInput;
376
- /**
377
- * Heuristic detection of `SupervisorContract` — the contract carries
378
- * a `signature` getter same as workflows, but supervisors expose
379
- * `resume()` while workflows expose `resume(runId, options)` too.
380
- * Cleanest distinguisher in the public surface: supervisors carry
381
- * the `asTool` method name `as` … unfortunately so do workflows.
382
- * Use the `streamableType` brand if we add one in v2; for now lean
383
- * on a duck-typed check that's good enough for the ctx.run path
384
- * (incorrect routing for workflows would still produce a runnable
385
- * call — workflow.execute accepts the same args either way).
386
- */
387
- private isSupervisor;
388
383
  /**
389
384
  * Invoke the underlying dispatchable unit. Agents and workflows
390
385
  * both satisfy `ExecutableContract<string, …>` so the call shape
@@ -473,6 +468,21 @@ declare class SupervisorExecution<TOutput> {
473
468
  * specialist commits.
474
469
  */
475
470
  private mergeAckIntoState;
471
+ /**
472
+ * Single funnel for "shallow-merge a model-influenced slice into
473
+ * `this.state`". Wraps the shared {@link mergeSafely} guard so no
474
+ * merge site can assign `__proto__` / `constructor` / `prototype`
475
+ * onto the run's state object, and logs when something tried.
476
+ *
477
+ * Every slice reaching state is model- or tool-influenced (agent
478
+ * outputs validated against a DEVELOPER-supplied schema, which may
479
+ * legitimately be permissive: `z.record()`, `.passthrough()`,
480
+ * `z.any()`), so the key names are untrusted input even when the
481
+ * values are shaped.
482
+ */
483
+ private mergeIntoState;
484
+ /** Shared logging for refused prototype-tampering keys. */
485
+ private warnOnUnsafeKeys;
476
486
  /**
477
487
  * Run the iter-0 classifier prelude (Phase 7 / decisions §37).
478
488
  * Resolves the configured classifier (agent / callback / entry
@@ -1 +1 @@
1
- {"version":3,"file":"execution.d.mts","names":[],"sources":["../../../../../../../ai/src/supervisor/execution.ts"],"mappings":"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"}
1
+ {"version":3,"file":"execution.d.mts","names":[],"sources":["../../../../../../../ai/src/supervisor/execution.ts"],"mappings":"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"}