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/__init__.py +38 -0
- backspin/cli.py +379 -0
- backspin/cost.py +85 -0
- backspin/diff.py +121 -0
- backspin/export.py +71 -0
- backspin/fakes.py +151 -0
- backspin/integrations/__init__.py +0 -0
- backspin/integrations/anthropic.py +279 -0
- backspin/integrations/openai.py +298 -0
- backspin/proxy.py +367 -0
- backspin/py.typed +0 -0
- backspin/pytest_plugin.py +83 -0
- backspin/recorder.py +346 -0
- backspin/redaction.py +49 -0
- backspin/replay.py +327 -0
- backspin/runfile.py +182 -0
- backspin/server.py +91 -0
- backspin/share.py +58 -0
- backspin/testing.py +121 -0
- backspin/tui.py +130 -0
- backspin/ui/app.js +327 -0
- backspin/ui/i18n.js +75 -0
- backspin/ui/index.html +68 -0
- backspin/ui/style.css +214 -0
- backspin-0.5.1.dist-info/METADATA +308 -0
- backspin-0.5.1.dist-info/RECORD +29 -0
- backspin-0.5.1.dist-info/WHEEL +4 -0
- backspin-0.5.1.dist-info/entry_points.txt +5 -0
- backspin-0.5.1.dist-info/licenses/LICENSE +21 -0
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
|
+
[](https://github.com/zaibuchihuoji/backspin/actions/workflows/ci.yml)
|
|
50
|
+
[](https://pypi.org/project/backspin/)
|
|
51
|
+
[](https://pypi.org/project/backspin/)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
[](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
|
+

|
|
236
|
+
|
|
237
|
+

|
|
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,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.
|