Skip to content

Releases: Deuz-AI/Deuz-SDK

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 16:32

Full Changelog: v1.9.0...v2.0.0

v1.9.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 21:40
npm install @deuz-sdk/core@1.9.0
npm install @deuz-sdk/react@1.9.0   # optional: useChat, useObject, headless UI

The 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.

  1. A symlink or NTFS junction inside the root walked straight out. The guard was pure string math, and
    path.resolve/path.relative cannot follow a link — so <root>/escape → /outside passed 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. resolveInside now re-verifies the deepest existing ancestor through
    fs.realpath, the only layer that resolves links.
  2. On Windows, a foreign drive letter defeated both guard layers. Nothing had to be planted — the model
    just had to emit D:/x/y. path.relative returns 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.
  • agentTool runs — no longer invisible in useChat. 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 shapesstreamChat 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.content would misattribute
    prose, and folding its tool_use parts into the parent's would make assistantMessageFromTurn re-emit
    them as the parent's own, where each one needs a tool_result or the next request 400s;
  • UIMessagePart is a closed pinned union consumers switch on exhaustively, so a new member there would be
    breaking (the same reasoning that keeps ToolRunState at 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. defaultShouldFallback hopped on
    any TimeoutError; once layer: '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 throttleMs assertion 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...
Read more

v1.8.0 — Autonomous Agent Runtime

Choose a tag to compare

@github-actions github-actions released this 22 Jul 15:40

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. Persist plan.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 into is_error results.
  • Planner → Executor → Verifier (./autonomy, verifyStep) — planTasks + TaskList reducers; verifyStep on both loops; bestOfN, selfConsistency, parallelAgents.
  • Background runs (./runtime, ./runtime/node) — RunManager, pollStaleRuns, live plan-update / activity stream 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) — published createMockModel, runEval, golden-replay fixtures.
  • Azure / Bedrock / KimicreateAzure, createBedrock (Mantle Bearer; no SigV4), createKimi alias.

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   # optional

Full Changelog: v1.7.1...v1.8.0

v1.7.1 — Chat & Memory Correctness

Choose a tag to compare

@github-actions github-actions released this 21 Jul 08:00

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 chatId memory isolation. Chat-scoped get, search, and list operations now match chatId exactly in both the in-memory and Markdown stores, preventing memories from leaking across sibling chats. Markdown records also preserve chatId through frontmatter round-trips; broader legacy scopes remain compatible.
  • Conversation-safe ChatStore persistence. Stored chats now retain the caller’s original transcript plus real assistant/tool additions, never compaction summaries or prepareStep prompt 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 executingtool-resultcomplete, while failures and denials terminate with error. Negotiated v1 streams remain unchanged.
  • Buffered result parity. Suspended buffered runs now expose result.memory when memory extraction is enabled, resolving safely to an empty mutation list. Final text-only assistant turns are included in result.response.messages exactly 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

Choose a tag to compare

@U-C4N U-C4N released this 19 Jul 04:35

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

  1. Built-in cross-session memorymemory: { seams, scope } on any call: recall before the first token, mem0-style extract→reconcile after the turn, non-blocking.
  2. Live USD cost on the wire — a cumulative cost stream part per step from the in-library price catalog, with prompt-cache savings as its own field.
  3. Budget guardrailbudget: { usd, tokens } hard-stops the loop with a typed budget-exceeded part.
  4. Cryptographic approval trail — HMAC-signed, runId-bound, expiring approval tokens through the UI wire; forged/expired/token-less approvals are denied on resume.
  5. Durable × resumable, vendor-free — F5 mid-tool-loop: the run continues from its checkpoint AND the stream reconnects gaplessly (resumeDeuzChatResponse, one endpoint).
  6. Cross-provider failoverfallbackModels hops 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-ID replay, StreamStateStore seam, multi-client live follow, connectDeuzStream auto-reconnect — v1 clients keep working byte-identically.
  • Typed data parts (writeData + Standard Schema validation on-stream), tool state machine, built-in RAG citations.
  • ChatStore persistence + pure chat engine + regenerate / edit-and-branch.
  • New package: @deuz-sdk/react — useChat v2, useObject, headless ToolApprovalCard + CostBadge. (@deuz-sdk/core/react keeps 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

Choose a tag to compare

@U-C4N U-C4N released this 13 Jul 19:46

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 redact hook 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. (maskSecret and every 1.5 log/error path are untouched — the P0 pin holds.)
  • Composite observers: per-sink capture projection. composeObservers children now each receive only what their own capture options allow: disallowed captured* fields are stripped, error.message is gated per child on capture.errorMessages, and a child redact hook 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?.settled on generateText / streamChat / embed / embedMany — await it before observer.close() so async cost.calculated enrichments (an async priceProvider) 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 alongside maxEvents, evicting by the existing overflow strategy (drop-oldest default) and counting into droppedCount.

  • deps.tracerMode: 'legacy' — opt back into the 1.5 flat span topology (one parent-less invoke per model call, deuz.step.count: 1, retries on that call's own span) for dashboards built against it. The default remains 'hierarchical'.

Notes

  • eventId is 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_TOKEN if present, otherwise npm trusted publishing (OIDC); --provenance on both paths (npm pinned to the 11 line — npm 12 changed pack --json output).

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

Choose a tag to compare

@U-C4N U-C4N released this 13 Jul 14:51

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, endReason incl. 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 runId across suspend/resume legs, fresh executionId per leg, checkpoint.loaded before run.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 by agentPath, with durable childRunId

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
  • streamChat stays 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

Choose a tag to compare

@U-C4N U-C4N released this 13 Jul 08:45

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:verify command is shared by local and CI releases; the tag workflow requires vX.Y.Z to match package.json, reruns the complete gate, and is wired for npm provenance publishing only after verification succeeds. A normal push to main cannot publish a release.

  • Packed package and export verification: releases are checked as the actual npm pack artifact rather than trusting the source tree. Every declared target must exist inside the tarball, stay under dist, and load through both ESM import and CommonJS require. Source, test, environment, smoke-test, and TypeScript build-state files are rejected from the package. publint --strict and 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 /v1 prefix is preserved exactly once. The SDK never invents /v1 for 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 NetworkError rather than leaking inconsistent low-level exceptions. isDeuzError() provides stable detection across duplicate package copies and realms, while DeuzError.toJSON() emits a predictable, secret-safe diagnostic shape that excludes raw causes, headers, bodies, and credentials. Stream errors surface consistently through the error part and the rejected usage and finishReason promises.

  • 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

Choose a tag to compare

@U-C4N U-C4N released this 07 Jul 18:50
309944b

Minor Changes

  • Durable sessions — session option + SessionStore seam + AgentCheckpoint (new @deuz-sdk/core/durable subpath, everything also re-exported from /edge; all additive): pass session: { store, runId? } on any agentic call and both loops (generateText and streamChat) save a serializable AgentCheckpoint at every step boundary{ version, runId, stepId: '${runId}#${stepIndex}', stepIndex, status: 'running' | 'suspended' | 'completed', messages, usage, pendingApprovals?, agentPath?, createdAt }. messages is the full immutable history (the loop never mutates prior arrays, so snapshots stay true and prompt-cache prefixes stay byte-stable across legs); usage is CUMULATIVE across all resume legs while each leg's result still reports that leg's own cost. The result carries runIdGenerateTextResult.runId, and synchronously on StreamChatResult.runId. Persistence is best-effort by contract: a throwing store.save logs deps.logger.error and the run continues. Single-turn calls (no tools) have no step boundaries — no checkpoint, no runId; an aborted call deliberately doesn't checkpoint the interrupted step (a checkpoint is only honest at a completed boundary). SessionStore is two required methods (save/load, plus delete and optional list) over any backend — no vendor runtime; createInMemorySessionStore() is the reference (latest save wins per runId). For persistent stores, serializeCheckpoint/deserializeCheckpoint are a binary-part-safe JSON codec: Uint8Array values anywhere in the message tree (raw image parts) round-trip as real Uint8Arrays instead of decaying into index-keyed objects — including Node Buffers, whose own toJSON would otherwise preempt the codec (the replacer reads the pre-toJSON holder value); $deuzBytes is 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 pending tool_use ids from approvalResponses, and step indices + cumulative usage continue across legs (budget stops totalTokensExceed/costExceeds and prepareStep see 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 unanswered tool_use to the provider — an explicitly-empty approvalResponses: [] 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). prepareStep sees 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 (ToolApprovalResponse carries a verdict, not a result — the escape hatch is loading the checkpoint, appending the tool_result message yourself, and re-calling with the same session), and a durable sub-agent suspending out of a parallel tool batch discards that step's sibling executions (re-run on resume). Unknown runId: the buffered resume rejects with the new CheckpointNotFoundError; the streaming twin keeps the sync-return contract (G2) and surfaces it as an error part + rejected usage/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 an agentTool (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 + agentPath on the tool-approval-request stream part, both additive). Resuming the parent with verdicts keyed by the same approvalIds 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. Without session, the 1.4 contract is unchanged (clear is_error, parent model can react).
  • HMAC-signed approvals — createApprovalSigner({ secret, clock? }) (WebCrypto crypto.subtle, edge-safe): sign(request, { runId? }) produces a v1.<payload>.<mac> token over the full ToolApprovalRequest + optional run binding + issuedAt; verify(token, { maxAgeMs? }) returns the payload on a valid MAC — a forged, tampered, garbled, or expired token is a verdict of null, never a thrown exception (a token whose age is ≥ maxAgeMs is 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 empty secret throws a TypeError at construction instead of surfacing as an unhandled importKey rejection. The clock is injectable for deterministic tests; ToolApprovalRequest.approvalId was kept distinct from toolCallId in 1.3 exactly so this scheme could land additively. Closes the approvalResponses trust-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

Choose a tag to compare

@U-C4N U-C4N released this 04 Jul 12:38

Minor Changes

  • Loop hooks — prepareStep + activeTools (on CommonCallOptions; work in both generateText and streamChat whenever tools is present): prepareStep(ctx) runs before every model step, after automatic compaction, and may return { messages, activeTools, toolChoice, model }messages becomes the base history for this and all following steps (doubling as a user-controlled compaction/rewrite hook, including system-prompt edits via the system-role message), while activeTools/toolChoice/model apply to that step only. A thrown prepareStep fails the call like any caller code — it is never swallowed. Static activeTools restricts which tools are sent every step (unknown names warn and are dropped; matching nothing fails open to the full list); prepareStep's activeTools overrides it. prepareStep is a plain call option on the free functions (no agent class to instantiate) and composes with the automatic compaction below — compaction runs first, prepareStep sees the result.
  • Budget stop conditions — totalTokensExceed / costExceeds (StopCondition factories, exported from root + /edge alongside stepCountIs / hasToolCall, now also exported): stop the loop once cumulative REAL usage or cost — all steps and sub-agents included — crosses a bound, OR-ed into stopWhen like any condition and evaluated at the step boundary (never mid-tool-batch). A budget stop never changes finishReason (the locked union is untouched); instead it sets providerMetadata.deuz.stoppedBy: 'totalTokensExceed' | 'costExceeds' on the result / finish part (GenerateTextResult.providerMetadata is a new additive field). costExceeds needs deps.priceProvider — without one it warns once and never fires. Token- and cost-budget bounds are first-class StopCondition factories here (alongside stepCountIs/hasToolCall), and stoppedBy reports which bound ended the loop.
  • Automatic layered compaction (compaction?: 'auto' | CompactionPolicy on CommonCallOptions; 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 last keepRecentSteps assistant 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 new compaction StreamPart/UI part per layer that ran; buffered calls log it. Anthropic's native providerOptions.anthropic.context_management still 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; AgentToolDef exported type): wraps a { model, tools, system, maxSteps, maxDepth, ... } definition into a Tool that 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 as agentPath-tagged sub-agent parts, rather than surfacing only the final text. The parent's server-mode approveToolCall is 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 with meta.agentPath; maxDepth (default 2) guards against runaway nesting; the parent signal propagates 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.