@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,34 @@
1
+ package mapper
2
+
3
+ // Shared display-title rules for live, catalogued and hydrated sessions (src/pi/session-title.ts).
4
+
5
+ const (
6
+ // NewSessionTitle is the title of a session nobody has named or spoken in yet.
7
+ NewSessionTitle = "New Session"
8
+ untitledSessionTitle = "Untitled session"
9
+ titleFallbackLength = 60
10
+ )
11
+
12
+ // FallbackSessionTitle is what pi displays when no explicit session name exists: the first user
13
+ // message, collapsed. ok is false when there is nothing to show.
14
+ func FallbackSessionTitle(text string) (string, bool) {
15
+ collapsed := collapseSpace(text)
16
+ if collapsed == "" {
17
+ return "", false
18
+ }
19
+ if utf16Len(collapsed) > titleFallbackLength {
20
+ return sliceUTF16(collapsed, 0, titleFallbackLength-1) + "…", true
21
+ }
22
+ return collapsed, true
23
+ }
24
+
25
+ // SessionDisplayTitle is the explicit name, else the first user message, else "Untitled session".
26
+ func SessionDisplayTitle(name, firstUserMessage string) string {
27
+ if n := trimJS(name); n != "" {
28
+ return n
29
+ }
30
+ if t, ok := FallbackSessionTitle(firstUserMessage); ok {
31
+ return t
32
+ }
33
+ return untitledSessionTitle
34
+ }
@@ -0,0 +1,87 @@
1
+ package mapper
2
+
3
+ import (
4
+ "strconv"
5
+ "strings"
6
+
7
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
8
+ )
9
+
10
+ // NormalizeImageMimeType normalizes MIME values at the AHP/pi image boundary ("" when the value
11
+ // is not an image type).
12
+ func NormalizeImageMimeType(mimeType string) string {
13
+ base, _, _ := strings.Cut(mimeType, ";")
14
+ base = strings.ToLower(trimJS(base))
15
+ if !strings.HasPrefix(base, "image/") {
16
+ return ""
17
+ }
18
+ if base == "image/jpg" {
19
+ return "image/jpeg"
20
+ }
21
+ return base
22
+ }
23
+
24
+ func blocksOf(content any) []map[string]any {
25
+ arr, _ := content.([]any)
26
+ var out []map[string]any
27
+ for _, b := range arr {
28
+ if m, ok := b.(map[string]any); ok {
29
+ out = append(out, m)
30
+ }
31
+ }
32
+ return out
33
+ }
34
+
35
+ // TextFromPiUserContent is the text of a pi user message (a string or content blocks).
36
+ func TextFromPiUserContent(content any) string {
37
+ if s, ok := content.(string); ok {
38
+ return s
39
+ }
40
+ var sb strings.Builder
41
+ for _, b := range blocksOf(content) {
42
+ if b["type"] == "text" {
43
+ if s, ok := b["text"].(string); ok {
44
+ sb.WriteString(s)
45
+ }
46
+ }
47
+ }
48
+ return sb.String()
49
+ }
50
+
51
+ func imageAttachments(content any) []ahptypes.MessageAttachment {
52
+ var out []ahptypes.MessageAttachment
53
+ for _, b := range blocksOf(content) {
54
+ data, okData := b["data"].(string)
55
+ mime, okMime := b["mimeType"].(string)
56
+ if b["type"] != "image" || !okData || !okMime {
57
+ continue
58
+ }
59
+ contentType := NormalizeImageMimeType(mime)
60
+ if contentType == "" {
61
+ continue
62
+ }
63
+ kind := "image"
64
+ out = append(out, ahptypes.MessageAttachment{Value: &ahptypes.MessageEmbeddedResourceAttachment{
65
+ Type: ahptypes.MessageAttachmentKindEmbeddedResource,
66
+ Label: "Image " + strconv.Itoa(len(out)+1),
67
+ DisplayKind: &kind,
68
+ Data: data,
69
+ ContentType: contentType,
70
+ }})
71
+ }
72
+ return out
73
+ }
74
+
75
+ // UserMessageFromPiContent reconstructs the protocol-visible part of a user message stored by
76
+ // pi. pi persists image bytes but not client-side labels, so replay uses stable ordinal labels
77
+ // while preserving the actual image and MIME type.
78
+ func UserMessageFromPiContent(content any) ahptypes.Message {
79
+ msg := ahptypes.Message{
80
+ Text: TextFromPiUserContent(content),
81
+ Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser},
82
+ }
83
+ if atts := imageAttachments(content); len(atts) > 0 {
84
+ msg.Attachments = atts
85
+ }
86
+ return msg
87
+ }
@@ -0,0 +1,296 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "reflect"
7
+ "sync"
8
+ "testing"
9
+
10
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
11
+
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
16
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
17
+ )
18
+
19
+ // Twins of upstream test/active-turn-reconnect.test.ts: a client that drops in the middle of a
20
+ // turn resumes it by replay, or by snapshot when the replay buffer has moved on.
21
+
22
+ const (
23
+ reconnectClientID = "active-turn-reconnect-client"
24
+ activeTurnID = "active-turn"
25
+ )
26
+
27
+ // controlledBackend records prompts and lets the test stream a response by hand.
28
+ type controlledBackend struct {
29
+ mu sync.Mutex
30
+ prompts []string
31
+ steers []string
32
+ aborts int
33
+ listeners []func(mapper.Event)
34
+ }
35
+
36
+ func (b *controlledBackend) Subscribe(l func(mapper.Event)) func() {
37
+ b.mu.Lock()
38
+ b.listeners = append(b.listeners, l)
39
+ b.mu.Unlock()
40
+ return func() {}
41
+ }
42
+ func (b *controlledBackend) Prompt(_ context.Context, text string, _ []mapper.Image) error {
43
+ b.mu.Lock()
44
+ b.prompts = append(b.prompts, text)
45
+ b.mu.Unlock()
46
+ return nil
47
+ }
48
+ func (b *controlledBackend) Steer(_ context.Context, text string, _ []mapper.Image) error {
49
+ b.mu.Lock()
50
+ b.steers = append(b.steers, text)
51
+ b.mu.Unlock()
52
+ return nil
53
+ }
54
+ func (b *controlledBackend) Abort(context.Context) error {
55
+ b.mu.Lock()
56
+ b.aborts++
57
+ b.mu.Unlock()
58
+ return nil
59
+ }
60
+ func (b *controlledBackend) emit(t *testing.T, literal string) {
61
+ b.mu.Lock()
62
+ ls := append([]func(mapper.Event){}, b.listeners...)
63
+ b.mu.Unlock()
64
+ for _, l := range ls {
65
+ l(event(t, literal))
66
+ }
67
+ }
68
+ func (b *controlledBackend) startResponse(t *testing.T, text string) {
69
+ b.emit(t, `{"type":"agent_start"}`)
70
+ b.emit(t, `{"type":"message_start","message":{"role":"assistant"}}`)
71
+ b.emit(t, `{"type":"message_update","message":{"role":"assistant"},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"`+text+`"}}`)
72
+ }
73
+ func (b *controlledBackend) finishResponse(t *testing.T) {
74
+ b.emit(t, `{"type":"message_end","message":{"role":"assistant"}}`)
75
+ b.emit(t, `{"type":"agent_end","messages":[],"willRetry":false}`)
76
+ b.emit(t, `{"type":"agent_settled"}`)
77
+ }
78
+ func (b *controlledBackend) count(f func(*controlledBackend) int) int {
79
+ b.mu.Lock()
80
+ defer b.mu.Unlock()
81
+ return f(b)
82
+ }
83
+
84
+ type activeTurn struct {
85
+ t *testing.T
86
+ backend *controlledBackend
87
+ h *harness
88
+ client *testkit.Client
89
+ session, chat string
90
+ baseline int64
91
+ }
92
+
93
+ func startActiveTurn(t *testing.T, replayBuffer int) *activeTurn {
94
+ t.Helper()
95
+ backend := &controlledBackend{}
96
+ h := startHarness(t, harnessOptions{replayBuffer: replayBuffer, createBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil }})
97
+ client := testkit.Connect(t, h.host)
98
+ t.Cleanup(client.Close)
99
+ client.Initialize(reconnectClientID, nil)
100
+ id := newID()
101
+ f := &activeTurn{t: t, backend: backend, h: h, client: client, session: wire.SessionURI(id), chat: wire.ChatURI(id)}
102
+ client.Must("createSession", obj{"channel": f.session})
103
+ client.Subscribe(f.session)
104
+ client.Subscribe(f.chat)
105
+ client.Dispatch(f.chat, obj{"type": "chat/turnStarted", "turnId": activeTurnID, "startedAt": "2025-01-01T00:00:00.000Z", "message": userMessage("keep working")})
106
+ testkit.Eventually(t, "the initial prompt to reach the backend", func() bool { return backend.count(func(b *controlledBackend) int { return len(b.prompts) }) == 1 })
107
+ if a := h.host.Store().Chat(f.chat).ActiveTurn; a == nil || a.Id != activeTurnID {
108
+ t.Fatalf("activeTurn = %+v", a)
109
+ }
110
+ f.baseline = h.host.ServerSeq()
111
+ return f
112
+ }
113
+
114
+ func (f *activeTurn) reconnect(c *testkit.Client, into any) {
115
+ f.t.Helper()
116
+ c.Decode(c.Must("reconnect", obj{"channel": wire.RootChannel, "clientId": reconnectClientID, "lastSeenServerSeq": f.baseline, "subscriptions": []string{f.session, f.chat}}), into)
117
+ }
118
+
119
+ func responseText(state *ahptypes.ChatState) string {
120
+ turn := (*ahptypes.Turn)(nil)
121
+ var parts []ahptypes.ResponsePart
122
+ if state.ActiveTurn != nil {
123
+ parts = state.ActiveTurn.ResponseParts
124
+ } else if n := len(state.Turns); n > 0 {
125
+ turn = &state.Turns[n-1]
126
+ parts = turn.ResponseParts
127
+ }
128
+ for _, p := range parts {
129
+ if md, ok := p.Value.(*ahptypes.MarkdownResponsePart); ok {
130
+ return md.Content
131
+ }
132
+ }
133
+ return ""
134
+ }
135
+
136
+ func (f *activeTurn) prompts() []string {
137
+ f.backend.mu.Lock()
138
+ defer f.backend.mu.Unlock()
139
+ return append([]string(nil), f.backend.prompts...)
140
+ }
141
+
142
+ func TestActiveTurnReconnect(t *testing.T) {
143
+ twin.Run(t, "active-turn-reconnect", "replays offline output and accepts steering and cancellation after reconnect", func(t *testing.T) {
144
+ f := startActiveTurn(t, 64)
145
+ observer := testkit.Connect(t, f.h.host)
146
+ t.Cleanup(observer.Close)
147
+ observer.Initialize(nextClientID(), nil)
148
+ observer.Subscribe(f.chat)
149
+
150
+ f.client.Close()
151
+ testkit.Eventually(t, "only the observer to remain subscribed", func() bool { return f.h.host.SubscriberCount(f.chat) == 1 })
152
+ f.backend.startResponse(t, "offline partial")
153
+ n, ok := observer.Await(func(n testkit.Notification) bool {
154
+ e, isAction := n.Envelope()
155
+ if !isAction {
156
+ return false
157
+ }
158
+ _, part := e.Action.Value.(*ahptypes.ChatResponsePartAction)
159
+ return part
160
+ }, testkit.Timeout)
161
+ if !ok {
162
+ t.Fatal("the observer saw no response part")
163
+ }
164
+ observed, _ := n.Envelope()
165
+
166
+ resumed := testkit.Connect(t, f.h.host)
167
+ t.Cleanup(resumed.Close)
168
+ var result struct {
169
+ Type string
170
+ Actions []ahptypes.ActionEnvelope
171
+ }
172
+ f.reconnect(resumed, &result)
173
+ if result.Type != "replay" {
174
+ t.Fatalf("type = %q", result.Type)
175
+ }
176
+ found := false
177
+ for _, e := range result.Actions {
178
+ found = found || e.ServerSeq == observed.ServerSeq
179
+ }
180
+ if !found {
181
+ t.Fatal("the replay lacks the action the observer saw")
182
+ }
183
+ if got := responseText(f.h.host.Store().Chat(f.chat)); got != "offline partial" {
184
+ t.Fatalf("response = %q", got)
185
+ }
186
+ if got := f.prompts(); !reflect.DeepEqual(got, []string{"keep working"}) {
187
+ t.Fatalf("prompts = %v", got)
188
+ }
189
+
190
+ resumed.Dispatch(f.chat, obj{"type": "chat/pendingMessageSet", "kind": "steering", "id": "after-reconnect", "message": userMessage("focus here")})
191
+ testkit.Eventually(t, "reconnected steering to reach the backend", func() bool { return f.backend.count(func(b *controlledBackend) int { return len(b.steers) }) == 1 })
192
+ resumed.Dispatch(f.chat, obj{"type": "chat/turnCancelled", "turnId": activeTurnID, "duration": 0})
193
+ testkit.Eventually(t, "reconnected cancellation to reach the backend", func() bool { return f.backend.count(func(b *controlledBackend) int { return b.aborts }) == 1 })
194
+ state := f.h.host.Store().Chat(f.chat)
195
+ if state.ActiveTurn != nil || state.Turns[len(state.Turns)-1].State != ahptypes.TurnStateCancelled {
196
+ t.Fatalf("activeTurn %+v, turns %+v", state.ActiveTurn, turnShapes(state.Turns))
197
+ }
198
+ })
199
+
200
+ twin.Run(t, "active-turn-reconnect", "replays a turn that completed while its client was offline", func(t *testing.T) {
201
+ f := startActiveTurn(t, 64)
202
+ f.client.Close()
203
+ testkit.Eventually(t, "the original chat subscription to disconnect", func() bool { return f.h.host.SubscriberCount(f.chat) == 0 })
204
+ f.backend.startResponse(t, "finished offline")
205
+ f.backend.finishResponse(t)
206
+
207
+ resumed := testkit.Connect(t, f.h.host)
208
+ t.Cleanup(resumed.Close)
209
+ var result struct {
210
+ Type string
211
+ Actions []ahptypes.ActionEnvelope
212
+ }
213
+ f.reconnect(resumed, &result)
214
+ if result.Type != "replay" {
215
+ t.Fatalf("type = %q", result.Type)
216
+ }
217
+ completed := false
218
+ for _, e := range result.Actions {
219
+ _, ok := e.Action.Value.(*ahptypes.ChatTurnCompleteAction)
220
+ completed = completed || ok
221
+ }
222
+ if !completed {
223
+ t.Fatal("the replay lacks chat/turnComplete")
224
+ }
225
+ state := f.h.host.Store().Chat(f.chat)
226
+ if state.Turns[len(state.Turns)-1].State != ahptypes.TurnStateComplete || responseText(state) != "finished offline" {
227
+ t.Fatalf("turns %+v response %q", turnShapes(state.Turns), responseText(state))
228
+ }
229
+ if got := f.prompts(); !reflect.DeepEqual(got, []string{"keep working"}) {
230
+ t.Fatalf("prompts = %v", got)
231
+ }
232
+ })
233
+
234
+ twin.Run(t, "active-turn-reconnect", "falls back to an active-turn snapshot and continues streaming", func(t *testing.T) {
235
+ f := startActiveTurn(t, 4)
236
+ f.client.Close()
237
+ testkit.Eventually(t, "the original chat subscription to disconnect", func() bool { return f.h.host.SubscriberCount(f.chat) == 0 })
238
+ f.backend.startResponse(t, "snapshot partial")
239
+ for i := int64(0); i < 6; i++ {
240
+ f.h.host.DispatchServerAction(wire.RootChannel, activeSessions(i))
241
+ }
242
+ resumed := testkit.Connect(t, f.h.host)
243
+ t.Cleanup(resumed.Close)
244
+ // The snapshot list also carries the session: decode chat state only for its own entry.
245
+ raw := resumed.Must("reconnect", obj{"channel": wire.RootChannel, "clientId": reconnectClientID, "lastSeenServerSeq": f.baseline, "subscriptions": []string{f.session, f.chat}})
246
+ var generic struct {
247
+ Type string
248
+ Snapshots []struct {
249
+ Resource string
250
+ State map[string]any
251
+ }
252
+ }
253
+ resumed.Decode(raw, &generic)
254
+ if generic.Type != "snapshot" {
255
+ t.Fatalf("type = %q", generic.Type)
256
+ }
257
+ var chatState *ahptypes.ChatState
258
+ for _, s := range generic.Snapshots {
259
+ if s.Resource == f.chat {
260
+ var st ahptypes.ChatState
261
+ resumedJSON := testkit.JSON(t, s.State)
262
+ if err := jsonUnmarshal(resumedJSON, &st); err != nil {
263
+ t.Fatal(err)
264
+ }
265
+ chatState = &st
266
+ }
267
+ }
268
+ if chatState == nil || responseText(chatState) != "snapshot partial" {
269
+ t.Fatalf("chat snapshot = %+v", chatState)
270
+ }
271
+ f.backend.finishResponse(t)
272
+ if _, ok := resumed.Await(func(n testkit.Notification) bool {
273
+ e, isAction := n.Envelope()
274
+ if !isAction {
275
+ return false
276
+ }
277
+ _, done := e.Action.Value.(*ahptypes.ChatTurnCompleteAction)
278
+ return done
279
+ }, testkit.Timeout); !ok {
280
+ t.Fatal("the resumed client never saw the turn complete")
281
+ }
282
+ state := f.h.host.Store().Chat(f.chat)
283
+ if state.Turns[len(state.Turns)-1].State != ahptypes.TurnStateComplete {
284
+ t.Fatalf("turns %+v", turnShapes(state.Turns))
285
+ }
286
+ if got := f.prompts(); !reflect.DeepEqual(got, []string{"keep working"}) {
287
+ t.Fatalf("prompts = %v", got)
288
+ }
289
+ })
290
+ }
291
+
292
+ func jsonUnmarshal(s string, v any) error { return json.Unmarshal([]byte(s), v) }
293
+
294
+ func activeSessions(n int64) ahptypes.StateAction {
295
+ return ahptypes.StateAction{Value: &ahptypes.RootActiveSessionsChangedAction{Type: ahptypes.ActionTypeRootActiveSessionsChanged, ActiveSessions: n}}
296
+ }
@@ -0,0 +1,64 @@
1
+ // Package pi is the agent-facing half of the host: it drives a Pi agent through a [Backend], owns
2
+ // the live-session registry, and serves session history. It ports pi-ahp's src/pi. The
3
+ // protocol-shaped half (state and summaries) lives in package channels and knows nothing about
4
+ // the agent; the dependency runs one way.
5
+ package pi
6
+
7
+ import (
8
+ "context"
9
+ "encoding/json"
10
+
11
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
12
+
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
14
+ )
15
+
16
+ // Backend is the agent a chat drives. Subscribe delivers Pi's agent events (decoded JSON).
17
+ //
18
+ // Prompt and Steer take a context: cancelling it abandons the call (the agent itself is stopped
19
+ // with Abort). The optional capabilities are separate interfaces: [ModelSelector],
20
+ // [SelectionReporter], [Truncater], [Disposer].
21
+ type Backend interface {
22
+ Subscribe(listener func(mapper.Event)) (unsubscribe func())
23
+ Prompt(ctx context.Context, text string, images []mapper.Image) error
24
+ Steer(ctx context.Context, text string, images []mapper.Image) error
25
+ Abort(ctx context.Context) error
26
+ }
27
+
28
+ // Disposer releases a backend's resources. A failure is logged; it never blocks removal.
29
+ type Disposer interface{ Dispose() error }
30
+
31
+ // ModelSelector switches the model a turn runs on.
32
+ type ModelSelector interface {
33
+ SelectModel(ctx context.Context, selection ahptypes.ModelSelection) error
34
+ }
35
+
36
+ // SelectionReporter reports the model and reasoning effort in effect.
37
+ type SelectionReporter interface {
38
+ CurrentSelection() *ahptypes.ModelSelection
39
+ }
40
+
41
+ // Truncater moves the agent back to a session entry; false means it was refused.
42
+ type Truncater interface {
43
+ Truncate(ctx context.Context, entryID string) (bool, error)
44
+ }
45
+
46
+ // messageInput converts a typed AHP message to Pi's prompt shape.
47
+ func messageInput(message ahptypes.Message) (mapper.MessageInput, error) {
48
+ raw, err := json.Marshal(message)
49
+ if err != nil {
50
+ return mapper.MessageInput{}, err
51
+ }
52
+ var generic any
53
+ if err := json.Unmarshal(raw, &generic); err != nil {
54
+ return mapper.MessageInput{}, err
55
+ }
56
+ return mapper.MessageInputForPi(generic)
57
+ }
58
+
59
+ func rejectionReason(message ahptypes.Message) string {
60
+ raw, _ := json.Marshal(message)
61
+ var generic any
62
+ _ = json.Unmarshal(raw, &generic)
63
+ return mapper.MessageRejectionReason(generic)
64
+ }