@warlock.js/ai 4.15.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 (89) hide show
  1. package/CHANGELOG.md +182 -163
  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
@@ -134,6 +134,10 @@ function validateFactoryConfig(config) {
134
134
  authoring: true,
135
135
  maxIterations: config.maxIterations
136
136
  } });
137
+ if (config.maxFanOut !== void 0 && (!Number.isInteger(config.maxFanOut) || config.maxFanOut < 1)) throw new SupervisorFailedError(`ai.supervisor("${config.name}"): \`maxFanOut\` must be an integer >= 1`, { context: {
138
+ authoring: true,
139
+ maxFanOut: config.maxFanOut
140
+ } });
137
141
  }
138
142
  function generateRunId() {
139
143
  return `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/supervisor.ts"],"sourcesContent":["import type { SupervisorEventMap } from \"../contracts/events/event-map.type\";\nimport type { ExecutionReport } from \"../contracts/result/execution-report.type\";\nimport type { SupervisorResult } from \"../contracts/result/supervisor-result.type\";\nimport type { StreamContract } from \"../contracts/stream/stream.contract\";\nimport type { SupervisorIntentValue } from \"../contracts/supervisor/intent-entry.type\";\nimport type {\n SupervisorConfig,\n SupervisorEventHandler,\n} from \"../contracts/supervisor/supervisor-config.type\";\nimport type {\n SupervisorExecuteOptions,\n SupervisorResumeOptions,\n} from \"../contracts/supervisor/supervisor-execute-options.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport type { SupervisorStreamEvent } from \"../contracts/supervisor/supervisor-stream-event.type\";\nimport type {\n SupervisorAsToolOptions,\n SupervisorContract,\n} from \"../contracts/supervisor/supervisor.contract\";\nimport { SupervisorFailedError } from \"../errors\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport type { ToolContract } from \"../tool/tool\";\nimport { asTool } from \"./as-tool\";\nimport { SupervisorEmitter } from \"./emitter\";\nimport { assertRouterDescriptions, resolveIntentEntries } from \"./entries\";\nimport { SupervisorExecution } from \"./execution\";\nimport { computeSignature } from \"./signature\";\nimport { loadSnapshotForResume } from \"./snapshot\";\nimport { createSupervisorStream } from \"./supervisor-stream\";\n\n/**\n * `ai.supervisor(config)` — construct a `SupervisorContract`. Validates\n * the config at author time (throws `SupervisorFailedError` on bad\n * shape), resolves agent entries, computes a stable structural\n * signature, wires the three-tier event emitter, and returns an\n * instance that satisfies `ExecutableContract` so it can compose into\n * tools, outer agents, and (future) orchestrators uniformly.\n *\n * @example\n * const support = ai.supervisor({\n * name: \"customer-support\",\n * router: routerAgent,\n * intents: { triage, orderLookup, billingLookup, resolver },\n * evaluate: (ctx) => ctx.result.resolver?.output ? { satisfied: true } : undefined,\n * output: z.object({ response: z.string(), refund: z.boolean() }),\n * maxIterations: 6,\n * });\n */\nexport function supervisor<\n TOutput = unknown,\n TState = TOutput,\n TIntents extends Record<string, SupervisorIntentValue> = Record<string, SupervisorIntentValue>,\n TArtifacts = Record<string, unknown>,\n>(config: SupervisorConfig<TOutput, TState, TIntents, TArtifacts>): SupervisorContract<TOutput> {\n validateFactoryConfig(config as unknown as SupervisorConfig<TOutput>);\n\n const entries = resolveIntentEntries(config.intents, config.name);\n\n assertRouterDescriptions(config as SupervisorConfig<unknown>, entries);\n\n if (config.initialAgent && !entries.has(config.initialAgent)) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`initialAgent\\` \"${config.initialAgent}\" is not a key in \\`intents\\``,\n { context: { authoring: true } },\n );\n }\n\n const signature = computeSignature(config as SupervisorConfig<unknown>, entries);\n const emitter = new SupervisorEmitter(config.on);\n\n async function execute(\n input: SupervisorInput,\n options?: SupervisorExecuteOptions,\n ): Promise<SupervisorResult<TOutput>> {\n const runId = options?.runId ?? generateRunId();\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input,\n runId,\n options,\n });\n\n const result = await execution.run();\n\n // Route the finished report to any resolved observers (F1/F3).\n // Gated by `config.observe` + the global observe-all flag; observer\n // errors are swallowed inside `notifyObservers`. `ai.team(...)`\n // forwards its `observe` into this same config, so a team inherits\n // observability through here with no extra wiring. Bridge the\n // pre-existing `SupervisorReport = Omit<BaseReport, \"type\">` drift\n // (the report carries `type: \"supervisor\"` at runtime) so this call\n // site adds no new type error beyond the documented baseline.\n await notifyObservers(config.observe, result.report as unknown as ExecutionReport);\n\n return result;\n }\n\n function stream(\n input: SupervisorInput,\n options?: SupervisorExecuteOptions,\n ): StreamContract<SupervisorResult<TOutput>, SupervisorStreamEvent> {\n const runId = options?.runId ?? generateRunId();\n const { controller, stream: contract } = createSupervisorStream<SupervisorResult<TOutput>>();\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input,\n runId,\n options,\n streamController: controller,\n });\n\n // Route the finished report to resolved observers once the streamed\n // run settles. Attached to the run promise (not awaited — `stream`\n // returns synchronously); `notifyObservers` swallows observer errors.\n void execution\n .run()\n .then((result) =>\n notifyObservers(config.observe, result.report as unknown as ExecutionReport),\n );\n\n return contract;\n }\n\n async function resume(\n runId: string,\n options?: SupervisorResumeOptions,\n ): Promise<SupervisorResult<TOutput>> {\n const snapshot = await loadSnapshotForResume({\n config: config as SupervisorConfig<unknown>,\n signature,\n runId,\n options,\n });\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input: snapshot.input,\n runId,\n options,\n resumeFrom: snapshot,\n });\n\n const result = await execution.run();\n\n await notifyObservers(config.observe, result.report as unknown as ExecutionReport);\n\n return result;\n }\n\n const instance: SupervisorContract<TOutput> = {\n name: config.name,\n inputSchema: config.inputSchema,\n signature,\n execute,\n stream,\n resume,\n on<K extends keyof SupervisorEventMap>(\n event: K,\n handler: SupervisorEventHandler<K>,\n ): () => void {\n return emitter.on(event, handler);\n },\n off<K extends keyof SupervisorEventMap>(event: K, handler: SupervisorEventHandler<K>): void {\n emitter.off(event, handler);\n },\n asTool<TToolInput = string>(\n options: SupervisorAsToolOptions<TToolInput>,\n ): ToolContract<TToolInput, TOutput> {\n return asTool<TOutput, TToolInput>(instance, options);\n },\n };\n\n return instance;\n}\n\n/**\n * Factory-time validation. Enforces the XOR + pairing rules the design\n * locked in §2 and surfaces any violation as a typed\n * `SupervisorFailedError` tagged `authoring: true`.\n */\nfunction validateFactoryConfig<T>(config: SupervisorConfig<T>): void {\n if (!config.name || typeof config.name !== \"string\") {\n throw new SupervisorFailedError(\"ai.supervisor: `name` is required and must be a string\", {\n context: { authoring: true },\n });\n }\n\n if (!config.intents || typeof config.intents !== \"object\") {\n throw new SupervisorFailedError(`ai.supervisor(\"${config.name}\"): \\`intents\\` is required`, {\n context: { authoring: true },\n });\n }\n\n const hasRoute = typeof config.route === \"function\";\n const hasRouter = !!config.router;\n\n if (hasRouter) {\n const router = config.router as { execute?: unknown } | { agent?: { execute?: unknown } };\n const isBareAgent = typeof (router as { execute?: unknown }).execute === \"function\";\n const isEntryForm =\n !isBareAgent &&\n typeof (router as { agent?: { execute?: unknown } }).agent === \"object\" &&\n typeof (router as { agent?: { execute?: unknown } }).agent?.execute === \"function\";\n\n if (!isBareAgent && !isEntryForm) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`router\\` must be an agent contract or a \\`{ agent, placeholders?, input? }\\` entry`,\n { context: { authoring: true } },\n );\n }\n }\n\n if (hasRoute && hasRouter) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`route\\` and \\`router\\` are mutually exclusive — configure exactly one`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 7 / decisions §37 — `classifier` is the iter-0 prelude;\n // satisfies the \"must have a dispatch source\" rule on its own.\n // Composes with router/route (classifier drives iter 0; router/route\n // takes iter 1+). When configured alone, supervisor terminates after\n // iter 0's branch settles.\n const hasClassifier = config.classifier !== undefined;\n\n if (!hasRoute && !hasRouter && !hasClassifier) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): one of \\`route\\`, \\`router\\`, or \\`classifier\\` is required`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 7 — classifier and initialAgent both decide what runs first.\n // Coexistence is meaningless; throw loudly.\n if (hasClassifier && config.initialAgent) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`classifier\\` and \\`initialAgent\\` are mutually exclusive — both decide which intent runs first. Pick one.`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 3.4 (Q9) — evaluate now pairs with both `route` and\n // `router`. State-driven termination is useful in either dispatch\n // mode; the historical router-only restriction was incidental,\n // not principled.\n\n if (config.ack !== undefined) {\n const ack = config.ack;\n const isCallback = typeof ack === \"function\";\n const isAgentEntry =\n typeof ack === \"object\" &&\n ack !== null &&\n typeof (ack as { agent?: { execute?: unknown } }).agent?.execute === \"function\";\n const isRunEntry =\n typeof ack === \"object\" &&\n ack !== null &&\n typeof (ack as { run?: unknown }).run === \"function\";\n\n if (!isCallback && !isAgentEntry && !isRunEntry) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`ack\\` must be an \\`{ agent, ... }\\` entry, an \\`{ run, ... }\\` entry, or a bare callback function`,\n { context: { authoring: true } },\n );\n }\n\n if (isAgentEntry && isRunEntry) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`ack\\` cannot declare both \\`agent\\` and \\`run\\` — pick one`,\n { context: { authoring: true } },\n );\n }\n }\n\n if (config.maxIterations !== undefined && config.maxIterations < 1) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`maxIterations\\` must be >= 1`,\n { context: { authoring: true, maxIterations: config.maxIterations } },\n );\n }\n}\n\nfunction generateRunId(): string {\n return `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,WAKd,QAA8F;CAC9F,sBAAsB,MAA8C;CAEpE,MAAM,UAAU,qBAAqB,OAAO,SAAS,OAAO,IAAI;CAEhE,yBAAyB,QAAqC,OAAO;CAErE,IAAI,OAAO,gBAAgB,CAAC,QAAQ,IAAI,OAAO,YAAY,GACzD,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,wBAAwB,OAAO,aAAa,gCAC1E,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAGF,MAAM,YAAY,iBAAiB,QAAqC,OAAO;CAC/E,MAAM,UAAU,IAAI,kBAAkB,OAAO,EAAE;CAE/C,eAAe,QACb,OACA,SACoC;EAapC,MAAM,SAAS,MAAM,IAVC,oBAA6B;GACzC;GACR;GACA;GACA;GACA;GACA,OARY,SAAS,SAAS,cAAc;GAS5C;EACF,CAE6B,CAAC,CAAC,IAAI;EAUnC,MAAM,gBAAgB,OAAO,SAAS,OAAO,MAAoC;EAEjF,OAAO;CACT;CAEA,SAAS,OACP,OACA,SACkE;EAClE,MAAM,QAAQ,SAAS,SAAS,cAAc;EAC9C,MAAM,EAAE,YAAY,QAAQ,aAAa,uBAAkD;EAgB3F,AAAK,IAdiB,oBAA6B;GACzC;GACR;GACA;GACA;GACA;GACA;GACA;GACA,kBAAkB;EACpB,CAKa,CAAC,CACX,IAAI,CAAC,CACL,MAAM,WACL,gBAAgB,OAAO,SAAS,OAAO,MAAoC,CAC7E;EAEF,OAAO;CACT;CAEA,eAAe,OACb,OACA,SACoC;EACpC,MAAM,WAAW,MAAM,sBAAsB;GACnC;GACR;GACA;GACA;EACF,CAAC;EAaD,MAAM,SAAS,MAAM,IAXC,oBAA6B;GACzC;GACR;GACA;GACA;GACA,OAAO,SAAS;GAChB;GACA;GACA,YAAY;EACd,CAE6B,CAAC,CAAC,IAAI;EAEnC,MAAM,gBAAgB,OAAO,SAAS,OAAO,MAAoC;EAEjF,OAAO;CACT;CAEA,MAAM,WAAwC;EAC5C,MAAM,OAAO;EACb,aAAa,OAAO;EACpB;EACA;EACA;EACA;EACA,GACE,OACA,SACY;GACZ,OAAO,QAAQ,GAAG,OAAO,OAAO;EAClC;EACA,IAAwC,OAAU,SAA0C;GAC1F,QAAQ,IAAI,OAAO,OAAO;EAC5B;EACA,OACE,SACmC;GACnC,OAAO,OAA4B,UAAU,OAAO;EACtD;CACF;CAEA,OAAO;AACT;;;;;;AAOA,SAAS,sBAAyB,QAAmC;CACnE,IAAI,CAAC,OAAO,QAAQ,OAAO,OAAO,SAAS,UACzC,MAAM,IAAI,sBAAsB,0DAA0D,EACxF,SAAS,EAAE,WAAW,KAAK,EAC7B,CAAC;CAGH,IAAI,CAAC,OAAO,WAAW,OAAO,OAAO,YAAY,UAC/C,MAAM,IAAI,sBAAsB,kBAAkB,OAAO,KAAK,8BAA8B,EAC1F,SAAS,EAAE,WAAW,KAAK,EAC7B,CAAC;CAGH,MAAM,WAAW,OAAO,OAAO,UAAU;CACzC,MAAM,YAAY,CAAC,CAAC,OAAO;CAE3B,IAAI,WAAW;EACb,MAAM,SAAS,OAAO;EACtB,MAAM,cAAc,OAAQ,OAAiC,YAAY;EACzE,MAAM,cACJ,CAAC,eACD,OAAQ,OAA6C,UAAU,YAC/D,OAAQ,OAA6C,OAAO,YAAY;EAE1E,IAAI,CAAC,eAAe,CAAC,aACnB,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,2FAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAEJ;CAEA,IAAI,YAAY,WACd,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,8EAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAQF,MAAM,gBAAgB,OAAO,eAAe;CAE5C,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,eAC9B,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,kEAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAKF,IAAI,iBAAiB,OAAO,cAC1B,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,kHAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAQF,IAAI,OAAO,QAAQ,QAAW;EAC5B,MAAM,MAAM,OAAO;EACnB,MAAM,aAAa,OAAO,QAAQ;EAClC,MAAM,eACJ,OAAO,QAAQ,YACf,QAAQ,QACR,OAAQ,IAA0C,OAAO,YAAY;EACvE,MAAM,aACJ,OAAO,QAAQ,YACf,QAAQ,QACR,OAAQ,IAA0B,QAAQ;EAE5C,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,YACnC,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,0GAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;EAGF,IAAI,gBAAgB,YAClB,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,mEAC9B,EAAE,SAAS,EAAE,WAAW,KAAK,EAAE,CACjC;CAEJ;CAEA,IAAI,OAAO,kBAAkB,UAAa,OAAO,gBAAgB,GAC/D,MAAM,IAAI,sBACR,kBAAkB,OAAO,KAAK,qCAC9B,EAAE,SAAS;EAAE,WAAW;EAAM,eAAe,OAAO;CAAc,EAAE,CACtE;AAEJ;AAEA,SAAS,gBAAwB;CAC/B,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE;AACjF"}
1
+ {"version":3,"file":"supervisor.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/supervisor.ts"],"sourcesContent":["import type { SupervisorEventMap } from \"../contracts/events/event-map.type\";\nimport type { ExecutionReport } from \"../contracts/result/execution-report.type\";\nimport type { SupervisorResult } from \"../contracts/result/supervisor-result.type\";\nimport type { StreamContract } from \"../contracts/stream/stream.contract\";\nimport type { SupervisorIntentValue } from \"../contracts/supervisor/intent-entry.type\";\nimport type {\n SupervisorConfig,\n SupervisorEventHandler,\n} from \"../contracts/supervisor/supervisor-config.type\";\nimport type {\n SupervisorExecuteOptions,\n SupervisorResumeOptions,\n} from \"../contracts/supervisor/supervisor-execute-options.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport type { SupervisorStreamEvent } from \"../contracts/supervisor/supervisor-stream-event.type\";\nimport type {\n SupervisorAsToolOptions,\n SupervisorContract,\n} from \"../contracts/supervisor/supervisor.contract\";\nimport { SupervisorFailedError } from \"../errors\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport type { ToolContract } from \"../tool/tool\";\nimport { asTool } from \"./as-tool\";\nimport { SupervisorEmitter } from \"./emitter\";\nimport { assertRouterDescriptions, resolveIntentEntries } from \"./entries\";\nimport { SupervisorExecution } from \"./execution\";\nimport { computeSignature } from \"./signature\";\nimport { loadSnapshotForResume } from \"./snapshot\";\nimport { createSupervisorStream } from \"./supervisor-stream\";\n\n/**\n * `ai.supervisor(config)` — construct a `SupervisorContract`. Validates\n * the config at author time (throws `SupervisorFailedError` on bad\n * shape), resolves agent entries, computes a stable structural\n * signature, wires the three-tier event emitter, and returns an\n * instance that satisfies `ExecutableContract` so it can compose into\n * tools, outer agents, and (future) orchestrators uniformly.\n *\n * @example\n * const support = ai.supervisor({\n * name: \"customer-support\",\n * router: routerAgent,\n * intents: { triage, orderLookup, billingLookup, resolver },\n * evaluate: (ctx) => ctx.result.resolver?.output ? { satisfied: true } : undefined,\n * output: z.object({ response: z.string(), refund: z.boolean() }),\n * maxIterations: 6,\n * });\n */\nexport function supervisor<\n TOutput = unknown,\n TState = TOutput,\n TIntents extends Record<string, SupervisorIntentValue> = Record<string, SupervisorIntentValue>,\n TArtifacts = Record<string, unknown>,\n>(config: SupervisorConfig<TOutput, TState, TIntents, TArtifacts>): SupervisorContract<TOutput> {\n validateFactoryConfig(config as unknown as SupervisorConfig<TOutput>);\n\n const entries = resolveIntentEntries(config.intents, config.name);\n\n assertRouterDescriptions(config as SupervisorConfig<unknown>, entries);\n\n if (config.initialAgent && !entries.has(config.initialAgent)) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`initialAgent\\` \"${config.initialAgent}\" is not a key in \\`intents\\``,\n { context: { authoring: true } },\n );\n }\n\n const signature = computeSignature(config as SupervisorConfig<unknown>, entries);\n const emitter = new SupervisorEmitter(config.on);\n\n async function execute(\n input: SupervisorInput,\n options?: SupervisorExecuteOptions,\n ): Promise<SupervisorResult<TOutput>> {\n const runId = options?.runId ?? generateRunId();\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input,\n runId,\n options,\n });\n\n const result = await execution.run();\n\n // Route the finished report to any resolved observers (F1/F3).\n // Gated by `config.observe` + the global observe-all flag; observer\n // errors are swallowed inside `notifyObservers`. `ai.team(...)`\n // forwards its `observe` into this same config, so a team inherits\n // observability through here with no extra wiring. Bridge the\n // pre-existing `SupervisorReport = Omit<BaseReport, \"type\">` drift\n // (the report carries `type: \"supervisor\"` at runtime) so this call\n // site adds no new type error beyond the documented baseline.\n await notifyObservers(config.observe, result.report as unknown as ExecutionReport);\n\n return result;\n }\n\n function stream(\n input: SupervisorInput,\n options?: SupervisorExecuteOptions,\n ): StreamContract<SupervisorResult<TOutput>, SupervisorStreamEvent> {\n const runId = options?.runId ?? generateRunId();\n const { controller, stream: contract } = createSupervisorStream<SupervisorResult<TOutput>>();\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input,\n runId,\n options,\n streamController: controller,\n });\n\n // Route the finished report to resolved observers once the streamed\n // run settles. Attached to the run promise (not awaited — `stream`\n // returns synchronously); `notifyObservers` swallows observer errors.\n void execution\n .run()\n .then((result) =>\n notifyObservers(config.observe, result.report as unknown as ExecutionReport),\n );\n\n return contract;\n }\n\n async function resume(\n runId: string,\n options?: SupervisorResumeOptions,\n ): Promise<SupervisorResult<TOutput>> {\n const snapshot = await loadSnapshotForResume({\n config: config as SupervisorConfig<unknown>,\n signature,\n runId,\n options,\n });\n\n const execution = new SupervisorExecution<TOutput>({\n config: config as unknown as SupervisorConfig<TOutput>,\n entries,\n signature,\n emitter,\n input: snapshot.input,\n runId,\n options,\n resumeFrom: snapshot,\n });\n\n const result = await execution.run();\n\n await notifyObservers(config.observe, result.report as unknown as ExecutionReport);\n\n return result;\n }\n\n const instance: SupervisorContract<TOutput> = {\n name: config.name,\n inputSchema: config.inputSchema,\n signature,\n execute,\n stream,\n resume,\n on<K extends keyof SupervisorEventMap>(\n event: K,\n handler: SupervisorEventHandler<K>,\n ): () => void {\n return emitter.on(event, handler);\n },\n off<K extends keyof SupervisorEventMap>(event: K, handler: SupervisorEventHandler<K>): void {\n emitter.off(event, handler);\n },\n asTool<TToolInput = string>(\n options: SupervisorAsToolOptions<TToolInput>,\n ): ToolContract<TToolInput, TOutput> {\n return asTool<TOutput, TToolInput>(instance, options);\n },\n };\n\n return instance;\n}\n\n/**\n * Factory-time validation. Enforces the XOR + pairing rules the design\n * locked in §2 and surfaces any violation as a typed\n * `SupervisorFailedError` tagged `authoring: true`.\n */\nfunction validateFactoryConfig<T>(config: SupervisorConfig<T>): void {\n if (!config.name || typeof config.name !== \"string\") {\n throw new SupervisorFailedError(\"ai.supervisor: `name` is required and must be a string\", {\n context: { authoring: true },\n });\n }\n\n if (!config.intents || typeof config.intents !== \"object\") {\n throw new SupervisorFailedError(`ai.supervisor(\"${config.name}\"): \\`intents\\` is required`, {\n context: { authoring: true },\n });\n }\n\n const hasRoute = typeof config.route === \"function\";\n const hasRouter = !!config.router;\n\n if (hasRouter) {\n const router = config.router as { execute?: unknown } | { agent?: { execute?: unknown } };\n const isBareAgent = typeof (router as { execute?: unknown }).execute === \"function\";\n const isEntryForm =\n !isBareAgent &&\n typeof (router as { agent?: { execute?: unknown } }).agent === \"object\" &&\n typeof (router as { agent?: { execute?: unknown } }).agent?.execute === \"function\";\n\n if (!isBareAgent && !isEntryForm) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`router\\` must be an agent contract or a \\`{ agent, placeholders?, input? }\\` entry`,\n { context: { authoring: true } },\n );\n }\n }\n\n if (hasRoute && hasRouter) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`route\\` and \\`router\\` are mutually exclusive — configure exactly one`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 7 / decisions §37 — `classifier` is the iter-0 prelude;\n // satisfies the \"must have a dispatch source\" rule on its own.\n // Composes with router/route (classifier drives iter 0; router/route\n // takes iter 1+). When configured alone, supervisor terminates after\n // iter 0's branch settles.\n const hasClassifier = config.classifier !== undefined;\n\n if (!hasRoute && !hasRouter && !hasClassifier) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): one of \\`route\\`, \\`router\\`, or \\`classifier\\` is required`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 7 — classifier and initialAgent both decide what runs first.\n // Coexistence is meaningless; throw loudly.\n if (hasClassifier && config.initialAgent) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`classifier\\` and \\`initialAgent\\` are mutually exclusive — both decide which intent runs first. Pick one.`,\n { context: { authoring: true } },\n );\n }\n\n // Phase 3.4 (Q9) — evaluate now pairs with both `route` and\n // `router`. State-driven termination is useful in either dispatch\n // mode; the historical router-only restriction was incidental,\n // not principled.\n\n if (config.ack !== undefined) {\n const ack = config.ack;\n const isCallback = typeof ack === \"function\";\n const isAgentEntry =\n typeof ack === \"object\" &&\n ack !== null &&\n typeof (ack as { agent?: { execute?: unknown } }).agent?.execute === \"function\";\n const isRunEntry =\n typeof ack === \"object\" &&\n ack !== null &&\n typeof (ack as { run?: unknown }).run === \"function\";\n\n if (!isCallback && !isAgentEntry && !isRunEntry) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`ack\\` must be an \\`{ agent, ... }\\` entry, an \\`{ run, ... }\\` entry, or a bare callback function`,\n { context: { authoring: true } },\n );\n }\n\n if (isAgentEntry && isRunEntry) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`ack\\` cannot declare both \\`agent\\` and \\`run\\` — pick one`,\n { context: { authoring: true } },\n );\n }\n }\n\n if (config.maxIterations !== undefined && config.maxIterations < 1) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`maxIterations\\` must be >= 1`,\n { context: { authoring: true, maxIterations: config.maxIterations } },\n );\n }\n\n // Width bound on parallel dispatch (see `maxFanOut` docs). Same\n // authoring-error shape as `maxIterations`, but integer-only — a\n // fractional cap would silently reject legitimate widths.\n if (\n config.maxFanOut !== undefined &&\n (!Number.isInteger(config.maxFanOut) || config.maxFanOut < 1)\n ) {\n throw new SupervisorFailedError(\n `ai.supervisor(\"${config.name}\"): \\`maxFanOut\\` must be an integer >= 1`,\n { context: { authoring: true, maxFanOut: config.maxFanOut } },\n );\n }\n}\n\nfunction generateRunId(): string {\n return `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -440,7 +440,7 @@ Only a *handler bug* — a non-sentinel throw from your handler — propagates,
440
440
 
441
441
  ---
442
442
  name: attach-ai-middleware
443
- description: 'Wire agent middleware — ai.middleware.budget (token / USD caps + SLO/cost contract w/ maxLatencyMs + onViolation fallback), ai.middleware.guardrail (pre / post content checks), ai.middleware.semanticCache (exact + vector cache), supervisor-level middleware, plus authoring custom hooks (execute / trip / tool). Triggers: `ai.middleware.budget`, `ai.middleware.guardrail`, `ai.middleware.semanticCache`, `ai.middleware.compose`, `ai.middleware.forTool`, `AgentMiddleware`, `BudgetExceededError`, `GuardrailViolationError`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`; ''cap token cost'', ''SLO budget'', ''block pii in prompts'', ''semantic cache before LLM'', ''supervisor-level middleware'', ''write custom hook''; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; cache drivers — `@warlock.js/ai/persist-ai-data/SKILL.md`; competing libs `langchain` callbacks.'
443
+ description: 'Wire agent middleware — ai.middleware.budget (token / USD caps + SLO/cost contract w/ maxLatencyMs + onViolation fallback), ai.middleware.guardrail (pre / post content checks), ai.middleware.semanticCache (exact + vector cache), supervisor-level middleware, plus authoring custom hooks (execute / trip / tool). Triggers: `ai.middleware.budget`, `ai.middleware.guardrail`, `ai.middleware.semanticCache`, `ai.middleware.compose`, `ai.middleware.forTool`, `AgentMiddleware`, `BudgetExceededError`, `GuardrailViolationError`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`, `SemanticCacheOptions`, `SemanticCacheScope`; ''cap token cost'', ''SLO budget'', ''block pii in prompts'', ''semantic cache before LLM'', ''supervisor-level middleware'', ''write custom hook'', ''isolate semantic cache per session/tenant''; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; cache drivers — `@warlock.js/ai/persist-ai-data/SKILL.md`; competing libs `langchain` callbacks.'
444
444
  ---
445
445
 
446
446
  # Middleware — agent-level pipeline
@@ -545,6 +545,7 @@ ai.middleware.semanticCache({
545
545
  threshold: 0.95,
546
546
  ttlMs: 60 * 60 * 1000,
547
547
  namespace: "support-faq",
548
+ // scope: "session" (default) — see below
548
549
  });
549
550
  ```
550
551
 
@@ -558,6 +559,21 @@ ai.middleware.semanticCache({
558
559
  - **Trip-zero only** — only first-trip responses are cached. Tool-using loops never serve cached tool-call responses (would infinite-loop).
559
560
  - **Never use memory drivers in production** — linear scan per query.
560
561
 
562
+ ### Session-scoped by default — `scope` (4.15.0)
563
+
564
+ A `semanticCache` is normally built once at app boot and shared by every end user, and a hit is returned as the model's answer with **no LLM call in between** — so without isolation, user B's merely-*similar* prompt could be served user A's cached answer, personal context included. `SemanticCacheOptions.scope` (default `"session"`) keys every entry off the run's `AgentExecuteOptions.sessionId` (`"session:<id>"`) and re-checks it as exact equality on read — the key alone never authorizes a hit.
565
+
566
+ ```ts
567
+ ai.middleware.semanticCache({ embedder, threshold: 0.95, scope: "shared" }); // opt back into one shared pool
568
+ ai.middleware.semanticCache({ embedder, threshold: 0.95, scope: (ctx) => tenantIdFrom(ctx) }); // custom boundary
569
+ ```
570
+
571
+ - **`"session"`** (default) — isolated per `sessionId`; a run made *without* a `sessionId` shares one unscoped pool (unchanged behavior for those calls). Thread `sessionId` through `agent.execute()` to get the isolation — composite primitives (supervisor, orchestrator) already forward their own.
572
+ - **`"shared"`** — one pool for every caller, regardless of session — the pre-4.15.0 behavior. The explicit opt-in for genuinely public Q&A (docs bot, FAQ) where cross-user hit rate is the point and no response can carry a caller's private context.
573
+ - **`(context) => key | undefined`** — derive your own boundary, e.g. per tenant. Returning `undefined` falls back to the unscoped pool.
574
+
575
+ Entries written before the upgrade are unscoped and are only read by unscoped (or `"shared"`) runs. The vector lookup overscans before filtering (mirroring the memory tiers) so a noisy foreign scope can't occupy the top-`k` and mask a caller's own hit.
576
+
561
577
  ## Writing your own middleware
562
578
 
563
579
  One object. Any subset of three hook maps.
@@ -3230,6 +3246,8 @@ await kb.index(await ai.rag.loadWeb("https://docs.example.com/guide", {
3230
3246
 
3231
3247
  HTML responses run through the same tag-strip pass as `loadHtml`; non-HTML text (`text/plain`, markdown) is used verbatim. `metadata.source` is the resolved URL, `metadata.contentType` the server-reported type. A non-OK response, a policy block, a timeout, or an over-cap body throws `OutboundPolicyError`.
3232
3248
 
3249
+ **Redirects are re-validated per hop, not delegated to the platform (4.15.0).** A page a crawl reaches can `3xx` — `guardedFetch` re-runs each `Location` through the same scheme/host/private-IP checks before following it, capped at `policy.maxRedirects` (default `5`), and strips `authorization`/`cookie`/`proxy-authorization` on a cross-origin hop. So a redirect can never smuggle `loadWeb` into a private/metadata address the original URL couldn't have reached. Full guard detail (including `assertUrlAllowed`, `fetchTextWithPolicy`, and the other call sites sharing it): [`@warlock.js/ai/secure-outbound-requests/SKILL.md`](@warlock.js/ai/secure-outbound-requests/SKILL.md).
3250
+
3233
3251
  ### `loadPdf` — lazy optional peer, page-precise citations
3234
3252
 
3235
3253
  `pdf-parse` is an **optional** peer, dynamic-imported on the FIRST `loadPdf` call — importing `@warlock.js/ai` never forces it. When it is absent, the curated `PDF_PARSE_INSTALL_INSTRUCTIONS` string is thrown as a plain `Error` (a missing infra peer, not a content problem), never a raw module-resolution stack trace.
@@ -3328,6 +3346,7 @@ The `embedder`'s `dimensions` MUST equal the store's `dimensions` — a mismatch
3328
3346
 
3329
3347
  - [[run-ai-rag]] — the chunk → embed → retrieve → rerank → cite pipeline that **consumes** these loaders and stores (`ai.rag({ embedder, store })`, `index()` / `retrieve()`).
3330
3348
  - [[embed-text]] — the `sdk.embedder` primitive whose `dimensions` must match the store's `vector(N)` width.
3349
+ - [[secure-outbound-requests]] — the full `guardedFetch` / `OutboundPolicy` guard `loadWeb` delegates to, including per-hop redirect revalidation and the other consumers sharing it.
3331
3350
  - [`@warlock.js/cache/use-cache-similarity/SKILL.md`](@warlock.js/cache/use-cache-similarity/SKILL.md) — the cache driver `cacheVectorStore` adapts.
3332
3351
 
3333
3352
 
@@ -3753,6 +3772,8 @@ attachments: [
3753
3772
 
3754
3773
  Model must declare `capabilities.vision`. OpenAI adapter auto-infers from name; override with `openai.model({ name, vision: true })`.
3755
3774
 
3775
+ A URL *image* attachment is passed to the provider as a URL — the provider fetches it, not the framework, so there's no server-side SSRF surface. A **remote `{ type: "text", source: <url> }` attachment IS fetched server-side** (the adapter needs the raw text inline) and is default-DENY: it throws unless `attachmentPolicy.allowRemoteFetch: true`, and when enabled runs through the shared `guardedFetch` / `OutboundPolicy` guard — see [`@warlock.js/ai/secure-outbound-requests/SKILL.md`](@warlock.js/ai/secure-outbound-requests/SKILL.md).
3776
+
3756
3777
  ## Pattern — streaming
3757
3778
 
3758
3779
  ```ts
@@ -3865,6 +3886,7 @@ The one field a bare agent config doesn't surface ergonomically is `budget` (`Bu
3865
3886
  - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — tool wiring + schema validation
3866
3887
  - [`@warlock.js/ai/write-system-prompt/SKILL.md`](@warlock.js/ai/write-system-prompt/SKILL.md) — persona / instruction builders
3867
3888
  - [`@warlock.js/ai/handle-ai-errors/SKILL.md`](@warlock.js/ai/handle-ai-errors/SKILL.md) — `AIError` hierarchy
3889
+ - [`@warlock.js/ai/secure-outbound-requests/SKILL.md`](@warlock.js/ai/secure-outbound-requests/SKILL.md) — the `guardedFetch` / `OutboundPolicy` guard behind a remote text attachment fetch
3868
3890
 
3869
3891
 
3870
3892
  ## run-ai-rag `@warlock.js/ai/run-ai-rag/SKILL.md`
@@ -4556,11 +4578,14 @@ ai.orchestrator({
4556
4578
  recall: { k: 5, threshold: 0.7, tier: "semantic" }, // k: 0 = write-only memory
4557
4579
  remember: true, // false = read-only (recall, never write)
4558
4580
  rememberTier: "semantic",
4581
+ scope: "session", // DEFAULT — isolate memories per sessionId
4559
4582
  injectKey: "memories", // ctx.context[injectKey] holds RecalledMemory[]
4560
4583
  },
4561
4584
  });
4562
4585
  ```
4563
4586
 
4587
+ **Memory is session-scoped by default (4.15.0).** One store instance backs every session of the orchestrator, so `scope` decides what a turn may read: `"session"` (default) keys recall + write-back to the executing `sessionId`, so one user can never recall another's remembered turns. `"shared"` pools every session into one namespace — the pre-4.15.0 behavior, safe only when every session is trusted to see every other's memories. `(sessionId) => key` derives your own boundary (e.g. a tenant id). Memories written before 4.15.0 are unscoped and are only visible under `scope: "shared"`.
4588
+
4564
4589
  Recalled memories land in the per-turn `context` bag under `injectKey` (default `"memories"`) — every route / router / evaluate / dispatch callback reads them at `ctx.context.memories`. Memory never mutates the prompt itself; surfacing it stays explicit. Cancelled / failed turns never remember (they revert), regardless of `remember`. See [`@warlock.js/ai/use-ai-memory/SKILL.md`](@warlock.js/ai/use-ai-memory/SKILL.md).
4565
4590
 
4566
4591
  ## `asTool()` — orchestrator as a tool
@@ -4578,7 +4603,9 @@ const concierge = ai.agent({ model, tools: [supportTool] });
4578
4603
 
4579
4604
  The tool boundary is **opaque**: the parent's `signal` / `context` / events do NOT auto-forward — anything the wrapped orchestrator needs must ride on the `inputSchema` payload. `sessionScope`:
4580
4605
  - **`"fresh"`** (default) — each invocation gets a generated `sessionId` and empty history; no continuity across calls.
4581
- - **`"shared"`** — the parent threads `sessionId` (and optionally `history`) through the validated payload; the orchestrator participates in that session. A missing/blank `sessionId` throws.
4606
+ - **`"shared"`** — the orchestrator joins an existing session named by the DEVELOPER through `session`, never by the model: either a literal id fixed at construction (`session: "sess_42"`) or a resolver reading the out-of-band tool context (`session: (ctx) => String(ctx?.artifacts?.supportSessionId)`). Building a `"shared"` tool without `session` throws at construction, and `sessionId` / `history` in the payload are stripped, not honored.
4607
+
4608
+ A `sessionId` is bearer-equivalent to read/write on that session, so it must not be a model-visible `inputSchema` field: before 4.15.0 it was, and a prompt injection reaching the outer agent could make the nested orchestrator resume, mutate, and echo back a *victim's* conversation. `unsafeAllowModelSessionId: true` restores the old payload path — only for a fully trusted outer context where you verify session ownership yourself.
4582
4609
 
4583
4610
  ## Drift detection
4584
4611
 
@@ -4610,7 +4637,7 @@ await orch.execute(input, { sessionId, history, on: { "orchestrator.drift.checke
4610
4637
 
4611
4638
  ---
4612
4639
  name: run-planner
4613
- description: 'Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered execution plan over your registered capabilities (agents / workflows / supervisors / tools), then the planner EXECUTES it, threading each step output into the next, and returns the unified {data, report, usage, error} envelope with report.type "planner". Supports DAG scheduling (dag:true + maxConcurrency off dependsOn), adaptive re-planning (replan:{maxReplans} + the onStep continue/abort/replan directive), and plan-only / approval (mode:"plan-only" → status "awaiting-approval" → approvedPlan). A plan step may delegate via ai.spawnSubAgent({...}) — a GENERAL one-shot-agent helper covered in `@warlock.js/ai/run-ai-agent/SKILL.md`; it is not planner-specific. Triggers: `ai.planner`, `planner.execute`, `spawnSubAgent`, `PlannerConfig`, `PlannerCapability`, `PlannerResult`, `PlannerReport`, `PlannerPlan`, `PlannerStep`, `PlannerStepDirective`, `PlannerPlanInvalidError`, `maxSteps`, `dag`, `maxConcurrency`, `dependsOn`, `replan`, `onStep`, `mode`, `approvedPlan`, `awaiting-approval`, `report.plan`, `report.executedSteps`; ''let the model plan the steps'', ''dynamic plan from a goal'', ''run independent steps in parallel'', ''re-plan when a step fails'', ''generate a plan for approval before running it''; typical import `import { ai } from "@warlock.js/ai"`. Skip: a FIXED known pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; routing one input to a specialist each turn — `@warlock.js/ai/run-supervisor/SKILL.md`; a single model + tools call — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.'
4640
+ description: 'Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered execution plan over your registered capabilities (agents / workflows / supervisors / tools), then the planner EXECUTES it, threading each step output into the next, and returns the unified {data, report, usage, error} envelope with report.type "planner". Supports DAG scheduling (dag:true + maxConcurrency off dependsOn), adaptive re-planning (replan:{maxReplans} + the onStep continue/abort/replan directive), and plan-only / approval (mode:"plan-only" → status "awaiting-approval" → approvedPlan). A plan step may delegate via ai.spawnSubAgent({...}) — a GENERAL one-shot-agent helper covered in `@warlock.js/ai/run-ai-agent/SKILL.md`; it is not planner-specific. Triggers: `ai.planner`, `planner.execute`, `spawnSubAgent`, `PlannerConfig`, `PlannerCapability`, `PlannerResult`, `PlannerReport`, `PlannerPlan`, `PlannerStep`, `PlannerStepDirective`, `PlannerPlanInvalidError`, `maxSteps`, `dag`, `maxConcurrency`, `dependsOn`, `replan`, `onStep`, `mode`, `approvedPlan`, `awaiting-approval`, `report.plan`, `report.executedSteps`, `parsedStepCeiling`; ''let the model plan the steps'', ''dynamic plan from a goal'', ''run independent steps in parallel'', ''re-plan when a step fails'', ''generate a plan for approval before running it''; typical import `import { ai } from "@warlock.js/ai"`. Skip: a FIXED known pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; routing one input to a specialist each turn — `@warlock.js/ai/run-supervisor/SKILL.md`; a single model + tools call — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.'
4614
4641
  ---
4615
4642
 
4616
4643
  # `ai.planner()` — LLM-generated, then executed, plans
@@ -4640,7 +4667,7 @@ const research = ai.planner({
4640
4667
  { name: "summarize", description: "Summarize text into bullet points", executable: summarizer },
4641
4668
  { name: "write", description: "Draft a final report", executable: writerAgent },
4642
4669
  ],
4643
- maxSteps: 6, // hard cap; steps beyond it are recorded as "skipped"
4670
+ maxSteps: 6, // soft cap; steps beyond it are recorded as "skipped" — see the parse-time ceiling below
4644
4671
  });
4645
4672
 
4646
4673
  const { data, report, usage, error } = await research.execute("Compare React vs Vue in 2026");
@@ -4663,6 +4690,10 @@ for (const step of report.executedSteps) { // forensic, in execution order
4663
4690
 
4664
4691
  `report.type === "planner"`; `report.children[]` carries every dispatched capability report (plus the planning trip), with usage rolled up. `report.executedSteps` is the authoritative per-step record (`PlannerStepSnapshot[]`). Lazy capability loading is **deferred** — every capability is fully constructed up front.
4665
4692
 
4693
+ ### Parse-time step ceiling (4.15.0)
4694
+
4695
+ `maxSteps` can't be expressed in the strict-mode JSON Schema the planning model is given (no `maxItems`), so a provider/proxy that ignores the prompt's step budget could make the planner deserialize an arbitrarily long `steps[]` array before `PlannerRun`'s tail-truncation logic ever ran — `maxSteps` only trimmed *after* the whole array was already parsed and normalized. Plan validation now enforces a hard **parse-time** ceiling of `maxSteps * 4` (or `100` when the schema is built without a `maxSteps`) and **rejects** — rather than truncates — a plan that exceeds it, surfacing `PlannerPlanInvalidError`. The 4× slack keeps the normal case (a model overshooting "at most N steps" slightly) working exactly as before — that overshoot is still truncated to `skipped` steps at execution time, not rejected at parse time. A plan several times its budget is treated as a malfunction worth surfacing, not a prefix worth silently executing.
4696
+
4666
4697
  ## DAG scheduling — `dag: true` + `maxConcurrency`
4667
4698
 
4668
4699
  Run independent steps in parallel instead of array-order:
@@ -4729,7 +4760,7 @@ const final = await planner.execute(goal, { approvedPlan: draft.plan! });
4729
4760
 
4730
4761
  `execute()` never throws — failures surface on `result.error`:
4731
4762
 
4732
- - **`PlannerPlanInvalidError`** (`PLANNER_PLAN_INVALID`, category `schema`) — empty plan, a step naming an unknown capability, a DAG cycle, a `dependsOn` naming an unknown step, a stale `approvedPlan`, or a final-output validation failure.
4763
+ - **`PlannerPlanInvalidError`** (`PLANNER_PLAN_INVALID`, category `schema`) — empty plan, a step naming an unknown capability, a DAG cycle, a `dependsOn` naming an unknown step, a stale `approvedPlan`, a final-output validation failure, or (4.15.0) a plan exceeding the parse-time step ceiling (`maxSteps * 4`, default `100`).
4733
4764
  - **`PlannerCancelledError`** (`PLANNER_CANCELLED`, category `cancelled`) — the `AbortSignal` fired. `report.status === "cancelled"`, `report.cancelledAt` set; remaining steps are `skipped`.
4734
4765
  - A child capability's own error (agent / tool / provider) flows through unchanged on the failing step's snapshot and as `result.error`. The planner stops at the first failed step and marks the rest `skipped`.
4735
4766
  - **`PlannerFailedError`** is the base for the `PLANNER_*` family.
@@ -4855,6 +4886,16 @@ ai.supervisor({
4855
4886
 
4856
4887
  Each key references the same underlying unit; the description defaults to the unit's. Override the key base with `{ keyPrefix }` and the per-entry text with `{ description }`.
4857
4888
 
4889
+ #### `maxFanOut` — width cap (default `10`)
4890
+
4891
+ `maxIterations` bounds how DEEP a run goes; `maxFanOut` bounds how WIDE one decision goes. Duplicate intent names in a fan-out array are collapsed silently (branch results are indexed by intent — duplicates only burn tokens); if the DEDUPED list is still longer than the cap, the decision is rejected with `SupervisorRoutingError` (`SUPERVISOR_INVALID_ROUTE`), same as an unknown intent name. Applies to every dispatch source: `router`, `route`, `evaluate.reassignTo`, `intent.next`.
4892
+
4893
+ ```ts
4894
+ ai.supervisor({ intents: { ...ai.fanOut(writer, 20), vote }, maxFanOut: 20, route });
4895
+ ```
4896
+
4897
+ Raise it deliberately when you fan out wider than 10. Why it exists: the router's per-turn prompt embeds supervisor `state` and prior branch outputs, so text injected into a tool result can push an LLM router to emit a very wide `next` array — every element a real agent/workflow run, all inside the allowlist.
4898
+
4858
4899
  ## The `intents` map — five accepted shapes
4859
4900
 
4860
4901
  ```ts
@@ -4902,7 +4943,7 @@ const refundSupervisor = ai.supervisor<RefundOutput>({
4902
4943
  });
4903
4944
  ```
4904
4945
 
4905
- Each branch's output strip-merges into state per its declared `output` schema. Last-write-wins on fan-out conflict (warning logged).
4946
+ Each branch's output strip-merges into state per its declared `output` schema. Last-write-wins on fan-out conflict (warning logged). Keys named `__proto__` / `constructor` / `prototype` are dropped from every merged slice (branch output, `ack`, classifier, `refine`, artifacts) and logged as `state.merge.unsafe-key` — a permissive `output` schema would otherwise let a model-supplied key repoint the run state's prototype.
4906
4947
 
4907
4948
  ## Per-intent `next` — skip the router
4908
4949
 
@@ -5122,6 +5163,95 @@ const escalationAgent = ai.agent({ model, tools: [supportTool] });
5122
5163
  - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — tool artifacts side-channel
5123
5164
 
5124
5165
 
5166
+ ## secure-outbound-requests `@warlock.js/ai/secure-outbound-requests/SKILL.md`
5167
+
5168
+ ---
5169
+ name: secure-outbound-requests
5170
+ description: 'The shared SSRF / resource-exhaustion guard every server-side outbound HTTP request in the framework goes through — `guardedFetch(url, policy, init?)`, `OutboundPolicy`, `assertUrlAllowed`, `fetchTextWithPolicy`, `readTextCapped`. Scheme allowlist (https-only default), host allowlist, post-DNS private/loopback/link-local/metadata-address deny, byte cap, timeout, and (4.15.0) per-hop redirect revalidation with a `maxRedirects` cap and cross-origin credential stripping. Consumed by `ai.rag.loadWeb`, remote text attachments (`prepareAttachmentPart`), and the skills `urlSource` manifest fetch — never a raw `fetch()` on a caller-influenced URL. Triggers: `guardedFetch`, `OutboundPolicy`, `ResolvedOutboundPolicy`, `assertUrlAllowed`, `fetchTextWithPolicy`, `readTextCapped`, `resolveOutboundPolicy`, `OutboundPolicyError`, `maxRedirects`, `denyPrivateIPsAfterDNS`, `hostAllowlist`, `allowedSchemes`, `maxBytes`, `SSRF`, `redirect: "manual"`, `redirect: "error"`; ''SSRF-safe fetch'', ''block a redirect into a private IP'', ''fetch a URL an agent gave me'', ''cap outbound response size'', ''allowlist hosts for outbound requests'', ''strip auth headers on a cross-origin redirect''; typical import `import { guardedFetch, assertUrlAllowed } from "@warlock.js/ai"` (also re-exported per call site). Skip: the RAG loader that wraps this for `loadWeb` — `@warlock.js/ai/rag-loaders-and-stores/SKILL.md`; the skills manifest source that wraps this for `urlSource` — `@warlock.js/ai/use-runtime-skills/SKILL.md`; prompt-injection / content guardrails (a different trust boundary) — `@warlock.js/ai/guard-input-output/SKILL.md` (ai-guard package).'
5171
+ ---
5172
+
5173
+ # Outbound request policy — the SSRF guard
5174
+
5175
+ One `OutboundPolicy` + `guardedFetch` backs **every** server-side HTTP request the framework makes on behalf of user/model-controlled input: `ai.rag.loadWeb`, the remote-text branch of `prepareAttachmentPart` (agent `attachments`), and the skills catalog `urlSource` manifest fetch. A single audited guard instead of N ad-hoc `fetch()` call sites.
5176
+
5177
+ ```ts
5178
+ import { guardedFetch, fetchTextWithPolicy, assertUrlAllowed, OutboundPolicyError } from "@warlock.js/ai";
5179
+
5180
+ const response = await guardedFetch("https://docs.example.com/page", {
5181
+ hostAllowlist: ["docs.example.com"],
5182
+ maxBytes: 2_000_000,
5183
+ timeoutMs: 5_000,
5184
+ });
5185
+ ```
5186
+
5187
+ ## Strict-by-default policy
5188
+
5189
+ Every field is optional; `resolveOutboundPolicy` fills safe defaults, so an untuned call is already hardened:
5190
+
5191
+ | Field | Default | Guards against |
5192
+ | --- | --- | --- |
5193
+ | `allowedSchemes` | `["https"]` | plaintext / `file:` / `data:` exfil — `http` must be opted in |
5194
+ | `hostAllowlist` | unset (any host) | pinning outbound targets to known hosts, e.g. `docs.example.com` allows `a.docs.example.com` |
5195
+ | `denyPrivateIPsAfterDNS` | `true` | **the SSRF guard itself** — resolves the host through DNS and rejects loopback / private / link-local / unique-local / cloud-metadata (`169.254.169.254`) addresses; a public hostname that resolves inward is caught |
5196
+ | `maxRedirects` | `5` | a redirect chain used to bypass the checks above (4.15.0 — see below) |
5197
+ | `maxBytes` | `5_242_880` (5 MiB) | unbounded response bodies |
5198
+ | `timeoutMs` | `10_000` | a hung/slow endpoint tying up the request |
5199
+ | `signal` | unset | caller-supplied `AbortSignal`, merged with the internal timeout |
5200
+ | `fetch` | global `fetch` | inject a stub for tests, or a wrapper enforcing your own app-level rules |
5201
+
5202
+ Every violation throws `OutboundPolicyError` with `context` carrying the offending URL/host/address — never a silent fallback.
5203
+
5204
+ ## Redirects are never delegated to the platform (4.15.0)
5205
+
5206
+ Before 4.15.0, `assertUrlAllowed` validated only the *initial* URL, then handed the request to `fetch` with automatic redirect following — so a URL that passed validation could `3xx` into a private/metadata address or an off-allowlist host with no re-check.
5207
+
5208
+ `guardedFetch` now issues **every hop** with `redirect: "manual"` and re-runs the `Location` header through the exact same `assertUrlAllowed` (scheme, host allowlist, post-DNS private-IP deny) before following it:
5209
+
5210
+ - Capped at `policy.maxRedirects` (default `5`) — the `(maxRedirects + 1)`th hop throws `OutboundPolicyError`.
5211
+ - **Credential headers stripped cross-origin.** `authorization`, `cookie`, `proxy-authorization` are dropped the moment a hop's target origin differs from the current one — a redirect can't exfiltrate credentials meant for the original host.
5212
+ - **Method/body semantics match platform behavior.** `303` — and the legacy convention of `301`/`302` on a non-`GET`/`HEAD` method — re-issue the next hop as a bodyless `GET`.
5213
+ - Pass `init.redirect: "manual"` to get the raw 3xx response back (no following, no throw); `init.redirect: "error"` rejects on any redirect.
5214
+ - The net effect: a redirect can never reach a URL the original request could not have reached directly.
5215
+
5216
+ ```ts
5217
+ // A caller that wants to inspect redirects itself, unfollowed:
5218
+ const res = await guardedFetch(url, policy, { redirect: "manual" });
5219
+ if (res.status >= 300 && res.status < 400) {
5220
+ console.log(res.headers.get("location"));
5221
+ }
5222
+ ```
5223
+
5224
+ ## Reading the body — `readTextCapped` / `fetchTextWithPolicy`
5225
+
5226
+ `guardedFetch` returns the raw `Response`; read its body through `readTextCapped(response, maxBytes)` to enforce the cap (a declared `content-length` over the cap fails fast, otherwise the stream is read chunk-by-chunk and aborted the moment the running total exceeds it). `fetchTextWithPolicy(url, policy, init?)` is the one-call convenience — `guardedFetch` + `readTextCapped`, returning `{ ok, status, statusText, text }` (body only read when `ok`).
5227
+
5228
+ ```ts
5229
+ const { ok, status, text } = await fetchTextWithPolicy(url, { hostAllowlist: ["api.example.com"] });
5230
+ if (!ok) throw new Error(`fetch failed: ${status}`);
5231
+ ```
5232
+
5233
+ ## Who consumes this
5234
+
5235
+ | Call site | Entry point | Notes |
5236
+ | --- | --- | --- |
5237
+ | RAG web loader | `ai.rag.loadWeb(url, { policy })` | [`@warlock.js/ai/rag-loaders-and-stores/SKILL.md`](@warlock.js/ai/rag-loaders-and-stores/SKILL.md) |
5238
+ | Remote text attachment | `prepareAttachmentPart` via `agent.execute({ attachments })` | default-DENY — requires `attachmentPolicy.allowRemoteFetch: true`; policy travels as `attachmentPolicy.outbound`. URL *image* attachments are handed to the provider as a URL and never fetched server-side, so they carry no SSRF surface here |
5239
+ | Skills catalog manifest | `ai.skills({ sources: [urlSource(url, { policy })] })` | [`@warlock.js/ai/use-runtime-skills/SKILL.md`](@warlock.js/ai/use-runtime-skills/SKILL.md) — the fetched manifest is also runtime-validated record-by-record before being trusted |
5240
+
5241
+ Each call site passes its own `policy` (or `{}` for the strict defaults) — there is no global policy singleton, so tune per source (e.g. `hostAllowlist` for a known-good docs domain vs. an open web crawl).
5242
+
5243
+ ## Testing
5244
+
5245
+ Inject a stubbed `policy.fetch` (`(url, init) => Response`) instead of hitting the network — every consumer above accepts `policy.fetch` all the way through. Regression coverage lives in `src/security/outbound-policy.spec.ts` (redirect-to-metadata/loopback/private block, off-allowlist redirect block, hop cap, credential stripping, clean-redirect follow).
5246
+
5247
+ ## See also
5248
+
5249
+ - [`@warlock.js/ai/rag-loaders-and-stores/SKILL.md`](@warlock.js/ai/rag-loaders-and-stores/SKILL.md) — `loadWeb`, the primary consumer
5250
+ - [`@warlock.js/ai/use-runtime-skills/SKILL.md`](@warlock.js/ai/use-runtime-skills/SKILL.md) — `urlSource`'s manifest fetch
5251
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — `attachments`, including the remote-text fetch path
5252
+ - [`@warlock.js/ai/handle-ai-errors/SKILL.md`](@warlock.js/ai/handle-ai-errors/SKILL.md) — `OutboundPolicyError`
5253
+
5254
+
5125
5255
  ## transcribe-audio `@warlock.js/ai/transcribe-audio/SKILL.md`
5126
5256
 
5127
5257
  ---
@@ -5287,14 +5417,14 @@ Scripting `[{ error: new ProviderRateLimitError("slow down") }]` drives the neve
5287
5417
 
5288
5418
  ---
5289
5419
  name: use-ai-memory
5290
- description: 'Agent memory with ai.memory({...}) — a provider-neutral store with FOUR tiers: WORKING (in-run scratch, recalled by recency), SEMANTIC (durable facts by cosine similarity over a @warlock.js/cache vector driver via .similar()), EPISODIC (durable events, similarity blended with recency), and PROCEDURAL (durable how-tos, similarity blended with reinforcement). remember() / recall() / clear(); wire it into ai.orchestrator({ memory }). Triggers: `ai.memory`, `memory.remember`, `memory.recall`, `memory.clear`, `MemoryContract`, `MemoryConfig`, `MemoryItem`, `RecalledMemory`, `MemoryTier`, `SemanticMemoryConfig`, `EpisodicMemoryConfig`, `ProceduralMemoryConfig`, `working`, `semantic`, `episodic`, `procedural`, `defaultTier`, `threshold`, `recencyWeight`, `halfLifeMs`, `reinforcementWeight`, `injectKey`; ''give the agent memory'', ''remember user preferences'', ''semantic recall'', ''per-session working memory'', ''episodic / event memory'', ''procedural / how-to memory'', ''recency-weighted recall'', ''reinforce a procedure''; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator wiring of the memory — `@warlock.js/ai/run-orchestrator/SKILL.md`; the vector cache driver itself — `@warlock.js/cache/cache-basics/SKILL.md`; embeddings primitive — `@warlock.js/ai/embed-text/SKILL.md`; competing libs `mem0`, `langchain` memory.'
5420
+ description: 'Agent memory with ai.memory({...}) — a provider-neutral store with FOUR tiers: WORKING (in-run scratch, recalled by recency), SEMANTIC (durable facts by cosine similarity over a @warlock.js/cache vector driver via .similar()), EPISODIC (durable events, similarity blended with recency), and PROCEDURAL (durable how-tos, similarity blended with reinforcement). remember() / recall() / clear(); wire it into ai.orchestrator({ memory }). Triggers: `ai.memory`, `memory.remember`, `memory.recall`, `memory.clear`, `MemoryContract`, `MemoryConfig`, `MemoryItem`, `RecalledMemory`, `MemoryTier`, `SemanticMemoryConfig`, `EpisodicMemoryConfig`, `ProceduralMemoryConfig`, `working`, `semantic`, `episodic`, `procedural`, `defaultTier`, `threshold`, `recencyWeight`, `halfLifeMs`, `reinforcementWeight`, `injectKey`, `maxItems`, `scope`, `RecallOptions.scope`; ''give the agent memory'', ''remember user preferences'', ''semantic recall'', ''per-session working memory'', ''episodic / event memory'', ''procedural / how-to memory'', ''recency-weighted recall'', ''reinforce a procedure'', ''cap working memory size'', ''isolate memory per session/tenant''; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator wiring of the memory — `@warlock.js/ai/run-orchestrator/SKILL.md`; the vector cache driver itself — `@warlock.js/cache/cache-basics/SKILL.md`; embeddings primitive — `@warlock.js/ai/embed-text/SKILL.md`; competing libs `mem0`, `langchain` memory.'
5291
5421
  ---
5292
5422
 
5293
5423
  # `ai.memory()` — agent memory store
5294
5424
 
5295
5425
  A single provider-neutral store that holds and retrieves what an agent / orchestrator should remember across turns. Four tiers ship in 4.3.0:
5296
5426
 
5297
- - **working** — in-run scratch threaded across turns of one session. Volatile, unscored, recalled in insertion order (recency). On by default.
5427
+ - **working** — in-run scratch threaded across turns of one session. Volatile, unscored, recalled in insertion order (recency). On by default, size-bounded (`working: { maxItems }`, default `1000` — see below).
5298
5428
  - **semantic** — durable *facts* stored as embeddings in a `@warlock.js/cache` driver, retrieved by cosine similarity via the driver's native `.similar()` — the same delegation the `semanticCache` middleware uses. Activates only when you pass `semantic` config.
5299
5429
  - **episodic** — durable *events*: a timestamped log retrieved by similarity **blended with recency** (recent episodes rank higher). Embedder-backed like semantic; tune with `recencyWeight` + `halfLifeMs`.
5300
5430
  - **procedural** — durable *how-tos*: learned procedures retrieved by similarity **blended with reinforcement** — re-remembering a procedure increments its use count so well-worn procedures rank higher. Tune with `reinforcementWeight`.
@@ -5364,7 +5494,7 @@ await mem.remember({ text: "User is on the Enterprise plan.", tier: "semantic",
5364
5494
  await mem.remember([{ text: "a" }, { text: "b", tier: "working" }]); // batch
5365
5495
  ```
5366
5496
 
5367
- A `MemoryItem` is `{ text, tier?, id?, metadata? }`. `text` is the only required field — it's what gets embedded (semantic) and surfaced back on recall. `tier` defaults to the factory `defaultTier`. Semantic items are embedded + indexed; working items append to the in-run buffer. **Re-remembering an item whose id (explicit or text-derived) already exists overwrites in place rather than duplicating.** `metadata` is an opaque bag round-tripped verbatim onto the recalled memory.
5497
+ A `MemoryItem` is `{ text, tier?, id?, scope?, metadata? }`. `text` is the only required field — it's what gets embedded (semantic) and surfaced back on recall. `tier` defaults to the factory `defaultTier`. Semantic items are embedded + indexed; working items append to the in-run buffer. **Re-remembering an item whose id (explicit or text-derived) already exists overwrites in place rather than duplicating.** `metadata` is an opaque bag round-tripped verbatim onto the recalled memory. `scope` is the ISOLATION key — see below.
5368
5498
 
5369
5499
  ### `recall(query, options?)`
5370
5500
 
@@ -5373,6 +5503,7 @@ const hits = await mem.recall("which plan is the user on?", {
5373
5503
  k: 5, // cap result count (defaults to factory k)
5374
5504
  threshold: 0.75, // raise the semantic floor for this call
5375
5505
  tier: "semantic", // restrict to one tier; omit to query every enabled tier
5506
+ scope: "tenant-42", // isolation key — only memories remembered under this exact scope
5376
5507
  });
5377
5508
 
5378
5509
  for (const hit of hits) {
@@ -5384,6 +5515,38 @@ Returns `RecalledMemory[]` scored and ordered by descending relevance. By defaul
5384
5515
 
5385
5516
  **Memory never mutates the prompt.** `recall()` hands you scored entries; surfacing the recalled text (system prefix, a synthesized "what you remember" block, …) is YOUR call so the injection point stays explicit.
5386
5517
 
5518
+ ### Isolation — `scope` (4.15.0)
5519
+
5520
+ One store instance is normally shared by many callers (built once at boot, passed into `ai.orchestrator({ memory })`), so `scope` is what keeps one caller's memories out of another's recall:
5521
+
5522
+ ```ts
5523
+ await mem.remember({ text: "User A's account email is a@example.com", scope: "user-a" });
5524
+
5525
+ await mem.recall("what is my email?", { scope: "user-b" }); // [] — never sees user A
5526
+ await mem.recall("what is my email?", { scope: "user-a" }); // user A's own memories
5527
+ await mem.recall("what is my email?"); // only the UNSCOPED pool
5528
+ ```
5529
+
5530
+ - The match is **exact equality**, enforced inside every tier (`working` / `semantic` / `episodic` / `procedural`) before hits are scored, merged, or sliced — not something the caller filters afterward.
5531
+ - Omitting `scope` is **not** a wildcard: an unscoped recall reads only unscoped entries. There is no "all scopes" query.
5532
+ - Identical text under two scopes stays two independent entries (including the procedural tier's reinforcement counter).
5533
+ - `ai.orchestrator({ memory })` sets this automatically from the turn's `sessionId` — see [`@warlock.js/ai/run-orchestrator/SKILL.md`](@warlock.js/ai/run-orchestrator/SKILL.md).
5534
+ - `clear(tier?)` is scope-agnostic: it drops the tier for every scope.
5535
+
5536
+ ### Working-memory cap — `working: { maxItems }` (4.15.0)
5537
+
5538
+ ```ts
5539
+ const mem = ai.memory({
5540
+ working: { maxItems: 2_000 }, // default 1000; bare `working: true` also works
5541
+ });
5542
+ ```
5543
+
5544
+ The working tier holds everything it's told in **process** memory for the lifetime of the `memory()` instance — which `ai.orchestrator({ memory })` resolves once and reuses for every session. Before 4.15.0 it had no cap, so a memory-backed orchestrator on the open internet was a cheap memory-exhaustion path: one permanent entry per request, forever.
5545
+
5546
+ The buffer now evicts on overflow, **FIFO over insertion order, not LRU** — recall on this tier is a pure recency proxy (newest `k`, never reordered), so the oldest entries are exactly the ones a bounded recall would never have returned anyway. `maxItems` is validated as an integer `>= 1` at construction; there is no unbounded setting — "no cap" was the vulnerability, not a configuration choice. Raise it deliberately for a long-lived single-tenant process, and put durable recall in the semantic / episodic tiers (which delegate retention to a `CacheDriver`, not process memory).
5547
+
5548
+ The bound is **global**, not per-scope — a busy session can push another session's older entries out. That's a recall-quality degradation on a volatile scratch tier, never a disclosure (the `scope` isolation filter above still applies).
5549
+
5387
5550
  ### `clear(tier?)`
5388
5551
 
5389
5552
  ```ts
@@ -5456,7 +5619,7 @@ const lib = ai.skills({
5456
5619
  ### Sources — `SkillSource` (discriminated by `type`, never `kind`)
5457
5620
 
5458
5621
  - `{ type: "directory", path }` — reads `path/<folder>/SKILL.md` off disk (lazy `node:fs/promises`).
5459
- - `{ type: "url", url, headers? }` — `fetch()`es a JSON manifest of skills.
5622
+ - `{ type: "url", url, headers?, policy?, cacheTtlMs? }` — `urlSource(url, options)` fetches a JSON manifest of skills through the shared `guardedFetch` / `OutboundPolicy` guard (scheme/host allowlist, post-DNS private-IP deny, byte cap, timeout, per-hop redirect revalidation) — never a raw `fetch()`. A remote skill source is a prompt supply chain (bodies flow straight into model context), so every fetched record is also runtime-validated before it can be served. `policy` tunes the guard (e.g. `hostAllowlist`); see [`@warlock.js/ai/secure-outbound-requests/SKILL.md`](@warlock.js/ai/secure-outbound-requests/SKILL.md). The result is cached for the source's lifetime, or `cacheTtlMs` when set.
5460
5623
  - `{ type: "store", store }` — any `SkillsStoreContract`, e.g. `MockSkillsStore`.
5461
5624
 
5462
5625
  Sources merge in order; a later source wins on a name collision.
@@ -5519,6 +5682,7 @@ The optional `analytics` sink fires `catalogued` / `loaded` / `used` / `saved` /
5519
5682
  - [`@warlock.js/ai/write-system-prompt/SKILL.md`](@warlock.js/ai/write-system-prompt/SKILL.md) — static persona / instruction blocks (vs. dynamic loaded skills)
5520
5683
  - [`@warlock.js/ai/use-ai-memory/SKILL.md`](@warlock.js/ai/use-ai-memory/SKILL.md) — the procedural memory tier `proceduralSkillStore` unifies with
5521
5684
  - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — the agent the `skills` option attaches to
5685
+ - [`@warlock.js/ai/secure-outbound-requests/SKILL.md`](@warlock.js/ai/secure-outbound-requests/SKILL.md) — the `guardedFetch` / `OutboundPolicy` guard the `url` source's manifest fetch runs through
5522
5686
 
5523
5687
 
5524
5688
  ## write-system-prompt `@warlock.js/ai/write-system-prompt/SKILL.md`
package/llms.txt CHANGED
@@ -9,7 +9,7 @@
9
9
  - [ai-basics](@warlock.js/ai/ai-basics/SKILL.md): Start with @warlock.js/ai — provider-agnostic core for agents / tools / workflows / supervisors / orchestrators. 4-primitive ladder (agent → workflow → supervisor → orchestrator, all shipped) plus planner, memory, stores, DX helpers, and the optional @warlock.js/ai-panoptic observability sidecar. Every primitive returns {data, error, usage, report}. Triggers: `ai.agent`, `ai.tool`, `ai.workflow`, `ai.supervisor`, `ai.orchestrator`, `ai.planner`, `ai.memory`, `ai.systemPrompt`, `ExecuteResult`, `BaseReport`, `AIError`, `panoptic`; 'which AI primitive do I use', 'what is warlock ai', 'pick an AI skill', 'how do I observe / trace AI runs'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent details — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langchain`, `llamaindex`, `ai` (Vercel SDK); raw `openai` / `@anthropic-ai/sdk`.
10
10
  - [ai-dx-helpers](@warlock.js/ai/ai-dx-helpers/SKILL.md): Developer-experience helpers across @warlock.js/ai — ai.batch (fan-out an executable over a dataset w/ concurrency + per-item retry), ai.fallbackModel (ordered model failover), agent.eval + ai.eval scorers + Vitest matchers (registerAiMatchers / toRouteTo / toConverge / toPassStep / toOutputShape) + ai.mockRouter, SLO/cost budget contracts (ai.middleware.budget({contract}) + readBudgetFallbackSignal), supervisor-level middleware, ai.systemPrompt.fromFile, and auto-adapt executables in tools:[]. Triggers: `ai.batch`, `BatchResult`, `ai.fallbackModel`, `FallbackModelContract`, `agent.eval`, `ai.eval`, `EvalReport`, `EvalScorer`, `ai.eval.judge`, `registerAiMatchers`, `toRouteTo`, `toConverge`, `toPassStep`, `toOutputShape`, `ai.mockRouter`, `MockSDK`, `mockAgent`, `budget({contract})`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`, `systemPrompt.fromFile`; 'run an agent over a list', 'fail over to a backup model', 'evaluate / score an agent', 'SLO budget', 'test a supervisor without an LLM', 'prompt from a file'; typical import `import { ai } from "@warlock.js/ai"`. Skip: core agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; the budget/guardrail/semanticCache basics — `@warlock.js/ai/attach-ai-middleware/SKILL.md`; competing libs `promptfoo`, `langsmith`.
11
11
  - [approve-tool-calls](@warlock.js/ai/approve-tool-calls/SKILL.md): Gate an agent's tool calls behind a human with `ai.human.approval(options)` (the `tool.before` approval-gate middleware) — ships in @warlock.js/ai core. Triggers: `ai.human.approval`, `humanApproval`, `HumanApprovalOptions`, `ApprovalRequest`, `ApprovalDecision`, `ApprovalHandler`, `InterruptPolicy`, `evaluatePolicy`, `ApprovalRejectedError`, `policy: { type: "allowlist" | "denylist" | "predicate" }`, decision `{ type: "approve" | "reject" | "edit" }`; 'human in the loop', 'approve a tool call before it runs', 'ask a human before the agent sends/charges/deletes', 'pause before a dangerous tool', 'let an operator edit the tool args', 'reject a tool call with a reason the model can self-correct from'. Typical import `import { ai } from "@warlock.js/ai"`. Skip: persisting the request and resuming hours later out-of-process — `@warlock.js/ai/durable-resume/SKILL.md`; the agent/middleware/tool primitives themselves — `@warlock.js/ai`.
12
- - [attach-ai-middleware](@warlock.js/ai/attach-ai-middleware/SKILL.md): Wire agent middleware — ai.middleware.budget (token / USD caps + SLO/cost contract w/ maxLatencyMs + onViolation fallback), ai.middleware.guardrail (pre / post content checks), ai.middleware.semanticCache (exact + vector cache), supervisor-level middleware, plus authoring custom hooks (execute / trip / tool). Triggers: `ai.middleware.budget`, `ai.middleware.guardrail`, `ai.middleware.semanticCache`, `ai.middleware.compose`, `ai.middleware.forTool`, `AgentMiddleware`, `BudgetExceededError`, `GuardrailViolationError`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`; 'cap token cost', 'SLO budget', 'block pii in prompts', 'semantic cache before LLM', 'supervisor-level middleware', 'write custom hook'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; cache drivers — `@warlock.js/ai/persist-ai-data/SKILL.md`; competing libs `langchain` callbacks.
12
+ - [attach-ai-middleware](@warlock.js/ai/attach-ai-middleware/SKILL.md): Wire agent middleware — ai.middleware.budget (token / USD caps + SLO/cost contract w/ maxLatencyMs + onViolation fallback), ai.middleware.guardrail (pre / post content checks), ai.middleware.semanticCache (exact + vector cache), supervisor-level middleware, plus authoring custom hooks (execute / trip / tool). Triggers: `ai.middleware.budget`, `ai.middleware.guardrail`, `ai.middleware.semanticCache`, `ai.middleware.compose`, `ai.middleware.forTool`, `AgentMiddleware`, `BudgetExceededError`, `GuardrailViolationError`, `BudgetContract`, `maxLatencyMs`, `onViolation`, `readBudgetFallbackSignal`, `supervisor middleware`, `SemanticCacheOptions`, `SemanticCacheScope`; 'cap token cost', 'SLO budget', 'block pii in prompts', 'semantic cache before LLM', 'supervisor-level middleware', 'write custom hook', 'isolate semantic cache per session/tenant'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent lifecycle — `@warlock.js/ai/run-ai-agent/SKILL.md`; cache drivers — `@warlock.js/ai/persist-ai-data/SKILL.md`; competing libs `langchain` callbacks.
13
13
  - [define-ai-tool](@warlock.js/ai/define-ai-tool/SKILL.md): Define tools with ai.tool({...}) — typed validated async functions the model can call. Covers name / description / action / mode (feedback / silent) / input / execute, `ctx.artifacts` side-channel, `ToolExecutionError`. Triggers: `ai.tool`, `ToolContract`, `ToolContext`, `ToolCall`, `ToolExecutionError`, `artifactsSchema`, `mode: "silent"`, `workflow.asTool`; 'define a tool', 'wire tool into agent', 'tool input validation', 'side-channel artifacts'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent loop — `@warlock.js/ai/run-ai-agent/SKILL.md`; supervisor artifacts — `@warlock.js/ai/run-supervisor/SKILL.md`; competing libs `langchain` tools, raw `openai` function-calling.
14
14
  - [detect-and-redact-pii](@warlock.js/ai/detect-and-redact-pii/SKILL.md): Detect and redact PII (and run model-graded moderation) with @warlock.js/ai-guard detectors — `ai.guardrail.pii(...)` and the optional `ai.guardrail.moderation(...)` peer. Triggers: `ai.guardrail.pii`, `piiDetector`, `PiiDetectorOptions`, `PiiCategory`, `mask`, `{label}`, `dictionary`, `onMatch`, `ai.guardrail.moderation`, `openAiModeration`, `OpenAiModerationOptions`, `blockOn`, `omni-moderation-latest`; 'redact PII from model output', 'mask SSN / credit card / email / phone / IP', 'stop PII leaking into a tool call', 'scrub sensitive data', 'add OpenAI moderation', 'block violent / self-harm content'; typical import `import "@warlock.js/ai-guard"` (registers `ai.guardrail.pii` / `.moderation`) or `import { pii, moderation } from "@warlock.js/ai-guard"`. Skip: composing the guard / wiring it into an agent — `@warlock.js/ai-guard/guard-input-output/SKILL.md`; routing a block to a human — `@warlock.js/ai-guard/escalate-block-to-human/SKILL.md`.
15
15
  - [durable-agent-runs](@warlock.js/ai/durable-agent-runs/SKILL.md): Mid-run crash-resume for agents AND planners — opt in with durable: { store, deleteOnComplete? } on the config, pass a stable runId to execute(), and call agent.resume(runId) / planner.resume(runId) after a crash to continue from the last settled trip / plan node. Reuses the ai.snapshot.{memory,pg,redis} stores; checkpoints per-trip (agent) / per-node (planner); completed trips + nodes never re-run their tools and usage is never double-counted; a drifted definition throws AgentDriftError / PlannerDriftError (bypass with { force: true }). Triggers: `durable`, `agent.resume`, `planner.resume`, `resume(runId)`, `runId`, `AgentSnapshot`, `PlannerSnapshot`, `AgentSnapshotStatus`, `PlannerSnapshotStatus`, `AgentDriftError`, `PlannerDriftError`, `computeAgentSignature`, `agent.signature`, `deleteOnComplete`, `defaultSnapshotStore`, `ai.snapshot.pg`, `ai.snapshot.memory`, `SnapshotStore`, `force: true`; 'resume an agent after a crash', 'durable agent run', 'continue a planner from where it crashed', 'checkpoint agent state', 'idempotent tool re-run on resume', 'signature drift on resume'; typical import `import { ai } from "@warlock.js/ai"`. Skip: durable human-in-the-loop approval resume (ai.human.resume of a PendingInterrupt) — `@warlock.js/ai/durable-resume/SKILL.md`; supervisor/workflow iterate-mid-turn snapshot resume + the store contracts themselves — `@warlock.js/ai/manage-ai-stores/SKILL.md`; competing libs `temporal`, `inngest`, `restate`.
@@ -35,9 +35,10 @@
35
35
  - [run-ai-team](@warlock.js/ai/run-ai-team/SKILL.md): Manager-led multi-agent teams with ai.team({...}) — transparent sugar over ai.supervisor that maps a manager → route/router, members → intents, and a gate → evaluate, returning a REAL SupervisorContract (no new loop, no new contract). Covers the built-in gate strings "quality" (review-then-fix) and "verify" (test-then-fix), a custom gate function, role mapping (roles / gateKey), and the verbatim supervisor pass-throughs (goal / output / state / maxIterations / snapshotStore / on / observe). Triggers: `ai.team`, `TeamConfig`, `TeamGate`, `TeamGateFn`, `TeamMemberValue`, `manager`, `members`, `gate`, `roles`, `gateKey`, `buildQualityGate`, `buildVerifyGate`, `SupervisorContract`, `ReportType`; 'build a team of agents', 'manager that delegates to members', 'review then fix loop', 'test then fix loop', 'quality gate for a multi-agent run', 'report type team'; typical import `import { ai } from "@warlock.js/ai"`. Skip: routing one input to a fixed roster directly — `@warlock.js/ai/run-supervisor/SKILL.md` (team is sugar over it); durable cross-turn sessions — `@warlock.js/ai/run-orchestrator/SKILL.md`; LLM-generated plans — `@warlock.js/ai/run-planner/SKILL.md`; competing libs `crewai`, `autogen`.
36
36
  - [run-ai-workflow](@warlock.js/ai/run-ai-workflow/SKILL.md): Build durable resumable pipelines with ai.workflow({...}) + ai.step({...}) — lifecycle (skip / before / run|agent|parallel / output / after / nextStep), retry, parallel groups, snapshot resume. Triggers: `ai.workflow`, `ai.step`, `wf.execute`, `wf.resume`, `WorkflowContext`, `WorkflowResult`, `StepSnapshot`, `nextStep`, `onFailure`, `WorkflowDriftError`; 'build a workflow', 'define a step', 'resume after crash', 'parallel steps', 'retry with backoff'; typical import `import { ai } from "@warlock.js/ai"`. Skip: agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; supervisor — `@warlock.js/ai/run-supervisor/SKILL.md`; competing libs `temporal`, `inngest`, `bullmq`.
37
37
  - [run-orchestrator](@warlock.js/ai/run-orchestrator/SKILL.md): Durable stateful sessions with ai.orchestrator({...}) — the capstone of the 4-primitive ladder. Wraps a supervisor with cross-turn session state (checkpointStore), per-turn windowing, drift detection, post-turn compaction, mid-turn resume (iterate: true + snapshotStore), per-turn memory, typed commands, asTool, and a 3-tier event model. Triggers: `ai.orchestrator`, `orchestrator.execute`, `orchestrator.resume`, `orchestrator.command`, `orchestrator.stream`, `OrchestratorConfig`, `OrchestratorResult`, `OrchestratorReport`, `OrchestratorContract`, `CheckpointStore`, `OrchestratorDriftError`, `sessionId`, `iterate`, `historyWindow`, `summarize`, `keepSnapshots`, `awaiting-input`, `turns[]`, `TurnSnapshot`, `CompactionResult`, `initialAgent`, `checkpointStore`; 'multi-turn conversation that persists', 'durable session across calls', 'resume an interrupted turn', 'compact session history', 'per-session memory'; typical import `import { ai } from "@warlock.js/ai"`. Skip: a single routing turn with no session — `@warlock.js/ai/run-supervisor/SKILL.md`; a fixed pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; the store factories themselves — `@warlock.js/ai/manage-ai-stores/SKILL.md`; competing libs `langgraph`, `crewai`.
38
- - [run-planner](@warlock.js/ai/run-planner/SKILL.md): Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered execution plan over your registered capabilities (agents / workflows / supervisors / tools), then the planner EXECUTES it, threading each step output into the next, and returns the unified {data, report, usage, error} envelope with report.type "planner". Supports DAG scheduling (dag:true + maxConcurrency off dependsOn), adaptive re-planning (replan:{maxReplans} + the onStep continue/abort/replan directive), and plan-only / approval (mode:"plan-only" → status "awaiting-approval" → approvedPlan). A plan step may delegate via ai.spawnSubAgent({...}) — a GENERAL one-shot-agent helper covered in `@warlock.js/ai/run-ai-agent/SKILL.md`; it is not planner-specific. Triggers: `ai.planner`, `planner.execute`, `spawnSubAgent`, `PlannerConfig`, `PlannerCapability`, `PlannerResult`, `PlannerReport`, `PlannerPlan`, `PlannerStep`, `PlannerStepDirective`, `PlannerPlanInvalidError`, `maxSteps`, `dag`, `maxConcurrency`, `dependsOn`, `replan`, `onStep`, `mode`, `approvedPlan`, `awaiting-approval`, `report.plan`, `report.executedSteps`; 'let the model plan the steps', 'dynamic plan from a goal', 'run independent steps in parallel', 're-plan when a step fails', 'generate a plan for approval before running it'; typical import `import { ai } from "@warlock.js/ai"`. Skip: a FIXED known pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; routing one input to a specialist each turn — `@warlock.js/ai/run-supervisor/SKILL.md`; a single model + tools call — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.
38
+ - [run-planner](@warlock.js/ai/run-planner/SKILL.md): Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered execution plan over your registered capabilities (agents / workflows / supervisors / tools), then the planner EXECUTES it, threading each step output into the next, and returns the unified {data, report, usage, error} envelope with report.type "planner". Supports DAG scheduling (dag:true + maxConcurrency off dependsOn), adaptive re-planning (replan:{maxReplans} + the onStep continue/abort/replan directive), and plan-only / approval (mode:"plan-only" → status "awaiting-approval" → approvedPlan). A plan step may delegate via ai.spawnSubAgent({...}) — a GENERAL one-shot-agent helper covered in `@warlock.js/ai/run-ai-agent/SKILL.md`; it is not planner-specific. Triggers: `ai.planner`, `planner.execute`, `spawnSubAgent`, `PlannerConfig`, `PlannerCapability`, `PlannerResult`, `PlannerReport`, `PlannerPlan`, `PlannerStep`, `PlannerStepDirective`, `PlannerPlanInvalidError`, `maxSteps`, `dag`, `maxConcurrency`, `dependsOn`, `replan`, `onStep`, `mode`, `approvedPlan`, `awaiting-approval`, `report.plan`, `report.executedSteps`, `parsedStepCeiling`; 'let the model plan the steps', 'dynamic plan from a goal', 'run independent steps in parallel', 're-plan when a step fails', 'generate a plan for approval before running it'; typical import `import { ai } from "@warlock.js/ai"`. Skip: a FIXED known pipeline — `@warlock.js/ai/run-ai-workflow/SKILL.md`; routing one input to a specialist each turn — `@warlock.js/ai/run-supervisor/SKILL.md`; a single model + tools call — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.
39
39
  - [run-supervisor](@warlock.js/ai/run-supervisor/SKILL.md): Multi-intent routing with ai.supervisor({...}) — classifier (iter-0 dispatch), router agent OR route callback, intents as agents / workflows / callbacks, fan-out, evaluate quality loop, ack receptionist, supervisor-level middleware. A callback that calls agent.execute() directly auto-nests agent → tool under the callback span (ambient RunFrame) with usage / cost rolled up — same for team members and orchestrator turns. Triggers: `ai.supervisor`, `ai.router`, `ai.fanOut`, `supervisor.execute`, `supervisor.resume`, `intents`, `router`, `route`, `classifier`, `evaluate`, `ack`, `artifactsSchema`, `middleware`, `END`, `ctx.intents.X.execute`, `ctx.run`, `RunFrame`, `callback span`, `children`, `parentRunId`, `rootRunId`, `trace nesting`, `sub-agent`; 'route one input across specialists', 'multi-intent dispatch', 'fan-out then evaluate', 'classifier then router', 'supervisor middleware', 'self-consistency / voting', 'why is my callback agent not nested / cost is $0', 'nest a sub-agent under a callback'; typical import `import { ai } from "@warlock.js/ai"`. Skip: durable multi-turn sessions — `@warlock.js/ai/run-orchestrator/SKILL.md`; fixed pipelines — `@warlock.js/ai/run-ai-workflow/SKILL.md`; single agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langgraph`, `crewai`.
40
+ - [secure-outbound-requests](@warlock.js/ai/secure-outbound-requests/SKILL.md): The shared SSRF / resource-exhaustion guard every server-side outbound HTTP request in the framework goes through — `guardedFetch(url, policy, init?)`, `OutboundPolicy`, `assertUrlAllowed`, `fetchTextWithPolicy`, `readTextCapped`. Scheme allowlist (https-only default), host allowlist, post-DNS private/loopback/link-local/metadata-address deny, byte cap, timeout, and (4.15.0) per-hop redirect revalidation with a `maxRedirects` cap and cross-origin credential stripping. Consumed by `ai.rag.loadWeb`, remote text attachments (`prepareAttachmentPart`), and the skills `urlSource` manifest fetch — never a raw `fetch()` on a caller-influenced URL. Triggers: `guardedFetch`, `OutboundPolicy`, `ResolvedOutboundPolicy`, `assertUrlAllowed`, `fetchTextWithPolicy`, `readTextCapped`, `resolveOutboundPolicy`, `OutboundPolicyError`, `maxRedirects`, `denyPrivateIPsAfterDNS`, `hostAllowlist`, `allowedSchemes`, `maxBytes`, `SSRF`, `redirect: "manual"`, `redirect: "error"`; 'SSRF-safe fetch', 'block a redirect into a private IP', 'fetch a URL an agent gave me', 'cap outbound response size', 'allowlist hosts for outbound requests', 'strip auth headers on a cross-origin redirect'; typical import `import { guardedFetch, assertUrlAllowed } from "@warlock.js/ai"` (also re-exported per call site). Skip: the RAG loader that wraps this for `loadWeb` — `@warlock.js/ai/rag-loaders-and-stores/SKILL.md`; the skills manifest source that wraps this for `urlSource` — `@warlock.js/ai/use-runtime-skills/SKILL.md`; prompt-injection / content guardrails (a different trust boundary) — `@warlock.js/ai/guard-input-output/SKILL.md` (ai-guard package).
40
41
  - [transcribe-audio](@warlock.js/ai/transcribe-audio/SKILL.md): Speech-to-text via ai.transcribe({ model: sdk.transcribe({ name }), audio }) — the audio-INPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Feed it an AudioInput = { base64; mediaType; filename? } — build one with ai.audioFromFile(path) (reads disk, infers media type incl. WhatsApp .ogg/.opus) or ai.audioFromBuffer(bytes, mediaType). Models: OpenAI whisper-1 (verbose_json, per-minute, segments + durationSeconds) or gpt-4o-transcribe (json, per-token). Triggers: `ai.transcribe`, `ai.audioFromFile`, `ai.audioFromBuffer`, `sdk.transcribe`, `openai.transcribe`, `TranscriptionModelContract`, `AudioInput`, `TranscriptionSegment`, `MockTranscriptionModel`; 'speech to text', 'transcribe audio', 'voice note to text', 'WhatsApp voice message', 'whisper', 'gpt-4o-transcribe', 'subtitle segments', 'audio input'; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: text-to-speech / synthesizing a voice — [[generate-speech]]; competing libs raw `openai.audio.transcriptions.create`, `whisper.cpp`.
41
- - [use-ai-memory](@warlock.js/ai/use-ai-memory/SKILL.md): Agent memory with ai.memory({...}) — a provider-neutral store with FOUR tiers: WORKING (in-run scratch, recalled by recency), SEMANTIC (durable facts by cosine similarity over a @warlock.js/cache vector driver via .similar()), EPISODIC (durable events, similarity blended with recency), and PROCEDURAL (durable how-tos, similarity blended with reinforcement). remember() / recall() / clear(); wire it into ai.orchestrator({ memory }). Triggers: `ai.memory`, `memory.remember`, `memory.recall`, `memory.clear`, `MemoryContract`, `MemoryConfig`, `MemoryItem`, `RecalledMemory`, `MemoryTier`, `SemanticMemoryConfig`, `EpisodicMemoryConfig`, `ProceduralMemoryConfig`, `working`, `semantic`, `episodic`, `procedural`, `defaultTier`, `threshold`, `recencyWeight`, `halfLifeMs`, `reinforcementWeight`, `injectKey`; 'give the agent memory', 'remember user preferences', 'semantic recall', 'per-session working memory', 'episodic / event memory', 'procedural / how-to memory', 'recency-weighted recall', 'reinforce a procedure'; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator wiring of the memory — `@warlock.js/ai/run-orchestrator/SKILL.md`; the vector cache driver itself — `@warlock.js/cache/cache-basics/SKILL.md`; embeddings primitive — `@warlock.js/ai/embed-text/SKILL.md`; competing libs `mem0`, `langchain` memory.
42
+ - [use-ai-memory](@warlock.js/ai/use-ai-memory/SKILL.md): Agent memory with ai.memory({...}) — a provider-neutral store with FOUR tiers: WORKING (in-run scratch, recalled by recency), SEMANTIC (durable facts by cosine similarity over a @warlock.js/cache vector driver via .similar()), EPISODIC (durable events, similarity blended with recency), and PROCEDURAL (durable how-tos, similarity blended with reinforcement). remember() / recall() / clear(); wire it into ai.orchestrator({ memory }). Triggers: `ai.memory`, `memory.remember`, `memory.recall`, `memory.clear`, `MemoryContract`, `MemoryConfig`, `MemoryItem`, `RecalledMemory`, `MemoryTier`, `SemanticMemoryConfig`, `EpisodicMemoryConfig`, `ProceduralMemoryConfig`, `working`, `semantic`, `episodic`, `procedural`, `defaultTier`, `threshold`, `recencyWeight`, `halfLifeMs`, `reinforcementWeight`, `injectKey`, `maxItems`, `scope`, `RecallOptions.scope`; 'give the agent memory', 'remember user preferences', 'semantic recall', 'per-session working memory', 'episodic / event memory', 'procedural / how-to memory', 'recency-weighted recall', 'reinforce a procedure', 'cap working memory size', 'isolate memory per session/tenant'; typical import `import { ai } from "@warlock.js/ai"`. Skip: orchestrator wiring of the memory — `@warlock.js/ai/run-orchestrator/SKILL.md`; the vector cache driver itself — `@warlock.js/cache/cache-basics/SKILL.md`; embeddings primitive — `@warlock.js/ai/embed-text/SKILL.md`; competing libs `mem0`, `langchain` memory.
42
43
  - [use-runtime-skills](@warlock.js/ai/use-runtime-skills/SKILL.md): Progressive-disclosure agent skills with ai.skills({...}) and the first-class `skills` option on ai.agent — an always-injected cheap metadata catalog plus an on-demand loadSkill tool, backed by directory / url / store sources. Covers inject ("all" | {select:"semantic",topK,embedder}), maxLoadsPerRun, scope tags, the MockSkillsStore, semantic preload, and the inert-by-default Phase-2 self-authoring (saveSkill + default-DENY review gate → promote). Triggers: `ai.skills`, `SkillsConfig`, `SkillsContract`, `SkillSource`, `SkillInjectMode`, `SkillRecord`, `SkillCatalogEntry`, `loadSkill`, `loadSkillTool`, `saveSkill`, `saveSkillTool`, `SkillReviewGate`, `runReviewGate`, `MockSkillsStore`, `proceduralSkillStore`, `maxLoadsPerRun`, `inject`, `scope`, `review`, the agent `skills:` option; 'give an agent loadable skills', 'progressive disclosure of instructions', 'catalog of skills the model pulls on demand', 'semantic preload of skill bodies', 'let an agent author and review a skill'; typical import `import { ai } from "@warlock.js/ai"`. Skip: composing static system prompts — `@warlock.js/ai/write-system-prompt/SKILL.md`; durable agent memory tiers — `@warlock.js/ai/use-ai-memory/SKILL.md`; defining callable tools — `@warlock.js/ai/define-ai-tool/SKILL.md`.
43
44
  - [write-system-prompt](@warlock.js/ai/write-system-prompt/SKILL.md): Compose system prompts via ai.systemPrompt() / ai.persona() / ai.instruction() — immutable builders with {{placeholder}} substitution, plus ai.systemPrompt.fromFile(path) to seed from a file read once at construction. Carry identity with .meta({ name, version, description, required }) (a name auto-registers in ai.prompts) and compose with merge(...blocks) / merge(contract) / merge(name, { fromVersion }) (provenance in meta.composedFrom). Triggers: `ai.systemPrompt`, `ai.systemPrompt.fromFile`, `ai.persona`, `ai.instruction`, `SystemPromptBlockContract`, `SystemPromptContract`, `SystemPromptMeta`, `SystemPromptMergeOptions`, `PersonaContract`, `InstructionContract`, `meta`, `merge`, `composedFrom`, `fromVersion`, `placeholders`, `{{placeholder|default}}`, `InvalidRequestError`; 'write a system prompt', 'compose persona + instructions', 'prompt from a file', 'name and version a prompt', 'merge prompts together', 'per-call prompt override', 'mustache placeholder'; typical import `import { ai } from "@warlock.js/ai"`. Skip: the named/versioned prompt registry (register / resolve / tag / diff / export / validate) — `@warlock.js/ai/manage-prompts/SKILL.md`; agent factory wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; competing libs `langchain` `PromptTemplate`, raw f-strings.
package/package.json CHANGED
@@ -15,9 +15,9 @@
15
15
  "@standard-schema/spec": "^1.0.0"
16
16
  },
17
17
  "peerDependencies": {
18
- "@warlock.js/ai-openai": "4.15.0",
19
- "@warlock.js/cache": "4.15.0",
20
- "@warlock.js/logger": "4.15.0",
18
+ "@warlock.js/ai-openai": "4.16.0",
19
+ "@warlock.js/cache": "4.16.0",
20
+ "@warlock.js/logger": "4.16.0",
21
21
  "langfuse": "*",
22
22
  "openai": "*",
23
23
  "pdf-parse": "*",
@@ -44,7 +44,7 @@
44
44
  "optional": true
45
45
  }
46
46
  },
47
- "version": "4.15.0",
47
+ "version": "4.16.0",
48
48
  "main": "./cjs/index.cjs",
49
49
  "module": "./esm/index.mjs",
50
50
  "types": "./esm/index.d.mts",
package/skills/README.md CHANGED
@@ -102,7 +102,11 @@ Goal-driven planning with ai.planner({...}) — an LLM GENERATES an ordered plan
102
102
 
103
103
  ### [`run-supervisor/`](./run-supervisor/SKILL.md)
104
104
 
105
- Multi-intent routing with ai.supervisor({...}) — classifier (iter-0 dispatch), router agent OR route callback (iter 1+), intents as agents / workflows / callbacks, fan-out, evaluate quality loop, ack receptionist, ctx.intents.X.execute composition, and sub-agent trace nesting (a callback that calls agent.execute() directly auto-nests agent → tool under the callback span with cost rolled up — same for team members + orchestrator turns). Load when routing one user input across a fixed roster of specialists, or when a callback sub-agent shows as a lone $0 span instead of nesting.
105
+ Multi-intent routing with ai.supervisor({...}) — classifier (iter-0 dispatch), router agent OR route callback (iter 1+), intents as agents / workflows / callbacks, fan-out (+ the maxFanOut width cap), evaluate quality loop, ack receptionist, ctx.intents.X.execute composition, and sub-agent trace nesting (a callback that calls agent.execute() directly auto-nests agent → tool under the callback span with cost rolled up — same for team members + orchestrator turns). Load when routing one user input across a fixed roster of specialists, bounding how wide a router can fan out, or when a callback sub-agent shows as a lone $0 span instead of nesting.
106
+
107
+ ### [`secure-outbound-requests/`](./secure-outbound-requests/SKILL.md)
108
+
109
+ The shared SSRF / resource-exhaustion guard — guardedFetch(url, policy, init?) + OutboundPolicy: scheme/host allowlist, post-DNS private/loopback/link-local/metadata-address deny, byte cap, timeout, and per-hop redirect revalidation (maxRedirects, default 5) with cross-origin credential stripping. Consumed by ai.rag.loadWeb, remote text attachments, and the skills urlSource manifest fetch. Load when fetching a URL an agent or user supplied, allowlisting outbound hosts, or investigating an OutboundPolicyError.
106
110
 
107
111
  ### [`use-ai-memory/`](./use-ai-memory/SKILL.md)
108
112