@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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. 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
+ }