Skip to content

Trace and Capture¤

Tracing connects the runtime to the harness compiler. The runtime can execute without tracing; adding a trace sink makes it emit canonical events.

Source map:

Area Implementation
event model jdsl/trace/events.py
sinks jdsl/trace/sink.py
JSONL storage jdsl/trace/jsonl.py
episode replay jdsl/trace/replay.py
harness store jdsl_harness/store.py
ingest server jdsl_harness/server.py

Event Envelope¤

Every observation becomes a TraceEvent:

{
  "schema_version": "jdsl.trace.v1",
  "event_id": "...",
  "capture_id": "cap_retail",
  "episode_id": "ep_1",
  "sequence": 12,
  "timestamp": "2026-09-01T10:00:00.000Z",
  "source": {"host": "jdsl", "adapter": "runtime", "model": null},
  "actor": "model",
  "kind": "tool.call.started",
  "parent_event_id": null,
  "payload": {
    "tool": {"host_name": "lookup", "logical_id": null},
    "arguments": {"email": "ada@example.com"}
  },
  "prev_event_hash": "sha256:...",
  "event_hash": "sha256:..."
}

The payload is deliberately flexible; the envelope is stable.

Hash Chain¤

Sinks assign sequence numbers and call TraceEvent.chain(prev_hash). The event hash covers the event body plus the previous event hash, so the JSONL spool is tamper-evident in order.

TraceEvent itself does not know where it will be stored. Sinks are responsible for stamping sequence numbers and sealing the chain. This keeps adapters simple: they construct events, and the store decides the final event order.

verify_chain in jdsl/trace/jsonl.py can later check that every self-hash and previous hash still matches.

Runtime Events¤

With trace_sink set, the interpreter emits:

Event Produced by
episode.started / episode.finished Root.run
node.enter / node.exit Node.tick
tool.call.started Action and React
tool.call.completed / tool.call.failed Action and React
model.requested / model.responded Predict and React
blackboard.write Blackboard.set via RunContext._install_trace
toolset.exposed React and MCP proxy

Those events are enough for the compiler to recover sequencing, arguments, results, dataflow, and residual model decisions.

Store Layout¤

HarnessStore writes:

harness.db
captures/<capture_id>.jsonl
blobs/sha256/<digest>

SQLite is an index. The JSONL event stream and blob store are the durable behavior evidence.

The store deliberately uses boring storage:

  • JSONL is append-only evidence
  • SQLite is a rebuildable metadata index
  • blobs are content-addressed files

That matters for compilation because raw evidence should not be rewritten when the compiler learns a better interpretation.

Ingest Server¤

IngestServer exposes loopback HTTP endpoints:

Endpoint Input
POST /ingest canonical TraceEvent dict
POST /hook/claude?cap=... Claude Code hook payload
POST /hook/gemini?cap=... Gemini CLI hook payload
POST /hook/opencode?cap=... jdsl OpenCode envelope
GET /captures capture list
GET /capture/<id>/summary capture summary

Bad hook payloads return { "ok": false } with HTTP 200. Capture should fail open so observation does not break the host agent loop.

The server also separates two jobs:

Plane Code Purpose
data plane IngestServer HTTP endpoints Fast local event ingestion from hooks/proxies.
control plane CaptureCoordinator, CLI, optional MCP server Start/finish captures, inspect lineage, compile.

That split is why plugin hooks can be small and failure-tolerant.

Capture Fidelity¤

The coordinator infers fidelity from event kinds:

  • F0: no useful behavior events
  • F1: messages
  • F2: tool calls
  • F3: state snapshots or deltas
  • F4: state plus decision context

This matters because the compiler can only trust what the trace actually saw.