@warlock.js/ai 5.0.2 → 5.1.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 +196 -179
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
# Changelog — @warlock.js/ai
|
|
2
|
-
|
|
3
|
-
All notable changes to `@warlock.js/ai` are documented in this file.
|
|
4
|
-
|
|
1
|
+
# Changelog — @warlock.js/ai
|
|
2
|
+
|
|
3
|
+
All notable changes to `@warlock.js/ai` are documented in this file.
|
|
4
|
+
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.1.0
|
|
8
|
+
|
|
9
|
+
No changes to `@warlock.js/ai`. Released in lockstep with the `@warlock.js/web`
|
|
10
|
+
React-execution fix and the `@warlock.js/core` CLI additions — see those packages'
|
|
11
|
+
changelogs.
|
|
12
|
+
|
|
13
|
+
## 5.0.2 - 2026-08-25
|
|
14
|
+
|
|
15
|
+
No changes to `@warlock.js/ai`. Released in lockstep with the `@warlock.js/web` SSR
|
|
16
|
+
fix (`ssr.noExternal`) — see that package's changelog.
|
|
17
|
+
|
|
18
|
+
## 5.0.1 - 2026-08-25
|
|
19
|
+
|
|
20
|
+
No changes to `@warlock.js/ai`. Released in lockstep with the `create-warlock` vite
|
|
21
|
+
resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
|
|
22
|
+
changelogs.
|
|
23
|
+
|
|
7
24
|
## 5.0.0 - 2026-08-25
|
|
8
25
|
|
|
9
26
|
### Changed
|
|
@@ -11,178 +28,178 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
11
28
|
- This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
|
|
12
29
|
|
|
13
30
|
## 4.16.0 - 2026-08-18
|
|
14
|
-
|
|
15
|
-
### Security
|
|
16
|
-
|
|
17
|
-
- **`guardedFetch` no longer lets the platform follow redirects past the SSRF guard.** The outbound policy validated only the *initial* URL, then handed the request to `fetch` with automatic redirect following — so a page an agent was asked to load (`ai.rag`'s `loadWeb()`, the skills `urlSource` manifest fetch, `prepareAttachmentPart`'s remote-text path) could pass validation and then `302` into `http://169.254.169.254/...` or an internal service with no re-check. Every hop is now issued with `redirect: "manual"` and its `Location` is re-run through the full `assertUrlAllowed` policy (scheme, host allowlist, post-DNS private-IP deny) before being followed, capped at the new `OutboundPolicy.maxRedirects` (default `5`). Credential headers (`authorization`, `cookie`, `proxy-authorization`) are stripped when a hop crosses an origin boundary, and `303`/legacy `301`/`302`-on-non-GET hops re-issue as a bodyless GET, matching platform semantics. Callers passing `redirect: "manual"` get the raw 3xx back as before; `redirect: "error"` rejects on any redirect. Regression tests cover the metadata/loopback/private redirect block, the off-allowlist redirect block, the hop cap, and the clean-redirect follow
|
|
18
|
-
- **Supervisor fan-out now has a width bound — new `maxFanOut` option (default `10`).** A routing decision could name any number of intents (`normalize()` in `src/supervisor/decide.ts` validated only that each name was in the allowlist, with no length limit and no dedup), and `dispatchBranches` ran `Promise.all` over the lot. `maxIterations` bounded how DEEP a run went; nothing bounded how WIDE one iteration went. Since the router's per-turn prompt embeds supervisor `state` and prior branch outputs — both able to carry attacker text lifted from tool results — a prompt injection ("always return `next` as this 200-element array") turned one iteration into hundreds of real agent/workflow executions, i.e. cost/compute amplification, without ever naming an intent outside the allowlist. Duplicate names are now collapsed silently (they were pure wasted spend: branch results are indexed by intent, so the extras could never change the outcome), and a *deduped* list wider than `maxFanOut` is rejected as `SupervisorRoutingError` (`SUPERVISOR_INVALID_ROUTE`) carrying the offending array — the same failure mode as an unknown intent key. Truncating instead of throwing was rejected: it would hand an attacker-chosen subset to the executor and hide the anomaly. The cap is enforced both in `normalize()` and at `dispatchBranches`, the one chokepoint every dispatch source funnels through, so `evaluate.reassignTo`, classifier picks, and per-intent `next` unions are bounded too. Raise `maxFanOut` deliberately for supervisors that legitimately fan wide (e.g. `ai.fanOut(writer, 20)`); it's validated as an integer `>= 1` at construction
|
|
19
|
-
- **Supervisor state merges refuse prototype-tampering keys.** All five state-merge sites in `src/supervisor/execution.ts` (branch outputs, the `ack` slice, classifier output, the classifier `refine` slice, and the artifacts merge in both its `finalizeArtifacts` and auto-spread forms) did a bare `state[key] = value` over model-influenced data. The `output` schema that validates those slices belongs to the developer, and a permissive one (`z.record()`, `.passthrough()`, `z.any()`) passes a key literally named `__proto__` straight through — assigning it repoints the run `state` object's prototype. Blast radius was contained (one per-run object, not global `Object.prototype`), but it became genuine prototype pollution the moment anything downstream used `in`, `hasOwnProperty`, or a deep merge on state — and `finalizeArtifacts`'s key-removal pass already used `key in merged`. A shared `mergeSafely` / `assignSafeKey` / `isUnsafeMergeKey` guard (new `src/security/safe-merge.ts`, exported from the package's security barrel) now drops `__proto__` / `constructor` / `prototype` at every one of those sites and logs the refusal as `state.merge.unsafe-key`; that `key in merged` check is now `Object.hasOwn`. Dropping rather than throwing is deliberate — those keys are never legitimate state fields, and mid-iteration is the wrong place to fail a settled run
|
|
20
|
-
|
|
21
|
-
- **`orchestrator.asTool({ sessionScope: "shared" })` no longer lets the calling model choose which session it joins.** The wrapper read `sessionId` (and `history`) straight out of the *validated tool-call payload* — i.e. out of arguments the outer agent's LLM wrote — and handed them to `orchestrator.execute()`, which loads that session's checkpoint, merges its persisted `state`, runs a turn against it and writes a fresh checkpoint back. The JSDoc actively instructed developers to thread the session id through `inputSchema`. A `sessionId` is bearer-equivalent to full read/write on the session, so any prompt injection reaching the outer agent (a summarized document, a poisoned tool result, a fetched page) could say "continue session `<victim-id>`" and have the nested orchestrator splice an attacker-directed turn into a stranger's live conversation and return its content — including prior state — into the outer transcript. The target session is now bound OUTSIDE the model-visible schema, via the new `OrchestratorAsToolOptions.session`: either a literal id fixed at `asTool()` construction, or a `(ctx) => sessionId | { sessionId, history }` resolver reading the invocation's `ToolContext` (the same out-of-band channel `signal` / `artifacts` already travel on, which an LLM cannot write to). `sessionId` / `history` are stripped from the payload before it is forwarded as `execute(input)`, and a resolver that returns nothing fails the call rather than falling back to the payload. **Breaking for `"shared"` scope only:** building such a tool without `session` now throws at construction. The pre-4.15.0 behavior is still reachable behind `unsafeAllowModelSessionId: true`, documented at the API surface as bearer-token-equivalent access that obliges the developer to verify session ownership themselves. `"fresh"` scope (the default) is unchanged
|
|
22
|
-
- **Orchestrator/agent memory is session-scoped by default — recall can no longer surface another user's remembered turns.** `OrchestratorConfig.memory` is resolved once per orchestrator instance and reused by every `execute()` / `resume()` regardless of `sessionId`, and neither `MemoryItem`, `RecallOptions`, `MemoryContract` nor the four tier implementations carried any session/tenant key — so `recall()` could not be scoped to the calling session and `remember` (ON by default) wrote every clean turn's input + outcome text into one shared namespace. In the framework's own documented integration pattern (one `ai.memory()` built at boot, passed to `ai.orchestrator({ memory })`, serving all end users) user A's remembered text was recallable by user B's semantically similar turn, with no attacker action required. `MemoryItem.scope` and `RecallOptions.scope` are new opaque isolation keys, enforced **inside** each tier as an exact-equality match before hits are scored, merged or sliced — never left to the caller — and folded into the stored key so two scopes writing identical text stay two entries (including the procedural tier's `uses` reinforcement counter). All four tiers enforce it: `working`, `semantic`, `episodic`, `procedural`. An unscoped `recall()` reads only the unscoped pool; there is no wildcard query. The orchestrator derives the scope from the execute-time `sessionId` (`"session:<id>"`) — not from the payload, the context bag, or the model — via the new `OrchestratorMemoryConfig.scope`, which defaults to `"session"`. **Behavior change:** memories seeded or written before the upgrade are unscoped and are no longer recalled by a session-scoped turn. Cross-session pooling is now an explicit opt-in — `scope: "shared"` restores the pre-4.15.0 single-pool behavior (and keeps reading pre-upgrade entries); `scope: (sessionId) => key` derives a custom boundary, e.g. per tenant. The vector tiers overscan before filtering so a noisy neighbouring scope cannot starve a scoped recall of its top-`k`
|
|
23
|
-
|
|
24
|
-
- **The working-memory tier is size-bounded — new `working: { maxItems }` (default `1000`).** `WorkingMemory` backed its buffer with a plain `Map` that grew by one entry per unique `remember()` and had no cap, TTL or eviction of any kind. It is also the one tier that keeps everything it is told in *process* memory, for the lifetime of the `memory()` instance — which `ai.orchestrator({ memory })` resolves ONCE and reuses for every session, for as long as the process runs. Since distinct text derives a distinct id nothing dedups, so an attacker able to drive turns through a memory-backed orchestrator (with `remember` on by default) added a permanent entry per request until the process ran out of memory: a cheap memory-exhaustion DoS against any internet-facing deployment. The buffer now evicts on overflow. **Policy is FIFO over insertion order, not LRU,** and deliberately so: recall on this tier is a pure recency proxy (it reverses insertion order and slices the newest `k`, never reordering), so the front of the buffer is by construction the region recall reaches last — FIFO evicts exactly the entries a bounded recall would never have returned, while true LRU would need read-time reordering that would also rewrite the `score` every recall reports. Re-remembering an existing id still updates in place and keeps its slot. `maxItems` is validated as an integer `>= 1` at construction and has no unbounded setting — "no cap" is 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`. Known and documented limitation: the bound is global rather than per-scope, so a busy session can push another's older entries out — a recall-quality degradation on a volatile scratch tier, never a disclosure (the scope filter still applies), and a per-scope quota would not help against an attacker holding many sessions anyway
|
|
25
|
-
- **`semanticCache()` is per-session-scoped by default — one caller's cached answer is no longer served to another.** The middleware is built once at app boot and shared by every end user, its `namespace` was a static string, and a hit is returned as a synthetic `ModelResponse` with no LLM call in between — so both lookup paths (the exact prompt-hash key and the vector `similar()` match) could serve user A's cached response, personal context and all, to user B's merely *similar* prompt, and let an attacker seed an entry engineered to sit near a predictable class of future queries and have it answered from the store thereafter. Entries now carry a `scope` derived from the run's own `AgentExecuteOptions.sessionId` (`"session:<id>"`, the same derivation the memory fix uses) — read out of the execute options, never out of the prompt or the model's output — folded into the stored key (hashed, since a `sessionId` is caller-supplied and may contain the key delimiter) *and* re-checked as exact equality on the stored entry, so key-level separation is never the thing authorizing a read. The vector path overscans before filtering, mirroring the memory tiers, so a noisy foreign session cannot occupy the top-`k` and mask a caller's own hit. New `SemanticCacheOptions.scope`: `"session"` (default), `"shared"` (one pool for every caller — the explicit opt-in for genuinely public Q&A, and the pre-4.15.0 behavior), or `(context) => key` for a custom boundary such as per-tenant. **Two behavior changes to expect:** entries written before the upgrade are unscoped and are only read by unscoped runs, and scoping trades cross-user hit rate for isolation — a public FAQ deployment where no response can carry a caller's private context should now set `scope: "shared"` on purpose. Runs made *without* a `sessionId` continue to share one unscoped pool (unchanged behavior for them); thread `sessionId` through `execute()` — composites already do — to get the isolation
|
|
26
|
-
- **The planner's plan schema rejects an over-long plan at parse time.** Strict-mode JSON Schema cannot express `maxItems`, so `maxSteps` was never on the wire (`plan-schema.ts` discarded the parameter outright with `void maxSteps`) and the only enforcement was `PlannerRun`'s tail truncation — which runs *after* the whole `steps[]` array has been parsed, normalized into `PlannerStep[]` and stored on `this.plan`. A provider or proxy that ignores the prompt's step budget could therefore make the planner deserialize an arbitrarily long array before anything trimmed it. `validate()` now enforces a hard ceiling of `maxSteps * 4` (or `100` when `planSchema` is built without a `maxSteps`), rejecting rather than truncating: a plan several times its budget is a malfunction worth surfacing as the typed `PlannerPlanInvalidError`, not a prefix worth silently executing. The slack factor keeps the normal case — a model overshooting "at most N steps" slightly, which the runtime still truncates to `skipped` — working exactly as before
|
|
27
|
-
|
|
28
|
-
### Notes
|
|
29
|
-
|
|
30
|
-
- Suite: **176 files / 1943 tests, 1940 passing** (3 pre-existing environmental cold-import timeouts). Adds regression tests for every security fix above: the `guardedFetch` redirect/SSRF block, the supervisor `maxFanOut` cap + prototype-key state-merge guard, the `asTool` session binding and per-tier memory scope isolation, the working-memory cap, the `semanticCache` session scope, and the planner parse-time step ceiling
|
|
31
|
-
|
|
32
|
-
## 4.15.0 - 2026-08-16
|
|
33
|
-
|
|
34
|
-
### Fixed
|
|
35
|
-
|
|
36
|
-
- **`ctx.run(agent, payload)` now stringifies a non-string payload, as it always claimed to.** `coerceInlineInput` in `src/supervisor/execution.ts` gated on `!("signature" in executable)` to decide whether the target was an agent — but every member of `SupervisableExecutable` (`AgentContract`, `WorkflowInstance`, `SupervisorContract`) declares `signature`, so the condition was **permanently false and the coercion never ran**. A supervisor intent calling `ctx.run(someAgent, { question: "why", attempt: 2 })` handed the raw object to `agent.execute()`, where it landed as the user message `content` — `[object Object]` in the prompt, or a provider-side payload rejection, depending on the adapter. The check now discriminates on `isAnonymous`, the one member unique to `AgentContract`. A regression test covers it; the old guard fails it with `Expected: "string" / Received: "object"`. The unreferenced `isSupervisor()` duck-type helper — whose own JSDoc admitted it could not tell a supervisor from a workflow — is removed
|
|
37
|
-
- **`ToolMeta` no longer forces `label` and `actionLabel` on every tool that supplies `meta`.** It was declared as `Record<"label" | "actionLabel" | (string & {}), unknown>`, which makes both keys **required**, not optional — so any tool author who set one metadata field was made to set all of them. Now an optional-key shape with an index signature
|
|
38
|
-
- **`ToolConfig.action` is checked bivariantly**, via a `ToolActionResolver<T>` method-in-wrapper. The strictly contravariant parameter position rejected heterogeneous tool arrays that work correctly at runtime
|
|
39
|
-
- **`new Error(msg, { cause })` compiles.** `tsconfig.json` declared no `lib`, so it inherited the `target` default of ES2020, where `ErrorOptions` does not exist. `lib` is now `["ES2022"]`; this also resolves the `Array.at` and `String.replaceAll` errors. Emit is unchanged — `target` is still ES2020. Note `src/skills/sources/url-source.ts:122` was **not** a defect: the `cause` was always passed at runtime, the compiler simply had no type for it
|
|
40
|
-
- **`TeamMemberValue` accepts the callback member form** (`IntentCallback`), which has always worked at runtime and was only rejected by the type
|
|
41
|
-
- **`PlanSchema` no longer erases `~standard.jsonSchema`** from its return type
|
|
42
|
-
|
|
43
|
-
### Changed
|
|
44
|
-
|
|
45
|
-
- **`MockModelResponse.usage` is a new `MockUsage` type rather than the emitted `Usage`.** The script is an input, not a result: `MockModel.buildResponse` honours only `input` / `output` / `cachedTokens`, so a fixture declaring `cost` or `reasoningTokens` was silently discarded while the type promised otherwise. `total` is optional and documented as derived, because the mock recomputes it as `input + output` — an existing spec deliberately asserts that a mismatched scripted `total` is overridden
|
|
46
|
-
- **The mock honours `deltas`.** Fixtures already declared the field; the mock ignored it
|
|
47
|
-
- `MockSDK.model()` declares its `MockModel` return type — it always returned one, so `callHistory` is now reachable without a cast. `MockUsage` is exported from the barrel
|
|
48
|
-
|
|
49
|
-
### Notes
|
|
50
|
-
|
|
51
|
-
- Typecheck against the package's own TypeScript 6.0.3: **89 → 24 errors, and all 24 remaining are a monorepo-only artifact** (`TS6059`, `@warlock.js/cache` resolved through a `paths` mapping outside `rootDir`). They do not affect the published package, which ships built `exports` and `.d.mts`. In-scope errors are zero
|
|
52
|
-
- Suite: **173 files / 1878 tests passing**, up one from the new regression test
|
|
53
|
-
|
|
54
|
-
## 4.12.0
|
|
55
|
-
|
|
56
|
-
### Changed
|
|
57
|
-
|
|
58
|
-
- Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
|
|
59
|
-
|
|
60
|
-
## 4.9.0 - 2026-08-06
|
|
61
|
-
|
|
62
|
-
### Added
|
|
63
|
-
|
|
64
|
-
- `StepSnapshot.children` — reports a workflow `run` step captured from any executable its callback invoked DIRECTLY (`agent.execute(...)` rather than the declarative `agent:` field), via the same ambient `RunFrame` a supervisor/team/orchestrator callback already gets. `report.children` now includes these alongside `step.agent` reports.
|
|
65
|
-
|
|
66
|
-
### Fixed
|
|
67
|
-
|
|
68
|
-
- A workflow `run` step that calls `agent.execute()` directly no longer produces two disconnected top-level traces (one "agent", one "workflow") with the agent missing from `report.children` — it now nests correctly (`workflow → agent → tool`, usage/cost rolled up) and no longer also self-routes as a separate observed trace. Declarative `step.agent` was already correct; this closes the gap for ad-hoc calls inside `run` (self-documented in `workflow/engine.ts` as a known limitation).
|
|
69
|
-
|
|
70
|
-
## 4.8.2 - 2026-07-22
|
|
71
|
-
|
|
72
|
-
### Added
|
|
73
|
-
|
|
74
|
-
- `judgePromptBody` / `formatCriteria` / `JudgeOutcome` — the LLM-as-judge building blocks `ai.prompts().validate()` already used internally are now public, so other packages (`@warlock.js/ai-panoptic`'s trace-level system-prompt evaluation) can grade arbitrary prompt text against a model + rubric without a second judging implementation
|
|
75
|
-
|
|
76
|
-
### Fixed
|
|
77
|
-
|
|
78
|
-
- `redact()` no longer collapses a raw `Error` (or an `Error` nested in a `cause` chain) to `{}` — `name` / `message` / `stack` aren't own-enumerable on `Error` instances, so the previous `Object.entries()` walk saw none of them. This was silently dropping tool/agent error `cause` detail wherever `redact()` runs it, including `@warlock.js/ai-panoptic`'s trace `cause` field (a failed tool's `ToolExecutionError.cause` showed as an empty object in the dashboard instead of the underlying thrown error)
|
|
79
|
-
- `@warlock.js/ai-openai` and `pdf-parse` declared as optional `peerDependencies` — both are lazily `import()`ed (the skills-catalog embedder probe; `ai.rag.loadPdf`) but weren't listed in either `dependencies` or `peerDependencies`, so pkgist's bundler vendored their source directly into `ai`'s own build instead of leaving them external (the same split-brain class of bug as `core`'s missing `@warlock.js/ai` peerDependency, fixed in 4.8.1). For `@warlock.js/ai-openai` specifically this meant the skills-catalog embedder-installed probe always resolved against the vendored copy bundled into `ai`, so it reported an embedder provider as "installed" even when the app never installed `@warlock.js/ai-openai` itself
|
|
80
|
-
|
|
81
|
-
## 4.8.1 - 2026-07-21
|
|
82
|
-
|
|
83
|
-
### Fixed
|
|
84
|
-
|
|
85
|
-
- `setAIConfig`'s `onConfigApplied` listener notification no longer swallows a misbehaving listener's exception silently — it's now logged via `log.error("ai", "configListener", error)`
|
|
86
|
-
|
|
87
|
-
## 4.8.0 - 2026-07-19
|
|
88
|
-
|
|
89
|
-
### Added
|
|
90
|
-
|
|
91
|
-
- **`reasoning: { effort: "none" }`** — a neutral "run without reasoning, explicitly" level on `ReasoningEffort`; OpenAI emits `reasoning_effort: "none"` so gpt-5 / o-series accept function tools, and budget-based adapters (Anthropic / Bedrock / Google / Ollama) disable thinking.
|
|
92
|
-
|
|
93
|
-
## 4.7.0
|
|
94
|
-
|
|
95
|
-
### Added
|
|
96
|
-
|
|
97
|
-
- **`systemPrompt().refined({ model, criteria, store })`** — the prompt compiler. Humans keep writing human prompt text; the refined wrapper lazily rewrites it into a model-optimized version on first agent use and pins the result like a lockfile (re-compiled only when the source text, refiner model, `criteria`, or recipe version change — never silently). `await refined.refine()` returns the compiled template **string** (placeholders intact — routes / previews / warmup / CI; throws `PromptRefinementError` on failure) and `await refined.refinePrompt()` returns a composable prompt with `meta.refinedFrom` / `meta.refinerModel` provenance (register it to `diff` original vs refined). Placeholder parity is machine-enforced (one repair re-ask, then rejected); the lazy agent path never throws — it warns once and serves the original.
|
|
98
|
-
- **`ai.prompts.validate({ criteria })`** — validate a prompt against your **own** rules. Pass `criteria` (a string or a list of short rules) and, when a `judge` model is supplied, it replaces the built-in quality rubric so the judge's `score` / `issues` reflect your criteria (a failed rule is named in `issues`). Advisory only — never flips the deterministic `ok`; folded into the `judgeCache` key so different rules re-run.
|
|
99
|
-
|
|
100
|
-
## 4.6.0
|
|
101
|
-
|
|
102
|
-
### Added
|
|
103
|
-
|
|
104
|
-
- **`ai.image(params)`** — image generation, the first verb of the output-modality track (Theme I). Wraps an `ImageModelContract` in the uniform never-throws `{ data, error, usage, report }` envelope, with cost-truth (per-token for `gpt-image`, per-image for DALL·E / Imagen) folded into the same `Usage.cost` rollup and a `type: "image"` report routed to observers. Ships on the OpenAI + Google adapters.
|
|
105
|
-
- **`SDKAdapterContract.image?(config)`** — the image-model capability seam, mirroring `embedder?()`. Adds `ImageModelContract`, `GeneratedImage` (discriminated `base64` | `url`), `ImageModelPricing`, and `ImageGenerationOptions`.
|
|
106
|
-
- **`MockSDK().image(...)` + `MockImageModel`** — deterministic image doubles (scriptable responses, recorded calls, pricing) for tests.
|
|
107
|
-
- **`ai.speech(params)` + `ai.transcribe(params)`** — text-to-speech and speech-to-text, the audio verbs of the modality track. Same uniform never-throws envelope + cost-truth (per-character / per-minute / per-token). New `SpeechModelContract` / `TranscriptionModelContract` on `SDKAdapterContract.speech?()` / `transcribe?()`, plus `MockSpeechModel` / `MockTranscriptionModel`.
|
|
108
|
-
- **`ai.audioFromFile(path)` / `ai.audioFromBuffer(bytes, mediaType)` / `ai.audioMediaTypeForFilename(name)`** — non-AI utilities that package audio (WhatsApp `.ogg`/`.opus`, iOS `.m4a`, …) into the `AudioInput` shape `ai.transcribe` consumes.
|
|
109
|
-
- **`ai.rag.pgVectorStore({ client })`** — a Postgres + pgvector vector store satisfying `VectorStoreContract` (upsert / query / removeNamespace), with an `ensureSchema()` DDL helper and a lazy `pg` optional peer.
|
|
110
|
-
- **`ai.rag.loadText` / `loadHtml` / `loadWeb` / `loadPdf`** — document loaders producing `RagDocument`s for `.index()`. `loadWeb` is SSRF-safe (routes through `guardedFetch` / `OutboundPolicy`); `loadPdf` uses a lazy `pdf-parse` optional peer.
|
|
111
|
-
- **Durable mid-run crash-resume** — opt-in `durable: { store, deleteOnComplete? }` on `ai.agent` / `ai.planner` with a stable `runId` + `agent.resume(runId)` / `planner.resume(runId)`. Per-trip (agent) / per-node (planner) checkpoints reuse `ai.snapshot.{memory,pg,redis}`; drift detection via `AgentDriftError` / `PlannerDriftError` (bypass with `{ force: true }`); completed work never re-runs its tools and usage is never double-counted.
|
|
112
|
-
- **`ai.rag.*` namespace** now also carries `chunk`, `cacheVectorStore`, `pgVectorStore`, `loadText`/`loadHtml`/`loadWeb`/`loadPdf`, `bm25Rank`, `reciprocalRankFusion`, `hybridRank`, `multiQuery` (previously standalone-only exports), for `ai.*`-namespace consistency.
|
|
113
|
-
|
|
114
|
-
## 4.5.0 - 2026-07-01
|
|
115
|
-
|
|
116
|
-
### Added
|
|
117
|
-
|
|
118
|
-
- **`ai.rag(config)`** — retrieval-augmented generation in core: a chunk → embed → retrieve → cite pipeline that reuses your existing embedder and cache, with zero new dependencies. Includes hybrid retrieval (dense + BM25 reciprocal-rank fusion), keyword / LLM rerankers, and multi-query expansion.
|
|
119
|
-
- **`ai.team(config)`** — manager-led multi-agent teams: thin sugar over `ai.supervisor` for the review-then-fix and test-then-fix shapes.
|
|
120
|
-
- **`ai.skills(config)`** — runtime agent skills with progressive disclosure: a cheap always-injected catalog plus an on-demand `loadSkill` tool. Adds a `skills` option on `ai.agent`.
|
|
121
|
-
- **`ai.streamObject(...)`** — structured-output streaming: partial-object snapshots as tokens arrive, with a strict final parse against the response schema.
|
|
122
|
-
- **`ai.serve(executable, options)`** — serve any agent / workflow / supervisor as an SSE HTTP endpoint.
|
|
123
|
-
- **Multimodal attachments** — `ContentPart` gains `pdf` and `audio` variants alongside text / image, resolved to provider-ready parts (PDF wired on the Anthropic and Bedrock adapters).
|
|
124
|
-
- **Planner DAG execution, re-planning, and plan-only approval** — run independent steps concurrently, revise the plan when a step fails, or return a plan for approval before it executes.
|
|
125
|
-
- **Generic `Observer` seam** — route any flow's run report to pluggable observers (e.g. `@warlock.js/ai-panoptic`) without coupling core to a backend.
|
|
126
|
-
- **`ai.prompts` + `ai.prompt`** — a process-wide registry of named, versioned `systemPrompt(...)` builders (resolved by `name@version` / `name@tag`) with `define` / `tag` / `diff` / `export` / `import` and a unified `validate` (deterministic missing-placeholder check plus an optional Nova-safe LLM-judge); `ai.prompt` is a thin facade over it.
|
|
127
|
-
- **`SystemPromptContract` identity + provenance** — `.meta({ name, version, description, required })` (a name auto-registers in `ai.prompts`), `.merge(...blocks)` / `.merge(contract)` / `.merge(name, { fromVersion })`, and deterministic `meta.composedFrom` labels.
|
|
128
|
-
- **`ai.dataset(options)`** — filterable, shardable evaluation case sets that feed `agent.eval`, with baseline / regression detection and CI reporters.
|
|
129
|
-
- **`ai.vcr(model, options)`** — record / replay any model against an on-disk cassette for deterministic, offline tests, with `recordRequest` modes and `redactRequest` / `redactResponse` / `redactError` hooks.
|
|
130
|
-
- **`ai.agent.judge(config)`** — judge-safe agent preset (also `ai.agent({ judge: true })`): lenient JSON parsing, bounded repair re-asks, and never-throw verdicts on Nova-class models.
|
|
131
|
-
- **Human-in-the-loop approval now ships in core** (`ai.human.*`, formerly `@warlock.js/ai-human`) — a tool-approval gate plus durable interrupt / resume.
|
|
132
|
-
- **Content guardrails now ship in core** (`ai.guardrail.*`, formerly `@warlock.js/ai-guard`) — PII / topic / injection / moderation detectors.
|
|
133
|
-
- **Orchestrator `sessionLock`** — per-session turn serialization (default in-process mutex keyed by `sessionId`, pluggable distributed lock) so concurrent same-session turns can't lose a checkpoint update.
|
|
134
|
-
- **Sub-agent trace nesting** — a supervisor / team / orchestrator callback that calls `agent.execute()` directly now nests `callback → agent → tool` with rolled-up usage / cost.
|
|
135
|
-
- **`AgentReport.systemPrompt`** — the resolved system prompt sent to the model is now recorded on the agent report.
|
|
136
|
-
|
|
137
|
-
### Changed
|
|
138
|
-
|
|
139
|
-
- **`ai.team` runs report `type: "team"`** — a first-class `ReportType` (was `"supervisor"`) so observers distinguish team runs on the wire.
|
|
140
|
-
- **Deterministic parallel workflow state merge** — parallel children merge into the parent in declaration order (last-declared wins on a conflicting key) instead of completion order; an optional per-step `mergeState` reducer overrides it.
|
|
141
|
-
- **Safer batch / RAG defaults** — `ai.batch` warns once on a large unbounded run (pass an explicit `concurrency` or `"unbounded"`); `ai.rag` accepts `limits` (`maxDocuments` / `maxChunks` / `maxBytes`) that fail before any embedding spend.
|
|
142
|
-
|
|
143
|
-
### Fixed
|
|
144
|
-
|
|
145
|
-
- **Cancellation propagates through composite tools** — a cancelled outer agent now aborts a nested agent / workflow / supervisor invoked via `.asTool()` (the run signal threads into the nested `execute`).
|
|
146
|
-
- **Observer / event-handler errors are surfaced, not swallowed** — a throwing observer or `on` handler stays isolated (never crashes the run) but is now warned once / routed to a hook instead of disappearing silently.
|
|
147
|
-
- **`budget({ maxCostUSD })` fail-open closed** — a cost cap with no matching model pricing now warns once (naming the model) instead of silently never tripping.
|
|
148
|
-
|
|
149
|
-
### Security
|
|
150
|
-
|
|
151
|
-
- **Shared `OutboundPolicy` + `redact()`** — one SSRF-safe outbound-fetch guard (scheme + host allowlist, post-DNS private-IP deny, max-bytes, timeout, injectable fetch) and one redaction utility, consumed across attachments, URL skills, VCR, and the error path.
|
|
152
|
-
- **Attachment trust boundary** (`AttachmentPolicy`) — remote-text attachment fetch is default-deny (opt in with a policy), local reads honor an `allowedRoots` sandbox, and bare-string local paths warn (staged deprecation).
|
|
153
|
-
- **URL skill sources hardened** — the manifest fetch runs through `OutboundPolicy` and every record is runtime-validated before it enters model context; adds cache-TTL controls.
|
|
154
|
-
- **Guardrail coverage documented** — input detectors inspect text only; non-text attachment content needs an attachment-level policy.
|
|
155
|
-
|
|
156
|
-
## 4.4.0 - 2026-06-21
|
|
157
|
-
|
|
158
|
-
### Fixed
|
|
159
|
-
|
|
160
|
-
- **Planner: OpenAI strict structured-output `400`.** The generated plan schema now lists every property in `required` and drops `minItems` / `maxItems`, so `ai.planner()` no longer fails against OpenAI strict `json_schema` mode.
|
|
161
|
-
- **Report / result types no longer collapse to `never` under strict TypeScript.** The narrowing report / result types now override the discriminant via `Omit<…>` instead of intersection. Type-only — no runtime change.
|
|
162
|
-
|
|
163
|
-
## 4.3.0 - 2026-06-21
|
|
164
|
-
|
|
165
|
-
### ⚠ BREAKING
|
|
166
|
-
|
|
167
|
-
- **Supervisor + workflow snapshot persistence moved from `CacheDriver` to the dedicated `SnapshotStore` contract.** The per-primitive fallback is now `ai.config({ defaultSnapshotStore })`. **Migration:** replace `snapshotStore: cache.driver("redis", { client })` with `snapshotStore: ai.snapshot.redis({ client })` (and `ai.snapshot.{memory,pg}` for the other tiers).
|
|
168
|
-
|
|
169
|
-
### Added
|
|
170
|
-
|
|
171
|
-
- `ai.orchestrator()` — stateful session manager over a supervisor: durable session / history / context, drift detection, history compaction, resume, and a command surface (`orchestrator.asTool()`, a 3-tier event surface, and `OrchestratorContract` / config / error types).
|
|
172
|
-
- `ai.checkpoint.{memory,pg,redis}()` and `ai.snapshot.{memory,pg,redis}()` — durable orchestrator-session and supervisor / workflow run stores, with matching `defaultCheckpointStore` / `defaultSnapshotStore` config fields.
|
|
173
|
-
- `ai.memory()` — agent-memory store with four tiers: **working** (in-run scratch), **semantic** (durable facts), **episodic** (durable, recency-blended events), and **procedural** (durable, reinforcement-blended how-tos). Wired into the orchestrator via a `memory?` field.
|
|
174
|
-
- `ai.planner()` — an LLM generates an ordered plan over your registered capabilities, then executes it step-by-step.
|
|
175
|
-
- `ai.spawnSubAgent()` — one-shot delegation to a fresh single-use agent with an optional per-task budget; usable from a planner step, a tool, or a workflow.
|
|
176
|
-
- Cost-truth contract surface across all five adapters — `Usage.reasoningTokens`, per-channel `ModelPricing`, and `ModelCallOptions.{reasoning, cacheControl}` (ignored by adapters that lack the capability).
|
|
177
|
-
- DX helpers — `ai.router()`, `ai.fanOut()`, `ai.batch()`, `ai.fallbackModel()`, `ai.mockRouter()`, `agent.eval()` + built-in `ai.eval.*` scorers, Vitest matchers (`registerAiMatchers()`), supervisor-level middleware, and `ai.systemPrompt.fromFile(path)`.
|
|
178
|
-
- Executables passed in an agent's `tools: [...]` are auto-adapted into tools (workflows / supervisors / orchestrators compose directly via `.asTool()`).
|
|
179
|
-
|
|
180
|
-
## 4.2.0
|
|
181
|
-
|
|
182
|
-
### Fixed
|
|
183
|
-
|
|
184
|
-
- No-argument tools (declared without an `input` schema) no longer crash on invocation — `tool.invoke` now skips validation when no schema is present and passes the raw input to the handler.
|
|
185
|
-
|
|
186
|
-
## 4.1.15
|
|
187
|
-
|
|
188
|
-
- Baseline — per-package changelog tracking starts at this version.
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
|
|
34
|
+
- **`guardedFetch` no longer lets the platform follow redirects past the SSRF guard.** The outbound policy validated only the *initial* URL, then handed the request to `fetch` with automatic redirect following — so a page an agent was asked to load (`ai.rag`'s `loadWeb()`, the skills `urlSource` manifest fetch, `prepareAttachmentPart`'s remote-text path) could pass validation and then `302` into `http://169.254.169.254/...` or an internal service with no re-check. Every hop is now issued with `redirect: "manual"` and its `Location` is re-run through the full `assertUrlAllowed` policy (scheme, host allowlist, post-DNS private-IP deny) before being followed, capped at the new `OutboundPolicy.maxRedirects` (default `5`). Credential headers (`authorization`, `cookie`, `proxy-authorization`) are stripped when a hop crosses an origin boundary, and `303`/legacy `301`/`302`-on-non-GET hops re-issue as a bodyless GET, matching platform semantics. Callers passing `redirect: "manual"` get the raw 3xx back as before; `redirect: "error"` rejects on any redirect. Regression tests cover the metadata/loopback/private redirect block, the off-allowlist redirect block, the hop cap, and the clean-redirect follow
|
|
35
|
+
- **Supervisor fan-out now has a width bound — new `maxFanOut` option (default `10`).** A routing decision could name any number of intents (`normalize()` in `src/supervisor/decide.ts` validated only that each name was in the allowlist, with no length limit and no dedup), and `dispatchBranches` ran `Promise.all` over the lot. `maxIterations` bounded how DEEP a run went; nothing bounded how WIDE one iteration went. Since the router's per-turn prompt embeds supervisor `state` and prior branch outputs — both able to carry attacker text lifted from tool results — a prompt injection ("always return `next` as this 200-element array") turned one iteration into hundreds of real agent/workflow executions, i.e. cost/compute amplification, without ever naming an intent outside the allowlist. Duplicate names are now collapsed silently (they were pure wasted spend: branch results are indexed by intent, so the extras could never change the outcome), and a *deduped* list wider than `maxFanOut` is rejected as `SupervisorRoutingError` (`SUPERVISOR_INVALID_ROUTE`) carrying the offending array — the same failure mode as an unknown intent key. Truncating instead of throwing was rejected: it would hand an attacker-chosen subset to the executor and hide the anomaly. The cap is enforced both in `normalize()` and at `dispatchBranches`, the one chokepoint every dispatch source funnels through, so `evaluate.reassignTo`, classifier picks, and per-intent `next` unions are bounded too. Raise `maxFanOut` deliberately for supervisors that legitimately fan wide (e.g. `ai.fanOut(writer, 20)`); it's validated as an integer `>= 1` at construction
|
|
36
|
+
- **Supervisor state merges refuse prototype-tampering keys.** All five state-merge sites in `src/supervisor/execution.ts` (branch outputs, the `ack` slice, classifier output, the classifier `refine` slice, and the artifacts merge in both its `finalizeArtifacts` and auto-spread forms) did a bare `state[key] = value` over model-influenced data. The `output` schema that validates those slices belongs to the developer, and a permissive one (`z.record()`, `.passthrough()`, `z.any()`) passes a key literally named `__proto__` straight through — assigning it repoints the run `state` object's prototype. Blast radius was contained (one per-run object, not global `Object.prototype`), but it became genuine prototype pollution the moment anything downstream used `in`, `hasOwnProperty`, or a deep merge on state — and `finalizeArtifacts`'s key-removal pass already used `key in merged`. A shared `mergeSafely` / `assignSafeKey` / `isUnsafeMergeKey` guard (new `src/security/safe-merge.ts`, exported from the package's security barrel) now drops `__proto__` / `constructor` / `prototype` at every one of those sites and logs the refusal as `state.merge.unsafe-key`; that `key in merged` check is now `Object.hasOwn`. Dropping rather than throwing is deliberate — those keys are never legitimate state fields, and mid-iteration is the wrong place to fail a settled run
|
|
37
|
+
|
|
38
|
+
- **`orchestrator.asTool({ sessionScope: "shared" })` no longer lets the calling model choose which session it joins.** The wrapper read `sessionId` (and `history`) straight out of the *validated tool-call payload* — i.e. out of arguments the outer agent's LLM wrote — and handed them to `orchestrator.execute()`, which loads that session's checkpoint, merges its persisted `state`, runs a turn against it and writes a fresh checkpoint back. The JSDoc actively instructed developers to thread the session id through `inputSchema`. A `sessionId` is bearer-equivalent to full read/write on the session, so any prompt injection reaching the outer agent (a summarized document, a poisoned tool result, a fetched page) could say "continue session `<victim-id>`" and have the nested orchestrator splice an attacker-directed turn into a stranger's live conversation and return its content — including prior state — into the outer transcript. The target session is now bound OUTSIDE the model-visible schema, via the new `OrchestratorAsToolOptions.session`: either a literal id fixed at `asTool()` construction, or a `(ctx) => sessionId | { sessionId, history }` resolver reading the invocation's `ToolContext` (the same out-of-band channel `signal` / `artifacts` already travel on, which an LLM cannot write to). `sessionId` / `history` are stripped from the payload before it is forwarded as `execute(input)`, and a resolver that returns nothing fails the call rather than falling back to the payload. **Breaking for `"shared"` scope only:** building such a tool without `session` now throws at construction. The pre-4.15.0 behavior is still reachable behind `unsafeAllowModelSessionId: true`, documented at the API surface as bearer-token-equivalent access that obliges the developer to verify session ownership themselves. `"fresh"` scope (the default) is unchanged
|
|
39
|
+
- **Orchestrator/agent memory is session-scoped by default — recall can no longer surface another user's remembered turns.** `OrchestratorConfig.memory` is resolved once per orchestrator instance and reused by every `execute()` / `resume()` regardless of `sessionId`, and neither `MemoryItem`, `RecallOptions`, `MemoryContract` nor the four tier implementations carried any session/tenant key — so `recall()` could not be scoped to the calling session and `remember` (ON by default) wrote every clean turn's input + outcome text into one shared namespace. In the framework's own documented integration pattern (one `ai.memory()` built at boot, passed to `ai.orchestrator({ memory })`, serving all end users) user A's remembered text was recallable by user B's semantically similar turn, with no attacker action required. `MemoryItem.scope` and `RecallOptions.scope` are new opaque isolation keys, enforced **inside** each tier as an exact-equality match before hits are scored, merged or sliced — never left to the caller — and folded into the stored key so two scopes writing identical text stay two entries (including the procedural tier's `uses` reinforcement counter). All four tiers enforce it: `working`, `semantic`, `episodic`, `procedural`. An unscoped `recall()` reads only the unscoped pool; there is no wildcard query. The orchestrator derives the scope from the execute-time `sessionId` (`"session:<id>"`) — not from the payload, the context bag, or the model — via the new `OrchestratorMemoryConfig.scope`, which defaults to `"session"`. **Behavior change:** memories seeded or written before the upgrade are unscoped and are no longer recalled by a session-scoped turn. Cross-session pooling is now an explicit opt-in — `scope: "shared"` restores the pre-4.15.0 single-pool behavior (and keeps reading pre-upgrade entries); `scope: (sessionId) => key` derives a custom boundary, e.g. per tenant. The vector tiers overscan before filtering so a noisy neighbouring scope cannot starve a scoped recall of its top-`k`
|
|
40
|
+
|
|
41
|
+
- **The working-memory tier is size-bounded — new `working: { maxItems }` (default `1000`).** `WorkingMemory` backed its buffer with a plain `Map` that grew by one entry per unique `remember()` and had no cap, TTL or eviction of any kind. It is also the one tier that keeps everything it is told in *process* memory, for the lifetime of the `memory()` instance — which `ai.orchestrator({ memory })` resolves ONCE and reuses for every session, for as long as the process runs. Since distinct text derives a distinct id nothing dedups, so an attacker able to drive turns through a memory-backed orchestrator (with `remember` on by default) added a permanent entry per request until the process ran out of memory: a cheap memory-exhaustion DoS against any internet-facing deployment. The buffer now evicts on overflow. **Policy is FIFO over insertion order, not LRU,** and deliberately so: recall on this tier is a pure recency proxy (it reverses insertion order and slices the newest `k`, never reordering), so the front of the buffer is by construction the region recall reaches last — FIFO evicts exactly the entries a bounded recall would never have returned, while true LRU would need read-time reordering that would also rewrite the `score` every recall reports. Re-remembering an existing id still updates in place and keeps its slot. `maxItems` is validated as an integer `>= 1` at construction and has no unbounded setting — "no cap" is 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`. Known and documented limitation: the bound is global rather than per-scope, so a busy session can push another's older entries out — a recall-quality degradation on a volatile scratch tier, never a disclosure (the scope filter still applies), and a per-scope quota would not help against an attacker holding many sessions anyway
|
|
42
|
+
- **`semanticCache()` is per-session-scoped by default — one caller's cached answer is no longer served to another.** The middleware is built once at app boot and shared by every end user, its `namespace` was a static string, and a hit is returned as a synthetic `ModelResponse` with no LLM call in between — so both lookup paths (the exact prompt-hash key and the vector `similar()` match) could serve user A's cached response, personal context and all, to user B's merely *similar* prompt, and let an attacker seed an entry engineered to sit near a predictable class of future queries and have it answered from the store thereafter. Entries now carry a `scope` derived from the run's own `AgentExecuteOptions.sessionId` (`"session:<id>"`, the same derivation the memory fix uses) — read out of the execute options, never out of the prompt or the model's output — folded into the stored key (hashed, since a `sessionId` is caller-supplied and may contain the key delimiter) *and* re-checked as exact equality on the stored entry, so key-level separation is never the thing authorizing a read. The vector path overscans before filtering, mirroring the memory tiers, so a noisy foreign session cannot occupy the top-`k` and mask a caller's own hit. New `SemanticCacheOptions.scope`: `"session"` (default), `"shared"` (one pool for every caller — the explicit opt-in for genuinely public Q&A, and the pre-4.15.0 behavior), or `(context) => key` for a custom boundary such as per-tenant. **Two behavior changes to expect:** entries written before the upgrade are unscoped and are only read by unscoped runs, and scoping trades cross-user hit rate for isolation — a public FAQ deployment where no response can carry a caller's private context should now set `scope: "shared"` on purpose. Runs made *without* a `sessionId` continue to share one unscoped pool (unchanged behavior for them); thread `sessionId` through `execute()` — composites already do — to get the isolation
|
|
43
|
+
- **The planner's plan schema rejects an over-long plan at parse time.** Strict-mode JSON Schema cannot express `maxItems`, so `maxSteps` was never on the wire (`plan-schema.ts` discarded the parameter outright with `void maxSteps`) and the only enforcement was `PlannerRun`'s tail truncation — which runs *after* the whole `steps[]` array has been parsed, normalized into `PlannerStep[]` and stored on `this.plan`. A provider or proxy that ignores the prompt's step budget could therefore make the planner deserialize an arbitrarily long array before anything trimmed it. `validate()` now enforces a hard ceiling of `maxSteps * 4` (or `100` when `planSchema` is built without a `maxSteps`), rejecting rather than truncating: a plan several times its budget is a malfunction worth surfacing as the typed `PlannerPlanInvalidError`, not a prefix worth silently executing. The slack factor keeps the normal case — a model overshooting "at most N steps" slightly, which the runtime still truncates to `skipped` — working exactly as before
|
|
44
|
+
|
|
45
|
+
### Notes
|
|
46
|
+
|
|
47
|
+
- Suite: **176 files / 1943 tests, 1940 passing** (3 pre-existing environmental cold-import timeouts). Adds regression tests for every security fix above: the `guardedFetch` redirect/SSRF block, the supervisor `maxFanOut` cap + prototype-key state-merge guard, the `asTool` session binding and per-tier memory scope isolation, the working-memory cap, the `semanticCache` session scope, and the planner parse-time step ceiling
|
|
48
|
+
|
|
49
|
+
## 4.15.0 - 2026-08-16
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **`ctx.run(agent, payload)` now stringifies a non-string payload, as it always claimed to.** `coerceInlineInput` in `src/supervisor/execution.ts` gated on `!("signature" in executable)` to decide whether the target was an agent — but every member of `SupervisableExecutable` (`AgentContract`, `WorkflowInstance`, `SupervisorContract`) declares `signature`, so the condition was **permanently false and the coercion never ran**. A supervisor intent calling `ctx.run(someAgent, { question: "why", attempt: 2 })` handed the raw object to `agent.execute()`, where it landed as the user message `content` — `[object Object]` in the prompt, or a provider-side payload rejection, depending on the adapter. The check now discriminates on `isAnonymous`, the one member unique to `AgentContract`. A regression test covers it; the old guard fails it with `Expected: "string" / Received: "object"`. The unreferenced `isSupervisor()` duck-type helper — whose own JSDoc admitted it could not tell a supervisor from a workflow — is removed
|
|
54
|
+
- **`ToolMeta` no longer forces `label` and `actionLabel` on every tool that supplies `meta`.** It was declared as `Record<"label" | "actionLabel" | (string & {}), unknown>`, which makes both keys **required**, not optional — so any tool author who set one metadata field was made to set all of them. Now an optional-key shape with an index signature
|
|
55
|
+
- **`ToolConfig.action` is checked bivariantly**, via a `ToolActionResolver<T>` method-in-wrapper. The strictly contravariant parameter position rejected heterogeneous tool arrays that work correctly at runtime
|
|
56
|
+
- **`new Error(msg, { cause })` compiles.** `tsconfig.json` declared no `lib`, so it inherited the `target` default of ES2020, where `ErrorOptions` does not exist. `lib` is now `["ES2022"]`; this also resolves the `Array.at` and `String.replaceAll` errors. Emit is unchanged — `target` is still ES2020. Note `src/skills/sources/url-source.ts:122` was **not** a defect: the `cause` was always passed at runtime, the compiler simply had no type for it
|
|
57
|
+
- **`TeamMemberValue` accepts the callback member form** (`IntentCallback`), which has always worked at runtime and was only rejected by the type
|
|
58
|
+
- **`PlanSchema` no longer erases `~standard.jsonSchema`** from its return type
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **`MockModelResponse.usage` is a new `MockUsage` type rather than the emitted `Usage`.** The script is an input, not a result: `MockModel.buildResponse` honours only `input` / `output` / `cachedTokens`, so a fixture declaring `cost` or `reasoningTokens` was silently discarded while the type promised otherwise. `total` is optional and documented as derived, because the mock recomputes it as `input + output` — an existing spec deliberately asserts that a mismatched scripted `total` is overridden
|
|
63
|
+
- **The mock honours `deltas`.** Fixtures already declared the field; the mock ignored it
|
|
64
|
+
- `MockSDK.model()` declares its `MockModel` return type — it always returned one, so `callHistory` is now reachable without a cast. `MockUsage` is exported from the barrel
|
|
65
|
+
|
|
66
|
+
### Notes
|
|
67
|
+
|
|
68
|
+
- Typecheck against the package's own TypeScript 6.0.3: **89 → 24 errors, and all 24 remaining are a monorepo-only artifact** (`TS6059`, `@warlock.js/cache` resolved through a `paths` mapping outside `rootDir`). They do not affect the published package, which ships built `exports` and `.d.mts`. In-scope errors are zero
|
|
69
|
+
- Suite: **173 files / 1878 tests passing**, up one from the new regression test
|
|
70
|
+
|
|
71
|
+
## 4.12.0
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
|
|
76
|
+
|
|
77
|
+
## 4.9.0 - 2026-08-06
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- `StepSnapshot.children` — reports a workflow `run` step captured from any executable its callback invoked DIRECTLY (`agent.execute(...)` rather than the declarative `agent:` field), via the same ambient `RunFrame` a supervisor/team/orchestrator callback already gets. `report.children` now includes these alongside `step.agent` reports.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- A workflow `run` step that calls `agent.execute()` directly no longer produces two disconnected top-level traces (one "agent", one "workflow") with the agent missing from `report.children` — it now nests correctly (`workflow → agent → tool`, usage/cost rolled up) and no longer also self-routes as a separate observed trace. Declarative `step.agent` was already correct; this closes the gap for ad-hoc calls inside `run` (self-documented in `workflow/engine.ts` as a known limitation).
|
|
86
|
+
|
|
87
|
+
## 4.8.2 - 2026-07-22
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- `judgePromptBody` / `formatCriteria` / `JudgeOutcome` — the LLM-as-judge building blocks `ai.prompts().validate()` already used internally are now public, so other packages (`@warlock.js/ai-panoptic`'s trace-level system-prompt evaluation) can grade arbitrary prompt text against a model + rubric without a second judging implementation
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- `redact()` no longer collapses a raw `Error` (or an `Error` nested in a `cause` chain) to `{}` — `name` / `message` / `stack` aren't own-enumerable on `Error` instances, so the previous `Object.entries()` walk saw none of them. This was silently dropping tool/agent error `cause` detail wherever `redact()` runs it, including `@warlock.js/ai-panoptic`'s trace `cause` field (a failed tool's `ToolExecutionError.cause` showed as an empty object in the dashboard instead of the underlying thrown error)
|
|
96
|
+
- `@warlock.js/ai-openai` and `pdf-parse` declared as optional `peerDependencies` — both are lazily `import()`ed (the skills-catalog embedder probe; `ai.rag.loadPdf`) but weren't listed in either `dependencies` or `peerDependencies`, so pkgist's bundler vendored their source directly into `ai`'s own build instead of leaving them external (the same split-brain class of bug as `core`'s missing `@warlock.js/ai` peerDependency, fixed in 4.8.1). For `@warlock.js/ai-openai` specifically this meant the skills-catalog embedder-installed probe always resolved against the vendored copy bundled into `ai`, so it reported an embedder provider as "installed" even when the app never installed `@warlock.js/ai-openai` itself
|
|
97
|
+
|
|
98
|
+
## 4.8.1 - 2026-07-21
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- `setAIConfig`'s `onConfigApplied` listener notification no longer swallows a misbehaving listener's exception silently — it's now logged via `log.error("ai", "configListener", error)`
|
|
103
|
+
|
|
104
|
+
## 4.8.0 - 2026-07-19
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
- **`reasoning: { effort: "none" }`** — a neutral "run without reasoning, explicitly" level on `ReasoningEffort`; OpenAI emits `reasoning_effort: "none"` so gpt-5 / o-series accept function tools, and budget-based adapters (Anthropic / Bedrock / Google / Ollama) disable thinking.
|
|
109
|
+
|
|
110
|
+
## 4.7.0
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- **`systemPrompt().refined({ model, criteria, store })`** — the prompt compiler. Humans keep writing human prompt text; the refined wrapper lazily rewrites it into a model-optimized version on first agent use and pins the result like a lockfile (re-compiled only when the source text, refiner model, `criteria`, or recipe version change — never silently). `await refined.refine()` returns the compiled template **string** (placeholders intact — routes / previews / warmup / CI; throws `PromptRefinementError` on failure) and `await refined.refinePrompt()` returns a composable prompt with `meta.refinedFrom` / `meta.refinerModel` provenance (register it to `diff` original vs refined). Placeholder parity is machine-enforced (one repair re-ask, then rejected); the lazy agent path never throws — it warns once and serves the original.
|
|
115
|
+
- **`ai.prompts.validate({ criteria })`** — validate a prompt against your **own** rules. Pass `criteria` (a string or a list of short rules) and, when a `judge` model is supplied, it replaces the built-in quality rubric so the judge's `score` / `issues` reflect your criteria (a failed rule is named in `issues`). Advisory only — never flips the deterministic `ok`; folded into the `judgeCache` key so different rules re-run.
|
|
116
|
+
|
|
117
|
+
## 4.6.0
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- **`ai.image(params)`** — image generation, the first verb of the output-modality track (Theme I). Wraps an `ImageModelContract` in the uniform never-throws `{ data, error, usage, report }` envelope, with cost-truth (per-token for `gpt-image`, per-image for DALL·E / Imagen) folded into the same `Usage.cost` rollup and a `type: "image"` report routed to observers. Ships on the OpenAI + Google adapters.
|
|
122
|
+
- **`SDKAdapterContract.image?(config)`** — the image-model capability seam, mirroring `embedder?()`. Adds `ImageModelContract`, `GeneratedImage` (discriminated `base64` | `url`), `ImageModelPricing`, and `ImageGenerationOptions`.
|
|
123
|
+
- **`MockSDK().image(...)` + `MockImageModel`** — deterministic image doubles (scriptable responses, recorded calls, pricing) for tests.
|
|
124
|
+
- **`ai.speech(params)` + `ai.transcribe(params)`** — text-to-speech and speech-to-text, the audio verbs of the modality track. Same uniform never-throws envelope + cost-truth (per-character / per-minute / per-token). New `SpeechModelContract` / `TranscriptionModelContract` on `SDKAdapterContract.speech?()` / `transcribe?()`, plus `MockSpeechModel` / `MockTranscriptionModel`.
|
|
125
|
+
- **`ai.audioFromFile(path)` / `ai.audioFromBuffer(bytes, mediaType)` / `ai.audioMediaTypeForFilename(name)`** — non-AI utilities that package audio (WhatsApp `.ogg`/`.opus`, iOS `.m4a`, …) into the `AudioInput` shape `ai.transcribe` consumes.
|
|
126
|
+
- **`ai.rag.pgVectorStore({ client })`** — a Postgres + pgvector vector store satisfying `VectorStoreContract` (upsert / query / removeNamespace), with an `ensureSchema()` DDL helper and a lazy `pg` optional peer.
|
|
127
|
+
- **`ai.rag.loadText` / `loadHtml` / `loadWeb` / `loadPdf`** — document loaders producing `RagDocument`s for `.index()`. `loadWeb` is SSRF-safe (routes through `guardedFetch` / `OutboundPolicy`); `loadPdf` uses a lazy `pdf-parse` optional peer.
|
|
128
|
+
- **Durable mid-run crash-resume** — opt-in `durable: { store, deleteOnComplete? }` on `ai.agent` / `ai.planner` with a stable `runId` + `agent.resume(runId)` / `planner.resume(runId)`. Per-trip (agent) / per-node (planner) checkpoints reuse `ai.snapshot.{memory,pg,redis}`; drift detection via `AgentDriftError` / `PlannerDriftError` (bypass with `{ force: true }`); completed work never re-runs its tools and usage is never double-counted.
|
|
129
|
+
- **`ai.rag.*` namespace** now also carries `chunk`, `cacheVectorStore`, `pgVectorStore`, `loadText`/`loadHtml`/`loadWeb`/`loadPdf`, `bm25Rank`, `reciprocalRankFusion`, `hybridRank`, `multiQuery` (previously standalone-only exports), for `ai.*`-namespace consistency.
|
|
130
|
+
|
|
131
|
+
## 4.5.0 - 2026-07-01
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **`ai.rag(config)`** — retrieval-augmented generation in core: a chunk → embed → retrieve → cite pipeline that reuses your existing embedder and cache, with zero new dependencies. Includes hybrid retrieval (dense + BM25 reciprocal-rank fusion), keyword / LLM rerankers, and multi-query expansion.
|
|
136
|
+
- **`ai.team(config)`** — manager-led multi-agent teams: thin sugar over `ai.supervisor` for the review-then-fix and test-then-fix shapes.
|
|
137
|
+
- **`ai.skills(config)`** — runtime agent skills with progressive disclosure: a cheap always-injected catalog plus an on-demand `loadSkill` tool. Adds a `skills` option on `ai.agent`.
|
|
138
|
+
- **`ai.streamObject(...)`** — structured-output streaming: partial-object snapshots as tokens arrive, with a strict final parse against the response schema.
|
|
139
|
+
- **`ai.serve(executable, options)`** — serve any agent / workflow / supervisor as an SSE HTTP endpoint.
|
|
140
|
+
- **Multimodal attachments** — `ContentPart` gains `pdf` and `audio` variants alongside text / image, resolved to provider-ready parts (PDF wired on the Anthropic and Bedrock adapters).
|
|
141
|
+
- **Planner DAG execution, re-planning, and plan-only approval** — run independent steps concurrently, revise the plan when a step fails, or return a plan for approval before it executes.
|
|
142
|
+
- **Generic `Observer` seam** — route any flow's run report to pluggable observers (e.g. `@warlock.js/ai-panoptic`) without coupling core to a backend.
|
|
143
|
+
- **`ai.prompts` + `ai.prompt`** — a process-wide registry of named, versioned `systemPrompt(...)` builders (resolved by `name@version` / `name@tag`) with `define` / `tag` / `diff` / `export` / `import` and a unified `validate` (deterministic missing-placeholder check plus an optional Nova-safe LLM-judge); `ai.prompt` is a thin facade over it.
|
|
144
|
+
- **`SystemPromptContract` identity + provenance** — `.meta({ name, version, description, required })` (a name auto-registers in `ai.prompts`), `.merge(...blocks)` / `.merge(contract)` / `.merge(name, { fromVersion })`, and deterministic `meta.composedFrom` labels.
|
|
145
|
+
- **`ai.dataset(options)`** — filterable, shardable evaluation case sets that feed `agent.eval`, with baseline / regression detection and CI reporters.
|
|
146
|
+
- **`ai.vcr(model, options)`** — record / replay any model against an on-disk cassette for deterministic, offline tests, with `recordRequest` modes and `redactRequest` / `redactResponse` / `redactError` hooks.
|
|
147
|
+
- **`ai.agent.judge(config)`** — judge-safe agent preset (also `ai.agent({ judge: true })`): lenient JSON parsing, bounded repair re-asks, and never-throw verdicts on Nova-class models.
|
|
148
|
+
- **Human-in-the-loop approval now ships in core** (`ai.human.*`, formerly `@warlock.js/ai-human`) — a tool-approval gate plus durable interrupt / resume.
|
|
149
|
+
- **Content guardrails now ship in core** (`ai.guardrail.*`, formerly `@warlock.js/ai-guard`) — PII / topic / injection / moderation detectors.
|
|
150
|
+
- **Orchestrator `sessionLock`** — per-session turn serialization (default in-process mutex keyed by `sessionId`, pluggable distributed lock) so concurrent same-session turns can't lose a checkpoint update.
|
|
151
|
+
- **Sub-agent trace nesting** — a supervisor / team / orchestrator callback that calls `agent.execute()` directly now nests `callback → agent → tool` with rolled-up usage / cost.
|
|
152
|
+
- **`AgentReport.systemPrompt`** — the resolved system prompt sent to the model is now recorded on the agent report.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- **`ai.team` runs report `type: "team"`** — a first-class `ReportType` (was `"supervisor"`) so observers distinguish team runs on the wire.
|
|
157
|
+
- **Deterministic parallel workflow state merge** — parallel children merge into the parent in declaration order (last-declared wins on a conflicting key) instead of completion order; an optional per-step `mergeState` reducer overrides it.
|
|
158
|
+
- **Safer batch / RAG defaults** — `ai.batch` warns once on a large unbounded run (pass an explicit `concurrency` or `"unbounded"`); `ai.rag` accepts `limits` (`maxDocuments` / `maxChunks` / `maxBytes`) that fail before any embedding spend.
|
|
159
|
+
|
|
160
|
+
### Fixed
|
|
161
|
+
|
|
162
|
+
- **Cancellation propagates through composite tools** — a cancelled outer agent now aborts a nested agent / workflow / supervisor invoked via `.asTool()` (the run signal threads into the nested `execute`).
|
|
163
|
+
- **Observer / event-handler errors are surfaced, not swallowed** — a throwing observer or `on` handler stays isolated (never crashes the run) but is now warned once / routed to a hook instead of disappearing silently.
|
|
164
|
+
- **`budget({ maxCostUSD })` fail-open closed** — a cost cap with no matching model pricing now warns once (naming the model) instead of silently never tripping.
|
|
165
|
+
|
|
166
|
+
### Security
|
|
167
|
+
|
|
168
|
+
- **Shared `OutboundPolicy` + `redact()`** — one SSRF-safe outbound-fetch guard (scheme + host allowlist, post-DNS private-IP deny, max-bytes, timeout, injectable fetch) and one redaction utility, consumed across attachments, URL skills, VCR, and the error path.
|
|
169
|
+
- **Attachment trust boundary** (`AttachmentPolicy`) — remote-text attachment fetch is default-deny (opt in with a policy), local reads honor an `allowedRoots` sandbox, and bare-string local paths warn (staged deprecation).
|
|
170
|
+
- **URL skill sources hardened** — the manifest fetch runs through `OutboundPolicy` and every record is runtime-validated before it enters model context; adds cache-TTL controls.
|
|
171
|
+
- **Guardrail coverage documented** — input detectors inspect text only; non-text attachment content needs an attachment-level policy.
|
|
172
|
+
|
|
173
|
+
## 4.4.0 - 2026-06-21
|
|
174
|
+
|
|
175
|
+
### Fixed
|
|
176
|
+
|
|
177
|
+
- **Planner: OpenAI strict structured-output `400`.** The generated plan schema now lists every property in `required` and drops `minItems` / `maxItems`, so `ai.planner()` no longer fails against OpenAI strict `json_schema` mode.
|
|
178
|
+
- **Report / result types no longer collapse to `never` under strict TypeScript.** The narrowing report / result types now override the discriminant via `Omit<…>` instead of intersection. Type-only — no runtime change.
|
|
179
|
+
|
|
180
|
+
## 4.3.0 - 2026-06-21
|
|
181
|
+
|
|
182
|
+
### ⚠ BREAKING
|
|
183
|
+
|
|
184
|
+
- **Supervisor + workflow snapshot persistence moved from `CacheDriver` to the dedicated `SnapshotStore` contract.** The per-primitive fallback is now `ai.config({ defaultSnapshotStore })`. **Migration:** replace `snapshotStore: cache.driver("redis", { client })` with `snapshotStore: ai.snapshot.redis({ client })` (and `ai.snapshot.{memory,pg}` for the other tiers).
|
|
185
|
+
|
|
186
|
+
### Added
|
|
187
|
+
|
|
188
|
+
- `ai.orchestrator()` — stateful session manager over a supervisor: durable session / history / context, drift detection, history compaction, resume, and a command surface (`orchestrator.asTool()`, a 3-tier event surface, and `OrchestratorContract` / config / error types).
|
|
189
|
+
- `ai.checkpoint.{memory,pg,redis}()` and `ai.snapshot.{memory,pg,redis}()` — durable orchestrator-session and supervisor / workflow run stores, with matching `defaultCheckpointStore` / `defaultSnapshotStore` config fields.
|
|
190
|
+
- `ai.memory()` — agent-memory store with four tiers: **working** (in-run scratch), **semantic** (durable facts), **episodic** (durable, recency-blended events), and **procedural** (durable, reinforcement-blended how-tos). Wired into the orchestrator via a `memory?` field.
|
|
191
|
+
- `ai.planner()` — an LLM generates an ordered plan over your registered capabilities, then executes it step-by-step.
|
|
192
|
+
- `ai.spawnSubAgent()` — one-shot delegation to a fresh single-use agent with an optional per-task budget; usable from a planner step, a tool, or a workflow.
|
|
193
|
+
- Cost-truth contract surface across all five adapters — `Usage.reasoningTokens`, per-channel `ModelPricing`, and `ModelCallOptions.{reasoning, cacheControl}` (ignored by adapters that lack the capability).
|
|
194
|
+
- DX helpers — `ai.router()`, `ai.fanOut()`, `ai.batch()`, `ai.fallbackModel()`, `ai.mockRouter()`, `agent.eval()` + built-in `ai.eval.*` scorers, Vitest matchers (`registerAiMatchers()`), supervisor-level middleware, and `ai.systemPrompt.fromFile(path)`.
|
|
195
|
+
- Executables passed in an agent's `tools: [...]` are auto-adapted into tools (workflows / supervisors / orchestrators compose directly via `.asTool()`).
|
|
196
|
+
|
|
197
|
+
## 4.2.0
|
|
198
|
+
|
|
199
|
+
### Fixed
|
|
200
|
+
|
|
201
|
+
- No-argument tools (declared without an `input` schema) no longer crash on invocation — `tool.invoke` now skips validation when no schema is present and passes the raw input to the handler.
|
|
202
|
+
|
|
203
|
+
## 4.1.15
|
|
204
|
+
|
|
205
|
+
- Baseline — per-package changelog tracking starts at this version.
|
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": "5.0
|
|
19
|
-
"@warlock.js/cache": "5.0
|
|
20
|
-
"@warlock.js/logger": "5.0
|
|
18
|
+
"@warlock.js/ai-openai": "5.1.0",
|
|
19
|
+
"@warlock.js/cache": "5.1.0",
|
|
20
|
+
"@warlock.js/logger": "5.1.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": "5.0
|
|
47
|
+
"version": "5.1.0",
|
|
48
48
|
"main": "./cjs/index.cjs",
|
|
49
49
|
"module": "./esm/index.mjs",
|
|
50
50
|
"types": "./esm/index.d.mts",
|