Native agents
Typed agent results, strict native checkpoints, tool receipts, verification, shared execution budgets, and the V3 run path with compiled context, working memory and versioned memory (2.4).
El contenido de las páginas de documentación está en inglés. La navegación, la búsqueda y la interfaz siguen el idioma elegido.
runAgent and streamAgent provide the optional native agent engine. Existing
generateText, streamChat, and createAgent methods retain their existing
contracts. Import the native functions from @deuz-sdk/core/agent.
2.4 adds an opt-in third record version for runs that carry compiled context,
working memory and versioned-memory writes: see
V3 runs. Without checkpointVersion: 3, runs
behave as the rest of this page describes.
A typed, validated result
This complete example uses a deterministic model, so it needs no API key. A real
provider model can replace createMockModel without changing the result contract.
Raw JSON Schema requires a runtime validator. A Standard Schema value can provide
its own validator.
import { runAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
type Answer = { answer: number };
const result = await runAgent<Answer>({
model: createMockModel({ responses: [{ text: '{"answer":42}' }] }),
prompt: 'Return the answer as JSON.',
output: {
mode: 'json',
schema: {
type: 'object',
properties: { answer: { type: 'number' } },
required: ['answer'],
additionalProperties: false,
},
validate(value) {
if (value === null || typeof value !== 'object' ||
!('answer' in value) || typeof value.answer !== 'number') {
throw new Error('Expected a numeric answer');
}
return { answer: value.answer };
},
},
verify: ({ output }) => output.answer === 42
? { status: 'verified' }
: { status: 'rejected', feedback: 'Recheck the calculation.' },
maxSteps: 5,
maxOutputAttempts: 2,
maxVerifyAttempts: 2,
});
if (result.status === 'completed') {
console.log(result.output.answer); // Typed as number.
} else {
console.log(result.status);
}Only completed contains an accepted output. suspended exposes pending
approvals and client calls; stopped exposes a reason; failed exposes a
serializable error. Every result carries the run ID, messages, usage and model
step count. accounting exposes the shared ledger's spent, reserved and unknown
amounts. The result promise resolves to a failed result after a persistence
failure; it never reports completion before the terminal write succeeds. That
failure result may itself be unpersisted, so recover from the last committed envelope.
With tools, native execution uses the existing tool loop and then a separate
finalization request that offers the loop's tools with tool_choice: 'none', so the
model cannot call them (details below). Without tools, structured
output can begin directly in finalization. maxSteps covers the root tool-loop
turns and finalization/repair turns; the execution ledger also accounts for
auxiliary and descendant model requests. maxOutputAttempts bounds schema repair
for each verifier candidate, and maxVerifyAttempts bounds verifier invocations.
Verification has three outcomes: verified accepts, rejected requests another
candidate within the attempt limit, and inconclusive stops without certifying
the answer. Exceptions from the verifier are inconclusive, unless the run was
cancelled (it stops with reason aborted) or the execution deadline elapsed (it
fails with ExecutionPolicyError). Cancellation and the deadline are checked again
after the verifier returns, and they outrank its verdict. Schema validation and
verification are distinct checks; a valid JSON shape does not prove the answer.
If the write that follows a verdict fails, the run stays in finalizing. Resuming
it runs the verifier again on the same candidate, without a model call. The repeat
counts as a new attempt toward maxVerifyAttempts; when none is left, the resume
stops with verification-attempts-exhausted instead. Verifiers must therefore be
idempotent: a repeated verdict on the same candidate must have no further effect.
Key any side effect on the run and the candidate's text, not on attempt, which
differs on the repeat.
Streaming
streamAgent(options) returns immediately. Accessing its result, iterating a
stream, or calling consume() starts the run. Subscribe before execution if every
event matters.
import { streamAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
const stream = streamAgent({
model: createMockModel({ responses: [{ text: 'Hello' }] }),
prompt: 'Say hello.',
});
for await (const event of stream.events) {
if (event.type === 'result') console.log(event.result.status);
}
console.log((await stream.result).text);textStream, partialOutputStream and elementStream are additional views.
Partial output is an unknown draft, and array elements belong to an attempt;
only a completed result validates the entire final value. Native agent streams
are live streams. To put a run on the UI wire with a replayable journal, see
Native agents in a UI; for durable control events across a
DAG, use the swarm journal.
Native agents in a UI
Since 2.3, toDeuzAgentStreamResponse (@deuz-sdk/core/ui) serializes a
streamAgent or resumeStreamAgent stream to the Deuz UI wire.
The run's parts go out as a chat stream's do, and data-agent frames add the run
id, a sequence, the phases, drafts of the output, and the terminal state: the
validated output, the pending approvals and client calls, the stop reason with its
stopDetails, or the failure. On the client, agentStateFromParts folds the
frames, and useChat from @deuz-sdk/react drives the whole loop:
import { streamAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
import { toDeuzAgentStreamResponse } from '@deuz-sdk/core/ui';
export function POST(): Response {
const stream = streamAgent({
model: createMockModel({ responses: [{ text: 'Hello' }] }),
prompt: 'Say hello.',
});
// Subscribes before it returns: no event is lost, whatever the route does next.
return toDeuzAgentStreamResponse(stream, { runId: 'demo' });
}What the bridge guarantees:
- Typed output only from the terminal state. A draft (
partial) is neverT. Only acompletedstate carriesoutput, the one the run validated. - Every frame applies once. Each frame's
seqis its SSE event id. A reducer applies a frame only when itsseqis above the state's, so a replay that overlaps the live stream, a duplicate, or a stale frame never applies twice. - Replay needs a journal. Pass
storeandstreamId(one per run, such as the run id) and every leg numbers on from the last one, so a client resumes from its cursor throughresumeDeuzStreamResponse. Without a store, a response cannot be replayed. - One writer per stream. A run has one executor at a time. A leg the run refuses before it begins (a second resume while it executes, a run id stored under another scope, an unknown run) goes out on its own response only, without event ids: its client sees the failure, and the run's journal stays as it was.
- Resume in one request.
resumeStreamAgentproceeds only with a verdict for every pending approval and a result for every pending client call, so a client sendsrunIdwith both together.useChatdoes this for you. - Nothing private leaves the server: no transcript, no raw tool results (the
tool-outputevent'srawResult), no receipts, no ledger. Failure messages are secret-redacted.
The useChat native route example
shows a complete route and component: an approval and a client tool in one
suspension, resumed with the run id.
Strict checkpoints and approval resume
Native sessions use AgentRunEnvelope with kind: 'deuz-agent-run'. They do not
reuse a legacy AgentCheckpoint as the top-level envelope; the loop and sub-agent
checkpoints inside it are AgentCheckpoint records. Since 2.3 version is 1 | 2.
A save writes 2 when the envelope records memory, transcript, chat or
models, state a 2.2 reader would silently ignore, and while the stored turn has an
open call that a 2.2 resume would answer wrongly (see below), so a 2.2 process
refuses that envelope instead of resuming the run without that state. Every other
envelope stays 1, and a 2.3 resume reads both. The fields 2.3 adds are optional,
and absent from a run that does not use the feature:
| Field | What it records |
|---|---|
memory (AgentRunMemoryState) | The run's memory scope, the one recall it was given (recall.status, with the message's index and count), and, once the run completed, its extraction: an operationId recorded pending with the terminal commit, then completed or failed. Since 2.4 the extraction also records planned, the store mutations it planned, before it writes any memory. See Memory. |
transcript (AgentRunTranscript) | The run's user-facing transcript, kept for chat and memory: the caller's input, then the run's turns as they happened. Append-only, so compaction or a prepareStep history never rewrites it. See Chat projection. |
chat (AgentRunChatState) | Where the chat projection stands: the cursor of acknowledged messages, the projected messageIds, which assistant turn and tool invocation is which message, and the last attempt's status ('saved', 'failed', 'scope-mismatch' or 'superseded'). See Chat projection. |
models (AgentRunModelState) | Written at the first fail-over: the model the run's calls go to now, each fail-over so far (the failed model, the next one, the model step, the phase and the error) and the rules' version. See Model fail-over. |
retry (AgentRunRetry) | Written with a failed result whose cause was transient: { cause: 'model-call' | 'admission', phase }, which retryFailed: true resumes from. See When a model call fails. |
binding.memory, binding.chat, binding.fallback | The resume binding of the memory scope, of the chatId with its scope, and of the fallbackModels (each candidate's provider, protocol and model id, in order), as JSON. A resume that changes, adds or removes one fails before anything is written. binding.mcp and binding.handoffs, also new in 2.3, pin the MCP tool catalog and the handoff targets (see the paragraphs below the example). |
reconciled on a receipt (AgentToolReceipt) | true when the receipt's outcome came from reconciledToolResults rather than from the tool. See When the store fails. |
Of these fields only the first four make an envelope version 2. A 2.2 reader
ignores retry (it returns a failed run's stored result, as before) and
reconciled. It refuses an envelope of either version whose binding records any
of the 2.3 keys (mcp, handoffs, memory, chat, fallback) with Native agent binding changed across resume., because its binding check compares the whole
binding. Once 2.3 saved a run that uses mcp, handoffs, memory, chat or
fallbackModels, only 2.3 resumes it.
A run whose stored turn leaves calls unanswered (the run waits on them, or its
process stopped while they ran) is also saved as version 2 while an open call's
receipt records failed, a stage 2.2 does not know and would answer with no result,
or suspended, a sub-agent 2.2 would start over, and while a resume that took the
turn up still waits on a client call's result, which 2.2 would deny. A run that waits
on an approval is version 2 too, and so is a run stopped while a call is open whose
ID the run's approval gate issued a request under, such as a resume that took
approved calls up and stopped before it saved their answers: 2.3 issues approvalId
as the tool call ID, @ and a digest, and 2.2 looks a verdict up by the bare tool
call ID, so it would deny the call. The envelope is version 1 again once a
checkpoint answers the calls: 2.2 never reads their receipts after that.
A resume refuses an envelope of a version it does not read (2.3 reads 1 and 2).
Since 2.3 it also refuses one holding a checkpoint whose version is not 1, with
CheckpointVersionError (code checkpoint_version_unsupported). Both refusals
happen before anything is written, so the stored run stays resumable by the SDK
that wrote it. An AgentRunStore implements load(runId) and save(envelope).
save must finish its durable write before resolving; a rejected write stops
further execution.
The following complete approval example uses a volatile store for demonstration.
The tool returns a string and has no external effect. In an application, obtain
approval verdicts from the authenticated user before calling resumeAgent.
import {
createInMemoryAgentRunStore,
runAgent,
resumeAgent,
} from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
const store = createInMemoryAgentRunStore();
const options = {
model: createMockModel({ responses: [
{ toolCalls: [{ toolName: 'publish', args: {} }] },
{ text: 'Approved operation finished.' },
] }),
prompt: 'Run the publish demonstration.',
bindingId: 'publish-demo-v1',
session: { store, runId: 'demo-1', scope: 'tenant-a' },
tools: {
publish: {
description: 'Demonstrate an approval gate.',
parameters: { type: 'object' as const, properties: {}, additionalProperties: false },
needsApproval: true,
execute: () => 'demo complete',
},
},
};
const first = await runAgent(options);
if (first.status === 'suspended') {
const approvalResponses = first.pendingApprovals.map(request => ({
approvalId: request.approvalId,
approved: true, // Demonstration verdict; use the user's decision in production.
token: request.token,
}));
const resumed = await resumeAgent({ ...options, approvalResponses });
console.log(resumed.status);
}The in-memory store does not survive a process restart. Implement the native
store against durable storage, or use the SQLite-backed swarm adapter to persist
agent envelopes with their task state. Enforce tenant authorization at the
application boundary; a caller-selected scope is an identity check, not an
authentication mechanism. Native store run IDs must be unique within that store.
Resume requires the original run ID and scope. Keep bindingId stable for the
same implementation and bump it when tool or verifier semantics change. Stored
model/tool/output schemas and persisted limits are checked on resume, and since 2.3
so is each handoff target's model, instructions and tool schemas, and the MCP
servers' tool catalog (a resume with onCatalogChange: 'accept' records a catalog
that changed instead of failing: see
MCP in native runs). Function
bodies cannot be fingerprinted reliably: changing a validator, approval predicate,
tool implementation or verifier requires a new bindingId. A
completed native envelope returns its stored result without another model call; a
failed one does too, unless a resume asks for a retry of a transient failure
(When a model call fails).
Resuming without all pending approval verdicts keeps the run suspended. An absent
verdict is not a denial. Since 2.3 a pending approval's approvalId is bound to the
call it was issued for: the run, the step it suspended at, the tool call ID, the tool
name and a digest of the arguments. Answer with the approvalId you were given. A
verdict kept from an earlier gate then never approves a later call, even when a
provider that sends no call IDs reuses tool_0 for it. A verdict that names only the
tool call ID (the 2.2 form) still counts when the run used that ID once: it occurs once
in the history, and the run issued one approval request under it. The checkpoint keeps
that count (approvalRequestCounts), so a history that compaction or prepareStep has
shortened cannot make a reused ID look unique.
Client-side tools likewise remain suspended until
clientToolResults supplies every pending call ID; each entry has toolCallId,
output, and an optional isError. Supplied results pass through the same approval,
validation and model-projection path as server tools. Since 2.3 a supplied result is
never lost to an interrupted resume. The run keeps listing the calls as pending
until the resume has saved its answers, so a resume that dies before it recorded a
result accepts that result again, and a resume without it stays suspended on the
call. A result the resume did record (in the call's receipt) answers the call on
the next resume, which needs no result for it; one passed for it anyway is ignored
with a warning part (setting: 'clientToolResults').
Use one executor for a given native run. Concurrent starts or resumes through the
same AgentRunStore object are rejected within one JavaScript process. Across
processes, the shipped durable stores reject a save whose revision is not the
stored one plus one, so an executor that lost the run cannot overwrite a newer save;
that fence does not stop two executors from running the same step. Pass lease
(2.3) to hold the run across processes, see
Leasing a native run; without it an
application must serialize ownership itself. The in-process guard does not make a
custom store transactional.
Tool receipts distinguish executing, a known child-agent suspended state,
executed, failed (execute threw; the model got the error, and a resume replays
it without running the tool again), validation/projection failures, and completed
projection. They are
scoped to a root model turn plus the provider call ID, so an ID reused in a later
turn does not retrieve an unrelated result. Raw results are committed before
validation/projection; a projection can be retried without repeating the effect.
An interrupted executing receipt cannot establish whether an external system
accepted the effect. Resume returns tool-reconciliation-required until the
application reconciles that effect: it explicitly authorizes the relevant
retryToolCallIds, or since 2.3 records the outcome it found with
reconciledToolResults. Since 2.3 the stopped result lists those receipts in
unknownEffects (see When the store fails). Completed
receipts can reuse their result. These receipts do
not provide exactly-once execution across an external service and the local
checkpoint transaction.
A receipt is saved before the checkpoint that answers its call, so a run can stop
in between: the process dies while other calls of the step still run, the store
fails the write that records the step, or a sub-agent called in the same step
suspends on its own approval. The next resume then finds the call unanswered.
Since 2.3 a call whose receipt records an outcome
(executed, completed, failed, or a validation or projection failure, an
outcome recorded with reconciledToolResults included) is answered from that
receipt, exactly like a call that needs no approval. The tool does not run again,
and the approval gate does not hold the call again, whether needsApproval or the
execution policy's requireApproval gated it, so the resume needs no verdict for
it. A verdict you pass for it anyway decides nothing: the resume ignores it and
streams a warning part (setting: 'approvalResponses') that names the call.
Before 2.3 such a call was answered Tool call denied. Reason: No approval response. unless the resume passed its verdict again, although its effect had
landed. A call without a recorded outcome is gated as before: one that never
started, one in executing that retryToolCallIds runs again, and a sub-agent call
in suspended. Without its verdict the resume answers such a call as denied. A
client tool's call is answered from the result a resume supplied: from the receipt
that recorded it, or, when none did, from clientToolResults, which the run then
still waits for (see above).
A resume saves what its settle answered (each call's result, error, denial or
client result) as the run's checkpoint before its first model call (2.3). A
failure of that model call, or of any later write, then never leaves the same
calls to settle again: the next resume, a retryFailed one included, goes on
from those answers and needs no verdict or client result again. Before 2.3 a
process that died during that model call left the calls unanswered: the next
resume denied an approved call that had run, or ran it again, and ran a call
that needs no approval again.
Idempotent tools
Some effects are safe to repeat because the tool writes under a key it derives
from the call itself. Mark such a tool replay: 'idempotent' (2.2). An interrupted
call of that tool runs again on resume without retryToolCallIds; plain tools
still stop for reconciliation. Since 2.3 each receipt records the flag of the tool
that ran the call (AgentToolReceipt.idempotent), so a handoff target's own tool is
judged by its own flag, never by a root tool of the same name. Every native tool
also receives ctx.modelStep, the root model turn that issued the call. Together
with ctx.toolCallId it is stable across resume, so it can key the write:
import { runAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
const board = new Map<string, string>();
const result = await runAgent({
model: createMockModel({ responses: [
{ toolCalls: [{ toolName: 'note', args: { text: 'checked' } }] },
{ text: 'Noted.' },
] }),
prompt: 'Leave a note.',
tools: {
note: {
description: 'Append a note to the shared board.',
parameters: {
type: 'object' as const,
properties: { text: { type: 'string' } },
required: ['text'],
},
replay: 'idempotent',
// A repeated call writes the same key, so a replay adds nothing.
execute: ({ text }: { text: string }, ctx) => {
board.set(`${ctx.toolCallId}@${ctx.modelStep}`, text);
return 'noted';
},
},
},
});
console.log(result.status, board.size);Only mark a tool idempotent when repeating it truly has no further effect, and
bump bindingId when you change the flag on a durable run.
When the store fails
What a resume does depends on where the store failed:
- Before the effect. The
executingreceipt could not be written, so the tool never ran. The run fails; a resume replays the step from its last checkpoint. - Between the effect and its completed receipt. The outcome is unknown. The
resume stops with
tool-reconciliation-required, writes nothing, and lists the receipts inunknownEffects. Check each effect in the system it touched, then resume withretryToolCallIdsfor the calls to run again and, since 2.3,reconciledToolResultsfor the calls that landed. Areplay: 'idempotent'tool runs again by itself, under the same[modelStep, toolCallId]identity. - After the completed receipt (the step checkpoint). A resume replays the recorded result; the tool does not run again. Since 2.3 this holds for a call that waited for an approval too: the recorded result answers it without its verdict.
- After a resume settled the calls (the next model call or a later save). Since 2.3 the settle's answers are saved before that model call, so the next resume goes on from them: nothing is settled, run or denied again.
import { createInMemoryAgentRunStore, resumeAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
const options = {
model: createMockModel({ responses: [{ text: 'Charged.' }] }),
prompt: 'Charge the order.',
tools: {
charge: {
description: 'Charge the order once.',
parameters: { type: 'object' as const, properties: {} },
execute: () => 'charged',
},
},
session: { store: createInMemoryAgentRunStore(), runId: 'order-7', scope: 'tenant-a' },
};
// The payment system keeps its own record of each charge.
declare const payments: { charged(orderId: string): Promise<boolean> };
// A run interrupted between the effect and its receipt (not shown) resumes here.
const resumed = await resumeAgent(options);
if (resumed.status === 'stopped' && resumed.reason === 'tool-reconciliation-required') {
// Decide per call, from the system the effect touched.
const calls = (resumed.unknownEffects ?? []).map((receipt) => receipt.toolCallId);
if (await payments.charged('order-7')) {
// It landed: record what the tool would have returned. Nothing runs again.
const reconciledToolResults = calls.map((toolCallId) => ({ toolCallId, output: 'charged' }));
await resumeAgent({ ...options, reconciledToolResults });
} else {
// It never arrived: run the call once more.
await resumeAgent({ ...options, retryToolCallIds: calls });
}
}A call that is listed in retryToolCallIds runs once more; a completed run never
runs it again.
A call listed in reconciledToolResults (2.3) never runs again. Its receipt takes
output as the call's raw result, marked reconciled: true, and the step goes on
from it exactly as from a result the tool recorded: through the tool's
outputSchema, validateResult and toModelOutput, to the model. With
isError: true the call is answered with output as its error (a string, or JSON
for any other value). Only the run's interrupted calls are accepted: the calls in
unknownEffects, and a call of a replay: 'idempotent' tool whose receipt was
written before 2.3. Such a receipt records no replay policy, so a report on the
stored run (inspectRun) lists it too; the call would run again by itself, and an
outcome you record answers it instead. Each call is accepted once and never also in
retryToolCallIds; anything else fails the resume with a TypeError before
anything is written. Settle every listed call in the same
resume: a call left unsettled stops the resume again, that stop records nothing,
and it lists every call the store still holds as unknown. A reconciled call that
waited for an approval needs no verdict: the outcome you record answers it.
A call in retryToolCallIds that waited for an approval runs again only with its
verdict: pass the verdict you stored with it, or the call is answered as denied.
When a model call fails
A run that fails stays failed: a resume returns its stored result without a
model call. Since 2.3 the failed envelope also records, additively, whether the
cause was transient (AgentRunEnvelope.retry, which a 2.2 reader ignores):
- a model call that failed with a retryable transport or provider error: network, transport timeout, 5xx, 429/529 after the call's own retries, an open breaker (the errors model fail-over acts on);
- a model call whose response stream broke off;
- a budget admission the persistent store refused with a serialization conflict (40001 or 40P01) after its own retries.
A run that was cancelled, lost its lease or ran out of time records none. Resume
a run with a transient cause with retryFailed: true and it goes on from its
last checkpoint, in the phase it failed in, after every check of a resume:
completed calls answer from their receipts and never run again, and an unknown
effect still stops it for reconciliation. The verdicts and client results that
the failed leg applied before the call are part of that checkpoint, so the retry
needs none of them again. The failed call stays counted, as a
model step (and, when it was the final answer, as one of the run's
maxOutputAttempts, as after a crash) and, while its outcome is unknown, in the
accounting at its estimate. The retried call is reserved on its own, so nothing
is counted twice. For any other failure a resume with retryFailed fails with a
TypeError and writes nothing; for a run that did not fail the option is
ignored.
import { createInMemoryAgentRunStore, resumeAgent, runAgent } from '@deuz-sdk/core/agent';
import { createMockModel } from '@deuz-sdk/core/testing';
const options = {
model: createMockModel({ responses: [{ text: 'Summary ready.' }] }),
prompt: 'Summarize ticket 9.',
session: { store: createInMemoryAgentRunStore(), runId: 'ticket-9', scope: 'tenant-a' },
};
const first = await runAgent(options);
if (first.status === 'failed') {
// Once the provider answers again. A failure that was not transient is
// refused, and the stored run stays as it was.
const retried = await resumeAgent({ ...options, retryFailed: true });
console.log(retried.status);
}One path for every tool call
Since 2.3 every tool call of a run takes one path, in this order (roadmap 23.2):
- Finish-reason gate. A refused or cut-off turn (
content_filter,length) dispatches none of its calls. - Policy. The execution policy decides first, for the whole batch: a call it
forbids fails the run with
ExecutionPolicyError(code: 'tool_not_allowed',toolCallIdnames the call) before any guardrail runs on the batch, any human is asked to approve it, or any sibling starts. Then theonToolCallguardrails block or rewrite. - Approval.
needsApproval,requireApproval, andapproveToolCallor a client-mode suspension. - Admission. The deadline and cancellation are checked again right before the effect. A model call reserves its budget here.
- Effect identity.
[modelStep, toolCallId], and the receiptexecuting. - Execution.
- Receipt
completed, andonToolResult. The receipt keeps the raw result and the tool's own projection.onToolResultchanges only the copy the model sees, and runs again on a replayed receipt. - Checkpoint.
The same path serves local tools, MCP tools (at connect, after a hot refresh, with
an outputSchema), handoff transfers, swarm blackboard
tools, the calls of spawned swarm tasks and of sub-agents, and, for the model calls
they make, evolve mutations and MCP sampling requests. A tool that prepareStep
puts on the wire, or that an MCP refresh brings in, is gated like any other. The
resume settle takes the same path as a live step: it re-runs the policy before it
executes an approved call. A call whose receipt already records its outcome skips
the approval step, because it ran; its receipt answers it (see
Strict checkpoints and approval resume).
A transfer is gated like any call. A guardrail, requireApproval, or a
needsApproval on the transfer tool can refuse it; the refused transfer is answered
with its denial, and the run stays with the current agent. A transfer waiting for a
verdict suspends the run like any gated call.
A child only narrows the run. A sub-agent (agentTool) runs under a child of the
run's execution context (allowlists, deadline, budget, cancellation) and inherits
its approver and its onToolCall / onToolResult guardrails. A handoff target's
model and tools are held to the run's allowlists and spend from its ledger. A
spawned swarm task runs under the swarm's policy and budget.
Outcomes
Each way a call can end is distinguishable, on results and on stream parts:
| Outcome | Where it shows |
|---|---|
'tool-error' | ToolResult.outcome, the tool-result part and the terminal tool-state part: the tool threw, timed out, got invalid input, has no executor, or does not exist |
'permission-denied' | the same places: an onToolCall guardrail or the handoff budget refused the call, or onToolResult withheld its output. A call the execution policy forbids fails the run; a tool-state part names it first |
'approval-denied' | the same places: the approver said no, or the resume had no valid verdict for the call |
'approval-pending' | the awaiting-approval tool-state part; the run suspends with pendingApprovals |
'unknown-effect' | a resume that stops with tool-reconciliation-required lists the receipts in unknownEffects and streams a tool-state part for each; retryToolCallIds or reconciledToolResults settles them |
| model error | the run fails (status: 'failed', the provider error's name and code); the stream ends with an error part. A transient one can be retried with retryFailed (When a model call fails) |
| refusal | the run stops with reason 'refusal'; stopDetails carries the provider's explanation |
A successful result carries no outcome, as before 2.3.
Tool context and supported options
toolsContext supplies data by tool name. A tool can declare contextSchema and
validateContext; its execute and approval callbacks receive the validated value
as ctx.context. These values are not serialized into model requests. Scoped
contexts expose the call ID, messages, signal, agent path and execution policy;
they omit ambient dependencies, session internals and other tools' context.
Scoped contexts on SDK delegation tools such as agentTool are rejected before
execution: stripping their session and approval internals would break child-run
guarantees. Keep these tools on their ordinary trusted context path.
A native tool's outputSchema validates its raw result; raw JSON Schema requires
validateResult. toModelOutput projects the validated result into model history,
while receipts and tool-output events retain the raw result. Application code
must decide which raw result fields belong in a user interface.
Native options support the canonical tool-loop controls, including preparation,
compaction, approval gates and guardrails. Loop hooks apply to the tool-loop phase.
Finalization is one more turn of the same conversation: it ends with a user turn,
offers the loop's tools with tool_choice: 'none', and validates its own output.
With output in the json strategy, the Anthropic, OpenAI Chat Completions and
OpenAI Responses wires (2.3) send those tools unchanged beside the output format
(output_config.format, response_format, text.format), so the request prefix
stays the loop's for the prompt cache and preserved thinking. Gemini's native wire
sends the JSON schema alone: @google/genai documents no request that combines
function declarations with a response schema, and with 'none' the model could not
call them anyway. The tool strategy sends only its own forced output tool
(json_output unless output.name is set). None of this is live-verified.
After a handoff, the target agent's model and tools drive
every later step, finalization and verifier re-drives included, and its tools pass
through the same receipts as the root agent's.
MCP servers in mcp cross the same receipt boundary as local tools; see
MCP in native runs.
The native engine rejects the legacy completion hooks verifyStep, doneWhen, and
falseFinishGuard; use verify instead. chat, memory and fallbackModels
work in native runs since 2.3, bound to the checkpoint as described below. Legacy
generateObject, streamChat, and createAgent behavior is unchanged.
Memory, chat and model fail-over (2.3)
The legacy loops treat these three as best-effort side effects of a call. A
native run is checkpointed, so each is bound to the run's record instead, and
each is part of the resume binding: a resume with another memory scope, another
chat or other fallbackModels fails with Native agent binding changed across resume. before anything is written. A run without a session has no record:
it still recalls once, extracts after completing and writes its chat projection,
but nothing it does can be resumed or reconciled.
examples/14-native-memory-chat
runs all three without a key: a native run with memory, a chat projection
and fallbackModels, then reconcileAgentRun repairing a chat write that
failed.
Memory
The run recalls once, before its first model call, with the last user
message as the query. The memories enter the run's history as one appended
role: 'system' message marked providerMetadata.deuz.memoryRecall, the shape
the legacy loops use for preserved thinking: every step and every resume sends
the same prefix, and a resume never recalls again. A history that already
carries a recall after its last user message is not recalled again.
AgentRunEnvelope.memory.recall records what happened; a failing store or
embedder is logged and the run goes on without memories, as in the legacy loops.
Extraction runs only for a run that completed. A stopped, failed, cancelled
or suspended run never turns its output into memory. The terminal commit
records the extraction as pending under an operation id, nothing is written
to memory before that commit, and the extraction's outcome (completed with its
mutation count, or failed) follows in a later save. The run's result does not
depend on it. Every record the extraction writes carries
metadata.deuz = { runId, operationId }. The extraction reads the input's last
user turn and the run's own turns; the recall message, the finalization prompt
and repair or verifier feedback are left out. extract: false, recall: false,
writePolicy: 'manual' | 'session-end' and sweep: 'on-extract' behave as in
the legacy loops. The run waits for the extraction before it resolves.
A resume of a run that already ended (a terminal resume) never extracts again
and never calls a model. When an interruption or a failed save leaves an
extraction pending (or failed), reconcileAgentRun settles it.
Since 2.4 the run saves how many store mutations its extraction planned
(AgentRunMemoryExtraction.planned) before it writes any memory, and writes
nothing when that save fails: the extraction is then failed, and a later
reconcileAgentRun runs it again. Deletions, which leave no record to find, are
applied first and counted in mutations. reconcileAgentRun records the
extraction completed without another memory LLM call only when every planned
mutation is confirmed in the memory store. When only some are, it records the
extraction failed with the confirmed count in mutations and an error with
code 'memory-extraction-partial', never runs that pass again, and reports a
memory-extraction-partial warning on AgentRunReconciliation.warnings (and to
deps.logger.warn). With nothing of the operation in the store, it runs the
extraction now. An extraction record a 2.3 process left without planned is
settled as in 2.3: any record of the operation counts as completed. Extraction
is therefore at least once and never silent; dedup by content hash keeps a
repeat from writing the same fact twice.
2.4 behaviour changes in this path: a record that an extraction updates now
carries that extraction's metadata.deuz ids instead of those of the operation
that wrote it before; a run saves its record once more per extraction (twice
more when it both deletes and writes records); and a completed reconciliation no
longer keeps the error of an earlier failed attempt. The run's memory also
reports its 2.4 warnings (embedding-mismatch from the recall,
unaccounted-usage for a memory LLM or embedder that reports no usage) as
warning parts on its stream, and charges the memory LLM and embedder calls to
the run's execution context unless you set seams.execution yourself (see
Memory).
Call reconcileAgentRun only when no executor drives the run. An extraction
stays pending from the terminal commit until its outcome is saved, also while
the executor that completed the run is still extracting, and a second pass
would extract again (dedup catches only a fact worded the same way). With a
LeaseProvider, pass lease ({ provider, owner?, ttlMs? }, as for
runAgent) with the provider
the executors use. The call claims agentRunLeaseKey(session) before it reads
the run and, while an executor holds it, rejects with an error named
AgentRunLeaseError, code 'lease_held'; an executor keeps its lease until its
extraction is saved. The call renews the lease while it settles the run, saves
nothing more to the run once it has lost it ('lease_lost'), and gives it back
at the end. A cancel queued for the run is handled as by a resume that does not
take the run up: a run that ended drops it, and a suspended run keeps it for its
next executor.
import { createInMemoryAgentRunStore, reconcileAgentRun, runAgent } from '@deuz-sdk/core/agent';
import { createInMemoryMemoryStore } from '@deuz-sdk/core/memory';
import { createMockModel } from '@deuz-sdk/core/testing';
let id = 0;
const memory = {
seams: {
store: createInMemoryMemoryStore(),
// A real application wraps a model call here.
llm: async ({ system }: { system: string; user: string }) =>
system.includes('extract')
? '{"facts":["prefers tea"]}'
: '{"memory":[{"event":"ADD","text":"prefers tea"}]}',
clock: {
now: () => Date.now(),
setTimeout: (fn: () => void, ms: number) => {
const timer = setTimeout(fn, ms);
return () => clearTimeout(timer);
},
},
generateId: () => `memory-${id++}`,
},
scope: { userId: 'ada' },
};
const session = { store: createInMemoryAgentRunStore(), runId: 'tea-1', scope: 'tenant-a' };
const result = await runAgent({
model: createMockModel({ responses: [{ text: 'Noted: tea.' }] }),
prompt: 'I prefer tea.',
memory,
session,
});
const envelope = await session.store.load('tea-1');
console.log(result.status, envelope?.memory?.extraction?.status); // completed completed
// For a run an interruption left pending; a settled run reports its state.
console.log(await reconcileAgentRun({ session, memory })); // { runId: 'tea-1', memory: 'completed' }A run under another native run's execution (a run a tool starts with
execution: ctx.execution.child(...)) may only narrow that run's memory
scope: it may pin more fields, never drop one or change a user, agent or chat.
A wider scope fails the child run before any call. A sub-agent from agentTool
gets no memory at all. Runs that share one execution context instead of each
taking a .child() (a tool that hands its own ctx.execution to a nested run,
or concurrent runs given the same context) count as nested too: each may only
narrow the scope of the latest run still running on that context. Give a run
with another user's or agent's memory its own child context; a child still
shares the parent's budget.
Chat projection
With chat, the run's checkpoint stays the canonical record and the chat store
receives a projection of it. The run keeps a transcript: the input, then every
turn the run appended, raw (before compaction or a prepareStep history, with
reasoning blocks byte for byte). The recall message and the finalization and
feedback turns are part of it, because the next run replays exactly the prefix
the model's thinking is bound to. The transcript is saved to the chat store
after the checkpoint at every suspension and at the end of the run, each
time as a whole record with ChatRecord.messageIds. An input message keeps the
id the stored chat gave it, and a turn of the run gets <runId>:<n>.
AgentRunEnvelope.chat records which assistant turn and which tool invocation
(by toolCallId and model step) is which message, and how far the chat store
has acknowledged the transcript (cursor).
The two stores are written one after the other, never as one transaction:
- A chat store that throws never fails the run. The run reports a
warningpart (setting: 'chat'), recordsstatus: 'failed'with the cursor left behind, and keeps its checkpoint intact. - The next projection save repairs it: the next suspension, the end of the run,
a terminal
resumeAgent, orreconcileAgentRun({ session, chat }). Every save replaces the whole record, so a repeat never appends a message twice. - A stored chat of another scope is never overwritten (
status: 'scope-mismatch'). A stored chat that already holds the projection and more, because a later run continued it, is left as it is. - A late repair never overwrites a chat that moved on without the run. A
resume of a run that ended or is still waiting, and
reconcileAgentRun, only finish the write the run's last attempt meant: over the record that failed write found (or where none is stored, if it found none), or over one the run's transcript continues (its input, or an earlier projection of it). Any other record was saved or deleted since, for example by another run that went on from what the chat store held, and stays as it is: the run recordsstatus: 'superseded', a resume reports awarningpart, andreconcileAgentRunreturnschat: 'superseded'. The run's turns stay in its checkpoint (AgentRunEnvelope.transcript), and no later repair tries again. The check reads the record right before the write, and a legacyChatStorehas no conditional write, so a save that lands in between is still overwritten. A leg that runs (a resume that continues the run) writes its whole projection, and the last writer wins, as in the legacy loops. - With a
ConditionalChatStore(2.4) the projection is written withwriteChatat the revision it read, instead ofloadChatfollowed bysaveChat. On a conflict the run reads the chat again and decides again, three attempts in all; after that its chat state isfailedwith aRevisionConflictError, and a later save orreconcileAgentRunrepairs it. A deleted chat (a tombstone) makes the projectionsuperseded, whether a late repair or a running leg meets it, with a chat warning. Runs with other chat stores behave as above. - A failed checkpoint publishes nothing to the chat.
import { createInMemoryAgentRunStore, runAgent } from '@deuz-sdk/core/agent';
import { createInMemoryChatStore } from '@deuz-sdk/core/chat';
import { createMockModel } from '@deuz-sdk/core/testing';
const chats = createInMemoryChatStore();
await runAgent({
model: createMockModel({ responses: [{ text: 'Hello, Ada.' }] }),
prompt: 'Hi!',
chat: { store: chats, chatId: 'chat-1', scope: { userId: 'ada' } },
session: { store: createInMemoryAgentRunStore(), runId: 'hello-1', scope: 'tenant-a' },
});
const record = await chats.loadChat('chat-1');
console.log(record?.messageIds); // ['hello-1:0', 'hello-1:1']Model fail-over
fallbackModels fails a single model call over, inside the same run. A call
that fails with a retryable error before its first content is repeated on
the next candidate of [that model, ...fallbackModels]: a network error, a
transport timeout, a 5xx, a 429 or 529 after the call's own retries, or an open
circuit breaker. A step of the tool loop runs again from the run's last
checkpoint, so completed tool effects never run again. A failed finalization
call does not use up a repair attempt. These never fail over:
- a refusal (the run stops with reason
'refusal'); - a policy, budget or validation rejection;
- an abort, a cancel, or an elapsed deadline;
- an error after the call produced content.
The failed model is skipped for the rest of the run, also after a resume.
Routing is by model, so a handoff target's model or a prepareStep model fails
over the same way, and after the root model failed over, the loop's own
compaction summaries go to its replacement. A compaction summary call does not
fail over by itself: when it fails before the step's own call, the step fails as
before. The stream carries a warning part (setting: 'fallbackModels') in place of the call's error. AgentRunEnvelope.models
records each fail-over: the attempt number, the failed and the next model with
their provider and protocol, the model step, the phase and the error. It also
records the model the run calls now (selected) and the version of these rules
(policy: 1).
Every attempt is admitted against the run's execution. A failed attempt's budget
hold stays counted when the provider may have billed it (a 5xx or 529 answer, a
network error, a transport timeout), so a fallback never exceeds the shared
budget; an answer the provider does not bill, such as a 429, releases the hold,
and an open breaker reserves nothing (see
Open holds). A child run's attempts count in
its parent's ledger under its parent's model policy. Each
candidate gets the call's capabilities override and is sent with its own request
profile and object strategy (a Claude 5.x fallback, for example, drops the
sampling parameters it rejects). Reasoning crosses models only as each wire
allows: Claude thinking blocks reach another Claude model unchanged, and another
provider never receives them.
import { runAgent } from '@deuz-sdk/core/agent';
import { createOpenAI } from '@deuz-sdk/core/openai';
import { createMockModel } from '@deuz-sdk/core/testing';
// A primary that is down: every request answers 503.
const primary = createOpenAI({
apiKey: 'demo',
fetch: async () => new Response('{"error":{"message":"unavailable"}}', { status: 503 }),
})('gpt-5.2');
const result = await runAgent({
model: primary,
fallbackModels: [createMockModel({ responses: [{ text: 'Answered by the fallback.' }] })],
maxRetries: 0,
prompt: 'Hello?',
});
console.log(result.status, result.text); // completed Answered by the fallback.Provider behavior in this section is derived from the providers' documented wire shapes and tested offline; none of it is live-verified.
V3 runs (checkpointVersion: 3)
2.4 adds a third native record version for runs that carry
compiled context, working memory
and versioned-memory writes. Choose it per
run with checkpointVersion: 3 and a session on an AgentRunStoreV3.
runAgent, streamAgent, resumeAgent, resumeStreamAgent and
reconcileAgentRun all take the V3 options, and the same executor runs both
paths. Without checkpointVersion: 3 nothing changes, and a run with none of
the 2.4 options sends provider requests byte-identical to 2.3's.
import { createInMemoryAgentRunStoreV3, streamAgent } from '@deuz-sdk/core/agent';
import { createRagContextSource } from '@deuz-sdk/core/context';
import { createMockModel } from '@deuz-sdk/core/testing';
const runs = createInMemoryAgentRunStoreV3();
const stream = streamAgent({
checkpointVersion: 3,
session: { store: runs, runId: 'deploy-1', scope: 'acme' },
model: createMockModel({ responses: [{ text: 'Run CI, then deploy to eu-west.' }] }),
prompt: 'How do I deploy?',
context: {
sources: [
createRagContextSource({
retrieve: async () => [{ text: 'Deploys need a green CI run.', index: 0, score: 0.9 }],
}),
],
},
workingMemory: { create: { scope: 'acme', goal: 'Deploy the billing service.', now: Date.now() } },
});
for await (const event of stream.eventsV3) {
if (event.type === 'context') console.log('context', event.step, event.manifest.refresh); // context 0 initial
}
const result = await stream.result;
const record = await runs.load('deploy-1');
console.log(result.status, record?.version, record?.context?.steps.length); // completed 3 1
console.log((await runs.getBlob(record!.context!.steps[0]!.blob.digest)).status); // foundStores
| Store | Import | Notes |
|---|---|---|
createInMemoryAgentRunStoreV3({ deps? }) | @deuz-sdk/core/agent | Tests and one process; times from deps.clock |
createSqliteAgentRunStoreV3({ path, wal?, clock? } | { database, clock? }) | @deuz-sdk/core/ops/sqlite | A path is opened and closed by the store; a database handle is borrowed and left open |
createPostgresAgentRunStoreV3({ pool | connectionString, schema? }) | @deuz-sdk/core/ops/postgres | Needs a pool with connect(); schema defaults to 'public'; migrate() creates the tables; close() ends only a pool it opened; every write is one SERIALIZABLE transaction |
Times come from the store's clock: the injected clock for the in-memory and
SQLite stores, the database clock for Postgres. V3 runs live in their own
tables (deuz_runs_v3, _blobs, _blob_records, _blob_refs, _forgotten)
with their own version table, deuz_runs_v3_schema. The 2.3 run tables and
deuz_ops_schema are never touched, so a 2.3 process still opens the same
database, its run listing and pruning never see a V3 run, and its
resumeAgent and reconcileAgentRun refuse a V3 record (Native checkpoint version, identity, or scope mismatch.) before calling a model or a tool.
An AgentRunStoreV3 accepts only version 3 envelopes: a V3 run is never written
back as version 1 or 2, terminal and bookkeeping saves included. A save must
carry the stored revision plus one, otherwise it throws RevisionConflictError
(code: 'revision_conflict', subsystem: 'run', with the expected and the
stored revision). list(query) pages like AgentRunStore.list. Each compiled
context package is stored once as a content-addressed blob (putBlob,
getBlob), keyed by the sha256: of its blocks' canonical JSON; content that
does not match its digest is rejected. getBlob(digest) answers
{ status: 'found', content }, { status: 'redacted' } or
{ status: 'missing' }. inspectRun from
@deuz-sdk/core/headless reads a V3 store too.
Redis has no V3 store in 2.4. A Redis AgentRunStore with
checkpointVersion: 3 is refused at configuration time with
UnsupportedStoreError (code: 'unsupported_store', backend: 'redis',
feature: 'agent-run-v3'). Version 1 and 2 records
are never converted: existing runs finish on the 2.3 path.
What a V3 run refuses
These configurations fail before anything is read or written. runAgent and
streamAgent return a failed result; reconcileAgentRun rejects.
| Configuration | Error |
|---|---|
checkpointVersion: 3 without a session, or with a 2.3 AgentRunStore | TypeError |
A Redis run store with checkpointVersion: 3 | UnsupportedStoreError |
A checkpointVersion other than 3 | TypeError |
context, workingMemory, versionedMemory or a V3 store without checkpointVersion: 3 | TypeError (2.3 ignored such options; 2.4 refuses them) |
A compiler with a memory source plus memory with recall on | TypeError ("Memory would be injected twice"); set memory.recall: false |
A working memory snapshot or create for another scope than the session's, or a host tool named like a working memory tool | TypeError |
The record and the stream
AgentRunEnvelopeV3 is the 2.3 envelope with version: 3 and four optional
fields:
| Field | What it records |
|---|---|
context | The compiler's state and, per model step, the manifest, the package's blob reference and a digest of the messages it was compiled for |
workingMemory | The WorkingMemorySnapshot, with the task list itself and the active task, so the active task survives a restart |
memoryIntents | One entry per versioned-memory write: its operationId, status (intended, planned, committed, acknowledged) and, for a versioned extraction, the cited message ids, the request digest, the applied mutation count, the receipt and error when the run's own extraction pass failed (a tool write records its model step and tool call id) |
memory | The 2.3 memory state; its extraction status adds 'partial' |
streamAgent and resumeStreamAgent return an AgentStreamV3. It is still an
AgentStream, so toDeuzAgentStreamResponse serializes it unchanged and none
of the compiled context reaches the wire. Its eventsV3 carries every 2.3 event
plus three more: { type: 'context', step, manifest }, { type: 'working-memory', revision } and { type: 'memory-intent', operationId, status }.
Context and working memory
context takes a ContextCompiler or the options of createContextCompiler.
The loop calls it after compaction and your prepareStep, with the step's real
model and tools, and saves the package blob and manifest before the model call;
retrieval enters the history after the call succeeded, and the working memory is
the request's ephemeral tail, except on a model that binds preserved thinking to
the transcript prefix (Claude 5.x): there each step's copy joins the history after
the call succeeds and the next request ends with the current copy (the manifest
says placement.tailKept). A final answer call (a structured-output run
without tools included) is compiled the same way. A resumed run restores the
compiler's state from the record; a compiler you built yourself resumes only if
its state() equals the stored state. A host estimator given as
context.estimator is calibrated with each call's input, cache-read and
cache-write tokens. The full step order, overflow handling and placement rules
are on the context compiler page.
A step reopened after a restart with the same messages (its answer was never
saved) reuses its stored package without asking the sources. A package a
forget redacted is compiled again with refresh 'redacted' and
redacted: true, and on resume the stand-ins and any retrieval block that
held a forgotten record are removed from the run's history and checkpoints
(an overflow retry's block under the same marker, whose package is still
found, stays), which the manifest records as cachePrefixChanged: true.
workingMemory: { snapshot?, create?, tools? } keeps the snapshot in the
record; a resume keeps the stored one. Working memory without a compiler still
gets its tail. With tools: true the model gets working_memory_read,
working_memory_add and working_memory_update, and a model patch becomes
durable in the same save as its tool receipt, so a call replayed after a crash
is applied once.
Versioned memory: intent, commit, acknowledgment
versionedMemory: { session, tools?, extract? } connects a
versioned memory session. Every write follows
one protocol: a durable intent in the run record, the prepared plan, the atomic
memory commit with its receipt, then the run's acknowledgment. The run store and
the memory store are separate, so there is no transaction across them: when the
acknowledgment is lost after the commit, the receipt is read back by operation
id, and a memory change is never applied twice, even when a model call repeats
after a crash.
tools: trueoffers the session'smemory_remember,memory_recall,memory_reviseandmemory_retract. Each write's intent moves with its tool receipt (intendedwith the executing receipt,committedwith the executed one,acknowledgedwith the completed one) under the session's operation id.extract(off by default;trueor the extraction options withoutmessages) extracts the completed run's turns into the session. The intent is saved in the terminal commit. The run then saves its claim (planned, with the cited message ids and no plan), and the session prepares and commits the plan with its receipt in one call. The acknowledgment copiesrequestDigest(the request's digest) andplanned(the mutation count) from the receipt, withreceipt: { scopeRevision, committedAt }. The extraction reads the input's last user turn and the run's own turns, under the session's extraction policy. Whensession.extractthrows after the run's outcome, the run saves why on the intent (error:name,messageandcodewhen there is one) and keeps its status, soreconcileAgentRuncan still settle it; the acknowledgment clearserror.
import {
createInMemoryAgentRunStoreV3,
reconcileAgentRun,
runAgent,
} from '@deuz-sdk/core/agent';
import {
createInMemoryVersionedMemoryStore,
createMemorySession,
} from '@deuz-sdk/core/memory/versioned';
import { createMockModel } from '@deuz-sdk/core/testing';
const runs = createInMemoryAgentRunStoreV3();
const memory = createMemorySession({
store: createInMemoryVersionedMemoryStore(),
scope: { userId: 'ada' },
// Stands in for the memory LLM: one fact citing the user's message.
llm: async () => '{"facts":[{"text":"Ada prefers tea.","sources":["s0"]}]}',
// A forget redacts the run's stored context packages.
onForget: async (event) => {
await runs.redactForgotten(event);
},
});
const session = { store: runs, runId: 'tea-1', scope: 'acme' };
const result = await runAgent({
checkpointVersion: 3,
session,
model: createMockModel({ responses: [{ text: 'Noted: tea.' }] }),
prompt: 'I prefer tea.',
versionedMemory: { session: memory, extract: true },
});
const record = await runs.load('tea-1');
console.log(result.status, record?.memoryIntents?.map((i) => i.status)); // completed [ 'acknowledged' ]
const [hit] = await memory.recall({ text: 'tea' });
console.log(hit?.record.origin, hit?.record.sources[0]?.messageId); // inferred tea-1:0
// For a run whose acknowledgment was lost; a settled run reports its state.
console.log(await reconcileAgentRun({ checkpointVersion: 3, session, versionedMemory: { session: memory } }));
// { runId: 'tea-1' }reconcileAgentRun({ checkpointVersion: 3, session, versionedMemory: { session } })
settles intents whose acknowledgment was lost from the session's receipts (an
operation that committed is replayed, never applied again) and returns the
settled operation ids in memoryIntents. A resume replays a lost tool write the
same way.
The source references of an extraction cite each message by the id the chat
projection gives it (<runId>:<index>, or a stored chat's own id for an input
message the run continues), so
resolveSource
resolves them to the chat messages. The extraction's claim (planned, which
holds the cited messageIds and no plan) is saved before the session prepares
and commits the plan in one call, so a replay cites the same ids.
The legacy memory option works in a V3 run too. A legacy extraction that
wrote only part of its plan is stored as 'partial' with a
memory-extraction-partial warning, and reconcileAgentRun reports it as
memory: 'failed' with the warning, in the 2.3 answer shape.
Retention and forget
prune({ terminalBefore }) deletes terminal runs last saved before that time and
the blobs no remaining run points to, and returns the count. redactForgotten(event)
is the versioned-memory onForget listener: it redacts every stored package
that holds one of the forgotten revisions, also one written after the forget,
and returns how many it redacted. getBlob then answers { status: 'redacted' }
and the manifests that point to it load with redacted: true. The forget records
the store keeps hold record ids and revisions, never text.
The same call rewrites, in place, every stored copy of a redacted package's
retrieval block that holds a forgotten record, in the runs that point to it:
history, checkpoints and the stored result's messages, a terminal run's
result.messages included. Each copy becomes a stand-in that keeps the
<deuz_context pass="P" step="S"> marker and holds no record; the run keeps
its revision and last-save time. Only the exact blocks the compiler appended
are replaced: a user's message that only opens with such a marker stays, and
so does an overflow retry's block, which shares its first attempt's marker but
dropped the forgotten record. A later save of such a run is stored the same
way, and a resume still drops the stand-ins and any block that held a
forgotten record and compiles the step again. A forget does not reach a
retrieval block a run received in its input messages (a previous run's
result.messages passed to runAgent({ messages })): continue from the chat
projection or the transcript, which carry no retrieval blocks. Nor does it
reach text the model wrote from a block (assistant turns, result.text, a
compaction summary), memory tool results (memory_recall output in tool
messages, the run transcript, a chat store), the in-memory history of a leg
running at forget time until it ends or is resumed, or run stores not
subscribed through onForget. See Retention and forget.
Mandatory policy and shared budgets
createExecutionContext binds model/tool allowlists, approval requirements,
maximum depth, deadlines and shared token/USD accounting. Pass it through
execution; descendants receive narrowed child contexts. Tool context validation
and output validation are independent of policy. toModelOutput can project a
validated tool result before it enters model history.
Budget admission reserves an estimate before a potentially billable attempt. Known usage settles the reservation; uncertain outcomes and missing pricing keep amounts held as unknown. Accounting exposes spent, reserved, committed and unknown totals. A configured ceiling is enforced against the admitted estimates; it cannot guarantee a provider's eventual invoice stays below that ceiling when actual usage exceeds an estimate. Direct network calls made by application code also require application accounting.
Native sessions persist reservations before dispatch, including when an existing
execution context is supplied. The supplied context keeps the same shared ledger;
native persistence is attached for the duration of the run. Verifier callbacks
receive execution and signal and must pass them into any SDK model calls they make.
When the supplied context is a child of a shared ledger, the run checkpoints only
its own slice of that ledger (2.2): the reservations charged to its scope, marked
with ledger.subtree. The parent owns the shared ledger and persists it; a sibling's
reservation no longer rewrites this run's envelope. Resume such a run with the same
execution context. A slice can prove its own continuity but cannot seed a new
ledger, so resuming it without execution fails. A root context, or a run without
a supplied context, still checkpoints the complete ledger.
Persist the execution ledger with the owning run. Restore its snapshot instead of resetting the budget after a restart. Child policies and budgets can tighten inherited constraints; they cannot loosen them. See Swarm for a scheduler that persists the shared context for the entire DAG.
Evals
Versioned eval tasks run by runEvalSuite from @deuz-sdk/core/testing, with per-attempt metrics for success, false completion, cost, duration, human rescues, repeated effects and recovery, plus the offline tasks and fault-injection suite that make up the 2.3 baseline.
MCP in native runs
How runAgent and streamAgent put MCP tools behind effect receipts, pin the servers' tool catalog in the resume binding, and replay idempotent MCP calls (2.3).