@pi-in-go/pigpen-ahp 0.1.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 (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,387 @@
1
+ package mapper_test
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "regexp"
9
+ "sort"
10
+ "strings"
11
+ "testing"
12
+
13
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
14
+
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
16
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
17
+ )
18
+
19
+ // recorded is one captured pi event stream (upstream test/fixtures/*.json, recorded from a real
20
+ // model and scrubbed).
21
+ type recorded struct {
22
+ Name string `json:"name"`
23
+ Description string `json:"description"`
24
+ Prompt string `json:"prompt"`
25
+ Events []obj `json:"events"`
26
+ }
27
+
28
+ // expectedFixtures is the manifest of upstream's recorded corpus: a missing, extra or renamed
29
+ // fixture fails the load, as in upstream's loadRecordedFixtures.
30
+ var expectedFixtures = []string{
31
+ "abort", "bash-long-output", "compaction", "parallel-tools", "plain-text", "single-tool", "steering",
32
+ "tool-bash", "tool-edit", "tool-error", "tool-find", "tool-grep", "tool-loop", "tool-ls", "tool-write",
33
+ }
34
+
35
+ func loadRecorded(t testing.TB) []recorded {
36
+ t.Helper()
37
+ files, err := filepath.Glob("testdata/fixtures/*.json")
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ var names []string
42
+ for _, f := range files {
43
+ names = append(names, strings.TrimSuffix(filepath.Base(f), ".json"))
44
+ }
45
+ sort.Strings(names)
46
+ want := append([]string(nil), expectedFixtures...)
47
+ sort.Strings(want)
48
+ if fmt.Sprint(names) != fmt.Sprint(want) {
49
+ t.Fatalf("recorded fixture set differs from the manifest; expected=%v actual=%v", want, names)
50
+ }
51
+ var out []recorded
52
+ for _, n := range expectedFixtures {
53
+ raw, err := os.ReadFile(filepath.Join("testdata/fixtures", n+".json"))
54
+ if err != nil {
55
+ t.Fatal(err)
56
+ }
57
+ var r recorded
58
+ if err := json.Unmarshal(raw, &r); err != nil {
59
+ t.Fatalf("%s: %v", n, err)
60
+ }
61
+ if r.Name != n || r.Prompt == "" || len(r.Events) == 0 {
62
+ t.Fatalf("%s: malformed fixture", n)
63
+ }
64
+ out = append(out, r)
65
+ }
66
+ return out
67
+ }
68
+
69
+ func replayFixture(t testing.TB, r recorded) replayed {
70
+ t.Helper()
71
+ m := mapper.NewTurnMapper("replay-turn", 0)
72
+ actions := []ahptypes.StateAction{mapper.UserTurnStarted("replay-turn", r.Prompt, "1970-01-01T00:00:00.000Z")}
73
+ for _, e := range r.Events {
74
+ actions = append(actions, m.Handle(e)...)
75
+ }
76
+ return replayed{actions: actions, state: reduce(t, actions)}
77
+ }
78
+
79
+ func markdownText(state obj) string {
80
+ return strings.Join(contents(partsOfKind(state, "markdown")), "")
81
+ }
82
+
83
+ func toolResultText(call obj) string {
84
+ var sb strings.Builder
85
+ for _, b := range list(call, "content") {
86
+ if str(b, "type") == "text" {
87
+ sb.WriteString(str(b, "text"))
88
+ }
89
+ }
90
+ return sb.String()
91
+ }
92
+
93
+ func assertReplayInvariants(t *testing.T, r recorded, rep replayed) {
94
+ t.Helper()
95
+ opened := len(ofType(rep.actions, "chat/turnStarted"))
96
+ terminators := len(ofType(rep.actions, "chat/turnComplete")) + len(ofType(rep.actions, "chat/turnCancelled")) + len(ofType(rep.actions, "chat/error"))
97
+ // More than one turn means pi injected a message mid-run; each still has to terminate exactly once.
98
+ if terminators != opened {
99
+ t.Fatalf("%s: every opened turn must terminate once (%d opened, %d terminated)", r.Name, opened, terminators)
100
+ }
101
+ if rep.state["activeTurn"] != nil {
102
+ t.Fatalf("%s: active turn remained after replay", r.Name)
103
+ }
104
+ if n := len(list(rep.state, "turns")); n != opened {
105
+ t.Fatalf("%s: reduced turn count %d differs from opened turns %d", r.Name, n, opened)
106
+ }
107
+ assertActionsValid(t, rep.actions, r.Name)
108
+ // Every tool call ends completed or cancelled: none is left running.
109
+ for _, turn := range list(rep.state, "turns") {
110
+ for _, p := range list(turn, "responseParts") {
111
+ if str(p, "kind") == "toolCall" {
112
+ if st := str(p, "toolCall", "status"); st != "completed" && st != "cancelled" {
113
+ t.Fatalf("%s: %s ended in %s", r.Name, str(p, "toolCall", "toolName"), st)
114
+ }
115
+ }
116
+ }
117
+ }
118
+ // A delta naming an unknown partId is a silent reducer no-op, so validate creation order as
119
+ // well as uniqueness.
120
+ created := map[string]bool{}
121
+ var ids []string
122
+ for _, a := range rep.actions {
123
+ o := asObj(a)
124
+ switch o["type"] {
125
+ case "chat/responsePart":
126
+ if id := str(o, "part", "id"); id != "" {
127
+ ids = append(ids, id)
128
+ created[id] = true
129
+ }
130
+ case "chat/delta", "chat/reasoning":
131
+ if !created[str(o, "partId")] {
132
+ t.Fatalf("%s: delta targets unknown part %s", r.Name, str(o, "partId"))
133
+ }
134
+ }
135
+ }
136
+ seen := map[string]bool{}
137
+ for _, id := range ids {
138
+ if seen[id] {
139
+ t.Fatalf("%s: response part ids collided within a turn: %s", r.Name, id)
140
+ }
141
+ seen[id] = true
142
+ }
143
+ }
144
+
145
+ // Twins of upstream test/mapper-fixtures.test.ts.
146
+ func TestRecordedStreamReplay(t *testing.T) {
147
+ fixtures := loadRecorded(t)
148
+ byName := map[string]replayed{}
149
+ src := map[string]recorded{}
150
+ for _, r := range fixtures {
151
+ byName[r.Name] = replayFixture(t, r)
152
+ src[r.Name] = r
153
+ }
154
+
155
+ // One literal twin per recorded stream (the twin gate reads titles from source).
156
+ twin.Run(t, "mapper-fixtures", "plain-text — A reply with no tool calls — the simplest possible turn.", func(t *testing.T) {
157
+ assertReplayInvariants(t, src["plain-text"], byName["plain-text"])
158
+ })
159
+
160
+ twin.Run(t, "mapper-fixtures", "single-tool — One tool call, then an answer derived from its result.", func(t *testing.T) {
161
+ assertReplayInvariants(t, src["single-tool"], byName["single-tool"])
162
+ })
163
+
164
+ twin.Run(t, "mapper-fixtures", "parallel-tools — Several tool calls in one assistant message — exercises contentIndex fan-out.", func(t *testing.T) {
165
+ assertReplayInvariants(t, src["parallel-tools"], byName["parallel-tools"])
166
+ })
167
+
168
+ twin.Run(t, "mapper-fixtures", "tool-loop — Two or more assistant messages in one turn — the case where contentIndex restarts and partIds must not collide.", func(t *testing.T) {
169
+ assertReplayInvariants(t, src["tool-loop"], byName["tool-loop"])
170
+ })
171
+
172
+ twin.Run(t, "mapper-fixtures", "tool-error — A failing tool call the agent has to recover from.", func(t *testing.T) {
173
+ assertReplayInvariants(t, src["tool-error"], byName["tool-error"])
174
+ })
175
+
176
+ twin.Run(t, "mapper-fixtures", "abort — A turn cancelled while it is running.", func(t *testing.T) {
177
+ assertReplayInvariants(t, src["abort"], byName["abort"])
178
+ })
179
+
180
+ twin.Run(t, "mapper-fixtures", "steering — A steering message injected into a running turn.", func(t *testing.T) {
181
+ assertReplayInvariants(t, src["steering"], byName["steering"])
182
+ })
183
+
184
+ twin.Run(t, "mapper-fixtures", "tool-edit — An `edit` call — the one tool whose result carries a diff and a patch.", func(t *testing.T) {
185
+ assertReplayInvariants(t, src["tool-edit"], byName["tool-edit"])
186
+ })
187
+
188
+ twin.Run(t, "mapper-fixtures", "tool-write — A `write` call — a whole new file rather than an edit to one.", func(t *testing.T) {
189
+ assertReplayInvariants(t, src["tool-write"], byName["tool-write"])
190
+ })
191
+
192
+ twin.Run(t, "mapper-fixtures", "tool-bash — A `bash` call — stdout, exit status, and pi's truncation metadata.", func(t *testing.T) {
193
+ assertReplayInvariants(t, src["tool-bash"], byName["tool-bash"])
194
+ })
195
+
196
+ twin.Run(t, "mapper-fixtures", "tool-ls — An `ls` call — a directory listing, which reports its own entry limit.", func(t *testing.T) {
197
+ assertReplayInvariants(t, src["tool-ls"], byName["tool-ls"])
198
+ })
199
+
200
+ twin.Run(t, "mapper-fixtures", "tool-grep — A `grep` call — match counts and line truncation live in its details.", func(t *testing.T) {
201
+ assertReplayInvariants(t, src["tool-grep"], byName["tool-grep"])
202
+ })
203
+
204
+ twin.Run(t, "mapper-fixtures", "tool-find — A `find` call — path globbing, with its own result limit.", func(t *testing.T) {
205
+ assertReplayInvariants(t, src["tool-find"], byName["tool-find"])
206
+ })
207
+
208
+ twin.Run(t, "mapper-fixtures", "compaction — A manual compaction — `buildContextEntries` starts from the newest one, so this is the boundary history rebuilding and turn paging are built around.", func(t *testing.T) {
209
+ assertReplayInvariants(t, src["compaction"], byName["compaction"])
210
+ })
211
+
212
+ twin.Run(t, "mapper-fixtures", "bash-long-output — A bash call whose output is large enough for pi to stream updates and report truncation.", func(t *testing.T) {
213
+ assertReplayInvariants(t, src["bash-long-output"], byName["bash-long-output"])
214
+ })
215
+
216
+ twin.Run(t, "mapper-fixtures", "plain-text: answers with no tool calls", func(t *testing.T) {
217
+ st := byName["plain-text"].state
218
+ if got := str(st, "turns", 0, "state"); got != "complete" {
219
+ t.Fatal(got)
220
+ }
221
+ if n := len(toolCalls(st)); n != 0 {
222
+ t.Fatalf("tool calls = %d", n)
223
+ }
224
+ if !regexp.MustCompile(`(?i)PONG`).MatchString(markdownText(st)) {
225
+ t.Fatal(markdownText(st))
226
+ }
227
+ })
228
+
229
+ twin.Run(t, "mapper-fixtures", "describes a tool call in terms a user can act on", func(t *testing.T) {
230
+ // A heading, a line while it runs and a line once it has: a message that only repeats the
231
+ // tool's name spends all three on saying `read` three times.
232
+ for _, r := range fixtures {
233
+ for _, turn := range list(byName[r.Name].state, "turns") {
234
+ for _, p := range list(turn, "responseParts") {
235
+ call, _ := get(p, "toolCall").(obj)
236
+ if call == nil {
237
+ continue
238
+ }
239
+ if in := str(call, "toolInput"); in == "" || in == "{}" {
240
+ continue
241
+ }
242
+ if str(call, "invocationMessage") == str(call, "toolName") {
243
+ t.Fatalf("%s: %s announces itself with nothing but its own name", r.Name, str(call, "toolName"))
244
+ }
245
+ // Identical text leaves a completed call still claiming to be running.
246
+ if str(call, "pastTenseMessage") == str(call, "invocationMessage") {
247
+ t.Fatalf("%s: %s still says it is %s after it finished", r.Name, str(call, "toolName"), str(call, "invocationMessage"))
248
+ }
249
+ }
250
+ }
251
+ }
252
+ })
253
+
254
+ twin.Run(t, "mapper-fixtures", "shows a tool's subject rather than its arguments as JSON", func(t *testing.T) {
255
+ for _, r := range fixtures {
256
+ for _, turn := range list(byName[r.Name].state, "turns") {
257
+ for _, p := range list(turn, "responseParts") {
258
+ call, _ := get(p, "toolCall").(obj)
259
+ if call == nil {
260
+ continue
261
+ }
262
+ if in, ok := call["toolInput"].(string); ok && strings.HasPrefix(in, "{") {
263
+ t.Fatalf("%s: %s shows its arguments as JSON: %.60s", r.Name, str(call, "toolName"), in)
264
+ }
265
+ }
266
+ }
267
+ }
268
+ })
269
+
270
+ twin.Run(t, "mapper-fixtures", "tool-edit: hands the client the patch, not a count of edited blocks", func(t *testing.T) {
271
+ var edits []obj
272
+ for _, c := range toolCalls(byName["tool-edit"].state) {
273
+ if str(c, "toolName") == "edit" {
274
+ edits = append(edits, c)
275
+ }
276
+ }
277
+ if len(edits) < 1 {
278
+ t.Fatal("expected an edit call")
279
+ }
280
+ for _, c := range edits {
281
+ shown := toolResultText(c)
282
+ if !regexp.MustCompile(`^--- |\n--- `).MatchString(shown) {
283
+ t.Fatalf("edit result carries no patch: %.80s", shown)
284
+ }
285
+ if !regexp.MustCompile(`(?m)^\+.*$`).MatchString(shown) {
286
+ t.Fatal("a patch with no added lines is not a patch")
287
+ }
288
+ }
289
+ })
290
+
291
+ twin.Run(t, "mapper-fixtures", "single-tool: runs one tool and answers from its result", func(t *testing.T) {
292
+ st := byName["single-tool"].state
293
+ if got := str(st, "turns", 0, "state"); got != "complete" {
294
+ t.Fatal(got)
295
+ }
296
+ if len(toolCalls(st)) < 1 {
297
+ t.Fatal("no tool call")
298
+ }
299
+ if !strings.Contains(markdownText(st), "ALPHA BETA GAMMA") {
300
+ t.Fatal(markdownText(st))
301
+ }
302
+ })
303
+
304
+ twin.Run(t, "mapper-fixtures", "parallel-tools: keeps several tool calls in one message distinct", func(t *testing.T) {
305
+ st := byName["parallel-tools"].state
306
+ if n := len(toolCalls(st)); n < 2 {
307
+ t.Fatalf("expected multiple tool calls, got %d", n)
308
+ }
309
+ if !strings.Contains(markdownText(st), "FIRST") || !strings.Contains(markdownText(st), "SECOND") {
310
+ t.Fatal(markdownText(st))
311
+ }
312
+ })
313
+
314
+ twin.Run(t, "mapper-fixtures", "tool-loop: spans several assistant messages without part collisions", func(t *testing.T) {
315
+ assistant := 0
316
+ for _, e := range src["tool-loop"].Events {
317
+ if e["type"] == "message_start" && str(e, "message", "role") == "assistant" {
318
+ assistant++
319
+ }
320
+ }
321
+ // The scenario exists to produce more than one assistant message, where contentIndex restarts at 0.
322
+ if assistant < 2 {
323
+ t.Fatalf("expected multiple assistant messages, got %d", assistant)
324
+ }
325
+ st := byName["tool-loop"].state
326
+ if got := str(st, "turns", 0, "state"); got != "complete" {
327
+ t.Fatal(got)
328
+ }
329
+ if !strings.Contains(markdownText(st), "42") {
330
+ t.Fatal(markdownText(st))
331
+ }
332
+ })
333
+
334
+ twin.Run(t, "mapper-fixtures", "tool-error: surfaces a failed tool without failing the turn", func(t *testing.T) {
335
+ st := byName["tool-error"].state
336
+ var failed []obj
337
+ for _, c := range toolCalls(st) {
338
+ if c["success"] == false {
339
+ failed = append(failed, c)
340
+ }
341
+ }
342
+ if len(failed) < 1 {
343
+ t.Fatal("expected a failed tool call")
344
+ }
345
+ // What the client shows for a failure is error.message: pi says why (an ENOENT naming the
346
+ // path); repeating the tool's name tells the user nothing.
347
+ for _, c := range failed {
348
+ reported := toolResultText(c)
349
+ if reported == "" {
350
+ t.Fatalf("%s: no failure text to show", str(c, "toolName"))
351
+ }
352
+ if got := str(c, "error", "message"); got != reported {
353
+ t.Fatalf("%s: error.message %q should carry what pi reported %q", str(c, "toolName"), got, reported)
354
+ }
355
+ }
356
+ if got := str(st, "turns", 0, "state"); got != "complete" {
357
+ t.Fatal(got)
358
+ }
359
+ })
360
+
361
+ twin.Run(t, "mapper-fixtures", "abort: ends the turn as cancelled", func(t *testing.T) {
362
+ if got := str(byName["abort"].state, "turns", 0, "state"); got != "cancelled" {
363
+ t.Fatal(got)
364
+ }
365
+ })
366
+
367
+ twin.Run(t, "mapper-fixtures", "steering: the injected message becomes its own turn", func(t *testing.T) {
368
+ injected := 0
369
+ for _, e := range src["steering"].Events {
370
+ if e["type"] == "message_start" && str(e, "message", "role") == "user" {
371
+ injected++
372
+ }
373
+ }
374
+ if injected < 2 {
375
+ t.Fatal("expected the steering message to appear mid-run")
376
+ }
377
+ st := byName["steering"].state
378
+ // pi stores an injected message as an ordinary user message, so a rebuild from disk makes it
379
+ // a turn; the live path matches.
380
+ if n := len(list(st, "turns")); n != injected {
381
+ t.Fatalf("turns = %d, injected = %d", n, injected)
382
+ }
383
+ if !strings.Contains(str(st, "turns", -1, "message", "text"), "Stop counting") {
384
+ t.Fatal(str(st, "turns", -1, "message", "text"))
385
+ }
386
+ })
387
+ }
@@ -0,0 +1,251 @@
1
+ package mapper_test
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/microsoft/agent-host-protocol/clients/go/ahp"
10
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
11
+
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/channels"
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
14
+ )
15
+
16
+ // obj is a decoded JSON object; the pi events and the reduced chat state are compared as data,
17
+ // the way upstream's tests compare plain objects.
18
+ type obj = map[string]any
19
+
20
+ const (
21
+ chatURI = "ahp-chat:/c1"
22
+ turnID = "turn-1"
23
+ )
24
+
25
+ // pi mirrors the `pi` shorthand object of upstream's event-mapper.test.ts.
26
+ var pi = struct {
27
+ agentStart, settled, assistantStart func() obj
28
+ agentEnd func(willRetry bool) obj
29
+ assistantEnd func(usage obj) obj
30
+ text, thinking func(idx int, delta string) obj
31
+ textEnd, thinkingEnd func(idx int, content string) obj
32
+ toolStart func(idx int, id, name string) obj
33
+ toolDelta func(idx int, delta string) obj
34
+ toolEnd func(idx int, id, name string, args any) obj
35
+ execUpdate func(id, name, text string) obj
36
+ execEnd func(id, name, text string, isError bool) obj
37
+ streamError func(message string, aborted bool) obj
38
+ }{
39
+ agentStart: func() obj { return obj{"type": "agent_start"} },
40
+ agentEnd: func(w bool) obj { return obj{"type": "agent_end", "messages": []any{}, "willRetry": w} },
41
+ settled: func() obj { return obj{"type": "agent_settled"} },
42
+ assistantStart: func() obj {
43
+ return obj{"type": "message_start", "message": obj{"role": "assistant", "content": []any{}}}
44
+ },
45
+ assistantEnd: func(usage obj) obj {
46
+ msg := obj{"role": "assistant", "content": []any{}}
47
+ if usage != nil {
48
+ msg["usage"] = usage
49
+ msg["model"] = "test-model"
50
+ }
51
+ return obj{"type": "message_end", "message": msg}
52
+ },
53
+ text: func(i int, d string) obj { return update(obj{"type": "text_delta", "contentIndex": i, "delta": d}) },
54
+ thinking: func(i int, d string) obj { return update(obj{"type": "thinking_delta", "contentIndex": i, "delta": d}) },
55
+ textEnd: func(i int, c string) obj { return update(obj{"type": "text_end", "contentIndex": i, "content": c}) },
56
+ thinkingEnd: func(i int, c string) obj { return update(obj{"type": "thinking_end", "contentIndex": i, "content": c}) },
57
+ toolStart: func(i int, id, name string) obj {
58
+ content := make([]any, i+1)
59
+ for k := range content {
60
+ content[k] = obj{}
61
+ }
62
+ content[i] = obj{"type": "toolCall", "id": id, "name": name}
63
+ return update(obj{"type": "toolcall_start", "contentIndex": i, "partial": obj{"content": content}})
64
+ },
65
+ toolDelta: func(i int, d string) obj { return update(obj{"type": "toolcall_delta", "contentIndex": i, "delta": d}) },
66
+ toolEnd: func(i int, id, name string, args any) obj {
67
+ return update(obj{"type": "toolcall_end", "contentIndex": i, "toolCall": obj{"id": id, "name": name, "arguments": args}})
68
+ },
69
+ execUpdate: func(id, name, text string) obj {
70
+ return obj{"type": "tool_execution_update", "toolCallId": id, "toolName": name,
71
+ "partialResult": obj{"content": []any{obj{"type": "text", "text": text}}}}
72
+ },
73
+ execEnd: func(id, name, text string, isError bool) obj {
74
+ return obj{"type": "tool_execution_end", "toolCallId": id, "toolName": name,
75
+ "result": obj{"content": []any{obj{"type": "text", "text": text}}}, "isError": isError}
76
+ },
77
+ streamError: func(message string, aborted bool) obj {
78
+ reason := "error"
79
+ if aborted {
80
+ reason = "aborted"
81
+ }
82
+ return update(obj{"type": "error", "reason": reason, "error": obj{"errorMessage": message, "stopReason": reason}})
83
+ },
84
+ }
85
+
86
+ func update(ame obj) obj {
87
+ return obj{"type": "message_update", "message": obj{"role": "assistant"}, "assistantMessageEvent": ame}
88
+ }
89
+
90
+ // replayed is the outcome of running a turn end to end.
91
+ type replayed struct {
92
+ actions []ahptypes.StateAction
93
+ state obj // the reduced ChatState as JSON data
94
+ }
95
+
96
+ // reduce applies actions to a fresh chat with the protocol's own reducer.
97
+ func reduce(t testing.TB, actions []ahptypes.StateAction) obj {
98
+ t.Helper()
99
+ state := channels.InitialChatState(chatURI, "Test chat", nil)
100
+ for _, a := range actions {
101
+ ahp.ApplyActionToChat(state, a)
102
+ }
103
+ return toObj(t, state)
104
+ }
105
+
106
+ func toObj(t testing.TB, v any) obj {
107
+ t.Helper()
108
+ raw, err := json.Marshal(v)
109
+ if err != nil {
110
+ t.Fatal(err)
111
+ }
112
+ var out obj
113
+ if err := json.Unmarshal(raw, &out); err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ return out
117
+ }
118
+
119
+ func actionType(a ahptypes.StateAction) string { return string(asObj(a)["type"].(string)) }
120
+
121
+ func asObj(v any) obj {
122
+ raw, err := json.Marshal(v)
123
+ if err != nil {
124
+ panic(err)
125
+ }
126
+ var out obj
127
+ if err := json.Unmarshal(raw, &out); err != nil {
128
+ panic(err)
129
+ }
130
+ return out
131
+ }
132
+
133
+ func ofType(actions []ahptypes.StateAction, typ string) []obj {
134
+ var out []obj
135
+ for _, a := range actions {
136
+ if o := asObj(a); o["type"] == typ {
137
+ out = append(out, o)
138
+ }
139
+ }
140
+ return out
141
+ }
142
+
143
+ func newMapper() *mapper.TurnMapper { return mapper.NewTurnMapper(turnID, 0) }
144
+
145
+ // runTurn feeds events through a mapper and reduces the result, like upstream's runTurn.
146
+ func runTurn(t testing.TB, events []obj, text ...string) replayed {
147
+ t.Helper()
148
+ prompt := "Hello"
149
+ if len(text) > 0 {
150
+ prompt = text[0]
151
+ }
152
+ m := newMapper()
153
+ actions := []ahptypes.StateAction{mapper.UserTurnStarted(turnID, prompt, "1970-01-01T00:00:00.000Z")}
154
+ for _, e := range events {
155
+ actions = append(actions, m.Handle(e)...)
156
+ }
157
+ return replayed{actions: actions, state: reduce(t, actions)}
158
+ }
159
+
160
+ // get walks decoded JSON: strings index objects, ints index arrays.
161
+ func get(v any, path ...any) any {
162
+ for _, p := range path {
163
+ switch k := p.(type) {
164
+ case string:
165
+ m, ok := v.(obj)
166
+ if !ok {
167
+ return nil
168
+ }
169
+ v = m[k]
170
+ case int:
171
+ a, ok := v.([]any)
172
+ if !ok {
173
+ return nil
174
+ }
175
+ if k < 0 {
176
+ k += len(a)
177
+ }
178
+ if k < 0 || k >= len(a) {
179
+ return nil
180
+ }
181
+ v = a[k]
182
+ }
183
+ }
184
+ return v
185
+ }
186
+
187
+ func list(v any, path ...any) []any {
188
+ l, _ := get(v, path...).([]any)
189
+ return l
190
+ }
191
+
192
+ func str(v any, path ...any) string {
193
+ s, _ := get(v, path...).(string)
194
+ return s
195
+ }
196
+
197
+ // partsOfKind returns the response parts of the first (or active) turn with that kind.
198
+ func partsOfKind(state obj, kind string) []obj {
199
+ turn := get(state, "turns", 0)
200
+ if turn == nil {
201
+ turn = get(state, "activeTurn")
202
+ }
203
+ var out []obj
204
+ for _, p := range list(turn, "responseParts") {
205
+ if str(p, "kind") == kind {
206
+ out = append(out, p.(obj))
207
+ }
208
+ }
209
+ return out
210
+ }
211
+
212
+ func contents(parts []obj) []string {
213
+ out := []string{}
214
+ for _, p := range parts {
215
+ out = append(out, str(p, "content"))
216
+ }
217
+ return out
218
+ }
219
+
220
+ func toolCalls(state obj) []obj {
221
+ var out []obj
222
+ for _, p := range partsOfKind(state, "toolCall") {
223
+ out = append(out, p["toolCall"].(obj))
224
+ }
225
+ return out
226
+ }
227
+
228
+ func equal(t testing.TB, got, want any, what string) {
229
+ t.Helper()
230
+ g, _ := json.Marshal(got)
231
+ w, _ := json.Marshal(want)
232
+ if string(g) != string(w) {
233
+ t.Fatalf("%s:\n got %s\n want %s", what, g, w)
234
+ }
235
+ }
236
+
237
+ func assertActionsValid(t testing.TB, actions []ahptypes.StateAction, context string) {
238
+ t.Helper()
239
+ for _, a := range actions {
240
+ if err := checkAction(a); err != nil {
241
+ t.Fatalf("%s: non-conforming %s: %v", context, actionType(a), err)
242
+ }
243
+ }
244
+ }
245
+
246
+ func fail(t testing.TB, format string, args ...any) {
247
+ t.Helper()
248
+ t.Fatal(fmt.Sprintf(format, args...))
249
+ }
250
+
251
+ var _ = strings.TrimSpace