@hybridlabor-api/bdb-synapse 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +28 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
package adapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
"unicode/utf8"
|
|
11
|
+
|
|
12
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/model"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestAgentNodeIDIsStableAndRootScoped(t *testing.T) {
|
|
16
|
+
got := AgentNodeID("codex", "root-key", "codex-agent:child")
|
|
17
|
+
if got != AgentNodeID("codex", "root-key", "codex-agent:child") {
|
|
18
|
+
t.Fatal("unstable")
|
|
19
|
+
}
|
|
20
|
+
if !regexp.MustCompile(`^agt_[0-9a-f]{24}$`).MatchString(got) {
|
|
21
|
+
t.Fatalf("id=%q", got)
|
|
22
|
+
}
|
|
23
|
+
if got == AgentNodeID("codex", "other-root", "codex-agent:child") {
|
|
24
|
+
t.Fatal("not root scoped")
|
|
25
|
+
}
|
|
26
|
+
if got == AgentNodeID("claude-code", "root-key", "codex-agent:child") {
|
|
27
|
+
t.Fatal("not harness scoped")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func TestAgentInstructionPreviewNormalizesAndTruncatesRunes(t *testing.T) {
|
|
32
|
+
if got := AgentInstructionPreview(" review\n\tthis change "); got != "review this change" {
|
|
33
|
+
t.Fatalf("normalized preview = %q", got)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exact := strings.Repeat("字", 240)
|
|
37
|
+
if got := AgentInstructionPreview(exact); got != exact {
|
|
38
|
+
t.Fatalf("preview at limit changed: %q", got)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
got := AgentInstructionPreview(strings.Repeat("字", 241))
|
|
42
|
+
if len([]rune(got)) != 240 {
|
|
43
|
+
t.Fatalf("truncated preview is %d runes, want 240", len([]rune(got)))
|
|
44
|
+
}
|
|
45
|
+
if !strings.HasSuffix(got, "…") {
|
|
46
|
+
t.Fatalf("truncated preview missing ellipsis: %q", got)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestSessionKeyIsStableAndSourceSpecific(t *testing.T) {
|
|
51
|
+
path := filepath.Join(t.TempDir(), "session.jsonl")
|
|
52
|
+
first := SessionKey("codex", path)
|
|
53
|
+
if second := SessionKey("codex", path); second != first {
|
|
54
|
+
t.Fatalf("SessionKey changed: %q != %q", first, second)
|
|
55
|
+
}
|
|
56
|
+
if other := SessionKey("claude-code", path); other == first {
|
|
57
|
+
t.Fatalf("SessionKey ignored harness: %q", other)
|
|
58
|
+
}
|
|
59
|
+
if other := SessionKey("codex", path+".copy"); other == first {
|
|
60
|
+
t.Fatalf("SessionKey ignored path: %q", other)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func TestUserMessageNoteStaysWithinRuneBudget(t *testing.T) {
|
|
65
|
+
long := strings.Repeat("字", userMessageNoteLimit+50)
|
|
66
|
+
note := UserMessageNote(long)
|
|
67
|
+
if got := len([]rune(note)); got != userMessageNoteLimit {
|
|
68
|
+
t.Fatalf("truncated note is %d runes, want %d (ellipsis must fit the budget)", got, userMessageNoteLimit)
|
|
69
|
+
}
|
|
70
|
+
if !strings.HasSuffix(note, "…") {
|
|
71
|
+
t.Fatalf("truncated note missing ellipsis marker: %q", note[len(note)-12:])
|
|
72
|
+
}
|
|
73
|
+
exact := strings.Repeat("a", userMessageNoteLimit)
|
|
74
|
+
if UserMessageNote(exact) != exact {
|
|
75
|
+
t.Fatal("text at the limit must pass through untouched")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestSummarizeToolTruncatesCommandAtRuneBoundary(t *testing.T) {
|
|
80
|
+
command := strings.Repeat("a", 92) + "界tail"
|
|
81
|
+
summary := SummarizeTool("exec", map[string]any{"cmd": command}, nil, nil, false)
|
|
82
|
+
|
|
83
|
+
if !utf8.ValidString(summary) {
|
|
84
|
+
t.Fatalf("summary contains invalid UTF-8: %q", summary)
|
|
85
|
+
}
|
|
86
|
+
want := strings.Repeat("a", 92) + "界... -> 0 targets, 0 outside"
|
|
87
|
+
if summary != want {
|
|
88
|
+
t.Fatalf("summary = %q, want %q", summary, want)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestSummarizeToolExtractsCommandsFromExecWrapper(t *testing.T) {
|
|
93
|
+
tests := []struct {
|
|
94
|
+
name string
|
|
95
|
+
source string
|
|
96
|
+
want string
|
|
97
|
+
}{
|
|
98
|
+
{
|
|
99
|
+
name: "single command",
|
|
100
|
+
source: `const r = await tools.exec_command({cmd:"jq '.city | keys' snapshot.json",workdir:"/tmp"}); text(r.output)`,
|
|
101
|
+
want: "jq '.city | keys' snapshot.json -> 0 targets, 0 outside",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "multiple tool calls",
|
|
105
|
+
source: `const rs = await Promise.all([tools.exec_command({cmd:"go test ./..."}), tools.exec_command({cmd:"go vet ./..."})]); text(rs)`,
|
|
106
|
+
want: "go test ./... (+1 more tool call) -> 0 targets, 0 outside",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "dynamic command keeps nested tool identity",
|
|
110
|
+
source: `const r = await tools.exec_command({cmd,workdir:"/tmp"}); text(r.output)`,
|
|
111
|
+
want: "exec_command -> 0 targets, 0 outside",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "non-command tool keeps nested identity",
|
|
115
|
+
source: `const p = await tools.update_plan({plan:[]}); text(p)`,
|
|
116
|
+
want: "update_plan -> 0 targets, 0 outside",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "mixed calls retain count",
|
|
120
|
+
source: `const r = await tools.exec_command({cmd:"go test ./..."}); const p = await tools.update_plan({plan:[]}); text(r.output); text(p)`,
|
|
121
|
+
want: "go test ./... (+1 more tool call) -> 0 targets, 0 outside",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "plain orchestration remains generic",
|
|
125
|
+
source: `text("done")`,
|
|
126
|
+
want: "exec -> 0 targets, 0 outside",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for _, tt := range tests {
|
|
131
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
132
|
+
got := SummarizeTool("exec", map[string]any{"_raw": tt.source}, nil, nil, false)
|
|
133
|
+
if got != tt.want {
|
|
134
|
+
t.Fatalf("summary = %q, want %q", got, tt.want)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func TestInjectedUserMessageShape(t *testing.T) {
|
|
141
|
+
injected := []string{
|
|
142
|
+
"<system-reminder>context</system-reminder>",
|
|
143
|
+
"<command-name>/review</command-name>",
|
|
144
|
+
"<local-command-caveat>Caveat: …</local-command-caveat>",
|
|
145
|
+
"<environment_context>\nshell: zsh\n</environment_context>",
|
|
146
|
+
"<turn_aborted>true</turn_aborted>",
|
|
147
|
+
"# AGENTS.md instructions for /repo\n\nrules",
|
|
148
|
+
}
|
|
149
|
+
for _, text := range injected {
|
|
150
|
+
if !InjectedUserMessage(text) {
|
|
151
|
+
t.Fatalf("should be injected: %q", text)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Real tasks that merely start with markup must survive.
|
|
155
|
+
genuine := []string{
|
|
156
|
+
"fix the login bug",
|
|
157
|
+
"<div class=\"card\"> 这个组件为什么不居中",
|
|
158
|
+
"<Button onClick={…}/> renders twice, why",
|
|
159
|
+
"# AGENTS 文件怎么写",
|
|
160
|
+
}
|
|
161
|
+
for _, text := range genuine {
|
|
162
|
+
if InjectedUserMessage(text) {
|
|
163
|
+
t.Fatalf("real task misclassified as injected: %q", text)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func TestBuildEventKeepsExecAggregatedAndFindsSingleCommandTarget(t *testing.T) {
|
|
169
|
+
root := t.TempDir()
|
|
170
|
+
writeAdapterTestFile(t, root, "README.md")
|
|
171
|
+
source := `const r = await tools.exec_command({cmd:"sed -n '1,20p' README.md",workdir:` + jsonString(t, root) + `});`
|
|
172
|
+
|
|
173
|
+
event := buildExecEvent(root, map[string]any{"_raw": source})
|
|
174
|
+
if event.Tool != "exec" || event.Action != "read" {
|
|
175
|
+
t.Fatalf("event = %#v", event)
|
|
176
|
+
}
|
|
177
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != "README.md" || !event.Targets[0].Weak || event.Targets[0].Touch != "read" {
|
|
178
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func TestBuildEventExtractsApplyPatchFromExec(t *testing.T) {
|
|
183
|
+
root := t.TempDir()
|
|
184
|
+
writeAdapterTestFile(t, root, "src/main.go")
|
|
185
|
+
patch := "*** Begin Patch\n*** Update File: src/main.go\n@@\n-old\n+new\n*** End Patch"
|
|
186
|
+
source := `const patch = ` + jsonString(t, patch) + `; text(await tools.apply_patch(patch));`
|
|
187
|
+
|
|
188
|
+
event := buildExecEvent(root, map[string]any{"_raw": source})
|
|
189
|
+
if event.Tool != "exec" || event.Action != "edit" {
|
|
190
|
+
t.Fatalf("event = %#v", event)
|
|
191
|
+
}
|
|
192
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != "src/main.go" || event.Targets[0].Touch != "edit" || event.Targets[0].Weak {
|
|
193
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func TestBuildEventExtractsPromiseAllCommandTargets(t *testing.T) {
|
|
198
|
+
root := t.TempDir()
|
|
199
|
+
writeAdapterTestFile(t, root, "first/main.go")
|
|
200
|
+
writeAdapterTestFile(t, root, "second/main.go")
|
|
201
|
+
first := filepath.Join(root, "first")
|
|
202
|
+
second := filepath.Join(root, "second")
|
|
203
|
+
source := `const rs = await Promise.all([
|
|
204
|
+
tools.exec_command({cmd:"sed -n '1,20p' main.go",workdir:` + jsonString(t, first) + `}),
|
|
205
|
+
tools.exec_command({"cmd":"rg TODO main.go","workdir":` + jsonString(t, second) + `})
|
|
206
|
+
]);`
|
|
207
|
+
|
|
208
|
+
event := buildExecEvent(root, map[string]any{"code": source})
|
|
209
|
+
if event.Tool != "exec" || event.Action != "exec" {
|
|
210
|
+
t.Fatalf("event = %#v", event)
|
|
211
|
+
}
|
|
212
|
+
if len(event.Targets) != 2 {
|
|
213
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
214
|
+
}
|
|
215
|
+
want := []struct {
|
|
216
|
+
path string
|
|
217
|
+
touch string
|
|
218
|
+
}{{"first/main.go", "read"}, {"second/main.go", "hit"}}
|
|
219
|
+
for i, target := range event.Targets {
|
|
220
|
+
if target.Path != want[i].path || target.Touch != want[i].touch || !target.Weak {
|
|
221
|
+
t.Fatalf("target %d = %#v", i, target)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func TestBuildEventDecodesEscapedExecStrings(t *testing.T) {
|
|
227
|
+
root := t.TempDir()
|
|
228
|
+
workdir := filepath.Join(root, `quoted"dir`)
|
|
229
|
+
writeAdapterTestFile(t, workdir, "src/main.go")
|
|
230
|
+
command := `sed -n "1,20p" src/main.go`
|
|
231
|
+
source := `tools.exec_command({cmd:` + jsonString(t, command) + `,workdir:` + jsonString(t, workdir) + `});`
|
|
232
|
+
|
|
233
|
+
event := buildExecEvent(root, map[string]any{"script": source})
|
|
234
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != `quoted"dir/src/main.go` || !event.Targets[0].Weak {
|
|
235
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func TestBuildEventAcceptsWorkdirBeforeCommand(t *testing.T) {
|
|
240
|
+
root := t.TempDir()
|
|
241
|
+
workdir := filepath.Join(root, "sub")
|
|
242
|
+
writeAdapterTestFile(t, workdir, "main.go")
|
|
243
|
+
source := `tools.exec_command({workdir:` + jsonString(t, workdir) + `,cmd:"sed main.go"});`
|
|
244
|
+
|
|
245
|
+
event := buildExecEvent(root, map[string]any{"_raw": source})
|
|
246
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != "sub/main.go" {
|
|
247
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func TestBuildEventExecActionRequiresEveryCommandToVerify(t *testing.T) {
|
|
252
|
+
tests := []struct {
|
|
253
|
+
name string
|
|
254
|
+
source string
|
|
255
|
+
want string
|
|
256
|
+
}{
|
|
257
|
+
{
|
|
258
|
+
name: "all verification commands",
|
|
259
|
+
source: `Promise.all([tools.exec_command({cmd:"go test ./..."}), tools.exec_command({cmd:"make test"})])`,
|
|
260
|
+
want: "verify",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "verification and ordinary command",
|
|
264
|
+
source: `Promise.all([tools.exec_command({cmd:"go test ./..."}), tools.exec_command({cmd:"sed -n '1,20p' README.md"})])`,
|
|
265
|
+
want: "exec",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "verification and another tool",
|
|
269
|
+
source: `Promise.all([tools.exec_command({cmd:"go test ./..."}), tools.apply_patch("*** Begin Patch")])`,
|
|
270
|
+
want: "exec",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "dynamic command",
|
|
274
|
+
source: `tools.exec_command({cmd,workdir:"/tmp"})`,
|
|
275
|
+
want: "exec",
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for _, tt := range tests {
|
|
280
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
281
|
+
event := buildExecEvent(t.TempDir(), map[string]any{"_raw": tt.source})
|
|
282
|
+
if event.Tool != "exec" || event.Action != tt.want {
|
|
283
|
+
t.Fatalf("event = %#v, want action %q", event, tt.want)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func TestBuildEventClassifiesBashSearchCommands(t *testing.T) {
|
|
290
|
+
tests := []struct {
|
|
291
|
+
command string
|
|
292
|
+
want string
|
|
293
|
+
}{
|
|
294
|
+
{`grep -rn "Pair with AI" src --include="*.ts" | head -5`, "search"},
|
|
295
|
+
{`find . -name "*.go" | wc -l`, "search"},
|
|
296
|
+
{`cd /repo && rg TODO internal`, "search"},
|
|
297
|
+
{`git grep -n hook`, "search"},
|
|
298
|
+
{`FOO=1 grep -c x file.txt 2>/dev/null`, "search"},
|
|
299
|
+
{`ls web/src`, "search"},
|
|
300
|
+
{`grep x file > out.txt`, "exec"},
|
|
301
|
+
{`find . -name "*.tmp" -delete`, "exec"},
|
|
302
|
+
{`grep x file && rm file`, "exec"},
|
|
303
|
+
{`npm install 2>&1 | tail -3`, "exec"},
|
|
304
|
+
{`echo done`, "exec"},
|
|
305
|
+
{`go test ./... | tail -5`, "verify"},
|
|
306
|
+
{`cat internal/model/stats.go`, "read"},
|
|
307
|
+
{`sed -n '1,240p' web/src/ui/Hud.tsx`, "read"},
|
|
308
|
+
{`nl -ba main.go | head -50`, "read"},
|
|
309
|
+
{`head -n 20 Makefile`, "read"},
|
|
310
|
+
{`sed -i '' 's/a/b/g' main.go`, "exec"},
|
|
311
|
+
{`cat notes.md > backup.md`, "exec"},
|
|
312
|
+
{`cat main.go && rm main.go`, "exec"},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for _, tt := range tests {
|
|
316
|
+
t.Run(tt.command, func(t *testing.T) {
|
|
317
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: t.TempDir()}}
|
|
318
|
+
event := BuildEvent(trace, ToolCall{Name: "Bash", Input: map[string]any{"command": tt.command}}, ToolResult{})
|
|
319
|
+
if event.Action != tt.want {
|
|
320
|
+
t.Fatalf("action(%q) = %q, want %q", tt.command, event.Action, tt.want)
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func TestBuildEventPreservesOutcomeCertainty(t *testing.T) {
|
|
327
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: t.TempDir()}}
|
|
328
|
+
call := ToolCall{Name: "Bash", Input: map[string]any{"command": "go test ./..."}}
|
|
329
|
+
|
|
330
|
+
known := BuildEvent(trace, call, ToolResult{OutcomeKnown: true})
|
|
331
|
+
if !known.OutcomeKnown || known.IsError {
|
|
332
|
+
t.Fatalf("known event = %#v", known)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
unknown := BuildEvent(trace, call, ToolResult{})
|
|
336
|
+
if unknown.OutcomeKnown || unknown.IsError {
|
|
337
|
+
t.Fatalf("unknown event = %#v", unknown)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func TestBuildEventExecActionAllSearchCommands(t *testing.T) {
|
|
342
|
+
source := `Promise.all([tools.exec_command({cmd:"rg TODO main.go"}), tools.exec_command({cmd:"ls src"})])`
|
|
343
|
+
event := buildExecEvent(t.TempDir(), map[string]any{"_raw": source})
|
|
344
|
+
if event.Tool != "exec" || event.Action != "search" {
|
|
345
|
+
t.Fatalf("event = %#v, want action %q", event, "search")
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func TestBuildEventExecActionAllReadCommands(t *testing.T) {
|
|
350
|
+
source := `Promise.all([tools.exec_command({cmd:"sed -n '1,20p' main.go"}), tools.exec_command({cmd:"cat README.md"})])`
|
|
351
|
+
event := buildExecEvent(t.TempDir(), map[string]any{"_raw": source})
|
|
352
|
+
if event.Tool != "exec" || event.Action != "read" {
|
|
353
|
+
t.Fatalf("event = %#v, want action %q", event, "read")
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
func TestCommandReadPaths(t *testing.T) {
|
|
358
|
+
tests := []struct {
|
|
359
|
+
command string
|
|
360
|
+
want []string
|
|
361
|
+
}{
|
|
362
|
+
{`sed -n '1,240p' internal/adapter/adapter.go`, []string{"internal/adapter/adapter.go"}},
|
|
363
|
+
{`cat a.go b.go`, []string{"a.go", "b.go"}},
|
|
364
|
+
{`head -n 20 Makefile`, []string{"Makefile"}},
|
|
365
|
+
{`tail -f logs/app.log`, []string{"logs/app.log"}},
|
|
366
|
+
{`cat src/main.go | rg TODO`, []string{"src/main.go"}},
|
|
367
|
+
{`sed -i '' 's/x/y/' a.go`, nil},
|
|
368
|
+
{`cat file.go > copy.go`, nil},
|
|
369
|
+
{`grep -rn TODO src`, nil},
|
|
370
|
+
{`cat *.go`, nil},
|
|
371
|
+
{`cat <<EOF > notes.md`, nil},
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for _, tt := range tests {
|
|
375
|
+
t.Run(tt.command, func(t *testing.T) {
|
|
376
|
+
got := commandReadPaths(tt.command)
|
|
377
|
+
if len(got) != len(tt.want) {
|
|
378
|
+
t.Fatalf("commandReadPaths(%q) = %#v, want %#v", tt.command, got, tt.want)
|
|
379
|
+
}
|
|
380
|
+
for i := range got {
|
|
381
|
+
if got[i] != tt.want[i] {
|
|
382
|
+
t.Fatalf("commandReadPaths(%q) = %#v, want %#v", tt.command, got, tt.want)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func TestBuildEventDoesNotPairDistantExecWorkdir(t *testing.T) {
|
|
390
|
+
root := t.TempDir()
|
|
391
|
+
writeAdapterTestFile(t, root, "README.md")
|
|
392
|
+
source := `tools.exec_command({cmd:"sed README.md"}); const metadata = {workdir:"/tmp/not-the-command-workdir"};`
|
|
393
|
+
|
|
394
|
+
event := buildExecEvent(root, map[string]any{"_raw": source})
|
|
395
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != "README.md" || !event.Targets[0].Weak {
|
|
396
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
397
|
+
}
|
|
398
|
+
if len(event.Outside) != 0 {
|
|
399
|
+
t.Fatalf("outside = %#v", event.Outside)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
func TestBuildEventIgnoresExecExamplesInStringsAndComments(t *testing.T) {
|
|
404
|
+
root := t.TempDir()
|
|
405
|
+
writeAdapterTestFile(t, root, "README.md")
|
|
406
|
+
source := "const quoted = 'tools.exec_command({cmd:\"go test ./...\"})';\n" +
|
|
407
|
+
"const template = `tools.exec_command({cmd:\"sed README.md\"})`;\n" +
|
|
408
|
+
"// tools.exec_command({cmd:\"sed README.md\"})\n" +
|
|
409
|
+
"/* tools.exec_command({cmd:\"sed README.md\"}) */\n" +
|
|
410
|
+
"text(quoted + template);"
|
|
411
|
+
|
|
412
|
+
event := buildExecEvent(root, map[string]any{"_raw": source})
|
|
413
|
+
if event.Action != "exec" || len(event.Targets) != 0 {
|
|
414
|
+
t.Fatalf("event = %#v", event)
|
|
415
|
+
}
|
|
416
|
+
if event.Summary != "exec -> 0 targets, 0 outside" {
|
|
417
|
+
t.Fatalf("summary = %q", event.Summary)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
func TestBuildEventExtractsJSReplTargets(t *testing.T) {
|
|
422
|
+
root := t.TempDir()
|
|
423
|
+
writeAdapterTestFile(t, root, "packages/db/src/index.ts")
|
|
424
|
+
code := `const db = await import("./packages/db/src/index.ts")`
|
|
425
|
+
|
|
426
|
+
for _, key := range []string{"code", "_raw"} {
|
|
427
|
+
t.Run(key, func(t *testing.T) {
|
|
428
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: root}}
|
|
429
|
+
event := BuildEvent(trace, ToolCall{Name: "js_repl", Input: map[string]any{key: code}}, ToolResult{})
|
|
430
|
+
if event.Tool != "js_repl" || event.Action != "exec" {
|
|
431
|
+
t.Fatalf("event = %#v", event)
|
|
432
|
+
}
|
|
433
|
+
if len(event.Targets) != 1 || event.Targets[0].Path != "packages/db/src/index.ts" || !event.Targets[0].Weak {
|
|
434
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
func buildExecEvent(cwd string, input map[string]any) model.Event {
|
|
441
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: cwd}}
|
|
442
|
+
return BuildEvent(trace, ToolCall{Name: "exec", Input: input}, ToolResult{})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func writeAdapterTestFile(t *testing.T, root, path string) {
|
|
446
|
+
t.Helper()
|
|
447
|
+
fullPath := filepath.Join(root, filepath.FromSlash(path))
|
|
448
|
+
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
|
|
449
|
+
t.Fatal(err)
|
|
450
|
+
}
|
|
451
|
+
if err := os.WriteFile(fullPath, []byte("test\n"), 0o644); err != nil {
|
|
452
|
+
t.Fatal(err)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
func jsonString(t *testing.T, value string) string {
|
|
457
|
+
t.Helper()
|
|
458
|
+
encoded, err := json.Marshal(value)
|
|
459
|
+
if err != nil {
|
|
460
|
+
t.Fatal(err)
|
|
461
|
+
}
|
|
462
|
+
return string(encoded)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
func TestBuildEventClassifiesPiCoreTools(t *testing.T) {
|
|
466
|
+
root := t.TempDir()
|
|
467
|
+
writeAdapterTestFile(t, root, "src/main.go")
|
|
468
|
+
abs := filepath.Join(root, "src", "main.go")
|
|
469
|
+
|
|
470
|
+
tests := []struct {
|
|
471
|
+
tool string
|
|
472
|
+
input map[string]any
|
|
473
|
+
action string
|
|
474
|
+
path string
|
|
475
|
+
touch string
|
|
476
|
+
weak bool
|
|
477
|
+
}{
|
|
478
|
+
{"read", map[string]any{"path": abs, "offset": float64(10), "limit": float64(5)}, "read", "src/main.go", "read", false},
|
|
479
|
+
{"edit", map[string]any{"path": abs, "edits": []any{map[string]any{"oldText": "a", "newText": "b"}}}, "edit", "src/main.go", "edit", false},
|
|
480
|
+
{"write", map[string]any{"path": abs, "content": "x"}, "edit", "src/main.go", "edit", false},
|
|
481
|
+
{"grep", map[string]any{"pattern": "TODO", "path": abs}, "search", "src/main.go", "hit", true},
|
|
482
|
+
{"find", map[string]any{"pattern": "*.go", "path": abs}, "search", "src/main.go", "hit", true},
|
|
483
|
+
{"ls", map[string]any{"path": abs}, "search", "src/main.go", "hit", true},
|
|
484
|
+
{"bash", map[string]any{"command": "go test ./... | tail -5"}, "verify", "", "", false},
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
for _, tt := range tests {
|
|
488
|
+
t.Run(tt.tool, func(t *testing.T) {
|
|
489
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: root}}
|
|
490
|
+
event := BuildEvent(trace, ToolCall{Name: tt.tool, Input: tt.input}, ToolResult{})
|
|
491
|
+
if event.Action != tt.action {
|
|
492
|
+
t.Fatalf("action = %q, want %q", event.Action, tt.action)
|
|
493
|
+
}
|
|
494
|
+
if tt.path == "" {
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
if len(event.Targets) != 1 {
|
|
498
|
+
t.Fatalf("targets = %#v", event.Targets)
|
|
499
|
+
}
|
|
500
|
+
target := event.Targets[0]
|
|
501
|
+
if target.Path != tt.path || target.Touch != tt.touch || target.Weak != tt.weak {
|
|
502
|
+
t.Fatalf("target = %#v", target)
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func TestBuildEventPiReadRecordsLineRange(t *testing.T) {
|
|
509
|
+
trace := &model.Trace{Session: model.TraceSession{Cwd: t.TempDir()}}
|
|
510
|
+
input := map[string]any{"path": "/abs/elsewhere.go", "offset": float64(10), "limit": float64(5)}
|
|
511
|
+
event := BuildEvent(trace, ToolCall{Name: "read", Input: input}, ToolResult{})
|
|
512
|
+
if event.Action != "read" {
|
|
513
|
+
t.Fatalf("action = %q", event.Action)
|
|
514
|
+
}
|
|
515
|
+
if len(event.Outside) != 1 {
|
|
516
|
+
t.Fatalf("outside = %#v", event.Outside)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
package agy
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
|
|
9
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter"
|
|
10
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/model"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type Adapter struct {
|
|
14
|
+
Dir string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func DefaultDir() string {
|
|
18
|
+
home, err := os.UserHomeDir()
|
|
19
|
+
if err != nil {
|
|
20
|
+
return ""
|
|
21
|
+
}
|
|
22
|
+
return filepath.Join(home, ".gemini", "antigravity-cli", "brain")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func (a Adapter) Harness() string { return "agy" }
|
|
26
|
+
|
|
27
|
+
func (a Adapter) SessionDir() string {
|
|
28
|
+
if a.Dir != "" {
|
|
29
|
+
return a.Dir
|
|
30
|
+
}
|
|
31
|
+
return DefaultDir()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func (a Adapter) ListSessions() ([]model.SessionMeta, error) {
|
|
35
|
+
dir := a.SessionDir()
|
|
36
|
+
if info, err := os.Stat(dir); err != nil || !info.IsDir() {
|
|
37
|
+
return nil, nil
|
|
38
|
+
}
|
|
39
|
+
var metas []model.SessionMeta
|
|
40
|
+
entries, err := os.ReadDir(dir)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return nil, err
|
|
43
|
+
}
|
|
44
|
+
for _, entry := range entries {
|
|
45
|
+
if !entry.IsDir() {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
path := filepath.Join(dir, entry.Name(), ".system_generated", "logs", "transcript.jsonl")
|
|
49
|
+
if _, err := os.Stat(path); err == nil {
|
|
50
|
+
if meta, err := a.Summarize(path); err == nil {
|
|
51
|
+
metas = append(metas, meta)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return metas, nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type ToolCall struct {
|
|
59
|
+
Name string `json:"name"`
|
|
60
|
+
Args map[string]string `json:"args"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Step struct {
|
|
64
|
+
StepIndex int `json:"step_index"`
|
|
65
|
+
Source string `json:"source"`
|
|
66
|
+
Type string `json:"type"`
|
|
67
|
+
Status string `json:"status"`
|
|
68
|
+
CreatedAt string `json:"created_at"`
|
|
69
|
+
Content string `json:"content"`
|
|
70
|
+
Thinking string `json:"thinking"`
|
|
71
|
+
ToolCalls []ToolCall `json:"tool_calls"`
|
|
72
|
+
ExitCode *int `json:"exit_code"`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func (a Adapter) SummaryInputs(path string) []string {
|
|
76
|
+
return []string{path}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (a Adapter) Summarize(path string) (model.SessionMeta, error) {
|
|
80
|
+
f, err := os.Open(path)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return model.SessionMeta{}, err
|
|
83
|
+
}
|
|
84
|
+
defer f.Close()
|
|
85
|
+
|
|
86
|
+
id := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(path))))
|
|
87
|
+
meta := model.SessionMeta{
|
|
88
|
+
ID: id,
|
|
89
|
+
Harness: "agy",
|
|
90
|
+
Path: path,
|
|
91
|
+
Title: id,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
adapter.ReadJSONLines(f, func(data []byte) {
|
|
95
|
+
var step Step
|
|
96
|
+
if json.Unmarshal(data, &step) == nil {
|
|
97
|
+
if meta.StartedAt == "" && step.CreatedAt != "" {
|
|
98
|
+
meta.StartedAt = step.CreatedAt
|
|
99
|
+
}
|
|
100
|
+
if step.CreatedAt != "" {
|
|
101
|
+
meta.EndedAt = step.CreatedAt
|
|
102
|
+
}
|
|
103
|
+
if len(step.ToolCalls) > 0 {
|
|
104
|
+
meta.EventCount += len(step.ToolCalls)
|
|
105
|
+
}
|
|
106
|
+
if step.Source == "USER_EXPLICIT" {
|
|
107
|
+
meta.UserTurns++
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
return meta, nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func (a Adapter) Parse(path string) (*model.Trace, error) {
|
|
115
|
+
f, err := os.Open(path)
|
|
116
|
+
if err != nil {
|
|
117
|
+
return nil, err
|
|
118
|
+
}
|
|
119
|
+
defer f.Close()
|
|
120
|
+
|
|
121
|
+
id := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(path))))
|
|
122
|
+
trace := &model.Trace{
|
|
123
|
+
Version: 1,
|
|
124
|
+
Session: model.TraceSession{
|
|
125
|
+
ID: id,
|
|
126
|
+
Harness: "agy",
|
|
127
|
+
Path: path,
|
|
128
|
+
Title: id,
|
|
129
|
+
},
|
|
130
|
+
Events: []model.Event{},
|
|
131
|
+
Marks: []model.Mark{},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
adapter.ReadJSONLines(f, func(data []byte) {
|
|
135
|
+
var step Step
|
|
136
|
+
if json.Unmarshal(data, &step) != nil {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if trace.Session.StartedAt == "" && step.CreatedAt != "" {
|
|
141
|
+
trace.Session.StartedAt = step.CreatedAt
|
|
142
|
+
}
|
|
143
|
+
if step.CreatedAt != "" {
|
|
144
|
+
trace.Session.EndedAt = step.CreatedAt
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if step.Source == "USER_EXPLICIT" && step.Type == "USER_INPUT" {
|
|
148
|
+
trace.Marks = append(trace.Marks, model.Mark{
|
|
149
|
+
Seq: len(trace.Events),
|
|
150
|
+
Type: "user-message",
|
|
151
|
+
Note: adapter.UserMessageNote(step.Content),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if len(step.ToolCalls) > 0 {
|
|
156
|
+
for i, tc := range step.ToolCalls {
|
|
157
|
+
callID := fmt.Sprintf("step-%d-call-%d", step.StepIndex, i)
|
|
158
|
+
|
|
159
|
+
call := adapter.ToolCall{
|
|
160
|
+
ID: callID,
|
|
161
|
+
Name: tc.Name,
|
|
162
|
+
Input: make(map[string]any),
|
|
163
|
+
Timestamp: step.CreatedAt,
|
|
164
|
+
}
|
|
165
|
+
for k, v := range tc.Args {
|
|
166
|
+
call.Input[k] = v
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if tc.Name == "invoke_subagent" {
|
|
170
|
+
trace.Marks = append(trace.Marks, model.Mark{
|
|
171
|
+
Seq: len(trace.Events),
|
|
172
|
+
Type: "subagent",
|
|
173
|
+
Note: tc.Name,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
result := adapter.ToolResult{
|
|
178
|
+
OutcomeKnown: false,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
event := adapter.BuildEvent(trace, call, result)
|
|
182
|
+
trace.Events = append(trace.Events, event)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
for i := range trace.Events {
|
|
188
|
+
trace.Events[i].Seq = i
|
|
189
|
+
}
|
|
190
|
+
trace.Session.EventCount = len(trace.Events)
|
|
191
|
+
trace.Stats = model.ComputeStats(trace, 0, model.ObservabilityExact)
|
|
192
|
+
return trace, nil
|
|
193
|
+
}
|