@warlock.js/ai 4.14.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -141
- package/cjs/index.cjs +640 -139
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +3 -3
- package/esm/contracts/memory/index.d.mts +1 -1
- package/esm/contracts/memory/memory-config.type.d.mts +29 -3
- package/esm/contracts/memory/memory-config.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory-item.type.d.mts +15 -1
- package/esm/contracts/memory/memory-item.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory.contract.d.mts +15 -2
- package/esm/contracts/memory/memory.contract.d.mts.map +1 -1
- package/esm/contracts/memory/recall-options.type.d.mts +12 -0
- package/esm/contracts/memory/recall-options.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/index.d.mts +1 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts +38 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts +67 -3
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts.map +1 -1
- package/esm/contracts/supervisor/supervisor-config.type.d.mts +23 -0
- package/esm/contracts/supervisor/supervisor-config.type.d.mts.map +1 -1
- package/esm/contracts/team/team-config.type.d.mts +9 -6
- package/esm/contracts/team/team-config.type.d.mts.map +1 -1
- package/esm/contracts/tool.contract.d.mts +35 -3
- package/esm/contracts/tool.contract.d.mts.map +1 -1
- package/esm/index.d.mts +8 -7
- package/esm/index.mjs +3 -2
- package/esm/memory/episodic-memory.mjs +14 -6
- package/esm/memory/episodic-memory.mjs.map +1 -1
- package/esm/memory/index.d.mts +1 -1
- package/esm/memory/memory.d.mts +13 -1
- package/esm/memory/memory.d.mts.map +1 -1
- package/esm/memory/memory.mjs +41 -7
- package/esm/memory/memory.mjs.map +1 -1
- package/esm/memory/procedural-memory.mjs +20 -7
- package/esm/memory/procedural-memory.mjs.map +1 -1
- package/esm/memory/semantic-memory.mjs +27 -10
- package/esm/memory/semantic-memory.mjs.map +1 -1
- package/esm/memory/working-memory.mjs +70 -13
- package/esm/memory/working-memory.mjs.map +1 -1
- package/esm/middleware/builtins/semantic-cache.d.mts +46 -1
- package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -1
- package/esm/middleware/builtins/semantic-cache.mjs +60 -15
- package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
- package/esm/middleware/index.d.mts +1 -1
- package/esm/mock/index.d.mts +1 -1
- package/esm/mock/mock-config.type.d.mts +33 -4
- package/esm/mock/mock-config.type.d.mts.map +1 -1
- package/esm/mock/mock-model.d.mts +2 -1
- package/esm/mock/mock-model.d.mts.map +1 -1
- package/esm/mock/mock-model.mjs +5 -4
- package/esm/mock/mock-model.mjs.map +1 -1
- package/esm/mock/mock-sdk.d.mts +11 -3
- package/esm/mock/mock-sdk.d.mts.map +1 -1
- package/esm/mock/mock-sdk.mjs.map +1 -1
- package/esm/orchestrator/as-tool.d.mts +35 -9
- package/esm/orchestrator/as-tool.d.mts.map +1 -1
- package/esm/orchestrator/as-tool.mjs +67 -19
- package/esm/orchestrator/as-tool.mjs.map +1 -1
- package/esm/orchestrator/execution.d.mts.map +1 -1
- package/esm/orchestrator/execution.mjs +2 -2
- package/esm/orchestrator/execution.mjs.map +1 -1
- package/esm/orchestrator/index.d.mts +1 -1
- package/esm/orchestrator/index.mjs +1 -1
- package/esm/orchestrator/memory.d.mts +41 -5
- package/esm/orchestrator/memory.d.mts.map +1 -1
- package/esm/orchestrator/memory.mjs +53 -5
- package/esm/orchestrator/memory.mjs.map +1 -1
- package/esm/planner/plan-schema.d.mts +15 -4
- package/esm/planner/plan-schema.d.mts.map +1 -1
- package/esm/planner/plan-schema.mjs +27 -16
- package/esm/planner/plan-schema.mjs.map +1 -1
- package/esm/security/index.mjs +1 -0
- package/esm/security/outbound-policy.d.mts +9 -0
- package/esm/security/outbound-policy.d.mts.map +1 -1
- package/esm/security/outbound-policy.mjs +79 -5
- package/esm/security/outbound-policy.mjs.map +1 -1
- package/esm/security/outbound-policy.type.d.mts +8 -0
- package/esm/security/outbound-policy.type.d.mts.map +1 -1
- package/esm/security/safe-merge.d.mts +52 -0
- package/esm/security/safe-merge.d.mts.map +1 -0
- package/esm/security/safe-merge.mjs +68 -0
- package/esm/security/safe-merge.mjs.map +1 -0
- package/esm/supervisor/decide.mjs +52 -5
- package/esm/supervisor/decide.mjs.map +1 -1
- package/esm/supervisor/execution.d.mts +22 -12
- package/esm/supervisor/execution.d.mts.map +1 -1
- package/esm/supervisor/execution.mjs +47 -24
- package/esm/supervisor/execution.mjs.map +1 -1
- package/esm/supervisor/supervisor.mjs +4 -0
- package/esm/supervisor/supervisor.mjs.map +1 -1
- package/llms-full.txt +185 -14
- package/llms.txt +5 -4
- package/package.json +24 -4
- package/skills/README.md +6 -2
- package/skills/attach-ai-middleware/SKILL.md +17 -1
- package/skills/generate-images/SKILL.md +11 -4
- package/skills/rag-loaders-and-stores/SKILL.md +3 -0
- package/skills/run-ai-agent/SKILL.md +3 -0
- package/skills/run-orchestrator/SKILL.md +6 -1
- package/skills/run-planner/SKILL.md +7 -3
- package/skills/run-supervisor/SKILL.md +11 -1
- package/skills/secure-outbound-requests/SKILL.md +85 -0
- package/skills/use-ai-memory/SKILL.md +36 -3
- 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":"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"}
|
|
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.
|
|
@@ -1545,7 +1561,7 @@ if (report.regression && !report.regression.passed) process.exit(1);
|
|
|
1545
1561
|
|
|
1546
1562
|
---
|
|
1547
1563
|
name: generate-images
|
|
1548
|
-
description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google imagen-* (per-image). Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
|
|
1564
|
+
description: 'Text-to-image via ai.image({ model: sdk.image({ name }), prompt }) — the image-OUTPUT verb (Theme I), returning the uniform never-throws { data, error, usage, report } envelope with cost-truth + panoptic observation. Models come from an adapter''s image() factory: OpenAI gpt-image-* (token-metered) / dall-e-* (per-image), Google gemini-* (generateContent + responseModalities IMAGE, usage passed through) / imagen-* and every other id (per-image, generateImages — deprecated by Google); the id picks the transport and is never validated locally. Result images are a discriminated GeneratedImage = { type: "base64" } | { type: "url" }. Triggers: `ai.image`, `sdk.image`, `openai.image`, `google.image`, `ImageModelContract`, `GeneratedImage`, `ImageModelPricing`; ''generate an image'', ''text to image'', ''gpt-image'', ''dall-e'', ''imagen'', ''product thumbnail'', ''image output''; typical import `import { ai } from "@warlock.js/ai"` + `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: image INPUT / vision attachments to a chat agent — `@warlock.js/ai/run-ai-agent/SKILL.md`; embeddings — `@warlock.js/ai/embed-text/SKILL.md`; competing libs raw `openai.images.generate`, `langchain` image tools.'
|
|
1549
1565
|
---
|
|
1550
1566
|
|
|
1551
1567
|
# Generate images — the image-output verb (`ai.image`)
|
|
@@ -1628,20 +1644,27 @@ const dalle = openai.image({ name: "dall-e-3", pricing: { perImage: 0.04 } });
|
|
|
1628
1644
|
|
|
1629
1645
|
The model id is **not validated locally**. `openai.image({ name })` forwards the id to `client.images.generate` exactly as given, so a non-image id (`openai.image({ name: "gpt-4o" })`) constructs fine and fails at OpenAI — as a typed provider error on `result.error`, never as a local throw at construction.
|
|
1630
1646
|
|
|
1631
|
-
## Google — Imagen (per-image)
|
|
1647
|
+
## Google — Imagen (per-image) and Gemini (per-token)
|
|
1632
1648
|
|
|
1633
1649
|
```ts
|
|
1634
1650
|
import { GoogleSDK } from "@warlock.js/ai-google";
|
|
1635
1651
|
|
|
1636
1652
|
const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
|
|
1653
|
+
|
|
1654
|
+
// Imagen — per-image-metered, via ai.models.generateImages:
|
|
1637
1655
|
const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perImage: 0.04 } });
|
|
1638
1656
|
|
|
1657
|
+
// Gemini image model — token-metered, via ai.models.generateContent:
|
|
1658
|
+
const gemini = google.image({ name: "gemini-3.1-flash-lite-image", pricing: { input: 0.3, output: 30 } });
|
|
1659
|
+
|
|
1639
1660
|
const { data } = await ai.image({ model: imagen, prompt: "a watercolor lighthouse at dawn", aspectRatio: "3:4" });
|
|
1640
1661
|
```
|
|
1641
1662
|
|
|
1642
|
-
Imagen
|
|
1663
|
+
The **id picks the transport**: a `gemini-` id goes to `generateContent` (token `usage` is passed through as Google reports it — price with `{ input, output }`), anything else to `generateImages` (Imagen — always zero usage, price with `{ perImage }`). Both surface images in the same `GeneratedImage` shape (base64 bytes, no hosted URL). When Google filters the request, `ai.image` surfaces a typed `ContentFilterError` on `result.error`; a Gemini response that answered with text instead of an image surfaces a `ProviderError` quoting that text.
|
|
1664
|
+
|
|
1665
|
+
⚠ **No test calls the live API**, so the Gemini path rests on two tiers of evidence. Measured here: a `gemini-*` id reached `generateContent` and returned a quota error (429) where `generateImages` returned 404 — the endpoint accepts the id. Reported by the maintainer: with billing enabled, an image comes back end-to-end. Still unknown is whether these models report token usage — no `usageMetadata` from a successful image call has been seen. Google has also **deprecated `generateImages`** ("will be removed in the next major release (not before Jan. 1 2027)"), so the Imagen path is on a clock.
|
|
1643
1666
|
|
|
1644
|
-
Like every adapter, Google does **not** guard the model id
|
|
1667
|
+
Like every adapter, Google does **not** guard the model id — the id selects a route, it is never refused locally, so an id Google does not serve fails as a typed provider error on `result.error`, not with a local throw at construction.
|
|
1645
1668
|
|
|
1646
1669
|
## Cost-truth — one rollup, two metering models
|
|
1647
1670
|
|
|
@@ -3223,6 +3246,8 @@ await kb.index(await ai.rag.loadWeb("https://docs.example.com/guide", {
|
|
|
3223
3246
|
|
|
3224
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`.
|
|
3225
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
|
+
|
|
3226
3251
|
### `loadPdf` — lazy optional peer, page-precise citations
|
|
3227
3252
|
|
|
3228
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.
|
|
@@ -3321,6 +3346,7 @@ The `embedder`'s `dimensions` MUST equal the store's `dimensions` — a mismatch
|
|
|
3321
3346
|
|
|
3322
3347
|
- [[run-ai-rag]] — the chunk → embed → retrieve → rerank → cite pipeline that **consumes** these loaders and stores (`ai.rag({ embedder, store })`, `index()` / `retrieve()`).
|
|
3323
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.
|
|
3324
3350
|
- [`@warlock.js/cache/use-cache-similarity/SKILL.md`](@warlock.js/cache/use-cache-similarity/SKILL.md) — the cache driver `cacheVectorStore` adapts.
|
|
3325
3351
|
|
|
3326
3352
|
|
|
@@ -3746,6 +3772,8 @@ attachments: [
|
|
|
3746
3772
|
|
|
3747
3773
|
Model must declare `capabilities.vision`. OpenAI adapter auto-infers from name; override with `openai.model({ name, vision: true })`.
|
|
3748
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
|
+
|
|
3749
3777
|
## Pattern — streaming
|
|
3750
3778
|
|
|
3751
3779
|
```ts
|
|
@@ -3858,6 +3886,7 @@ The one field a bare agent config doesn't surface ergonomically is `budget` (`Bu
|
|
|
3858
3886
|
- [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — tool wiring + schema validation
|
|
3859
3887
|
- [`@warlock.js/ai/write-system-prompt/SKILL.md`](@warlock.js/ai/write-system-prompt/SKILL.md) — persona / instruction builders
|
|
3860
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
|
|
3861
3890
|
|
|
3862
3891
|
|
|
3863
3892
|
## run-ai-rag `@warlock.js/ai/run-ai-rag/SKILL.md`
|
|
@@ -4549,11 +4578,14 @@ ai.orchestrator({
|
|
|
4549
4578
|
recall: { k: 5, threshold: 0.7, tier: "semantic" }, // k: 0 = write-only memory
|
|
4550
4579
|
remember: true, // false = read-only (recall, never write)
|
|
4551
4580
|
rememberTier: "semantic",
|
|
4581
|
+
scope: "session", // DEFAULT — isolate memories per sessionId
|
|
4552
4582
|
injectKey: "memories", // ctx.context[injectKey] holds RecalledMemory[]
|
|
4553
4583
|
},
|
|
4554
4584
|
});
|
|
4555
4585
|
```
|
|
4556
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
|
+
|
|
4557
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).
|
|
4558
4590
|
|
|
4559
4591
|
## `asTool()` — orchestrator as a tool
|
|
@@ -4571,7 +4603,9 @@ const concierge = ai.agent({ model, tools: [supportTool] });
|
|
|
4571
4603
|
|
|
4572
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`:
|
|
4573
4605
|
- **`"fresh"`** (default) — each invocation gets a generated `sessionId` and empty history; no continuity across calls.
|
|
4574
|
-
- **`"shared"`** — the
|
|
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.
|
|
4575
4609
|
|
|
4576
4610
|
## Drift detection
|
|
4577
4611
|
|
|
@@ -4603,7 +4637,7 @@ await orch.execute(input, { sessionId, history, on: { "orchestrator.drift.checke
|
|
|
4603
4637
|
|
|
4604
4638
|
---
|
|
4605
4639
|
name: run-planner
|
|
4606
|
-
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`.'
|
|
4607
4641
|
---
|
|
4608
4642
|
|
|
4609
4643
|
# `ai.planner()` — LLM-generated, then executed, plans
|
|
@@ -4633,7 +4667,7 @@ const research = ai.planner({
|
|
|
4633
4667
|
{ name: "summarize", description: "Summarize text into bullet points", executable: summarizer },
|
|
4634
4668
|
{ name: "write", description: "Draft a final report", executable: writerAgent },
|
|
4635
4669
|
],
|
|
4636
|
-
maxSteps: 6, //
|
|
4670
|
+
maxSteps: 6, // soft cap; steps beyond it are recorded as "skipped" — see the parse-time ceiling below
|
|
4637
4671
|
});
|
|
4638
4672
|
|
|
4639
4673
|
const { data, report, usage, error } = await research.execute("Compare React vs Vue in 2026");
|
|
@@ -4656,6 +4690,10 @@ for (const step of report.executedSteps) { // forensic, in execution order
|
|
|
4656
4690
|
|
|
4657
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.
|
|
4658
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
|
+
|
|
4659
4697
|
## DAG scheduling — `dag: true` + `maxConcurrency`
|
|
4660
4698
|
|
|
4661
4699
|
Run independent steps in parallel instead of array-order:
|
|
@@ -4722,7 +4760,7 @@ const final = await planner.execute(goal, { approvedPlan: draft.plan! });
|
|
|
4722
4760
|
|
|
4723
4761
|
`execute()` never throws — failures surface on `result.error`:
|
|
4724
4762
|
|
|
4725
|
-
- **`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`,
|
|
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`).
|
|
4726
4764
|
- **`PlannerCancelledError`** (`PLANNER_CANCELLED`, category `cancelled`) — the `AbortSignal` fired. `report.status === "cancelled"`, `report.cancelledAt` set; remaining steps are `skipped`.
|
|
4727
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`.
|
|
4728
4766
|
- **`PlannerFailedError`** is the base for the `PLANNER_*` family.
|
|
@@ -4848,6 +4886,16 @@ ai.supervisor({
|
|
|
4848
4886
|
|
|
4849
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 }`.
|
|
4850
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
|
+
|
|
4851
4899
|
## The `intents` map — five accepted shapes
|
|
4852
4900
|
|
|
4853
4901
|
```ts
|
|
@@ -4895,7 +4943,7 @@ const refundSupervisor = ai.supervisor<RefundOutput>({
|
|
|
4895
4943
|
});
|
|
4896
4944
|
```
|
|
4897
4945
|
|
|
4898
|
-
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.
|
|
4899
4947
|
|
|
4900
4948
|
## Per-intent `next` — skip the router
|
|
4901
4949
|
|
|
@@ -5115,6 +5163,95 @@ const escalationAgent = ai.agent({ model, tools: [supportTool] });
|
|
|
5115
5163
|
- [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — tool artifacts side-channel
|
|
5116
5164
|
|
|
5117
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
|
+
|
|
5118
5255
|
## transcribe-audio `@warlock.js/ai/transcribe-audio/SKILL.md`
|
|
5119
5256
|
|
|
5120
5257
|
---
|
|
@@ -5280,14 +5417,14 @@ Scripting `[{ error: new ProviderRateLimitError("slow down") }]` drives the neve
|
|
|
5280
5417
|
|
|
5281
5418
|
---
|
|
5282
5419
|
name: use-ai-memory
|
|
5283
|
-
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.'
|
|
5284
5421
|
---
|
|
5285
5422
|
|
|
5286
5423
|
# `ai.memory()` — agent memory store
|
|
5287
5424
|
|
|
5288
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:
|
|
5289
5426
|
|
|
5290
|
-
- **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).
|
|
5291
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.
|
|
5292
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`.
|
|
5293
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`.
|
|
@@ -5357,7 +5494,7 @@ await mem.remember({ text: "User is on the Enterprise plan.", tier: "semantic",
|
|
|
5357
5494
|
await mem.remember([{ text: "a" }, { text: "b", tier: "working" }]); // batch
|
|
5358
5495
|
```
|
|
5359
5496
|
|
|
5360
|
-
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.
|
|
5361
5498
|
|
|
5362
5499
|
### `recall(query, options?)`
|
|
5363
5500
|
|
|
@@ -5366,6 +5503,7 @@ const hits = await mem.recall("which plan is the user on?", {
|
|
|
5366
5503
|
k: 5, // cap result count (defaults to factory k)
|
|
5367
5504
|
threshold: 0.75, // raise the semantic floor for this call
|
|
5368
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
|
|
5369
5507
|
});
|
|
5370
5508
|
|
|
5371
5509
|
for (const hit of hits) {
|
|
@@ -5377,6 +5515,38 @@ Returns `RecalledMemory[]` scored and ordered by descending relevance. By defaul
|
|
|
5377
5515
|
|
|
5378
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.
|
|
5379
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
|
+
|
|
5380
5550
|
### `clear(tier?)`
|
|
5381
5551
|
|
|
5382
5552
|
```ts
|
|
@@ -5449,7 +5619,7 @@ const lib = ai.skills({
|
|
|
5449
5619
|
### Sources — `SkillSource` (discriminated by `type`, never `kind`)
|
|
5450
5620
|
|
|
5451
5621
|
- `{ type: "directory", path }` — reads `path/<folder>/SKILL.md` off disk (lazy `node:fs/promises`).
|
|
5452
|
-
- `{ type: "url", url, headers? }` — `
|
|
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.
|
|
5453
5623
|
- `{ type: "store", store }` — any `SkillsStoreContract`, e.g. `MockSkillsStore`.
|
|
5454
5624
|
|
|
5455
5625
|
Sources merge in order; a later source wins on a name collision.
|
|
@@ -5512,6 +5682,7 @@ The optional `analytics` sink fires `catalogued` / `loaded` / `used` / `saved` /
|
|
|
5512
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)
|
|
5513
5683
|
- [`@warlock.js/ai/use-ai-memory/SKILL.md`](@warlock.js/ai/use-ai-memory/SKILL.md) — the procedural memory tier `proceduralSkillStore` unifies with
|
|
5514
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
|
|
5515
5686
|
|
|
5516
5687
|
|
|
5517
5688
|
## write-system-prompt `@warlock.js/ai/write-system-prompt/SKILL.md`
|