@patimweb/pi-tape
Record pi agent sessions, play them back deterministically, and splice them into composable recipes
Package details
Install @patimweb/pi-tape from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@patimweb/pi-tape- Package
@patimweb/pi-tape- Version
1.6.0- Published
- Oct 12, 2026
- Downloads
- 1,467/mo · 1,406/wk
- Author
- patimwep
- License
- Apache-2.0
- Types
- extension
- Size
- 2.2 MB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./dist/extensions/index.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-tape

A debugger and experiment bench for pi agent runs. Record a run, play it back deterministically, fork it at any tool result, and compare, step by step, what a new model — or a change to what the agent is told — would do instead.
An agent run is not reproducible. There is no stack trace, no breakpoint, and a failure often happens exactly once. pi-tape records what the model and the tools actually answered. A run can then be looked at again, changed in one place and compared — without calling a provider for the parts that did not change, and without running the original side effects a second time.
- Replay a run, for free. Recorded answers are served instead of calling a provider, and recorded tool results instead of running the tools. Nothing leaves the machine; nothing is deployed, written or deleted a second time.
- Ask "what if the tool had said something else?" Replace one tool result and let the model continue live from there. Everything before the change comes from the recording, so only the new branch costs tokens. That turns a guess about a cause into a test of it.
- Compare a change step by step. A new model, or an edit to
AGENTS.md, a skill or a prompt: replay a recorded run and ask the model at every step what it would do now. pi-tape reports which decisions changed — strict about actions, lenient about wording. With a real model the comparison is a tool for a person to read, not yet an automatic gate: a model varies on its own, and that noise was measured to be as large as the change. - Hand a run to someone else. A tape is one self-contained file; credentials are redacted before it is written.
- Recipes (experimental) — procedures distilled from several runs of the same task. See Recipes for what they do and what they have, and have not, been shown to do.
pi-tape works on any pi session, after the fact: a recording is derived from pi's own session file, so every session you have already run can be recorded.
Contents
- Install
- A two-minute example
- Usage — record · inspect · replay · fork · test a change · recipes
- What has been shown
- Where this sits
- How it works
- Reference —
/tapecommand · agent tools · CLI - Limits
- Development · Releasing · Licence and citation
Install
Requires Node 22.18.0+ (the first 22.x that runs TypeScript without a flag); Node 24 and 26 are tested as well.
pi install npm:@patimweb/pi-tape
That installs the pi extension (the /tape command and its tools) and the pi-tape
command line. To try it from a checkout without installing:
git clone https://github.com/Smotherer007/pi-tape.git && cd pi-tape
npm install && npm test
pi -e ./extensions/index.ts
A two-minute example
An agent was asked which port a service listens on. It read notes.md, answered
3000 — and the config says 8080. Was the stale note the cause, or would it have
been wrong anyway?
/tape record wrong
/tape fork wrong.tape --model deepseek/deepseek-flash --run --at 1 --result The service listens on port 8080.
# → 8080
/tape record corrected
--at 1 is the first tool call of the recording (the read of notes.md); --result
takes the rest of the line, or @file for something longer. --run sends the
recording's own prompt, so nothing has to be retyped.
The first request came from the recording. The read of notes.md returned the
corrected text instead of the file. Only the answer after it was asked of the model —
one request. The note was the cause.
pi-tape diff wrong.tape corrected.tape
# right was forked from left, so the divergence below is the experiment, not a regression
# fork point read turn 1
# replaced "The service listens on port 8080."
# live 1 request(s) asked of a provider
Usage
Record
From inside pi, the current session:
/tape record [name] [--redact] [--status success|failed|unknown]
From the command line, any session pi has stored:
pi-tape sessions # newest first
pi-tape record # newest session -> ./<id>.tape
pi-tape record 01a10cda --name "auth refactor" --status success
pi-tape record --redact --out shareable.tape # credentials replaced before writing
A recording states how the run ended. Without --status it is inferred, and silence is
never success: a run with no verification and no error is unknown.
Inspect and compare
pi-tape inspect run.tape --timeline # stats, context cost per tool, intervention points
pi-tape verify run.tape # is every recorded event servable?
pi-tape diff before.tape after.tape # where two runs diverged
╭─ tape "next big thing" sha256:825d832bed7b
│ format pi-tape v1 (profile: normal)
│ size 145.6 KiB on disk · 140 pooled strings · 116 entries
╰─
Run
messages 108 (4 user, 43 assistant, 60 tool results)
tokens 3,179,678 (72,970 in / 50,196 out)
cost $0.1005
Context cost per tool (result characters)
bash 28 calls ████████████████████████ 58,357 chars (~14,589 tok)
Replay
/tape play baseline.tape --run # arm, switch to the tape model, send the recorded prompts
/tape status # progress, misses, what the recording cost
/tape stop # disarm; /reload restores the real tools
--run works the same for fork and shadow: pi-tape switches to the tape model,
sends each recorded prompt in order, waits for that run to finish, asks for a
compaction where the recording made one, and prints the status (or, for a shadow,
the regression report) at the end. It stops early when a run is aborted, the replay
is disarmed, or a fork stops before its intervention. Without --run you drive it by
hand: /model tape/<model> (the notice names it), then the original prompts.
Every answer and every tool result comes from the recording. A replay keeps working when pi compacts the conversation along the way, when the thinking level differs from the recording, and with parallel tool calls.
Fork: change one thing, continue live
/tape fork <file> --model <provider>/<model> [options]
--run send the recorded prompts, then report
--at <turn|toolCallId> --result <text|@file> answer that tool call differently
--live-tools read,grep let these run for real past the change
--max-live-tokens N optional cap on what the branch may spend
pi-tape inspect <file> --timeline lists the turns you can intervene on. Up to the
intervention everything is served from the recording; after it, the model is asked.
A fork is careful about what it claims:
- Nothing runs twice. Recorded tool calls are always served from the tape — tools
named in
--live-toolsincluded — so the replayed part never repeats a side effect. - No stale answers. A live call that repeats a recorded read is answered from the recording only while the recording still describes the branch: for read-only tools, before the recording's next change, and never after a live tool has changed something. Every tool the recording declared is a stand-in, so a branch cannot reach a tool (a mail, a deploy) it was not allowed.
- No silent spending. A request that misses the recording before the intervention cannot be the experiment — nothing has diverged yet — so it is treated as the bug it is, and the fork stops without sending it.
How a tool call is answered in a fork:
flowchart TD
C["the model calls a tool"] --> K{"a recorded call id?"}
K -- "yes, the intervened one" --> O["return the replacement"]
K -- "yes, any other" --> T["return the recorded result"]
K -- "no, the branch's own call" --> RO{"read-only, before the recording's<br/>next change, nothing changed live?"}
RO -- "yes, and recorded with the same arguments" --> RC["return the recorded result"]
RO -- "no" --> LT{"named in --live-tools?"}
LT -- "yes" --> REAL["run pi's real tool"]
LT -- "no" --> STOP["refuse, and say why"]
A tape recorded from a fork remembers what it was forked from, and diff reports the
divergence as the experiment rather than a regression.
Test a change: shadow and regress
A shadow run serves the recording — the trajectory stays fixed — and asks a model the same request at every recorded step, with whatever it is configured with now:
/tape shadow golden.tape --model <provider>/<model> --run
# → the regression report, once every comparison is in
A model upgrade: shadow with the new model.
A change to what the agent knows — AGENTS.md, a skill, a prompt: shadow with the
same model after the change. Here, the rule "run bump before a prod deploy" was
deleted from AGENTS.md:
# golden: deepseek-flash → deepseek/deepseek-flash
# 4 request(s) · 3 identical · 1 structurally different · 0 only reworded or inspected differently
# structurally different requests:
# turn 2
# recorded: bash({"command":"./shipit bump billing"})
# new: bash({"command":"./shipit deploy billing --env prod"})
Nothing is executed, and the report names the step where behaviour moved. The
comparison is strict about actions and lenient about everything else: a rephrased
sentence, an unset optional parameter or a different way of looking around (ls -la
against ls) is not a different procedure. Always shadow the unchanged setup
too — what differs then is the model's own noise, the baseline a real regression has to
stand out from. bench/knowledge-regression.mjs runs
that control, a harmless rewording and a real regression in one go.
How far that gets you, measured: with a scripted model the deleted rule is caught at the step it matters, and the control and the rewording stay quiet. With DeepSeek Flash they do not separate. Out of 7 recorded steps, the actions that differed were:
| setup | shadow 1 | shadow 2 |
|---|---|---|
| unchanged (control) | 2 | 4 |
| reworded | 3 | 3 |
| rule deleted | 5 | 3 |
The model's own variation — searching with other words, inspecting the tool before
logging in, decompressing it to read the source — is as large as the effect of the
change. Lookups, cd, echoes and timeouts are already left out of the comparison; what
remains is the model. So a shadow report is a step-by-step account for a person to read
next to the control, not a pass/fail gate.
Recipes (experimental)
Several recordings of the same kind of task can be spliced into a recipe: what they share is the procedure, what varied is a parameter, and a step only some runs needed is kept with the condition under which it appeared.
pi-tape splice api-staging.tape web-staging.tape api-prod.tape --name shipit-deploy --save
# skeleton:
# ./shipit login --token-file .shipit-token
# ./shipit deploy {{arg}} --env {{env}}
# seen in some runs only:
# ./shipit bump {{arg}} — in 1 of 3 runs, before step 3, when env=prod
pi-tape dub shipit-deploy --set arg=billing --set env=prod
# ./shipit login --token-file .shipit-token
# ./shipit deploy billing --env prod
# seen in some runs only:
# ./shipit bump billing — in 1 of 3 runs, before step 3, when env=prod ← applies here
Recipes live as plain JSON in a project store (.tape/recipes/, shadowing the global
~/.pi/agent/tape/recipes/). When one clearly matches the task, the extension writes
its procedure into the agent's prompt. Placeholders are typed (secret, path, env,
choice, free), and a credential never reaches the file. Beyond that, recipes can be
linked into chains whose seams are checked, cut into reusable segments, and run as a
plan. All of it is described in docs/RECIPES.md.
Whether recipes make the next run better is an open question — see What has been shown.
What has been shown
The unit tests never start an agent. So the claims above were checked against a real pi
with a real model (DeepSeek Flash, thinking on) by the scripts in bench/.
They drive pi over RPC and check results in the world — files on disk, a tool's own log
— rather than taking the model's word for it.
| Check | What it does | Result |
|---|---|---|
replay-check |
record; change the files on disk; replay, also at another thinking level; fork with an intervention | 9 of 9, in a setup with ~200 tools from other extensions |
prove |
a longer run with parallel tool calls and a compaction; replay compacting at the same point; fork after the compaction with thinking on; the debugging case above | 14 of 14 |
knowledge-regression |
shadow a recorded run with unchanged, reworded and broken AGENTS.md; then run the broken one for real |
scripted model: caught at the right step, control and rewording quiet. DeepSeek Flash: control, rewording and regression do not separate |
recipe-experiment |
recipes vs. the agent's own notes vs. nothing, on deployments the training runs never did, with a readable and a sealed tool | the right recipe is learned; it saves nothing (below) |
The checks found what the unit tests could not. Each of these passed every unit test and failed against real pi:
- replay missed every request after the first, because
durationMsandthinkingLevel— fields that describe a past turn, not the next request — were part of the key; - a fixed live-token budget stopped forks after one step in a setup with many tools;
- a compaction during a replay was answered with a recorded tool call, and every answer after it shifted by one;
- recipe extraction learned the failed attempts of a run and dropped the step that worked.
Recipes did not pay off in the measured setting. Three training runs were recorded with a small in-house deploy tool, the agent wrote notes after each, and pi-tape spliced the recordings. The recipe it learned is the right one:
./shipit login --token-file .shipit-token
./shipit deploy {{arg}} --env {{env}}
seen in some runs only:
./shipit bump {{arg}} — in 1 of 3 runs, before step 2, when env=prod
Then three deployments the training never did ran three times each per condition — with the tool readable, and with it sealed (compressed, so its source does not show the procedure). Every working folder was isolated from the others:
| tool | condition | success | requests | tokens | cost |
|---|---|---|---|---|---|
| readable | nothing | 9/9 | 4.9 | 32,100 | $0.0024 |
| readable | the agent's own notes | 9/9 | 5.1 | 38,400 | $0.0032 |
| readable | recipe | 9/9 | 5.9 | 41,500 | $0.0028 |
| sealed | nothing | 9/9 | 5.4 | 40,200 | $0.0031 |
| sealed | the agent's own notes | 9/9 | 4.9 | 39,300 | $0.0035 |
| sealed | recipe | 9/9 | 6.0 | 49,500 | $0.0035 |
What it says:
- No memory made the runs cheaper or more successful. Running with nothing was the cheapest in both variants; the recipe cost about one request more, spent on looking it up and reading it.
- Exploring was never expensive. Every condition succeeded every time, in five to
six requests. The sealed tool did not stay sealed: the model decompressed it with
node -eand read the source. - Failed tool calls do not separate the conditions. An earlier run suggested that
recipes led to fewer of them; the isolated rerun did not repeat it. Most "failed"
calls were a
catof a state file the notes mention, made before the file existed.
That matches the research rather than contradicting it: learned procedures pay where finding the way is long and costly, and a capable model with a shell finds a small procedure by itself. Recipes stay an experiment. Where they could matter — long procedures, steps that cannot be inspected, tries that cost real time or money — has not been measured here.
Every check prints what it does and what it costs before it starts; most take a few minutes and a few cents. Run them against your own pi and model:
node bench/replay-check.mjs
Where this sits
Learning procedures from agent runs is an active line of research. Voyager (2023) kept a growing library of skills as code. Agent Workflow Memory (ICML 2025) induces reusable workflows from past trajectories and reports clear gains on web benchmarks. Agent Skill Induction (2025) keeps skills as programs and admits one only after it has been executed and shown to work. In all three, a language model does the abstraction — flexible, and not inspectable. pi-tape's recipes are derived deterministically, which makes them checkable and also brittle where a model would simply have understood. The research measures its gains on long tasks where exploring is expensive. The measurement here was a short task, and it found what that predicts: when a capable model finds the way in four requests, memory has nothing to save.
Replaying and branching runs exists in agent frameworks that checkpoint their state — LangGraph calls it time travel — for agents built in that framework from the start. pi-tape works on any pi session after the fact. It replaces tool results instead of re-running them, and it can change exactly one tool result and let the model take it from there.
The direction this points to is less either half than the combination: recorded runs as fixtures for agent knowledge. The research admits a skill after executing it again; pi-tape can replay a change to a skill, a note or a model against recorded runs without executing anything, and show the step where behaviour moved. The mechanism works. With a real model, telling a moved step from the model's own variability is the unsolved part — see the measurement.
How it works
pi-tape sits between pi's agent loop and everything the loop would otherwise reach. While a tape is armed, the model's requests go to a provider that pi-tape registers, and the tool calls go to stand-ins in place of the real tools. Both ask the replay engine, which answers from the recording — and, in a fork or a shadow run, hands what the recording cannot answer to a live model.
flowchart LR
subgraph PI["pi"]
direction TB
A["agent loop"]
S[("session file")]
end
subgraph TAPE["pi-tape"]
direction TB
P["tape provider<br/>answers the model's requests"]
W["tool stand-ins<br/>answer the tool calls"]
E["replay engine"]
end
T[(".tape")]
L["live model"]
RC[("recipes")]
A -- "request" --> P
A -- "tool call" --> W
P --> E
W --> E
E -- "reads" --> T
S -- "record" --> T
E -. "fork: what the tape lacks<br/>shadow: every request" .-> L
T -- "splice" --> RC
RC -. "matching procedure<br/>into the prompt" .-> A
A request is a function of its prefix. pi-tape hashes what a provider is actually sent — the messages, with the model and thinking level available as context — and leaves out what only describes how an earlier turn came about: timestamps, usage, durations, the provider that wrote it. Recorded answers are indexed by that hash and served instead of calling a provider. Tool results are served by tool call id, so a replay never runs a tool.
How a request is answered. A plain replay that misses falls back to the next
recorded answer, so it keeps going with an imperfect key — and logs the miss;
/tape status shows every one. A fork switches that fallback off: a new context gets a
new answer, not an old one.
flowchart TD
Q["pi sends a request"] --> H{"is this prefix<br/>in the recording?"}
H -- "yes" --> R1["serve the recorded answer"]
H -- "no" --> M{"mode"}
M -- "play" --> R2["serve the next recorded answer<br/>and log the miss"]
M -- "fork" --> I{"intervention<br/>served yet?"}
I -- "no" --> X["stop, send nothing:<br/>a hash mismatch, not the experiment"]
I -- "yes" --> R3["ask the live model"]
Compaction. A request after a recorded compaction is indexed twice: by the full history, and by what the model actually saw (the summary plus the kept messages). So it matches whether or not the replay compacts. When pi compacts during a replay, pi-tape answers with the recorded summary if the recording compacted at that point, and skips the compaction otherwise. Past a fork's divergence the branch is its own run, and a live model summarises it.
The .tape file is gzipped JSON with a string pool that stores each long string
once — about 73 % smaller than pi's session file (84 % at the minimal profile). It
contains the tool declarations as well as the results, so a replay needs neither your
settings nor your MCP servers nor your tool versions. Specification:
docs/FORMAT.md.
Reference
/tape — the command
/tape record [name] [--redact] [--status s] |
write this session to a .tape file |
/tape play <file> [--run] |
arm a deterministic replay; --run also sends the recorded prompts |
/tape fork <file> --model <p>/<m> [--run] [--at … --result …] [--live-tools a,b] [--max-live-tokens N] |
replay, then continue live where the recording ends or is changed |
/tape shadow <file> --model <p>/<m> [--run] |
replay, and ask that model the same requests |
/tape regress |
what the shadow model would have done differently |
/tape status |
progress, misses, compactions served, what a fork or shadow spent |
/tape stop |
disarm (then /reload to restore the real tools) |
/tape library · /tape index |
the recipe store, and its graph index |
Tools for the agent
All read-only: the agent still edits and runs things with its own tools.
| Tool | Answers |
|---|---|
tape_search |
do I already know how to do this? — local, offline, a few hundred tokens |
tape_show |
what exactly is the procedure, with its parameters and conditional steps? |
tape_dub |
what would it look like with these values? |
tape_plan |
what would running it take on this machine? — runs nothing |
tape_check |
is this recipe still true? |
tape_splice · tape_segment |
learn a recipe from tapes · cut one into reusable parts |
Before each run, a bounded block (about 1800 characters) tells the agent what the recipe store holds — or, when one recipe clearly matches the task, gives it that procedure directly. During a replay it stays silent, so a replayed run follows its recording.
pi-tape — the command line
| Command | |
|---|---|
sessions |
pi's sessions, newest first |
record [session] |
session → .tape (--name, --status, --redact, --profile, --out) |
inspect <tape> |
stats, context cost per tool, intervention points (--timeline) |
verify <tape> |
is every recorded event servable? |
diff <a> <b> |
where two runs diverged; a fork is reported as one |
splice <tape…> |
tapes → recipe (--name, --save, --scope) |
library · show <name> |
the recipe store · one recipe |
dub <name> --set k=v |
concrete steps for these values |
search <query> |
recipes for a task, within a token budget |
link · segment · run · check · index · rules |
composition, segments, execution, freshness, graph, rule packs — see docs/RECIPES.md |
extension |
the path for pi -e |
pi-tape help lists every flag.
Limits
A debugger that lies is worse than none, so:
- Tool overrides last for the process. pi cannot unregister a tool, so leaving a
replay needs
/reload./tape stopmakes the overrides refuse to run rather than pretend. - Replayed answers report zero cost. Token counts are kept so the shape of the run stays visible; the money is zeroed so session totals stay honest.
- Live tools run in today's directory. What
--live-toolslets through sees the folder as it is now, not as it was when the recording was made. A live branch of an old recording is not a faithful counterfactual. Only pi's built-in tools can be run this way; another tool named there is left alone, and its recorded calls run again. - Shadow comparisons carry the model's own noise. A model does not answer the same request the same way twice; measured with DeepSeek Flash, an unchanged setup differed in two to four of seven steps, as much as a deliberately broken one. Compare every report with a control, and do not use a single shadow run as an automatic pass/fail gate.
- Checked against one provider. The
bench/checks ran with DeepSeek Flash. Providers that sign their reasoning (Anthropic) are the open case for a fork, because replayed turns are sent back to them;bench/prove.mjschecks it when such a model is configured. - Recipes are an experiment. Extraction is heuristic — a shell command is a string,
not an AST. A recipe has no conditionals; a step only some runs needed carries the
pattern that separated them (
when env=prod), which with three recordings can be a coincidence. For a deterministic procedure, a script is exact and reviewable; a recipe is not a replacement for one. - Redaction is a layer, not a guarantee. It catches credentials by shape and by context, not one written as plain prose. Read a tape before you share it.
pi-tape runexecutes recorded command lines. It plans by default, quotes the values it binds, refuses a short list of destructive commands and stops at the first failure — a floor, not a sandbox. Read the plan before--yes.--runsends prompts one run at a time. A message the recording received while the agent was still working (steering, a follow-up typed mid-run) is sent as its own prompt after the run, so that part of a replay may miss.- Not implemented: picking a fork point interactively (use
inspect --timelineand--at), replaying image payloads.
Development
npm test # 217 tests, node:test, no agent started
npm run typecheck # a separate gate: code has run green while carrying type errors
npm run build # dist/, only needed to publish
node bench/prove.mjs # against a real pi and model; reports in .tape/check/
src/hash.ts what is part of a request, and what is not
src/replay.ts the replay engine — pure logic, no pi
src/record.ts session → tape, profiles, redaction
src/tape.ts the .tape format
src/diff.ts divergence between two recordings
src/regress.ts two answers compared: actions strict, wording lenient
src/recipe-*.ts extraction, alignment, query, store ← heuristic
src/normalize.ts command lines → shapes and templates ← heuristic
src/contract.ts what a step needs and delivers; linking
src/run.ts bind, probe, execute, verify
src/cli.ts the command line
extensions/ the pi extension
test/ unit tests; real-session.json is a capture from real pi
bench/ checks against a real pi and model (not shipped)
The package has no runtime dependencies. The extension declares its tool schemas
with typebox and uses pi's own tool implementations for --live-tools. pi supplies
both to extensions at runtime, so typebox is a "*" peer dependency: a second copy
could bypass pi's extension module mapping. Nothing in src/ imports pi, so all of it
is testable without starting an agent.
Releasing
Commit messages are the release mechanism. release.yml hands every push to main to
semantic-release. It derives the version from the commits —
fix: a patch, feat: a minor, feat!: or BREAKING CHANGE: a major,
docs:/chore:/test: nothing — and publishes to npm with provenance, writes the
changelog and opens the GitHub release. ci.yml runs the tests on Node 22.18 (the
declared floor), 24 and 26, plus the type check.
The one secret it needs is NPM_TOKEN, an npm automation token allowed to publish
@patimweb/* (Settings → Secrets and variables → Actions). A new version can take a few
minutes to show up in npm view; that is the registry's cache, not a failed publish.
The tarball is an allowlist — dist/, the sources, the extension, the docs, the
licence, the citation file and the screenshot — and installs nothing of its own.
Licence and citation
Apache License 2.0 — see LICENSE and NOTICE. A redistribution or a derivative keeps both files and states what it changed.
If you build on the ideas here, please say what you built on. A machine-readable CITATION.cff is in the repository, and GitHub renders a "Cite this repository" button from it.
The recipe index follows the design of pi-mindplace by the same author; docs/ATTRIBUTION.md says exactly what was borrowed.
The name
A tape records a performance. This one records a run — the same decisions, the same
tool calls, the same output — that you can play back, hand to someone else, or splice
with others into a master from which a new variant is dubbed. The vocabulary follows the
metaphor where it clarifies (record, play, splice, dub) and stays plain where it
would not (inspect, diff, search, recipe, step).