Skip to content

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
· 68 commits to main since this release
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.