backspin 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
backspin/ui/index.html ADDED
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>backspin — agent 运行查看器</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='0.9em' font-size='90'%3E%E2%8F%AA%3C/text%3E%3C/svg%3E">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <div id="logo">backspin<span class="logo-dim" data-i18n="logo.sub">· agent 飞行记录仪</span></div>
13
+ <div id="toolbar">
14
+ <select id="diff-select" hidden></select>
15
+ <button id="btn-diff" disabled data-i18n="btn.compare" data-i18n-title="tip.compare">对比…</button>
16
+ <button id="btn-close-diff" hidden data-i18n="btn.closeDiff">关闭对比</button>
17
+ <button id="btn-lang" data-i18n-title="tip.lang">EN</button>
18
+ </div>
19
+ </header>
20
+
21
+ <div id="layout">
22
+ <aside id="sidebar">
23
+ <h2 data-i18n="side.runs">运行记录</h2>
24
+ <ul id="run-list"></ul>
25
+ </aside>
26
+
27
+ <main id="main">
28
+ <div id="empty">
29
+ <h3 data-i18n="empty.title">尚未选择运行</h3>
30
+ <p data-i18n="empty.pick">从左侧选择一次运行,或者先录制一次:</p>
31
+ <pre>from backspin import Recorder
32
+
33
+ with Recorder(agent="my-agent") as rec:
34
+ client = rec.capture_openai(OpenAI())
35
+ ... # 你的 agent 代码</pre>
36
+ <p class="dim"><span data-i18n="empty.then">然后运行</span> <code>backspin ui</code> <span data-i18n="empty.refresh">并刷新本页。</span></p>
37
+ </div>
38
+ <section id="summary" class="cards" hidden></section>
39
+ <div id="banner" hidden></div>
40
+ <section id="waterfall-wrap" hidden>
41
+ <h2 id="wf-title" data-i18n="side.timeline">时间线</h2>
42
+ <div id="waterfall"></div>
43
+ </section>
44
+ <section id="diff-table-wrap" hidden>
45
+ <h2 data-i18n="side.diff">对比</h2>
46
+ <table id="diff-table"></table>
47
+ </section>
48
+ </main>
49
+
50
+ <aside id="inspector" hidden>
51
+ <div id="insp-head">
52
+ <span id="insp-title"></span>
53
+ <button id="insp-close" data-i18n-title="tip.close">✕</button>
54
+ </div>
55
+ <div id="insp-meta"></div>
56
+ <div id="insp-tabs" role="tablist">
57
+ <button class="tab" role="tab" data-tab="request" data-i18n="tab.request">请求</button>
58
+ <button class="tab" role="tab" data-tab="response" data-i18n="tab.response">响应</button>
59
+ <button class="tab active" role="tab" aria-selected="true" data-tab="raw" data-i18n="tab.raw">原始</button>
60
+ </div>
61
+ <pre id="insp-body"></pre>
62
+ </aside>
63
+ </div>
64
+
65
+ <script src="i18n.js"></script>
66
+ <script src="app.js"></script>
67
+ </body>
68
+ </html>
backspin/ui/style.css ADDED
@@ -0,0 +1,214 @@
1
+ :root {
2
+ --bg: #0d1117;
3
+ --panel: #161b22;
4
+ --panel-2: #1c2333;
5
+ --border: #30363d;
6
+ --text: #e6edf3;
7
+ --dim: #8b949e;
8
+ --accent: #58a6ff;
9
+ --green: #3fb950;
10
+ --red: #f85149;
11
+ --yellow: #d29922;
12
+ --purple: #bc8cff;
13
+ --mono: ui-monospace, "Cascadia Code", Consolas, "Courier New", monospace;
14
+ }
15
+
16
+ * { box-sizing: border-box; }
17
+
18
+ [hidden] { display: none !important; }
19
+
20
+ body {
21
+ margin: 0;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ font: 14px/1.5 -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
25
+ }
26
+
27
+ header {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ padding: 10px 16px;
32
+ border-bottom: 1px solid var(--border);
33
+ background: var(--panel);
34
+ }
35
+
36
+ #logo { font-weight: 700; font-size: 15px; letter-spacing: 0.3px; }
37
+ .logo-dim { color: var(--dim); font-weight: 400; margin-left: 8px; font-size: 12px; }
38
+
39
+ button, select {
40
+ background: var(--panel-2);
41
+ color: var(--text);
42
+ border: 1px solid var(--border);
43
+ border-radius: 6px;
44
+ padding: 5px 12px;
45
+ font: inherit;
46
+ cursor: pointer;
47
+ }
48
+ button:hover:not(:disabled) { border-color: var(--accent); }
49
+ button:disabled { opacity: 0.45; cursor: default; }
50
+
51
+ #layout {
52
+ display: flex;
53
+ align-items: stretch;
54
+ height: calc(100vh - 53px);
55
+ }
56
+
57
+ /* ---- sidebar ---- */
58
+ #sidebar {
59
+ width: 280px;
60
+ flex-shrink: 0;
61
+ border-right: 1px solid var(--border);
62
+ overflow-y: auto;
63
+ padding: 12px;
64
+ }
65
+ #sidebar h2, #wf-title, #diff-table-wrap h2 {
66
+ font-size: 11px;
67
+ text-transform: uppercase;
68
+ letter-spacing: 1px;
69
+ color: var(--dim);
70
+ margin: 4px 0 10px;
71
+ }
72
+ #run-list { list-style: none; margin: 0; padding: 0; }
73
+ #run-list li {
74
+ padding: 8px 10px;
75
+ border-radius: 6px;
76
+ cursor: pointer;
77
+ border: 1px solid transparent;
78
+ margin-bottom: 4px;
79
+ }
80
+ #run-list li:hover { background: var(--panel-2); }
81
+ #run-list li.active { background: var(--panel-2); border-color: var(--accent); }
82
+ #run-list .name { font-family: var(--mono); font-size: 12px; word-break: break-all; }
83
+ #run-list .meta { color: var(--dim); font-size: 11px; margin-top: 2px; }
84
+
85
+ /* ---- main ---- */
86
+ #main { flex: 1; overflow-y: auto; padding: 14px 18px; min-width: 0; }
87
+
88
+ #empty { color: var(--dim); max-width: 480px; margin: 60px auto; text-align: center; }
89
+ #empty pre {
90
+ background: var(--panel);
91
+ border: 1px solid var(--border);
92
+ border-radius: 8px;
93
+ padding: 12px;
94
+ text-align: left;
95
+ font-family: var(--mono);
96
+ font-size: 12px;
97
+ overflow-x: auto;
98
+ }
99
+ #empty .dim, .dim { color: var(--dim); }
100
+
101
+ .cards { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
102
+ .card {
103
+ background: var(--panel);
104
+ border: 1px solid var(--border);
105
+ border-radius: 8px;
106
+ padding: 10px 14px;
107
+ min-width: 110px;
108
+ }
109
+ .card .k { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; }
110
+ .card .v { font-size: 18px; font-weight: 600; font-family: var(--mono); }
111
+ .card .v small { font-size: 11px; color: var(--dim); font-weight: 400; }
112
+
113
+ #banner {
114
+ background: rgba(210, 153, 34, 0.12);
115
+ border: 1px solid var(--yellow);
116
+ color: var(--text);
117
+ border-radius: 8px;
118
+ padding: 10px 14px;
119
+ margin-bottom: 14px;
120
+ }
121
+ #banner.ok { border-color: var(--green); background: rgba(63, 185, 80, 0.1); }
122
+
123
+ /* ---- waterfall ---- */
124
+ .row {
125
+ display: grid;
126
+ grid-template-columns: 38px 190px 1fr 70px 90px;
127
+ align-items: center;
128
+ gap: 8px;
129
+ padding: 4px 6px;
130
+ border-radius: 6px;
131
+ cursor: pointer;
132
+ font-family: var(--mono);
133
+ font-size: 12px;
134
+ }
135
+ .row:hover { background: var(--panel-2); }
136
+ .row.selected { background: var(--panel-2); outline: 1px solid var(--accent); }
137
+ .row .seq { color: var(--dim); }
138
+ .row .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
139
+ .row .dur, .row .tok { text-align: right; color: var(--dim); }
140
+ .track { background: var(--panel-2); border-radius: 4px; height: 14px; position: relative; overflow: hidden; }
141
+ .bar { position: absolute; left: 0; top: 0; bottom: 0; min-width: 2px; border-radius: 3px; }
142
+ .kind-llm .bar { background: var(--accent); }
143
+ .kind-tool .bar { background: var(--green); }
144
+ .kind-log .bar { background: #3d444d; }
145
+ .kind-error .bar { background: var(--red); }
146
+ .kind-custom .bar { background: var(--purple); }
147
+ .kind-span .bar { background: var(--purple); opacity: 0.55; }
148
+ .kind-llm .lbl { color: var(--accent); }
149
+ .kind-tool .lbl { color: var(--green); }
150
+ .kind-span .lbl { color: var(--purple); }
151
+ .kind-error .lbl { color: var(--red); }
152
+ .row.error .dur { color: var(--red); }
153
+
154
+ /* ---- diff table ---- */
155
+ #diff-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; }
156
+ #diff-table th, #diff-table td {
157
+ text-align: left;
158
+ padding: 6px 10px;
159
+ border-bottom: 1px solid var(--border);
160
+ }
161
+ #diff-table th { color: var(--dim); font-weight: 500; text-transform: uppercase; font-size: 10px; letter-spacing: 0.8px; }
162
+ #diff-table tr.diverged td { background: rgba(248, 81, 73, 0.12); }
163
+ #diff-table tr.solo td { color: var(--dim); }
164
+ .badge-yes { color: var(--green); }
165
+ .badge-no { color: var(--red); font-weight: 700; }
166
+ .badge-solo { color: var(--dim); }
167
+
168
+ /* ---- inspector ---- */
169
+ #inspector {
170
+ width: 420px;
171
+ flex-shrink: 0;
172
+ border-left: 1px solid var(--border);
173
+ display: flex;
174
+ flex-direction: column;
175
+ background: var(--panel);
176
+ }
177
+ #insp-head {
178
+ display: flex;
179
+ justify-content: space-between;
180
+ align-items: center;
181
+ padding: 10px 14px;
182
+ border-bottom: 1px solid var(--border);
183
+ font-weight: 600;
184
+ }
185
+ #insp-close { border: none; background: none; color: var(--dim); font-size: 14px; }
186
+ #insp-meta { padding: 8px 14px; color: var(--dim); font-family: var(--mono); font-size: 11px; }
187
+ #insp-tabs { display: flex; gap: 4px; padding: 0 14px; border-bottom: 1px solid var(--border); }
188
+ .tab {
189
+ border: none;
190
+ border-bottom: 2px solid transparent;
191
+ background: none;
192
+ color: var(--dim);
193
+ border-radius: 0;
194
+ padding: 6px 10px;
195
+ }
196
+ .tab.active { color: var(--text); border-bottom-color: var(--accent); }
197
+ #insp-body {
198
+ margin: 0;
199
+ padding: 12px 14px;
200
+ overflow: auto;
201
+ flex: 1;
202
+ font-family: var(--mono);
203
+ font-size: 12px;
204
+ white-space: pre-wrap;
205
+ word-break: break-word;
206
+ }
207
+
208
+ /* keyboard focus rings for the clickable non-button elements */
209
+ #run-list li:focus-visible,
210
+ #waterfall .row:focus-visible {
211
+ outline: 2px solid var(--accent);
212
+ outline-offset: -2px;
213
+ border-radius: 4px;
214
+ }
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.5
2
+ Name: backspin
3
+ Version: 0.5.1
4
+ Summary: The flight recorder for AI agents — record, replay, diff and debug agent runs, 100% local.
5
+ Project-URL: Homepage, https://github.com/zaibuchihuoji/backspin
6
+ Project-URL: Repository, https://github.com/zaibuchihuoji/backspin
7
+ Project-URL: Issues, https://github.com/zaibuchihuoji/backspin/issues
8
+ Project-URL: Changelog, https://github.com/zaibuchihuoji/backspin/blob/main/CHANGELOG.md
9
+ Author: backspin contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,llm,mcp,observability,openai,record-replay,time-travel-debugging
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Requires-Python: >=3.9
26
+ Provides-Extra: anthropic
27
+ Requires-Dist: anthropic>=1.0; (python_version >= '3.10') and extra == 'anthropic'
28
+ Provides-Extra: dev
29
+ Requires-Dist: anthropic>=1.0; (python_version >= '3.10') and extra == 'dev'
30
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
31
+ Requires-Dist: httpx>=0.24; extra == 'dev'
32
+ Requires-Dist: mypy>=1.10; extra == 'dev'
33
+ Requires-Dist: openai>=1.30; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
35
+ Requires-Dist: pytest>=7; extra == 'dev'
36
+ Requires-Dist: ruff>=0.6; extra == 'dev'
37
+ Requires-Dist: uvicorn>=0.23; extra == 'dev'
38
+ Provides-Extra: proxy
39
+ Requires-Dist: fastapi>=0.100; extra == 'proxy'
40
+ Requires-Dist: httpx>=0.24; extra == 'proxy'
41
+ Requires-Dist: uvicorn>=0.23; extra == 'proxy'
42
+ Provides-Extra: ui
43
+ Requires-Dist: fastapi>=0.100; extra == 'ui'
44
+ Requires-Dist: uvicorn>=0.23; extra == 'ui'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # backspin
48
+
49
+ [![CI](https://github.com/zaibuchihuoji/backspin/actions/workflows/ci.yml/badge.svg)](https://github.com/zaibuchihuoji/backspin/actions/workflows/ci.yml)
50
+ [![PyPI](https://img.shields.io/pypi/v/backspin)](https://pypi.org/project/backspin/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/backspin)](https://pypi.org/project/backspin/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
53
+ [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
54
+
55
+ **The flight recorder for AI agents.** Record every LLM call and tool call of an agent run into one portable file, replay the run deterministically with zero API access, and diff two runs to find the exact step where behavior diverged. 100% local, zero core dependencies.
56
+
57
+ *Think [rr](https://rr-project.org/), but for agents instead of processes.*
58
+
59
+ ```python
60
+ from backspin import Recorder
61
+
62
+ with Recorder(agent="my-agent") as rec:
63
+ client = rec.capture_openai(OpenAI()) # any code that talks to chat.completions
64
+ ... # your agent, unchanged
65
+ ```
66
+
67
+ That's the whole integration. Everything the agent did — prompts, completions, tool calls, timings, token counts — is now in a single `runs/*.backspin.jsonl` file you can open, replay, diff, or attach to a bug report.
68
+
69
+ [中文文档](README.zh-CN.md)
70
+
71
+ ## Why
72
+
73
+ Your agent made forty LLM calls, called three tools, and then did something weird at 2am. Good luck reproducing that from a chat log.
74
+
75
+ Cloud observability tools (Langfuse, LangSmith, AgentOps…) answer *"what happened?"* on a dashboard. **backspin answers "make it happen again, exactly."** It's a debugger, not a dashboard:
76
+
77
+ - **Record** — one context manager captures every OpenAI-shaped call (sync, async, streaming) plus your own tool calls and logs.
78
+ - **Replay** — the run becomes a *cassette*: your agent re-runs offline with recorded responses injected, deterministically. Perfect for regression tests and reproducing bugs without API keys or cost.
79
+ - **Diff** — replay the same agent against a fix and diff the two runs; backspin pinpoints the first step where they stopped matching.
80
+ - **Local-first** — runs are plain JSONL files. No server, no account, no telemetry. `git attach`-friendly: the failing run *is* the bug report.
81
+
82
+ ## Install
83
+
84
+ ```bash
85
+ pip install "backspin[ui]" # SDK + CLI + local viewer
86
+ ```
87
+
88
+ Core has zero dependencies; `[ui]` adds FastAPI + uvicorn for the local viewer.
89
+
90
+ ## Record
91
+
92
+ ```python
93
+ from openai import OpenAI
94
+ from backspin import Recorder
95
+
96
+ rec = Recorder(agent="support-bot")
97
+
98
+ with rec:
99
+ client = rec.capture_openai(OpenAI())
100
+
101
+ @rec.tool
102
+ def lookup_order(order_id: str) -> str:
103
+ return "shipped"
104
+
105
+ rec.log("user asks about order #1234")
106
+ resp = client.chat.completions.create(
107
+ model="gpt-4o-mini",
108
+ messages=[{"role": "user", "content": "Where is order #1234?"}],
109
+ )
110
+
111
+ print(rec.path) # runs/20260829-142300-support-bot-9f31c2.backspin.jsonl
112
+ ```
113
+
114
+ Streaming and async clients are captured too; a streamed response is transparently reconstructed into one recorded completion.
115
+
116
+ ## Replay
117
+
118
+ ```python
119
+ from backspin import Cassette, load_run, stub_client
120
+
121
+ cassette = Cassette.from_run(load_run(rec.path))
122
+ stub = stub_client(cassette)
123
+
124
+ # Same agent code, zero network: responses come from the recording.
125
+ answer = run_agent(stub)
126
+ ```
127
+
128
+ Requests are matched by fingerprint (model + messages) and fall back to call order with a warning. Use `backspin.replay.patch_openai(cassette)` to patch `openai.OpenAI` itself when you can't inject a client.
129
+
130
+ **In tests** this becomes deterministic agent regression testing: record once, assert forever, at zero token cost. The pytest plugin does the asserting for you:
131
+
132
+ ```python
133
+ def test_my_agent(backspin): # pip-installed = auto-loaded
134
+ with backspin.record(agent="t") as rec:
135
+ run_agent(rec.capture_openai(client))
136
+ backspin.assert_replays_identically() # strict: fingerprint-exact replay
137
+ ```
138
+
139
+ ## What-if branching
140
+
141
+ The debugger superpower: change one answer, keep everything else constant, and see what the timeline looks like downstream.
142
+
143
+ ```python
144
+ from backspin import branch, diff_runs, load_run
145
+
146
+ branch_path = branch("runs/live.backspin.jsonl", {0: {"content": "Rome it is."}})
147
+ report = diff_runs(load_run("runs/live.backspin.jsonl"), load_run(branch_path), llm_only=True)
148
+ print(report.first_divergence) # the step where the two timelines split
149
+ ```
150
+
151
+ Or from the CLI: `backspin branch runs/live.jsonl --step 0 --content "Rome it is."` — writes a branch run (marked `branch_of`) and prints the divergence report.
152
+
153
+ ## Spans: structure, not just a flat list
154
+
155
+ ```python
156
+ with rec.span("research", meta={"topic": "weather"}):
157
+ with rec.span("tool:search"):
158
+ ...
159
+ resp = client.chat.completions.create(...) # recorded inside the span
160
+ ```
161
+
162
+ Every event inside a span carries its `span_id` and nesting `depth`; spans are safe under concurrency (each asyncio task gets its own stack) and the viewer renders the tree. Spans never inflate duration totals.
163
+
164
+ ## Zero-code integration: `backspin proxy`
165
+
166
+ Can't (or don't want to) instrument code? Run the OpenAI-compatible local proxy and point any agent at it — any framework, any language:
167
+
168
+ ```bash
169
+ backspin proxy --upstream https://api.openai.com --port 8840
170
+ # client: base_url = http://127.0.0.1:8840/v1 ← that's the whole integration
171
+ ```
172
+
173
+ Every call is forwarded and captured, streaming included. Flip the same proxy into **replay mode** and it serves a recorded run back as an API — deterministic replay for agents written in any language, no SDK required:
174
+
175
+ ```bash
176
+ backspin proxy --replay runs/live.backspin.jsonl --port 8840
177
+ ```
178
+
179
+ ## Multi-provider: OpenAI, Anthropic, and everything OpenAI-compatible
180
+
181
+ Claude natively? Same story, two lines:
182
+
183
+ ```python
184
+ rec.capture_anthropic(Anthropic()) # sync/async/streaming, tool_use included
185
+ ```
186
+
187
+ Anthropic events record with `provider: "anthropic"` and usage normalized to
188
+ the same token fields, so costs and diffs work across providers. And because
189
+ the proxy speaks `/v1/messages` too, Claude-native agents get the same
190
+ record-or-replay treatment with zero code changes.
191
+
192
+ Anything that speaks the OpenAI protocol (DeepSeek, Qwen, Kimi, GLM,
193
+ vLLM/Ollama, OpenRouter, …) is covered by `capture_openai` / the proxy out
194
+ of the box.
195
+
196
+ ## Export, share, TUI
197
+
198
+ ```bash
199
+ backspin export runs/live.jsonl --format sft -o train.jsonl # eval/SFT datasets
200
+ backspin share runs/live.jsonl # one self-contained HTML: run + viewer
201
+ backspin tui # keyboard-driven viewer for the terminal
202
+ ```
203
+
204
+ `share` bundles the entire viewer and the run into a single `.html` — send it
205
+ to a teammate, they open it in a browser and step through the run. Nothing
206
+ is uploaded anywhere.
207
+
208
+ ## Costs
209
+
210
+ A built-in price table (gpt-4o, claude, gemini, deepseek, …) turns token counts into money: `run.totals()["cost_usd"]`, a cost card in the viewer, `~$0.0142` in `backspin show`. Extending the table is a one-dict PR.
211
+
212
+ ## Diff
213
+
214
+ ```bash
215
+ backspin diff runs/live.backspin.jsonl runs/replay.backspin.jsonl
216
+ # runs diverge at step #14
217
+ # #13 llm gpt-4o-mini gpt-4o-mini yes
218
+ # #14 llm gpt-4o-mini gpt-4o-mini NO
219
+ ```
220
+
221
+ Steps are aligned and signed by *what the agent chose to do* (LLM request fingerprint / tool name), so the first mismatch marks exactly where two runs stopped matching — before costs and latencies are even considered.
222
+
223
+ ## CLI & local viewer
224
+
225
+ ```bash
226
+ backspin ls # list runs: agent, steps, tokens
227
+ backspin show runs/...jsonl # print a run's timeline
228
+ backspin show runs/... --step 7 # dump one step as JSON
229
+ backspin diff a b # diff two runs (exit 1 if they differ)
230
+ backspin ui # http://127.0.0.1:8787 — timeline, inspector, diff
231
+ ```
232
+
233
+ The viewer is a zero-build vanilla JS app served by the CLI: a waterfall timeline, a step inspector (request / response / raw), and side-by-side run diffing.
234
+
235
+ ![backspin timeline viewer](docs/ui-timeline.png)
236
+
237
+ ![backspin diff view](docs/ui-diff.png)
238
+
239
+ ## Keeping secrets out of recordings
240
+
241
+ Recordings contain full prompts and completions. When that's not okay, pass a `redact` function — every payload value goes through it before touching disk:
242
+
243
+ ```python
244
+ from backspin import Recorder
245
+ from backspin.redaction import mask, redact_strings
246
+
247
+ rec = Recorder(
248
+ agent="support-bot",
249
+ redact=redact_strings(mask(r"sk-[A-Za-z0-9]{8,}")),
250
+ )
251
+ ```
252
+
253
+ Structural fields (model, tool name, fingerprint, durations) stay readable so the viewer and diff keep working; everything else — including unknown custom payload keys — passes through the redactor. Fingerprints are computed pre-redaction, so replay matching is unaffected. Note the tradeoff: a redacted run still replays, but replayed values are the redacted ones.
254
+
255
+ ## The run file
256
+
257
+ One run = one self-contained JSONL file. First line is a header, every following line is a step:
258
+
259
+ ```json
260
+ {"kind": "llm", "seq": 3, "ts": 1756448402.1, "model": "gpt-4o-mini",
261
+ "duration_ms": 812.4, "fingerprint": "9f31c2ab77e01d44",
262
+ "request": {"messages": [...]}, "response": {"choices": [...]},
263
+ "usage": {"prompt_tokens": 120, "completion_tokens": 45}}
264
+ ```
265
+
266
+ Kinds: `llm`, `tool`, `log`, `error`, plus whatever custom events you record via `rec.event(kind, **payload)`. Because a run is one file, "please attach the failing run" finally works.
267
+
268
+ ## How backspin compares
269
+
270
+ | | Langfuse / LangSmith / AgentOps | backspin |
271
+ |---|---|---|
272
+ | Question it answers | what happened? | make it happen again, exactly |
273
+ | Where | cloud SaaS | your machine, plain files |
274
+ | Replay with recorded responses | no | yes, deterministic |
275
+ | First-divergence diffing | no | yes |
276
+ | Setup | SDK + account + ingestion | one context manager |
277
+ | Token cost of debugging | full price | zero after recording |
278
+
279
+ They compose well: keep the dashboard if you like it, attach a backspin run when someone says "I can't reproduce it."
280
+
281
+ ## Status & roadmap
282
+
283
+ backspin is a young project (v0.5) — record → replay → what-if → diff → view works end to end across OpenAI and Anthropic protocols, from Python and TypeScript, verified against the real SDKs. Next:
284
+
285
+ - [x] ~~Async + streaming capture, spans, redaction, costs, pytest plugin~~ (0.2/0.3)
286
+ - [x] ~~Sidecar proxy: record + replay, OpenAI protocol~~ (0.3)
287
+ - [x] ~~Anthropic native: SDK capture + proxy `/v1/messages`; TypeScript SDK; export/share/TUI~~ (0.4)
288
+ - [x] ~~Agent-level what-if (re-run the whole agent against a mutated cassette)~~ (0.5)
289
+ - [ ] Docs site with runnable examples
290
+ - [ ] Deterministic clock/random stubs for full boundary capture
291
+
292
+ ## Development
293
+
294
+ ```bash
295
+ pip install -e ".[dev]"
296
+ pytest # full suite incl. real-SDK integration tests
297
+ ruff check backspin/ tests/ # lint
298
+ mypy backspin/ # types
299
+ python examples/mock_agent.py # zero-setup demo: record → replay → diff
300
+ ```
301
+
302
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for ground rules (core stays
303
+ dependency-free; the run format is a contract) and [SECURITY.md](SECURITY.md)
304
+ for how to report vulnerabilities.
305
+
306
+ ## License
307
+
308
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,29 @@
1
+ backspin/__init__.py,sha256=bQpKJx_sdmdtrALxANCxfEsOfvmt8pjKnKEtOOzgsZE,837
2
+ backspin/cli.py,sha256=8lbdsXeYXG-eFwGYmkca1DnYQG6Pl1SjY5D-R-Kw7-Y,13365
3
+ backspin/cost.py,sha256=oyGuGDP8fAPIVXhdA69pKJPZpXxwbIxH--bhByF4XME,2912
4
+ backspin/diff.py,sha256=EPJnCEGpjtVz8qToPewbqoGlTYeZHOZLvDv0PWJLMcU,3850
5
+ backspin/export.py,sha256=eTyVtbN8wiA6RPq0RrhgLzCaYLdwWm8Vwef6HEenRDk,2498
6
+ backspin/fakes.py,sha256=vTr3DHaiOcw8R7PYz5VmpEIfRjxBuPQi8SeqTaN52bw,5780
7
+ backspin/proxy.py,sha256=efccAvB3IM49uqI3WbsLY2lWa4KQLi_36XJkKGCoMlA,14463
8
+ backspin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ backspin/pytest_plugin.py,sha256=z0SrlZha2-x4CNWg7gtAdwi0SbGeO8Z_cJ9YAlEWy5c,3252
10
+ backspin/recorder.py,sha256=ZKVgyrnSHBWxDCY8bIUDpjgJqYNF3YuVPVGJCzOj-0I,12760
11
+ backspin/redaction.py,sha256=jre1ntKyHnI0YyGvOeGDrKR6TUzABfzJm-UPapniPUk,1623
12
+ backspin/replay.py,sha256=6CuknY0vbm1DFoB_LPpuwjrgZ-3GuF3PE5RCfalEqB0,13116
13
+ backspin/runfile.py,sha256=XvoCQTlfZwE4AuGGcC50qjYFKgstX3yfpZfqd7zt2BA,5733
14
+ backspin/server.py,sha256=bK0WFH_jkGW8FrZ70fFht0Yv87K7cVWZS1sbP3YdIuk,3074
15
+ backspin/share.py,sha256=AGK_k4JdkhPDjgZCXu71h5zgU2R6ykk1raBhcannogg,2128
16
+ backspin/testing.py,sha256=aL2bqGZJim78fMmkfFKwymK-Y33QmrZJ7qonCYbx9lE,4102
17
+ backspin/tui.py,sha256=N3g-7mzCzvLNnfDtp3V1e-TVczJMHPX8nK2Q-gsXnfs,4631
18
+ backspin/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ backspin/integrations/anthropic.py,sha256=_RzIfVdpl7fIKEu5bwMzWIqF93kOdyrwffnglHQqKog,9636
20
+ backspin/integrations/openai.py,sha256=YI81Fig6kVTXnFBWnF5earrmirJ4H6OjtkQ31IXWgBA,9416
21
+ backspin/ui/app.js,sha256=50BocxnDWDbd4IuJ9JsTB8op7IoVCsRGiKuGs3xqys8,11719
22
+ backspin/ui/i18n.js,sha256=odjpuEFh_FWATima9H70Yr-BFPH5ObK2DFx8OgMJFJM,4303
23
+ backspin/ui/index.html,sha256=rqDULcSsSBisBJ2SRqUkW6jNkXGKJiY7q4Lp-rWbbJg,2616
24
+ backspin/ui/style.css,sha256=vHVgGd_OySXVOAqQxBKLPFk1GdXfnALLaHEH4YHT27o,6263
25
+ backspin-0.5.1.dist-info/METADATA,sha256=1lZ3BjhR4GWOQw2DIo_EY7WvDWqGQLdh-I5ckqrZVok,13363
26
+ backspin-0.5.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
27
+ backspin-0.5.1.dist-info/entry_points.txt,sha256=WDSjbXprsV95jBR_k_R0G96cOq0iEHEgd9J9i2hdZv8,93
28
+ backspin-0.5.1.dist-info/licenses/LICENSE,sha256=PU2EnDY2G9XOKy00CB8cwS2F-GQC0eoCqGLUy-_QE1o,1078
29
+ backspin-0.5.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ backspin = backspin.cli:main
3
+
4
+ [pytest11]
5
+ backspin = backspin.pytest_plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 backspin contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.