Deuz SDK 2.4 est sorti — mémoire versionnée et compilateur de contexte, superviseur d’objectifs et planificateur durable, exécuteur Docker local et flux de chat durables. Nouveautés de la 2.4
Deuz SDK
Modules

Observability

Local-first observation events for every run — model calls, retries, tools, approvals, checkpoints, compaction and sub-agents. No hosted service, no OTel dependency, privacy-first by default.

Le contenu des pages de documentation est en anglais. La navigation, la recherche et l’interface suivent la langue choisie.

Every Deuz run can emit a versioned event protocol (ObserveEvent, schemaVersion: 1) describing its full lifecycle: run start/end, model calls with TTFT and retries, agent steps, per-tool timings, approval decisions, durable checkpoints, compaction passes, sub-agent trees and cost. Everything stays in your process — no API key, no account, no data leaves your machine.

Quick start

Inject an observer through the Dependencies seam:

import { generateText } from '@deuz-sdk/core';
import { createMemoryObserver, summarizeRun } from '@deuz-sdk/core/observe';

const observer = createMemoryObserver();

await generateText({
  model,
  messages: [{ role: 'user', content: 'Prepare the release notes.' }],
  tools,
  maxSteps: 5,
  deps: { observer },
});

const summary = summarizeRun(observer.latestRun() ?? []);
// { status: 'completed', stepCount, modelCallCount, toolCallCount,
//   retryCount, approvalCount, checkpointCount, usage, costUsd?, errors }

A client-level observer (createClient({ deps: { observer } })) flows into every call; a per-call deps.observer overrides it.

JSONL persistence (Node)

import { createJsonlObserver, readJsonlEvents } from '@deuz-sdk/core/observe/node';

const observer = createJsonlObserver({ file: '.deuz/runs.jsonl' });
await generateText({ model, messages, tools, deps: { observer } });
await observer.close(); // flushes the internal queue

const events = await readJsonlEvents('.deuz/runs.jsonl');

One event per line, every line valid JSON, Uint8Array payloads round-trip via the durable $deuzBytes codec. emit() is synchronous — writes flow through a bounded internal queue (maxQueueSize, default 10 000; overflow drops and counts in droppedCount), so a slow or failing disk can never affect the run. You own the file's security — treat it like a log that may contain whatever you opted into capturing.

Built-in observers

FactoryWhat it does
createMemoryObserver({ maxEvents?, maxBytes?, overflow? })Capped in-memory ring buffer with events() / eventsForRun(runId) / latestRun()
createCallbackObserver(fn, options?)Wraps a plain callback; a throwing callback is swallowed
composeObservers(...observers)Fan-out with per-sink capture projection; one throwing child never blocks its siblings
filterObserver(observer, predicate)Forward only matching events
createJsonlObserver({ file, … })Node-only JSONL file sink (@deuz-sdk/core/observe/node)

createMemoryObserver accepts a total byte budget alongside the event cap (1.6.1): maxBytes measures each stored event's approximate JSON size and evicts by the shared overflow strategy ('drop-oldest' default / 'drop-newest'), counting evictions in droppedCount. maxEvents and maxBytes compose — whichever budget is exceeded first wins.

Privacy defaults and capture

By default events carry only counts, ids, names, durations and small enums — never prompts, tool inputs/outputs, reasoning or error message text. Raw content capture is opt-in per field:

const observer = createMemoryObserver({
  observation: {
    capture: { outputText: true, toolInputs: true }, // everything defaults to false
    sampleRate: 0.25,     // deterministic per runId — a run is all-in or all-out
    metadata: { app: 'deuz-console' }, // flat primitives, merged into every event
  },
});

Captured payloads always pass the observation redaction profile (API keys, Bearer/JWT/PEM patterns, password/cookie/token-style keys → [REDACTED]) plus structural limits (string/array/depth caps with [Truncated] markers). Since 1.6.1 the built-in profile also runs after your optional custom redact hook — a buggy or malicious redactor cannot reintroduce a secret, and truncation can never split a secret into a decodable prefix. This is regression-tested: planted secrets in prompts, tool outputs, headers and factory keys never reach an event or a JSONL line.

Composite observers see only what they opted into (1.6.1)

composeObservers still merges options for event production (capture = OR, sampleRate = max, limits = min), but each child now receives a projection of every event: captured* fields its own capture options don't allow are stripped, error.message is dropped unless it set capture.errorMessages, and a child redact hook only ever runs on that child's view. A composed sink with no options behaves exactly like a standalone observer with defaults — it gets no captured content.

const observer = composeObservers(
  createMemoryObserver({ observation: { capture: { messages: true } } }), // sees prompts
  remoteSink, // no capture options — receives counts/ids/durations only
);

Draining async events: observation.settled

cost.calculated can resolve asynchronously (an async priceProvider). Results expose an optional observation.settled promise — await it before closing a file-backed observer so late events aren't dropped:

const res = await generateText({ model, messages, deps: { observer } });
await res.observation?.settled; // all pending observe enrichments flushed
await observer.close();

The field exists only when a run was actually observed; nothing about the run itself ever waits on it (G2 holds).

Guarantees

  • Observers can never break a run. A throwing, slow or closed observer is isolated; usage/finishReason promises and stream parts are untouched.
  • Fast path. With no observer (and no tracer) the hot path pays one boolean branch: no event objects, no extra generateId() draws — scripted-id test fixtures stay stable.
  • streamChat stays lazy (G2). run.started fires when the pump actually starts, never at call time.
  • One terminal event per execution leg (run.completed / run.suspended / run.aborted / run.failed); an async cost.calculated may follow it.
  • Durable correlation. A durable run keeps one runId across suspend/resume legs; each leg gets a fresh executionId, and checkpoint.loaded precedes run.started { resumed: true }.
  • Sub-agents share the parent's runId and are identified by agentPath; their usage is folded into the parent exactly once.

2.4 spans

The 2.4 subsystems report through the same seam, with no new dependency. Each reported operation is an operation.started / operation.completed (or operation.failed) pair whose operation is the span name and whose subsystem is context, memory, goal, schedule, process or stream. Attributes ride on the events' attributes field, split between the two events (what is known at the start, then what the work found); createOtelObserver turns each pair into one OTel span of that name with all of them. A moment, such as a cancel request or a fencing refusal, is a zero-length span.

import { createGoal } from '@deuz-sdk/core/goal';
import { createDurableScheduler, createInMemoryGoalRuntimeStore } from '@deuz-sdk/core/goal/runtime';
import { createMemoryObserver } from '@deuz-sdk/core/observe';

const observer = createMemoryObserver();
const store = createInMemoryGoalRuntimeStore();
await store.save(createGoal({ goalId: 'digest', scope: 'acme', objective: 'Send the digest.' }, { now: 0 }));
const scheduler = createDurableScheduler({
  store,
  deps: { clock: { now: () => Date.parse('2026-10-15T08:00:00Z'), setTimeout: () => () => {} }, observer },
});
await scheduler.put({ scheduleId: 'daily', scope: 'acme', goalId: 'digest', spec: { cron: '0 9 * * *' } });
await scheduler.tick({ now: Date.parse('2026-10-15T09:00:05Z') });

for (const event of observer.events())
  if (event.type === 'operation.completed') console.log(event.subsystem, event.operation, event.attributes);
// schedule deuz.schedule.delivery { 'deuz.schedule.id': 'daily', 'deuz.schedule.occurrence_id': 'daily@1792054800000', ... }
SpanAttributes
deuz.context.compiledeuz.context.selected, .excluded, .tokens, .estimator, .refresh_reason, .fallback, .cache_prefix_changed, .manifest_digest, and per source deuz.context.source.<id>.selected, .excluded, .tokens
deuz.memory.opdeuz.memory.operation (remember, extract, revise, retract, forget, reembed, import; prune-history and prune-operations from the SQL stores), .record_count, .conflict_retries, .embedding_generation
deuz.goal.claim, .dispatch, .retry, .cancel_requested, .cancel_applied, .drain, .generationdeuz.goal.id, .revision, .spec_generation, .run_id, .attempt
deuz.schedule.deliverydeuz.schedule.id, .occurrence_id, .disposition, .catch_up, .reason (skipped only), deuz.goal.id
deuz.process.start, .poll, .cancel, .exit, .reconciledeuz.process.operation_id, .state, .replayed, .exit_code, .truncated, .matched, .orphans, .lost, .removed, .reconcile_result
deuz.stream.batch, deuz.stream.fenceddeuz.stream.batch_frames, .batch_bytes, .commit_ms; .fencing_token on a fenced producer

Attributes carry only counts, ids, digests, token values and small enums: never memory text, transcripts, tool output or payloads. In a native run, the deuz.context.compile span (the overflow recompile included) is a child of the step span and shares the run's runId and executionId. Chat writes and transcript searches report no span, and a skill proposal's model call appears as an ordinary generate-text run.

Each module page lists its own spans: context, versioned memory, goal runtime, Docker runner and durable streams.

Tracer bridge

An injected Dependencies.tracer now receives the full documented span hierarchy, driven by the same events:

invoke (one per run)
└── step (one per model round-trip; deuz.step.index, gen_ai.request.model)
    └── execute_tool (one per tool call; gen_ai.tool.name, deuz.tool.is_error)

Span names and attribute keys are unchanged from 1.5 (gen_ai.* semconv + deuz.*); the behavioral difference is that agentic loops now produce one invoke with children instead of N flat invokes. A user abort still ends the span without an exception. An OTel exporter plugs into this seam.

Dashboards built against the 1.5 flat topology can opt back into it with deps: { tracerMode: 'legacy' } (1.6.1): every model call becomes its own parent-less invoke span with deuz.step.count: 1, no step/execute_tool children, retries landing on that call's span. The default remains 'hierarchical'.

See the full event catalog in Observation events.

Sur cette page