Skip to content

Building adapters

An adapter teaches tally to move portable context in and out of a harness’s native session store. There are two levels; pick the smallest one that fits. The contract both levels implement is the PAC specification.

Level 1: a declarative manifest (most harnesses)

Section titled “Level 1: a declarative manifest (most harnesses)”

If your harness stores sessions as JSONL files (one JSON object per line, a header line carrying the working directory), you do not write any code. You write an adapter.toml and the generic engine does the rest. The full manifest schema lives in the adapter.toml reference.

tally adapter create myharness # scaffolds ./tally-adapter-myharness/adapter.toml
$EDITOR tally-adapter-myharness/adapter.toml
tally adapter validate ./tally-adapter-myharness # schema + round-trip conformance
tally adapter install ./tally-adapter-myharness # or install from a git URL
tally session export --harness myharness # it now works
tally session materialize --into myharness

Share it by publishing the directory as a git repo; anyone installs it with tally adapter install <git-url>. A community registry, searchable via tally adapter search, launches with the beta; the format is described in The adapter registry.

tally adapter validate proves the manifest with the PAC conformance smoke: it materializes a marker context into a temp directory, extracts it back, and asserts the task is recovered verbatim, plus per-line JSON validity and the find path. Fix the reported knobs until every check reads PASS.

Security posture and what community adapters can do

Section titled “Security posture and what community adapters can do”

The manifest is pure data and is never executed, but install refuses manifests containing shell-exec patterns unless you pass --allow-shell after reviewing it (the pattern list is in the adapter.toml reference).

Level 2: a native Rust adapter (exotic harnesses)

Section titled “Level 2: a native Rust adapter (exotic harnesses)”

If the harness’s store is not flat JSONL (SQLite, tree-linked events, date-partitioned rollouts, custom text), write a Rust module implementing the same three operations and wire it in as a built-in. The three in-tree adapters are the reference implementations, in increasing order of format complexity:

  • src/portable/pi.rs: parent-linked JSONL events, header + typed messages.
  • src/portable/claude_code.rs: unversioned JSONL with many event types, a tolerant-by-construction parser, plan extraction from tool calls.
  • src/portable/codex.rs: date-partitioned rollout files, payload-wrapped messages.

The contract each implements:

pub const HARNESS: &str = "..."; // provenance id
pub fn find_latest_session_file(root: &Path, cwd: &str) -> Result<PathBuf, CliError>;
pub fn extract_from_file(path: &Path) -> Result<Extracted, CliError>;
pub fn materialize(root: &Path, cwd: &str, ctx: &PortableCtx) -> Result<(String, PathBuf), CliError>;

House rules for a Level-2 adapter, learned from the first three:

  • Build from direct inspection of the public on-disk format, never from a third-party converter.
  • Parse tolerantly: skip unknown event types and unparseable lines; session formats grow new event types without notice.
  • Extraction is deterministic, no model in the loop, and honest: fields the format does not carry stay empty rather than being invented.
  • Prove acceptance with a live spike: forge a minimal session and confirm the real harness resumes it before shipping the adapter.
  • Test with the same round-trip property the Level-1 conformance smoke checks: materialize then extract must recover the task.

Open a PR adding the module under src/portable/ plus its wiring; the spec for the layer is the PAC specification.