@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
package/CREDITS.md ADDED
@@ -0,0 +1,27 @@
1
+ # Credits
2
+
3
+ `extensions/ahp` is a Go port of **pi-ahp**, https://github.com/Qusic/pi-ahp, by **Bang Lee**
4
+ (Qusic; MIT, Copyright (c) 2026 Bang Lee).
5
+
6
+ - Original: the `src/` tree of pi-ahp, a standalone host that embeds Pi and serves it over
7
+ Microsoft's Agent Host Protocol; npm package `pi-ahp`.
8
+ - Revision: commit `4065e98309b03c1d2d916300856c3c21a9c10674`.
9
+ - The unmodified `src/`, `test/` and metadata are kept in [`proof/oracle`](proof/oracle) as the
10
+ equivalence oracle, with the upstream license at [`proof/oracle/LICENSE`](proof/oracle/LICENSE).
11
+ The 430 leaf test cases of the original are listed in
12
+ [`upstream-tests.json`](extensions/ahp/testdata/upstream-tests.json); each has a Go twin or a named skipped
13
+ twin ([`proof/PORT.md`](proof/PORT.md)).
14
+
15
+ The protocol is **Agent Host Protocol** (AHP) by **Microsoft Corporation**,
16
+ https://github.com/microsoft/agent-host-protocol (MIT, Copyright (c) Microsoft Corporation),
17
+ protocol 0.9.0, commit `296b25e7b698a4a84a0ee5a28d9573e70048a0bf`. Its Go types and reducers
18
+ (`clients/go`, `ahp` and `ahptypes`) are vendored byte for byte, tests excluded, in
19
+ [`extensions/ahp/third_party/agent-host-protocol-go`](extensions/ahp/third_party/agent-host-protocol-go)
20
+ with the Microsoft license. The five protocol JSON schemas used by the tests are copied from the
21
+ same commit into `extensions/ahp/internal/testkit/schema`, and the recorded Pi event streams in
22
+ `extensions/ahp/internal/mapper/testdata/fixtures` come from pi-ahp's test fixtures.
23
+
24
+ The Go code, the scenarios and the tests were written for this Package by Michael Kinsy. The
25
+ behavior, wording and structure of the host follow the original. Modified paths: none of the
26
+ originals is modified; the port is a separate implementation, and its deviations are listed in
27
+ [`proof/PORT.md`](proof/PORT.md).
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bang Lee
4
+ Copyright (c) Microsoft Corporation. All rights reserved.
5
+ Copyright (c) 2026 Michael Kinsy (the Go port)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # ahp (Go port of pi-ahp)
2
+
3
+ A Go extension for PiG that serves the **running session** to remote clients over Microsoft's
4
+ [Agent Host Protocol](https://github.com/microsoft/agent-host-protocol) (AHP, protocol 0.9.0,
5
+ pinned): a WebSocket host with multi-client state, reconnect with replay or fresh snapshots,
6
+ token authentication, an origin check, model and session-config discovery, `@`-mentions, the
7
+ session catalogue with history paging, and live mirroring of the agent's turns, including prompts
8
+ typed locally.
9
+
10
+ It is a port of **pi-ahp** by Bang Lee (see [CREDITS.md](CREDITS.md)). pi-ahp is a standalone
11
+ host that embeds Pi and runs many sessions; a PiG extension lives inside one session, so this port
12
+ serves that session (and reads other sessions from their files). See [proof/PORT.md](proof/PORT.md)
13
+ for every deviation and gap.
14
+
15
+ ## Safe by default
16
+
17
+ - **No listener** exists unless you start one: `pig --ahp`, or `/ahp start`. Installing the
18
+ extension opens nothing and creates no file.
19
+ - Starting creates `<pig home>/ahp/settings.json` (mode 0600) with a free loopback port and a random
20
+ token, if it is missing. Clients connect to `ws://127.0.0.1:<port>/?token=<token>`; the listener also
21
+ takes the token as `?tkn=`, the spelling Visual Studio Code uses (see below).
22
+ A non-loopback `host` without a token is refused; a browser `Origin` is refused unless listed in
23
+ `allowedOrigins`.
24
+ - **Remote filesystem, watches, terminals and session deletion are separate opt-ins** in the same
25
+ file, all off unless set:
26
+
27
+ ```json
28
+ {
29
+ "port": 41234, "token": "…", "host": "127.0.0.1",
30
+ "allowedOrigins": [],
31
+ "filesystem": { "enabled": true, "roots": [], "unrestricted": false },
32
+ "terminals": { "enabled": false },
33
+ "allowSessionDeletion": false
34
+ }
35
+ ```
36
+
37
+ With `filesystem.enabled` and no `roots`, access is confined to the session's working directory;
38
+ `unrestricted: true` must be said explicitly. A terminal is a shell as you, claimed by the client
39
+ that started it. The dev tunnel of pi-ahp is not bundled.
40
+
41
+ **What a connected client can always do.** The opt-ins above cover the host's own filesystem,
42
+ watch, terminal and deletion services. They do not limit the agent. Anyone who holds the token
43
+ can do all of the following, so treat the token like a shell login:
44
+
45
+ - Send prompts to the running session. The agent answers with the session's tools (for example
46
+ `bash`, `read`, `write`, `edit`), as you, in its working directory. Slash commands typed by a
47
+ client are sent to the model as text and are not run.
48
+ - Switch the session's model and thinking level.
49
+ - List and read the stored history of **every** PiG session under the session directory, from all
50
+ projects, not only the running one.
51
+ - Get `@` completions, which list file names under the working directory.
52
+
53
+ The token is shown in the `/ahp` status and in the "listening" notice.
54
+
55
+ ## Commands and flags
56
+
57
+ | | |
58
+ |---|---|
59
+ | `--ahp` | start the listener when the session starts |
60
+ | `--ahp-settings <file>` | settings file (default `<pig home>/ahp/settings.json`) |
61
+ | `/ahp [status]` `/ahp start` `/ahp stop` | inspect, start or stop the listener |
62
+
63
+ If the PiG session is replaced (new, resume, fork, reload), the listener closes with it and connected
64
+ clients are dropped. With `--ahp` it opens again for the new session on the same port. After
65
+ `/ahp start` it stays closed, a warning says so, and `/ahp start` serves the new session.
66
+
67
+ ## Visual Studio Code
68
+
69
+ **Not verified with a live VS Code.** The listener is tested with the messages VS Code's client sends, as read from VS Code's
70
+ source for 1.140.0 and main (the handshake, the `tkn` token, text frames, a dash-free provider id;
71
+ `internal/ws/vscode_test.go`), not against VS Code itself. Until the owner has tried it, treat the steps below as the intended path.
72
+
73
+ VS Code's Agents window (**Chat: Open Agents Window**) can connect to any WebSocket AHP host:
74
+
75
+ 1. Start the listener in the session you want to serve: `pig --ahp`, or `/ahp start`. The notice (and `/ahp status`) starts with
76
+ the address in the form VS Code takes, `ws://127.0.0.1:<port>?tkn=<token>`, then gives the `/?token=` form for other clients.
77
+ 2. In the Agents window's Command Palette run **Agents: Add Remote Agent Host...** (`sessions.remoteAgentHost.add`), paste that
78
+ address, and name the host. VS Code moves the token out of the address into the entry's `connectionToken`. Or add it to your
79
+ user `settings.json` by hand:
80
+
81
+ ```json
82
+ "chat.remoteAgentHosts": [
83
+ { "name": "pig", "address": "ws://127.0.0.1:41234", "connectionToken": "<token>" }
84
+ ]
85
+ ```
86
+
87
+ 3. `chat.remoteAgentHostsEnabled` must be on (it is by default). If you are not signed in to GitHub in VS Code, also set
88
+ `"chat.agentHost.allowSignedOutWhenUsable": true`.
89
+
90
+ Reaching a host on another machine:
91
+
92
+ - **SSH port forward** (simplest): `ssh -L 41234:127.0.0.1:41234 <host>`, start the listener there, and use the forwarded
93
+ `ws://127.0.0.1:41234?tkn=<token>` locally. The host keeps its loopback listener, so no token ever crosses the network unprotected
94
+ beyond the tunnel.
95
+ - **VS Code's SSH remote agent host**: `chat.sshRemoteAgentHostCommand` (a development setting) replaces the command VS Code runs
96
+ on the remote. VS Code reads the first `ws://127.0.0.1:<port>?tkn=<token>` in the command's output, which is how the notice
97
+ starts. The command has to keep pig running without a terminal: `pig --mode rpc --ahp` prints the notice on standard output at
98
+ once and runs while its input stays open (checked with a plain shell, not yet with VS Code).
99
+ - **A dev tunnel** is not bundled (the pi-ahp tunnel feature is not ported), and VS Code's own tunnel support looks for VS Code agent
100
+ hosts, not this one. A tunnel you start yourself can forward the port and give a `wss://` address that **Add Remote Agent Host...**
101
+ accepts, but VS Code sends no tunnel credentials on that connection, so the tunnel has to admit it anonymously and the token is
102
+ the only protection. Not tried.
103
+
104
+ Debugging: set `"chat.agentHost.ahpJsonlLoggingEnabled": true` to have VS Code log every AHP message as JSON lines under the
105
+ window's log directory (it is off by default in stable builds).
106
+
107
+ What to expect, and the limits that apply to this port:
108
+
109
+ - VS Code 1.140 offers protocol 0.9.0 first, then 0.7.0 down to 0.5.1; later builds offer 0.10.0 first and still 0.9.0. This host
110
+ speaks only 0.9.0 and negotiates it; a VS Code that stops offering 0.9.0 will report the host as incompatible. A wrong
111
+ or missing token is refused at the upgrade with HTTP 403, as VS Code's own agent host refuses one (pi-ahp answers 401; see
112
+ [proof/PORT.md](proof/PORT.md), deviation 15). A browser `Origin` that is not allowed also gets 403.
113
+ - The host serves **the one running session** as one agent (`pi`). Other sessions are readable from their files; they are not
114
+ running agents. If the session is replaced (new, resume, fork, reload) clients are dropped (see Commands and flags).
115
+ - No git changeset channels: VS Code's change views will have nothing to show (the changeset service of pi-ahp is not ported).
116
+ - The remote filesystem, watches, terminals and session deletion are off until you opt in (see Safe by default).
117
+ - No dev tunnel is bundled, and project trust cannot be gated from an extension (see [proof/PORT.md](proof/PORT.md)).
118
+
119
+ ## Other AHP clients
120
+
121
+ Any client that speaks AHP 0.9.0 over WebSocket text frames can connect with `ws://127.0.0.1:<port>/?token=<token>` (or `?tkn=`).
122
+ Two others are named for trying; **neither has been tried against this host**, and what they support has not been checked here:
123
+
124
+ - **agent-host-protocol-ui**
125
+ - **copilot-remote-host**
126
+
127
+ Two things decide whether a client works: it must offer protocol 0.9.0 (this host does not down-convert from another version), and,
128
+ if it runs in a browser, its `Origin` must be listed in `allowedOrigins` in the settings file, because the host refuses a browser
129
+ `Origin` it does not know.
130
+
131
+ ## Install
132
+
133
+ ```sh
134
+ pig install ./components/ahp
135
+ ```
136
+
137
+ It builds from source on first use (Go toolchain required), or fuses into a Piglet Binary
138
+ (`packages: {ahp: local:../../components/ahp}` in a Piglet). It needs no Node runtime.
139
+
140
+ ## Layout
141
+
142
+ `extensions/ahp/internal`: `wire` (JSON-RPC, channel URIs, version negotiation), `host` (channel
143
+ state, sequencing, replay, connections), `ws` (RFC 6455 server), `mapper` (Pi events to AHP
144
+ actions), `pi` (chat driver, session registry, catalogue, hydration, history, models, completions),
145
+ `svc` (resources, watches, terminals, PTY), `live` (the running PiG session behind the SDK),
146
+ `compose` (wiring), `settings`. The Microsoft AHP Go types and reducers are vendored byte for byte
147
+ under `third_party/`. Tests are twins of pi-ahp's 430 test cases (`internal/twin`); `twins_test.go`
148
+ fails if one has neither a Go test nor a named skip.
@@ -0,0 +1,116 @@
1
+ package ahp
2
+
3
+ // Review (rev-pigpen-ahp): the capability wiring of the runtime was only exercised by the
4
+ // real-pig scenarios, which are skipped unless PIGPEN_AHP_REAL=1, and those did not notice a
5
+ // default root wider than the working directory. These unit tests pin it without a pig.
6
+
7
+ import (
8
+ "context"
9
+ "errors"
10
+ "os"
11
+ "path/filepath"
12
+ "testing"
13
+
14
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
15
+
16
+ "github.com/MichaelKinsy/pigpen/ahp/internal/compose"
17
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
18
+ "github.com/MichaelKinsy/pigpen/ahp/internal/settings"
19
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
20
+ )
21
+
22
+ func TestNoCapabilityUnlessTheSettingsGrantIt(t *testing.T) {
23
+ fs, terminals := accessOptions(settings.Access{}, "/work")
24
+ if fs != nil || terminals != nil {
25
+ t.Fatalf("an empty Access granted filesystem=%+v terminals=%+v", fs, terminals)
26
+ }
27
+ }
28
+
29
+ func TestFilesystemDefaultsToTheWorkingDirectoryOnly(t *testing.T) {
30
+ var a settings.Access
31
+ a.Filesystem.Enabled = true
32
+ fs, terminals := accessOptions(a, "/work")
33
+ if terminals != nil {
34
+ t.Fatal("enabling the filesystem must not enable terminals")
35
+ }
36
+ if fs == nil || fs.Unrestricted || len(fs.Roots) != 1 || fs.Roots[0] != "/work" {
37
+ t.Fatalf("filesystem without roots must be confined to the working directory, got %+v", fs)
38
+ }
39
+
40
+ a.Filesystem.Roots = []string{"/a", "/b"}
41
+ if fs, _ := accessOptions(a, "/work"); fs == nil || fs.Unrestricted || len(fs.Roots) != 2 || fs.Roots[0] != "/a" || fs.Roots[1] != "/b" {
42
+ t.Fatalf("configured roots must be used as given, got %+v", fs)
43
+ }
44
+
45
+ a.Filesystem.Roots, a.Filesystem.Unrestricted = nil, true
46
+ if fs, _ := accessOptions(a, "/work"); fs == nil || !fs.Unrestricted {
47
+ t.Fatalf("unrestricted must be passed on only when stated, got %+v", fs)
48
+ }
49
+ }
50
+
51
+ func TestTerminalsOnlyWhenEnabled(t *testing.T) {
52
+ var a settings.Access
53
+ a.Terminals.Enabled = true
54
+ fs, terminals := accessOptions(a, "/work")
55
+ if fs != nil || terminals == nil {
56
+ t.Fatalf("terminals enabled alone: filesystem=%+v terminals=%+v", fs, terminals)
57
+ }
58
+ }
59
+
60
+ // The default root, composed for real: a sibling of the working directory is refused.
61
+ func TestDefaultRootRefusesASiblingOfTheWorkingDirectory(t *testing.T) {
62
+ root := t.TempDir()
63
+ work, sibling := filepath.Join(root, "work"), filepath.Join(root, "secret.txt")
64
+ if err := os.MkdirAll(work, 0o755); err != nil {
65
+ t.Fatal(err)
66
+ }
67
+ for path, body := range map[string]string{filepath.Join(work, "in.txt"): "inside", sibling: "outside"} {
68
+ if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ }
72
+ var a settings.Access
73
+ a.Filesystem.Enabled = true
74
+ fs, terminals := accessOptions(a, work)
75
+ if fs == nil {
76
+ t.Fatal("filesystem enabled but not composed")
77
+ }
78
+ built, err := compose.Build(compose.Options{
79
+ WorkingDirectory: work, Filesystem: fs, Terminals: terminals,
80
+ CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return nil, os.ErrInvalid },
81
+ })
82
+ if err != nil {
83
+ t.Fatal(err)
84
+ }
85
+ defer built.Close()
86
+ read := func(path string) error {
87
+ _, err := built.Resources.Read(context.Background(), ahptypes.ResourceReadParams{Uri: wire.PathToFileURI(path)})
88
+ return err
89
+ }
90
+ if err := read(filepath.Join(work, "in.txt")); err != nil {
91
+ t.Fatalf("a file in the working directory must be readable: %v", err)
92
+ }
93
+ err = read(sibling)
94
+ var e *wire.Error
95
+ if !errors.As(err, &e) || e.Code != wire.CodePermissionDenied {
96
+ t.Fatalf("a sibling of the working directory must be refused with PermissionDenied, got %v", err)
97
+ }
98
+ }
99
+
100
+ func TestReplacementNotice(t *testing.T) {
101
+ // the listener is closed with the replaced session; say so unless --ahp brings it back
102
+ for _, reason := range []string{"new", "resume", "fork", "reload"} {
103
+ if n := replacementNotice(reason, true, false); n == "" {
104
+ t.Errorf("reason %q: a listener started with /ahp start closes silently", reason)
105
+ }
106
+ if n := replacementNotice(reason, true, true); n != "" {
107
+ t.Errorf("reason %q: --ahp restarts the listener, but got notice %q", reason, n)
108
+ }
109
+ if n := replacementNotice(reason, false, false); n != "" {
110
+ t.Errorf("reason %q: nothing was running, but got notice %q", reason, n)
111
+ }
112
+ }
113
+ if n := replacementNotice("quit", true, false); n != "" {
114
+ t.Errorf("quitting needs no notice, got %q", n)
115
+ }
116
+ }
@@ -0,0 +1,54 @@
1
+ // Package ahp is a Go port of pi-ahp (Bang Lee / Qusic, MIT): a host for Microsoft's Agent
2
+ // Host Protocol that serves the running PiG session to remote AHP clients over WebSocket.
3
+ //
4
+ // The extension opens no listener unless the user asks: with the --ahp flag, or with /ahp start.
5
+ // Remote filesystem and terminal access are further opt-ins in the settings file.
6
+ package ahp
7
+
8
+ import (
9
+ "fmt"
10
+ "strings"
11
+
12
+ sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
13
+ )
14
+
15
+ const (
16
+ flagStart = "ahp"
17
+ flagSettings = "ahp-settings"
18
+ )
19
+
20
+ // Extension registers the extension. It starts no listener unless the user configured one.
21
+ func Extension() *sdk.Extension {
22
+ e := sdk.New("ahp")
23
+ rt := newRuntime()
24
+
25
+ e.Flag(flagStart, sdk.FlagOptions{Type: sdk.FlagBoolean, Description: "Start the AHP listener for this session (settings file: see /ahp)"})
26
+ e.Flag(flagSettings, sdk.FlagOptions{Type: sdk.FlagString, Description: "AHP settings file (default: <pig home>/ahp/settings.json)"})
27
+
28
+ rt.live.Register(e, rt.onSessionStart, rt.onSessionShutdown)
29
+ rt.live.OnModelChanged(rt.onModelChanged)
30
+
31
+ e.Command("ahp", "AHP host: /ahp [status|start|stop]", func(ctx sdk.Context, args string) error {
32
+ switch strings.TrimSpace(args) {
33
+ case "", "status":
34
+ ctx.Notify(rt.status(), "info")
35
+ case "start":
36
+ addr, err := rt.start(ctx)
37
+ if err != nil {
38
+ ctx.Notify("AHP: "+err.Error(), "error")
39
+ return nil
40
+ }
41
+ ctx.Notify(addr, "info")
42
+ case "stop":
43
+ if rt.stop() {
44
+ ctx.Notify("AHP listener stopped", "info")
45
+ } else {
46
+ ctx.Notify("AHP listener is not running", "info")
47
+ }
48
+ default:
49
+ return fmt.Errorf("usage: /ahp [status|start|stop]")
50
+ }
51
+ return nil
52
+ })
53
+ return e
54
+ }
@@ -0,0 +1,12 @@
1
+ module github.com/MichaelKinsy/pigpen/ahp
2
+
3
+ go 1.26
4
+
5
+ // PiG resolves this requirement to the version-matched staged SDK at build time.
6
+ require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
7
+
8
+ // The Microsoft Agent Host Protocol Go types and reducers (MIT), vendored byte for byte.
9
+ // See third_party/agent-host-protocol-go/NOTICE-PIGPEN.md.
10
+ require github.com/microsoft/agent-host-protocol/clients/go v0.9.0
11
+
12
+ replace github.com/microsoft/agent-host-protocol/clients/go => ./third_party/agent-host-protocol-go
@@ -0,0 +1,2 @@
1
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1/go.mod h1:4j0XhCW0EPOBWvqYE7XKjTvIAahI7K1VyyW3KpjTKj8=
2
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1 h1:Ql9sTkc6dvAXaYQLq5nxqmSZgqu8S0L53ZTs8MkOuxw=
@@ -0,0 +1,8 @@
1
+ go 1.26
2
+
3
+ // Lists the vendored Microsoft AHP module so a fused Piglet Binary build (which does not honour
4
+ // replace directives of the extension's go.mod) resolves it locally. See third_party/.../NOTICE-PIGPEN.md.
5
+ use (
6
+ .
7
+ ./third_party/agent-host-protocol-go
8
+ )
@@ -0,0 +1,110 @@
1
+ package channels
2
+
3
+ import (
4
+ "encoding/json"
5
+ "time"
6
+
7
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
10
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
11
+ )
12
+
13
+ // InitialChatState is a fresh, idle chat (port of initialChatState in src/channels/chat.ts).
14
+ // Every session this host serves has exactly one chat, which is why the agent declares no
15
+ // `multipleChats` capability: its absence tells a client not to call `createChat`.
16
+ //
17
+ // A selection seeds the draft's model before the agent exists. Starting one takes seconds, and a
18
+ // client that subscribes in the meantime would otherwise find an empty model picker and be unable
19
+ // to send. The backend refines it once it is up.
20
+ func InitialChatState(uri, title string, selection *ahptypes.ModelSelection) *ahptypes.ChatState {
21
+ state := &ahptypes.ChatState{
22
+ Resource: uri,
23
+ Title: title,
24
+ Status: ahptypes.SessionStatusIdle,
25
+ ModifiedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
26
+ // A session's default chat exists because the user made the session.
27
+ Origin: &ahptypes.ChatOrigin{Value: &ahptypes.ChatUserOrigin{Kind: ahptypes.ChatOriginKindUser}},
28
+ Turns: []ahptypes.Turn{},
29
+ }
30
+ if selection != nil {
31
+ state.Draft = &ahptypes.Message{Text: "", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}, Model: selection}
32
+ }
33
+ return state
34
+ }
35
+
36
+ // ChatSummaryOf is the denormalised summary of a chat that its session carries.
37
+ func ChatSummaryOf(state *ahptypes.ChatState) ahptypes.ChatSummary {
38
+ return ahptypes.ChatSummary{
39
+ Resource: state.Resource, Title: state.Title, Status: state.Status, ModifiedAt: state.ModifiedAt,
40
+ Activity: state.Activity, Origin: state.Origin, Interactivity: state.Interactivity,
41
+ WorkingDirectories: state.WorkingDirectories,
42
+ }
43
+ }
44
+
45
+ // InstallDefaultChat creates a session's default chat and registers it in the session catalogue.
46
+ // The chat id is derived from the session id so the pairing is reconstructible without a lookup
47
+ // table.
48
+ func InstallDefaultChat(h *host.Host, sessionChannel, sessionID, title string, selection *ahptypes.ModelSelection) string {
49
+ uri := wire.ChatURI(sessionID)
50
+ h.Store().Create(uri, InitialChatState(uri, title, selection))
51
+ summary := ChatSummaryOf(h.Store().Chat(uri))
52
+ h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatAddedAction{Type: ahptypes.ActionTypeSessionChatAdded, Summary: summary}})
53
+ h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionDefaultChatChangedAction{Type: ahptypes.ActionTypeSessionDefaultChatChanged, DefaultChat: &uri}})
54
+ return uri
55
+ }
56
+
57
+ func jsonEqual(a, b any) bool {
58
+ ja, _ := json.Marshal(a)
59
+ jb, _ := json.Marshal(b)
60
+ return string(ja) == string(jb)
61
+ }
62
+
63
+ func equalSummary(a, b ahptypes.ChatSummary) bool {
64
+ return a.Resource == b.Resource && a.Title == b.Title && a.Status == b.Status && a.ModifiedAt == b.ModifiedAt &&
65
+ jsonEqual(a.Activity, b.Activity) && jsonEqual(a.Origin, b.Origin) && jsonEqual(a.Interactivity, b.Interactivity) &&
66
+ jsonEqual(a.WorkingDirectories, b.WorkingDirectories)
67
+ }
68
+
69
+ // SyncChatSummary mirrors a chat's denormalised summary fields into its owning session.
70
+ //
71
+ // Partial updates cannot express removal of an optional JSON field: a nil value disappears on
72
+ // the wire and means "unchanged". The action's full-summary upsert form is used when one of those
73
+ // fields is cleared. It reports whether anything was dispatched.
74
+ func SyncChatSummary(h *host.Host, sessionChannel, chatChannel string) bool {
75
+ chat := h.Store().Chat(chatChannel)
76
+ session := h.Store().Session(sessionChannel)
77
+ if chat == nil || session == nil {
78
+ return false
79
+ }
80
+ var previous *ahptypes.ChatSummary
81
+ for i := range session.Chats {
82
+ if session.Chats[i].Resource == chatChannel {
83
+ previous = &session.Chats[i]
84
+ break
85
+ }
86
+ }
87
+ if previous == nil {
88
+ return false
89
+ }
90
+ current := ChatSummaryOf(chat)
91
+ if equalSummary(*previous, current) {
92
+ return false
93
+ }
94
+ clears := (previous.Activity != nil && current.Activity == nil) ||
95
+ (previous.Origin != nil && current.Origin == nil) ||
96
+ (previous.Interactivity != nil && current.Interactivity == nil) ||
97
+ (previous.WorkingDirectories != nil && current.WorkingDirectories == nil)
98
+ if clears {
99
+ h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatAddedAction{Type: ahptypes.ActionTypeSessionChatAdded, Summary: current}})
100
+ return true
101
+ }
102
+ changes := ahptypes.PartialChatSummary{
103
+ Title: &current.Title, Status: &current.Status, Activity: current.Activity, ModifiedAt: &current.ModifiedAt,
104
+ Origin: current.Origin, WorkingDirectories: current.WorkingDirectories,
105
+ }
106
+ h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatUpdatedAction{
107
+ Type: ahptypes.ActionTypeSessionChatUpdated, Chat: chatChannel, Changes: changes,
108
+ }})
109
+ return true
110
+ }
@@ -0,0 +1,32 @@
1
+ package channels
2
+
3
+ import (
4
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
5
+
6
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
7
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
8
+ )
9
+
10
+ // Catalogue notifications are ephemeral and never replayed on reconnect; after reconnecting a
11
+ // client re-fetches the catalogue with listSessions.
12
+
13
+ // NotifySessionAdded announces a session on the root channel.
14
+ func NotifySessionAdded(h *host.Host, summary ahptypes.SessionSummary) {
15
+ h.Notify(wire.RootChannel, "root/sessionAdded", map[string]any{"summary": summary})
16
+ }
17
+
18
+ // NotifySessionRemoved announces a removed session.
19
+ func NotifySessionRemoved(h *host.Host, session string) {
20
+ h.Notify(wire.RootChannel, "root/sessionRemoved", map[string]any{"session": session})
21
+ }
22
+
23
+ // NotifySessionSummaryChanged announces changed catalogue fields.
24
+ func NotifySessionSummaryChanged(h *host.Host, session string, changes ahptypes.PartialSessionSummary) {
25
+ h.Notify(wire.RootChannel, "root/sessionSummaryChanged", map[string]any{"session": session, "changes": changes})
26
+ }
27
+
28
+ // TerminalInfoOf is the root catalogue projection of a terminal; process handles never enter
29
+ // protocol state.
30
+ func TerminalInfoOf(resource string, state *ahptypes.TerminalState) ahptypes.TerminalInfo {
31
+ return ahptypes.TerminalInfo{Resource: resource, Title: state.Title, Claim: state.Claim, Lifecycle: state.Lifecycle}
32
+ }
@@ -0,0 +1,128 @@
1
+ // Package channels builds the protocol-shaped state of the session and chat channels. It
2
+ // ports pi-ahp's src/channels (root.ts, session.ts, chat.ts) and knows nothing about Pi or PiG.
3
+ package channels
4
+
5
+ import (
6
+ "encoding/json"
7
+ "time"
8
+
9
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
10
+
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
12
+ )
13
+
14
+ // InitialSessionState is a fresh session in lifecycle "creating".
15
+ func InitialSessionState(provider, title, workingDirectory string) *ahptypes.SessionState {
16
+ return &ahptypes.SessionState{
17
+ Provider: provider,
18
+ Title: title,
19
+ Status: ahptypes.SessionStatusIdle,
20
+ Lifecycle: ahptypes.SessionLifecycleCreating,
21
+ WorkingDirectories: []ahptypes.URI{wire.PathToFileURI(workingDirectory)},
22
+ ActiveClients: []ahptypes.SessionActiveClient{},
23
+ Chats: []ahptypes.ChatSummary{},
24
+ }
25
+ }
26
+
27
+ // statusActivityMask: the low five bits encode activity; higher bits carry independent metadata
28
+ // flags.
29
+ const statusActivityMask = ahptypes.SessionStatus(1<<5 - 1)
30
+
31
+ // SessionChatAggregate is what a session derives from its chat catalogue.
32
+ type SessionChatAggregate struct {
33
+ Status ahptypes.SessionStatus
34
+ Activity *string
35
+ ModifiedAt string // "" when the session has no chats
36
+ }
37
+
38
+ // instant parses an ISO 8601 timestamp; an unparsable one sorts before every valid one.
39
+ func instant(summary ahptypes.ChatSummary) (t time.Time, ok bool) {
40
+ for _, layout := range []string{time.RFC3339Nano, "2006-01-02T15:04:05.999999999Z0700", "2006-01-02T15:04:05Z0700"} {
41
+ if parsed, err := time.Parse(layout, summary.ModifiedAt); err == nil {
42
+ return parsed, true
43
+ }
44
+ }
45
+ return time.Time{}, false
46
+ }
47
+
48
+ func newer(a, b ahptypes.ChatSummary) bool { // a is strictly more recent than b
49
+ ta, oka := instant(a)
50
+ tb, okb := instant(b)
51
+ switch {
52
+ case !oka:
53
+ return false
54
+ case !okb:
55
+ return true
56
+ }
57
+ return ta.After(tb)
58
+ }
59
+
60
+ func mostRecent(chats []ahptypes.ChatSummary, matches func(ahptypes.ChatSummary) bool) (ahptypes.ChatSummary, bool) {
61
+ var latest ahptypes.ChatSummary
62
+ found := false
63
+ for _, chat := range chats {
64
+ if matches(chat) && (!found || newer(chat, latest)) {
65
+ latest, found = chat, true
66
+ }
67
+ }
68
+ return latest, found
69
+ }
70
+
71
+ // AggregateSessionChats projects the session-level summary fields AHP derives from its chat
72
+ // catalogue. The current host exposes one chat, so this is a pass-through today; keeping the
73
+ // complete 0.9 rule makes the projection deterministic. It never mutates the state.
74
+ func AggregateSessionChats(state *ahptypes.SessionState) SessionChatAggregate {
75
+ if len(state.Chats) == 0 {
76
+ return SessionChatAggregate{Status: state.Status, Activity: state.Activity}
77
+ }
78
+ latest, ok := mostRecent(state.Chats, func(ahptypes.ChatSummary) bool { return true })
79
+ if !ok {
80
+ return SessionChatAggregate{Status: state.Status}
81
+ }
82
+ driver := latest
83
+ if state.DefaultChat != nil {
84
+ for _, chat := range state.Chats {
85
+ if chat.Resource == *state.DefaultChat {
86
+ driver = chat
87
+ break
88
+ }
89
+ }
90
+ }
91
+ if c, ok := mostRecent(state.Chats, func(c ahptypes.ChatSummary) bool {
92
+ return c.Status&ahptypes.SessionStatusInputNeeded == ahptypes.SessionStatusInputNeeded
93
+ }); ok {
94
+ driver = c
95
+ } else if c, ok := mostRecent(state.Chats, func(c ahptypes.ChatSummary) bool {
96
+ return c.Status&ahptypes.SessionStatusError == ahptypes.SessionStatusError
97
+ }); ok {
98
+ driver = c
99
+ }
100
+ metadata := state.Status &^ statusActivityMask
101
+ return SessionChatAggregate{
102
+ Status: metadata | (driver.Status & statusActivityMask),
103
+ Activity: driver.Activity,
104
+ ModifiedAt: latest.ModifiedAt,
105
+ }
106
+ }
107
+
108
+ // SessionSummaryOf is the root-catalogue projection of a session.
109
+ func SessionSummaryOf(resource, createdAt string, state *ahptypes.SessionState, meta map[string]json.RawMessage) ahptypes.SessionSummary {
110
+ aggregate := AggregateSessionChats(state)
111
+ modifiedAt := aggregate.ModifiedAt
112
+ if modifiedAt == "" {
113
+ modifiedAt = createdAt
114
+ }
115
+ summary := ahptypes.SessionSummary{
116
+ Resource: resource, Provider: state.Provider, Title: state.Title, Status: aggregate.Status,
117
+ // Root summary activity is deliberately omitted for AHP 0.9: its partial update shape
118
+ // cannot distinguish "clear" from "unchanged" on the JSON wire.
119
+ CreatedAt: createdAt, ModifiedAt: modifiedAt,
120
+ }
121
+ if state.WorkingDirectories != nil {
122
+ summary.WorkingDirectories = state.WorkingDirectories
123
+ }
124
+ if meta != nil {
125
+ summary.Meta = meta
126
+ }
127
+ return summary
128
+ }