Releases: Deuz-AI/Deuz-SDK
Release list
v2.0.0
Full Changelog: v1.9.0...v2.0.0
v1.9.0
npm install @deuz-sdk/core@1.9.0
npm install @deuz-sdk/react@1.9.0 # optional: useChat, useObject, headless UIThe release that closes the felt distance to the mainstream TypeScript AI SDKs — without giving up the
invariants: edge-safe core, zero runtime dependencies, one canonical StreamPart line, immutable
history, and an append-only public surface.
707 → 1183 core tests. Node ≥ 22, ESM and CJS.
Security — two sandbox escapes in createFileWorkspace
createFileWorkspace is the boundary an autonomous agent's file tools run inside. It had no tests.
Writing the first ones found two ways out, both now closed with regression guards that assert the escape is
refused and that nothing appeared on the far side.
- A symlink or NTFS junction inside the root walked straight out. The guard was pure string math, and
path.resolve/path.relativecannot follow a link — so<root>/escape → /outsidepassed every check:
a write landed outside, a read returned the outside file. A link gets there without anything unusual
happening: a shell or CodeAct tool in the same run, a git checkout, an unpacked archive, any other
process sharing the directory.resolveInsidenow re-verifies the deepest existing ancestor through
fs.realpath, the only layer that resolves links. - On Windows, a foreign drive letter defeated both guard layers. Nothing had to be planted — the model
just had to emitD:/x/y.path.relativereturns an absolute path when the two sides live on
different devices, so the'..'test never fired and the round-trip check passed.
The fix for (2) lives in normalizeWorkspacePath, which every backend shares (in-memory, your KV or
object store, the file backend), so it is closed once rather than per-backend. Drive-letter paths are now
rejected on every platform, not just Windows: a path that is contained on Linux and escapes the sandbox
on Windows is the worst possible split for a portable SDK. NUL bytes are rejected there too, instead of
being left to node:fs.
If you relied on a path shaped like C:name or C:/x resolving inside the workspace, it now throws.
@deuz-sdk/core/mcp/stdio and @deuz-sdk/core/browser/node also got their first tests.
Highlights
| Silent failures | A failing route no longer renders as an empty assistant bubble. An unregistered tool name self-heals instead of hanging the UI. generateObject/streamObject refuse the loop options they used to accept and ignore. verifyStep verdicts and token usage reach the UI. onFinish no longer fires twice. |
| Ergonomics | tool() types its own handler · prompt / instructions · timeout (ttft/total/step/tool) · consume() · PDF input on all four wires · createOpenAICompatible() · per-call capabilities · runnable examples/ |
| Chat UI | Ordered UIMessagePart[] — a multi-step turn finally renders in arrival order · writable hook state · multimodal sendMessage · throttleMs · auto-resume · validateChatRequest |
| Enterprise | createAgent (./agent) · an OpenTelemetry bridge (./otel) · Vertex service-account + ADC auth (./vertex/node) · renderRunReport |
| Research | createVerifier · doneWhen / falseFinishGuard · runGradedEval |
No dead API
Three surfaces were declared, locked into the API contract, and honestly documented as inert. Because this
package's surface is append-only, publishing them without producers would have made them permanent dead
API. They emit now, verified per call shape before release:
warnings— the escape valve for "we quietly did something other than what you asked". The default
logger is a no-op, so every degraded decision used to go nowhere at all. Populated on all six call shapes
plus the fail-over paths.- Approval denial — a human refusing a tool call is now distinguishable from a tool that threw, in
server mode, client mode and on a durable resume leg. agentToolruns — no longer invisible inuseChat. Sub-agent frames land in their own channel with a
splice point, so a sub-agent's words are never attributed to the parent.
Full detail below, and in packages/core/CHANGELOG.md.
Migration from the Vercel AI SDK: guide
· or let an agent do it: npx skills add Deuz-AI/Deuz-SDK --skill migrate-from-ai-sdk
Three surfaces that shipped as types with no producer now actually produce.
CallWarning, WarningPart, the warnings result fields, ToolStatePart.denied and the sub-agent wire
part were all declared, locked into the API contract, and documented as "declared but inert". This package's
surface is append-only, so publishing them dead would have made them permanent dead API. They have producers now.
warnings — the escape valve for "we quietly did something else"
Deuz deliberately never throws on something it can degrade: an unknown model slug falls back to a
conservative capability row, a sampling parameter a reasoning model rejects is stripped, a typo'd
activeTools name is ignored fail-open. That policy is right — a new provider slug must not break a running
app — but every one of those decisions went only to deps.logger.warn, and the default logger is a no-op,
so by default they went nowhere at all.
const res = await generateText({ model: openai('some-brand-new-slug'), prompt: 'hi' });
res.warnings; // [{ type: 'unknown-model', message: "Unknown model 'some-brand-new-slug' — …" }]Emitting sites: the unknown-slug fallback, a sampling parameter dropped by samplingRestrictions, effort
dropped on a model without reasoning, provider-executed tools the Chat Completions wire cannot carry, a
document handed to a model without nativePdf, and an activeTools name that matches nothing.
Verified populated on all six call shapes — streamChat single-turn and in the loop, generateText
with and without tools, generateObject, streamObject — plus the fallbackModels / withFallback paths.
StreamChatResult.warnings resolves alongside usage/finishReason, settles on every exit (success,
mid-stream error, abort) and never rejects; live warnings also arrive as warning parts on fullStream and
now cross the loop boundary and the UI wire instead of dying at a default: branch.
Details that matter in practice: one sink per run, not per step, so a capability the loop re-derives every
step is reported once rather than N times; sites that already log record without mirroring, so 1.9 adds a
typed channel without turning one log line into two; and the list is capped with an explicit
"N further warning(s) omitted" entry rather than truncating in silence.
Approval denial is distinguishable from a tool that threw
ToolStatePart.denied / deniedReason existed everywhere except in the code that should set them, so a
human refusing a tool call still rendered as "getWeather failed" — the wrong last frame for this SDK's
strongest feature. settlePendingApprovals now returns the full denial map (so a client-supplied reason
survives) and the loop tags the terminal tool-state part:
{ type: 'tool-state', toolCallId: 'call_1', toolName: 'wire', state: 'error', denied: true }What the model is told is unchanged — it still receives an is_error tool_result, and denials still do not
count toward the runaway-tool guard. Only the UI is better informed. Works in server mode
(approveToolCall), in client mode (approvalResponses) and on a durable resume leg.
agentTool runs are no longer invisible in useChat
applyUIPart had no sub-agent case, so a whole delegated run — its text, reasoning, tool cards, citations,
cost — reached the reducer and was dropped. A user watching a sub-agent work saw nothing.
Sub-agent frames now land in their own AssistantTurnState.subAgents channel rather than being folded into
the parent, and that separation is deliberate:
- a sub-agent's words are not the parent's — folding them into
message.contentwould misattribute
prose, and folding itstool_useparts into the parent's would makeassistantMessageFromTurnre-emit
them as the parent's own, where each one needs atool_resultor the next request 400s; UIMessagePartis a closed pinned union consumers switch on exhaustively, so a new member there would be
breaking (the same reasoning that keepsToolRunStateat six members);- the interleave still stays truthful: each frame records
afterPart, the count of the parent's ordered
elements when it opened, so a renderer splices the block back in exactly where the parent handed off.
Each frame holds a full AssistantTurnState folded by the same reducer, so a sub-agent's own ordered parts
are as complete as the parent's. A second-level sub-agent is a sibling frame with a two-segment
agentPath, not a nested one, so a renderer indents by agentPath.length and needs no recursion.
The false-finish part also reaches the wire and the reducer now, the same gap verify had in Sprint 1.
Two fixes found while wiring this up
- A step or tool timeout no longer triggers cross-provider fail-over.
defaultShouldFallbackhopped on
anyTimeoutError; oncelayer: 'step'/'tool'existed that meant a step deadline would switch models
and re-run the whole loop, repeating the side effects of tools that already executed. A caller-imposed
budget is not a provider failure. - A flaky React test is now pinned to the property it tests. The
throttleMsassertion pinned an exact
React commit count; with real timers and 24 sequential flushes React can legitimately commit once for an
unrelated state settle. The content invariant (nothing observable mid-window) is asserted exactly; the...
v1.8.0 — Autonomous Agent Runtime
1.8.0 — Autonomous Agent Runtime
The primitives to build a Manus-style autonomous system on an edge-safe, zero runtime dependency core. Heavy pieces stay behind Node seams you can swap (Docker, E2B, Playwright, …).
Packages: @deuz-sdk/core@1.8.0 · @deuz-sdk/react@1.8.0
What's new
- Workspace (
./workspace,./workspace/node) — path-addressed externalized memory, in-memory + sandboxed file backend,createWorkspaceTools. Persistplan.json, notes, and artifacts across compaction / checkpoints / restarts. - Compute / CodeAct (
./compute,./compute/node) —ComputeSandbox(runCode/runShell),codeActTool/shellTool, Node reference sandbox. Thrown runs self-heal intois_errorresults. - Planner → Executor → Verifier (
./autonomy,verifyStep) —planTasks+TaskListreducers;verifyStepon both loops;bestOfN,selfConsistency,parallelAgents. - Background runs (
./runtime,./runtime/node) —RunManager,pollStaleRuns, liveplan-update/activitystream parts (UI wire +useChat), mid-run steering. - Browser (
./browser,./browser/node) —createBrowserTools+ Playwright reference adapter (optional peer). - Providers + model router (
./providers) — published Groq / Mistral / DeepSeek / Together / OpenRouter / Cerebras / Fireworks / Moonshot·Kimi / Qwen / GLM / MiniMax +createProviderRegistry. - Testing (
./testing) — publishedcreateMockModel,runEval, golden-replay fixtures. - Azure / Bedrock / Kimi —
createAzure,createBedrock(Mantle Bearer; no SigV4),createKimialias.
All additive. Node-only code under */node. Stream/UI wire gains open-union parts (v1 clients unaffected).
Docs
Install
npm install @deuz-sdk/core@1.8.0
npm install @deuz-sdk/react@1.8.0 # optionalFull Changelog: v1.7.1...v1.8.0
v1.7.1 — Chat & Memory Correctness
Chat & Memory Correctness
A focused patch release hardening the chat, memory, and resume paths introduced in 1.7.0. No public exports, subpaths, or types changed.
Fixes
- Strict
chatIdmemory isolation. Chat-scopedget,search, andlistoperations now matchchatIdexactly in both the in-memory and Markdown stores, preventing memories from leaking across sibling chats. Markdown records also preservechatIdthrough frontmatter round-trips; broader legacy scopes remain compatible. - Conversation-safe
ChatStorepersistence. Stored chats now retain the caller’s original transcript plus real assistant/tool additions, never compaction summaries orprepareStepprompt rewrites. Durable resumes use a matching full chat record when available and skip writes after a load failure or scope mismatch, avoiding accidental history loss or cross-scope overwrites. - Complete resumed-tool lifecycle. Approval and client-tool settlement now emits the same v2 lifecycle ordering as initial execution: approved calls progress through
executing→tool-result→complete, while failures and denials terminate witherror. Negotiated v1 streams remain unchanged. - Buffered result parity. Suspended buffered runs now expose
result.memorywhen memory extraction is enabled, resolving safely to an empty mutation list. Final text-only assistant turns are included inresult.response.messagesexactly once, and memory extraction consumes the same additions exactly once.
Quality: 659 core tests and 20 React tests passed, along with type checks, package validation, runtime compatibility checks, bundle budgets, API-contract checks, and the full documentation build. Both packages were published through npm Trusted Publishing with SLSA provenance.
Docs: Core changelog · Documentation changelog
Full Changelog: v1.7.0...v1.7.1
v1.7.0 — The chatbot that remembers, knows its bill, and never breaks
Deuz 1.7: your chatbot now remembers, knows its bill, and never breaks — memory, live USD cost, budget guardrails, signed approvals, vendor-free resume, and cross-provider failover. All six inside the library. All six missing from the Vercel AI SDK (receipts in the README).
Six things even the Vercel AI SDK doesn't have
- Built-in cross-session memory —
memory: { seams, scope }on any call: recall before the first token, mem0-style extract→reconcile after the turn, non-blocking. - Live USD cost on the wire — a cumulative
coststream part per step from the in-library price catalog, with prompt-cache savings as its own field. - Budget guardrail —
budget: { usd, tokens }hard-stops the loop with a typedbudget-exceededpart. - Cryptographic approval trail — HMAC-signed, runId-bound, expiring approval tokens through the UI wire; forged/expired/token-less approvals are denied on resume.
- Durable × resumable, vendor-free — F5 mid-tool-loop: the run continues from its checkpoint AND the stream reconnects gaplessly (
resumeDeuzChatResponse, one endpoint). - Cross-provider failover —
fallbackModelshops providers mid-conversation with the identical canonical history, backed by a live circuit breaker.
Also in 1.7
- Resumable UI wire v2: SSE seq ids +
Last-Event-IDreplay,StreamStateStoreseam, multi-client live follow,connectDeuzStreamauto-reconnect — v1 clients keep working byte-identically. - Typed data parts (
writeData+ Standard Schema validation on-stream), tool state machine, built-in RAG citations. ChatStorepersistence + pure chat engine + regenerate / edit-and-branch.- New package:
@deuz-sdk/react— useChat v2, useObject, headlessToolApprovalCard+CostBadge. (@deuz-sdk/core/reactkeeps working, frozen.) - Repo is now an npm-workspaces monorepo; 30 core subpaths; 664 tests.
Full details: packages/core/CHANGELOG.md · v1.7.0.md (design spec)
🤖 Generated with Claude Code
v1.6.1
Observability Hardening
A patch release closing every actionable finding from the post-1.6.0 adversarial review: two security fixes in the observation pipeline, plus small additive controls for draining, budgeting and tracing. No public type changes beyond append-only additions.
Security fixes
- Redaction final barrier. The built-in observation redaction profile now also runs after your custom
redacthook and after structural truncation — a buggy or malicious redactor can no longer reintroduce a secret into an event, and truncation can never split a secret into a decodable prefix. The JWT pattern additionally catches tokens embedded mid-string. (maskSecretand every 1.5 log/error path are untouched — the P0 pin holds.) - Composite observers: per-sink capture projection.
composeObserverschildren now each receive only what their owncaptureoptions allow: disallowedcaptured*fields are stripped,error.messageis gated per child oncapture.errorMessages, and a childredacthook applies only to that child's view.
Important
Behavior change (security-motivated): a composed observer with no options no longer receives captured content from its siblings' opt-ins — it now matches a standalone observer's privacy defaults. If a sink should see prompts/tool payloads, give it its own capture options.
Additive
-
result.observation?.settledongenerateText/streamChat/embed/embedMany— await it beforeobserver.close()so asynccost.calculatedenrichments (an asyncpriceProvider) aren't dropped. Nothing about the run itself ever waits on it (G2 holds).const res = await generateText({ model, messages, deps: { observer } }); await res.observation?.settled; await observer.close(); // JSONL now contains cost.calculated
-
createMemoryObserver({ maxBytes })— a total byte budget alongsidemaxEvents, evicting by the existingoverflowstrategy (drop-oldestdefault) and counting intodroppedCount. -
deps.tracerMode: 'legacy'— opt back into the 1.5 flat span topology (one parent-lessinvokeper model call,deuz.step.count: 1, retries on that call's own span) for dashboards built against it. The default remains'hierarchical'.
Notes
eventIdis now derived as`${executionId}:${sequence}`— one fewer id draw per event; the format was never part of the contract.- Release workflow:
actions/*@v5, an idempotent GitHub-release step, and dual-auth publish —NPM_TOKENif present, otherwise npm trusted publishing (OIDC);--provenanceon both paths (npm pinned to the 11 line — npm 12 changedpack --jsonoutput).
Quality: 575 tests / 63 files (17 new: malicious-redactor + truncation-boundary secret matrix, per-sink projection splits, settled→close JSONL drains, legacy-tracer span shape), full 10-stage npm run check + docs build green. Bundle budgets not raised (core 99 547 / 100 000 raw bytes).
Docs: Observability guide · Event catalog · CHANGELOG
Full Changelog: v1.6.0...v1.6.1
v1.6.0 — Observable Runtime
Observable Runtime
Every Deuz run now emits a versioned observation event protocol (ObserveEvent, schemaVersion: 1) through the new Dependencies.observer seam — local-first, zero runtime dependencies, no hosted service, no OpenTelemetry dependency, and nothing recorded by default.
What you can see
- Runs & models — start/end, provider/model/surface, TTFT (tool-call-first responses now count), per-retry reason + backoff delay, finish reason, full token usage, sync/async USD cost via
priceProvider - Agent steps — effective per-step model (post-
prepareStep), per-step duration (new), per-step + cumulative usage,endReasonincl. the previously invisible runaway-tool-error stop - Tools — per-call timing in real parallel completion order, original thrown errors captured at the only site that still has them, machine-readable denial causes
- Approvals — requested/resolved with source (
server/client-response/default-deny) and resume wait time - Durable runs — checkpoint saves/loads/failures; one
runIdacross suspend/resume legs, freshexecutionIdper leg,checkpoint.loadedbeforerun.started { resumed: true } - Compaction — per-layer events with estimate token + message counts; the summarize side-call is a visible, tagged model call (usage counted exactly once)
- Sub-agents — full tree under the parent
runId, keyed byagentPath, with durablechildRunId
New subpaths
import { createMemoryObserver, summarizeRun } from '@deuz-sdk/core/observe';
import { createJsonlObserver, readJsonlEvents } from '@deuz-sdk/core/observe/node';
const observer = createMemoryObserver();
await generateText({ model, messages, tools, maxSteps: 5, deps: { observer } });
console.log(summarizeRun(observer.latestRun() ?? []));Built-ins: createMemoryObserver · createCallbackObserver · composeObservers · filterObserver · summarizeRun · Node JSONL persistence (one valid JSON line per event, binary-safe, bounded queue — a slow disk can never affect a run).
Privacy & safety guarantees
- Events carry only counts/ids/names/durations/enums; prompt/tool/reasoning/error content is opt-in per field and always passes a
[REDACTED]redaction profile + structural limits (regression-tested against planted secrets in every channel) - Observers can never break a run: isolated, never awaited; with no observer the hot path pays a single boolean branch and draws no ids
- Deterministic per-run sampling (
sampleRate) — a run is all-in or all-out streamChatstays synchronous and lazy (G2); exactly one terminal event per execution leg
Tracer bridge — the span hierarchy, completed
An injected Dependencies.tracer now receives the documented invoke → step → execute_tool hierarchy driven by the same events (1.5 emitted only flat per-model-call invoke spans). Span names and attribute keys are unchanged; agentic loops now produce one invoke with step/tool children instead of N flat invokes. A user abort still ends the span without an exception.
Behavior fix
A tool-call-first response now clears the TTFT timer — previously only text/reasoning deltas did, so a tool-first stream could falsely trip the 60-second ttft timeout.
Quality: 558 tests (147 new across 14 observation suites, including the repo's first span tests and a P0 secret-leak matrix), full 10-stage npm run check + docs build green. Bundle budgets raised once with measurement: core 86 000 → 100 000 raw bytes (measured 97.7 KB fully instrumented), edge 76 000 → 90 000.
Docs: Observability guide · Event catalog · Design spec (code-verified) · CHANGELOG
v1.5.1 — Foundation Hardening
v1.5.1 is deliberately a foundation release, not a feature headline. It turns packaging, compatibility, streaming, provider behavior, and public API stability into release-blocking contracts so v1.6 and v1.7 can evolve without destabilizing the core.
Patch Changes
-
CI and release pipeline — one gate, the same contract everywhere: CI now separates quality/API types, runtime, packed-artifact, and documentation checks. The runtime suite runs on Node.js 22 and 24 on Linux plus Node.js 22 on Windows. A single
npm run release:verifycommand is shared by local and CI releases; the tag workflow requiresvX.Y.Zto matchpackage.json, reruns the complete gate, and is wired for npm provenance publishing only after verification succeeds. A normal push tomaincannot publish a release. -
Packed package and export verification: releases are checked as the actual
npm packartifact rather than trusting the source tree. Every declared target must exist inside the tarball, stay underdist, and load through both ESMimportand CommonJSrequire. Source, test, environment, smoke-test, and TypeScript build-state files are rejected from the package.publint --strictand Are the Types Wrong? remain mandatory. The gate verifies 26 public export entries and 50 runtime targets. -
Public API stabilization: a checked contract now protects 141 root API symbols and all 26 package export entries from accidental removal or drift. Contract changes must be reviewed explicitly instead of appearing as incidental build output, giving future v1.6/v1.7 work a stable compatibility floor.
-
Browser, edge, and custom gateway compatibility: representative consumers of the root entry,
/edge, and provider entries must bundle with esbuild's browser platform and an ES2022 target without pulling in Node built-ins. The jsdom/Web Streams suite exercises the Responses wire with custom gateways:baseURL: 'https://gateway.example'resolves to/responses, while an explicitly supplied/v1prefix is preserved exactly once. The SDK never invents/v1for a custom Responses base URL. -
Bundle-size regression budgets: minified browser consumers now have checked raw and gzip ceilings with narrow headroom. At release time the measured bundles are 74,898 B raw / 22,757 B gzip for core, 65,695 B / 20,251 B for
/edge, and 596 B / 303 B for the OpenAI provider entry. Crossing the committed budget fails CI instead of silently growing the package. -
Stream protocol hardening: the shared SSE parser now covers LF, CRLF, and bare CR line endings; CRLF delimiters split between chunks; UTF-8 BOMs; arbitrary multibyte UTF-8 boundaries; comments and keep-alives; named events; multiline
data:fields; and final events without a trailing blank line. Early consumer cancellation propagates to the underlying reader instead of leaving the transport open. -
Provider conformance infrastructure: Anthropic Messages, OpenAI Chat Completions, OpenAI Responses, and Google native streaming now run through one public-SDK contract suite. Every case verifies route, method, authentication and streaming shape, canonical text deltas, normalized usage, exactly one finish part, and typed error behavior. Provider-specific wire shapes remain explicit while their observable SDK semantics stay aligned.
-
Standard error contract: OpenAI-compatible adapters retain the real provider id instead of reporting every compatible host as OpenAI. Exhausted DNS, TLS, and transport failures normalize to the new retryable
NetworkErrorrather than leaking inconsistent low-level exceptions.isDeuzError()provides stable detection across duplicate package copies and realms, whileDeuzError.toJSON()emits a predictable, secret-safe diagnostic shape that excludes raw causes, headers, bodies, and credentials. Stream errors surface consistently through theerrorpart and the rejectedusageandfinishReasonpromises. -
Documentation structure: compatibility guarantees, stream protocol behavior, provider conformance requirements, and the release process now live in a dedicated Reference section. Error handling and edge/runtime guidance match the executable contracts, and every release requires MDX type generation plus a production documentation build. The README and benchmark presentation were also rebuilt around checked-in measurement scripts and result artifacts.
49 runtime test files · 447 tests green · Node 22/24 Linux + Node 22 Windows · 26 export entries / 50 runtime targets verified · 141 root API symbols locked · ESM/CJS, browser/edge, bundle-size, API-contract, and docs gates clean.
Full details are in CHANGELOG.md.
Full Changelog: v1.5.0...v1.5.1
v1.5.0 — Durable sessions: checkpoint/resume, sub-agent approval, signed approvals
Minor Changes
- Durable sessions —
sessionoption +SessionStoreseam +AgentCheckpoint(new@deuz-sdk/core/durablesubpath, everything also re-exported from/edge; all additive): passsession: { store, runId? }on any agentic call and both loops (generateTextandstreamChat) save a serializableAgentCheckpointat every step boundary —{ version, runId, stepId: '${runId}#${stepIndex}', stepIndex, status: 'running' | 'suspended' | 'completed', messages, usage, pendingApprovals?, agentPath?, createdAt }.messagesis the full immutable history (the loop never mutates prior arrays, so snapshots stay true and prompt-cache prefixes stay byte-stable across legs);usageis CUMULATIVE across all resume legs while each leg's result still reports that leg's own cost. The result carriesrunId—GenerateTextResult.runId, and synchronously onStreamChatResult.runId. Persistence is best-effort by contract: a throwingstore.savelogsdeps.logger.errorand the run continues. Single-turn calls (notools) have no step boundaries — no checkpoint, norunId; an aborted call deliberately doesn't checkpoint the interrupted step (a checkpoint is only honest at a completed boundary).SessionStoreis two required methods (save/load, plusdeleteand optionallist) over any backend — no vendor runtime;createInMemorySessionStore()is the reference (latest save wins per runId). For persistent stores,serializeCheckpoint/deserializeCheckpointare a binary-part-safe JSON codec:Uint8Arrayvalues anywhere in the message tree (raw image parts) round-trip as realUint8Arrays instead of decaying into index-keyed objects — including NodeBuffers, whose owntoJSONwould otherwise preempt the codec (the replacer reads the pre-toJSONholder value);$deuzBytesis the codec's reserved key, and a garbled lookalike in tool data passes through as plain data instead of throwing out of resume. resumeFromCheckpoint(store, runId, options)/resumeStreamFromCheckpoint: load a checkpoint and continue the run — the stored history becomes the messages, the existing settle-on-resume mechanism answers the trailing pendingtool_useids fromapprovalResponses, and step indices + cumulative usage continue across legs (budget stopstotalTokensExceed/costExceedsandprepareStepsee whole-run usage). Resuming a suspended run without a verdict for a pending gated call now denies it by default (safe side) instead of resending an unansweredtool_useto the provider — an explicitly-emptyapprovalResponses: []array activates the same default-deny settle (previously it was ignored). A mid-step crash resumes from the last completed boundary and re-runs that step (the honest recovery unit is one step — documented; keep tool side effects idempotent).prepareStepsees continuing cross-leg step indices on a resume leg in BOTH loops (loop-symmetry). Documented limits: a resume cannot hand a client tool its real output (ToolApprovalResponsecarries a verdict, not a result — the escape hatch is loading the checkpoint, appending thetool_resultmessage yourself, and re-calling with the samesession), and a durable sub-agent suspending out of a parallel tool batch discards that step's sibling executions (re-run on resume). UnknownrunId: the buffered resume rejects with the newCheckpointNotFoundError; the streaming twin keeps the sync-return contract (G2) and surfaces it as anerrorpart + rejectedusage/finishReason, never a synchronous throw.- Client-mode approval inside sub-agents (the 1.4 limitation, removed): when the parent call carries
session, a gated tool call inside anagentTool(with no inherited server-mode approver) no longer returns an is_error — the child loop checkpoints itself as suspended under a per-call key (${parentRunId}::${agentName}#${toolCallId}— the model-issued tool_use id is stable across legs because it lives in the parent history, so parallel same-name sub-agents never collide), and the parent suspends too, carrying the pending approvals up tagged with the sub-agent path (ToolApprovalRequest.agentPath+agentPathon thetool-approval-requeststream part, both additive). Resuming the parent with verdicts keyed by the sameapprovalIds re-executes the sub-agent call, which finds its suspended checkpoint, settles its own pending calls from the forwarded verdicts, and continues where it left off — at any nesting depth. A suspended sub-agent's usage still folds into the parent's checkpoint before suspension. Withoutsession, the 1.4 contract is unchanged (clear is_error, parent model can react). - HMAC-signed approvals —
createApprovalSigner({ secret, clock? })(WebCryptocrypto.subtle, edge-safe):sign(request, { runId? })produces av1.<payload>.<mac>token over the fullToolApprovalRequest+ optional run binding +issuedAt;verify(token, { maxAgeMs? })returns the payload on a valid MAC — a forged, tampered, garbled, or expired token is a verdict ofnull, never a thrown exception (a token whose age is ≥maxAgeMsis expired; omit for no expiry; strictly three token segments — trailing garbage is rejected). Signing is loop-based base64 (no spread), so approval payloads carrying large tool inputs (e.g. a write-file body) don't overflow the stack; an emptysecretthrows aTypeErrorat construction instead of surfacing as an unhandledimportKeyrejection. The clock is injectable for deterministic tests;ToolApprovalRequest.approvalIdwas kept distinct fromtoolCallIdin 1.3 exactly so this scheme could land additively. Closes theapprovalResponsestrust-boundary gap documented in client-tools: sign pending approvals server-side, verify verdicts on resume, reject forgeries/replays.
v1.4.0 — Agent Core: sub-agents, compaction, budget stops, loop hooks
Minor Changes
- Loop hooks —
prepareStep+activeTools(onCommonCallOptions; work in bothgenerateTextandstreamChatwhenevertoolsis present):prepareStep(ctx)runs before every model step, after automatic compaction, and may return{ messages, activeTools, toolChoice, model }—messagesbecomes the base history for this and all following steps (doubling as a user-controlled compaction/rewrite hook, including system-prompt edits via thesystem-role message), whileactiveTools/toolChoice/modelapply to that step only. A thrownprepareStepfails the call like any caller code — it is never swallowed. StaticactiveToolsrestricts which tools are sent every step (unknown names warn and are dropped; matching nothing fails open to the full list);prepareStep'sactiveToolsoverrides it.prepareStepis a plain call option on the free functions (no agent class to instantiate) and composes with the automatic compaction below — compaction runs first,prepareStepsees the result. - Budget stop conditions —
totalTokensExceed/costExceeds(StopConditionfactories, exported from root +/edgealongsidestepCountIs/hasToolCall, now also exported): stop the loop once cumulative REAL usage or cost — all steps and sub-agents included — crosses a bound, OR-ed intostopWhenlike any condition and evaluated at the step boundary (never mid-tool-batch). A budget stop never changesfinishReason(the locked union is untouched); instead it setsproviderMetadata.deuz.stoppedBy: 'totalTokensExceed' | 'costExceeds'on the result /finishpart (GenerateTextResult.providerMetadatais a new additive field).costExceedsneedsdeps.priceProvider— without one it warns once and never fires. Token- and cost-budget bounds are first-classStopConditionfactories here (alongsidestepCountIs/hasToolCall), andstoppedByreports which bound ended the loop. - Automatic layered compaction (
compaction?: 'auto' | CompactionPolicyonCommonCallOptions; opt-in, off by default, active only inside the agentic loop): three cheapest-first layers — prune old tool results into[pruned N chars]stubs, prune old reasoning parts, summarize the oldest unprotected slice into one message — run before a step once estimated context fill crosses a threshold (default 92%), always leaving every system message, the first user message, the last message, and the lastkeepRecentStepsassistant turns untouched. History stays immutable and prefix-stable for prompt-cache hits; a failed summarize logs a warning and skips the layer instead of ending the call; token counts are a calibrated heuristic, not a real tokenizer. Streaming emits a newcompactionStreamPart/UI part per layer that ran; buffered calls log it. Anthropic's nativeproviderOptions.anthropic.context_managementstill works verbatim alongside this. This is automatic, layered, cache-aware context management from one opt-in flag — the alternative is to estimate tokens and prune inside a per-step hook yourself. - Sub-agents —
agentTool(exported from root +/edge;AgentToolDefexported type): wraps a{ model, tools, system, maxSteps, maxDepth, ... }definition into aToolthat runs a nested agentic loop and returns its final text — no new runtime. When the parent streams, the sub-agent's entire canonical stream forwards live asagentPath-taggedsub-agentparts, rather than surfacing only the final text. The parent's server-modeapproveToolCallis inherited to every nesting depth as first-class behavior, so a sub-agent's own tool calls stay gated with no extra wiring. Usage folds into the parent total and is tagged withmeta.agentPath;maxDepth(default 2) guards against runaway nesting; the parentsignalpropagates down. Client-mode approval inside a sub-agent isn't supported yet (needs durable suspend/resume — lands in 1.5); a gated sub-agent call with no inherited approver returns a clear is_error instead.