You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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 runId — GenerateTextResult.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.