@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,185 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "path/filepath"
7
+ "reflect"
8
+ "strings"
9
+ "sync"
10
+ "testing"
11
+
12
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
13
+
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
16
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pisession"
17
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
18
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
19
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
20
+ )
21
+
22
+ // restartBackend answers every prompt with an empty settled run.
23
+ type restartBackend struct {
24
+ mu sync.Mutex
25
+ prompts []string
26
+ listeners []func(mapper.Event)
27
+ }
28
+
29
+ func (b *restartBackend) Subscribe(l func(mapper.Event)) func() {
30
+ b.mu.Lock()
31
+ b.listeners = append(b.listeners, l)
32
+ b.mu.Unlock()
33
+ return func() {}
34
+ }
35
+ func (b *restartBackend) Prompt(_ context.Context, text string, _ []mapper.Image) error {
36
+ b.mu.Lock()
37
+ b.prompts = append(b.prompts, text)
38
+ ls := append([]func(mapper.Event){}, b.listeners...)
39
+ b.mu.Unlock()
40
+ for _, l := range ls {
41
+ l(mapper.Event{"type": "agent_start"})
42
+ l(mapper.Event{"type": "agent_settled"})
43
+ }
44
+ return nil
45
+ }
46
+ func (b *restartBackend) Steer(context.Context, string, []mapper.Image) error { return nil }
47
+ func (b *restartBackend) Abort(context.Context) error { return nil }
48
+
49
+ // Twin of upstream test/reconnect.test.ts "reconnect after host restart": the durable session
50
+ // written by a previous host is restored when a client that knew the old host reconnects.
51
+ func TestReconnectAfterHostRestart(t *testing.T) {
52
+ twin.Run(t, "reconnect", "repairs VS Code's channel-less reconnect and restores a durable session", func(t *testing.T) {
53
+ root, workspace := t.TempDir(), t.TempDir()
54
+ id := newID()
55
+ writer, err := pisession.Create(workspace, filepath.Join(root, "fixture"), pisession.Options{ID: id})
56
+ if err != nil {
57
+ t.Fatal(err)
58
+ }
59
+ if _, err := writer.AppendMessage(obj{"role": "user", "content": "before restart", "timestamp": 0}); err != nil {
60
+ t.Fatal(err)
61
+ }
62
+ if _, err := writer.AppendMessage(obj{"role": "assistant", "content": []any{obj{"type": "text", "text": "persisted reply"}}, "timestamp": 0}); err != nil {
63
+ t.Fatal(err)
64
+ }
65
+ backend := &restartBackend{}
66
+ h := startHarness(t, harnessOptions{sessionRoot: root, workingDir: workspace, createBackend: func(*pi.LiveSession) (pi.Backend, error) { return backend, nil }})
67
+
68
+ clientSession := "pi:/" + id
69
+ clientChat := "ahp-chat://default/" + base64.RawURLEncoding.EncodeToString([]byte(clientSession))
70
+ client := testkit.Connect(t, h.host)
71
+ t.Cleanup(client.Close)
72
+ // The reconnect names no channel, as VS Code's does.
73
+ var result struct {
74
+ Type string
75
+ Snapshots []struct {
76
+ Resource string
77
+ State map[string]any
78
+ }
79
+ }
80
+ client.Decode(client.Must("reconnect", obj{
81
+ "clientId": "vscode-from-previous-host", "lastSeenServerSeq": 42,
82
+ "subscriptions": []string{wire.RootChannel, clientSession}, "_meta": obj{"vscode.telemetryLevel": "off"},
83
+ }), &result)
84
+ if result.Type != "snapshot" {
85
+ t.Fatalf("type = %q", result.Type)
86
+ }
87
+ var resources []string
88
+ var sessionState map[string]any
89
+ for _, s := range result.Snapshots {
90
+ resources = append(resources, s.Resource)
91
+ if s.Resource == clientSession {
92
+ sessionState = s.State
93
+ }
94
+ }
95
+ if !reflect.DeepEqual(sortedCopy(resources), sortedCopy([]string{wire.RootChannel, clientSession})) {
96
+ t.Fatalf("snapshots = %v", resources)
97
+ }
98
+ if sessionState["defaultChat"] != clientChat {
99
+ t.Fatalf("defaultChat = %v, want %s", sessionState["defaultChat"], clientChat)
100
+ }
101
+
102
+ rpcErr := client.ExpectError("disposeSession", obj{"channel": clientSession}, wire.CodeInvalidRequest)
103
+ if !strings.Contains(rpcErr.Message, "VS Code provisional-session lifecycle bug") {
104
+ t.Fatalf("message = %q", rpcErr.Message)
105
+ }
106
+ if !h.host.Store().Has(wire.SessionURI(id)) {
107
+ t.Fatal("the durable session must survive the refused disposal")
108
+ }
109
+
110
+ chat := snapshotState[ahptypes.ChatState](t, client, clientChat)
111
+ turn := chat.Turns[0]
112
+ if turn.Message.Text != "before restart" || turn.State != ahptypes.TurnStateComplete {
113
+ t.Fatalf("restored turn = %+v", turn)
114
+ }
115
+ var reply string
116
+ for _, p := range turn.ResponseParts {
117
+ if md, ok := p.Value.(*ahptypes.MarkdownResponsePart); ok {
118
+ reply = md.Content
119
+ }
120
+ }
121
+ if reply != "persisted reply" {
122
+ t.Fatalf("reply = %q", reply)
123
+ }
124
+ if !h.host.Store().Has(wire.SessionURI(id)) || !h.host.Store().Has(wire.ChatURI(id)) {
125
+ t.Fatal("session and chat must both be restored")
126
+ }
127
+
128
+ client.Dispatch(clientChat, obj{"type": "chat/turnStarted", "turnId": "after-restart", "startedAt": "2025-01-01T00:00:00.000Z", "message": userMessage("continue after restart")})
129
+ testkit.Eventually(t, "the post-restart prompt to reach the backend", func() bool { backend.mu.Lock(); defer backend.mu.Unlock(); return len(backend.prompts) == 1 })
130
+ if !reflect.DeepEqual(backend.prompts, []string{"continue after restart"}) {
131
+ t.Fatalf("prompts = %v", backend.prompts)
132
+ }
133
+ testkit.Eventually(t, "the resumed turn to settle", func() bool {
134
+ s := h.host.Store().Chat(wire.ChatURI(id))
135
+ return s.ActiveTurn == nil && len(s.Turns) == 2
136
+ })
137
+ state := h.host.Store().Chat(wire.ChatURI(id))
138
+ if state.Turns[1].Message.Text != "continue after restart" || state.Turns[1].State != ahptypes.TurnStateComplete {
139
+ t.Fatalf("turns = %+v", turnShapes(state.Turns))
140
+ }
141
+ })
142
+ }
143
+
144
+ func sortedCopy(in []string) []string {
145
+ out := append([]string(nil), in...)
146
+ for i := range out {
147
+ for j := i + 1; j < len(out); j++ {
148
+ if out[j] < out[i] {
149
+ out[i], out[j] = out[j], out[i]
150
+ }
151
+ }
152
+ }
153
+ return out
154
+ }
155
+
156
+ // Twin of upstream test/session-storage.test.serial.ts.
157
+ func TestDefaultSessionStorage(t *testing.T) {
158
+ twin.Run(t, "session-storage", "keeps the default catalogue and Pi session writer on the same isolated profile", func(t *testing.T) {
159
+ agentDir, workspace := t.TempDir(), t.TempDir()
160
+ catalogueRoot, create := pi.DefaultStorage(agentDir)
161
+ sessionID := newID()
162
+ store, err := create(workspace, sessionID)
163
+ if err != nil {
164
+ t.Fatal(err)
165
+ }
166
+ manager := store.(*pisession.Manager)
167
+ if _, err := manager.AppendMessage(obj{"role": "user", "content": "hello", "timestamp": 0}); err != nil {
168
+ t.Fatal(err)
169
+ }
170
+ if _, err := manager.AppendMessage(obj{"role": "assistant", "content": []any{obj{"type": "text", "text": "hello"}}, "timestamp": 0}); err != nil {
171
+ t.Fatal(err)
172
+ }
173
+ file := store.File()
174
+ if file == "" {
175
+ t.Fatal("no session file")
176
+ }
177
+ found, err := pi.NewCatalogue(catalogueRoot).FindSessionFile(sessionID)
178
+ if err != nil || found != file {
179
+ t.Fatalf("catalogue found %q (%v), writer wrote %q", found, err, file)
180
+ }
181
+ if rel, err := filepath.Rel(filepath.Join(agentDir, "sessions"), file); err != nil || strings.HasPrefix(rel, "..") {
182
+ t.Fatalf("file %s is outside the profile (%q, %v)", file, rel, err)
183
+ }
184
+ })
185
+ }
@@ -0,0 +1,70 @@
1
+ package pi
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
7
+
8
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
9
+ )
10
+
11
+ // ServicesOptions wire the session services together.
12
+ type ServicesOptions struct {
13
+ Host *host.Host
14
+ // SessionRoot is where Pi's session files live (one sub-directory per working directory).
15
+ SessionRoot string
16
+ DefaultWorkingDirectory string
17
+ CreateBackend BackendFactory
18
+ CreateSessionManager SessionManagerFactory
19
+ DefaultSelection func() *ahptypes.ModelSelection
20
+ DeleteFile func(path string) (SessionFileDeletionResult, error)
21
+ WrapStore func(sessionID string, opened SessionStore) SessionStore
22
+ AdoptForeignTurns bool
23
+ Log func(format string, args ...any)
24
+ }
25
+
26
+ // Services is the registry, catalogue and hydrator wired to one host.
27
+ type Services struct {
28
+ Registry *Registry
29
+ Catalogue *Catalogue
30
+ Hydrator *Hydrator
31
+ }
32
+
33
+ // NewServices builds the session services.
34
+ func NewServices(opts ServicesOptions) *Services {
35
+ catalogue := NewCatalogue(opts.SessionRoot)
36
+ registry := NewRegistry(RegistryOptions{
37
+ Host: opts.Host, DefaultWorkingDirectory: opts.DefaultWorkingDirectory, CreateBackend: opts.CreateBackend,
38
+ CreateSessionManager: opts.CreateSessionManager, DefaultSelection: opts.DefaultSelection, DeleteFile: opts.DeleteFile,
39
+ FindSessionFile: catalogue.FindSessionFile, AdoptForeignTurns: opts.AdoptForeignTurns, Log: opts.Log,
40
+ })
41
+ hydrator := NewHydrator(HydratorOptions{
42
+ Host: opts.Host, Catalogue: catalogue,
43
+ IsLive: registry.Has,
44
+ IsDisposing: registry.IsDisposing,
45
+ FallbackSelection: opts.DefaultSelection,
46
+ Adopt: func(a AdoptedSession) { registry.Adopt(a) },
47
+ WrapStore: opts.WrapStore,
48
+ Log: opts.Log,
49
+ })
50
+ return &Services{Registry: registry, Catalogue: catalogue, Hydrator: hydrator}
51
+ }
52
+
53
+ type liveCatalogue struct{ s *Services }
54
+
55
+ func (c liveCatalogue) List(ctx context.Context, limit *int64, cursor *string) (ahptypes.ListSessionsResult, error) {
56
+ return c.s.Catalogue.List(ctx, limit, cursor, c.s.Registry.CatalogueOverrides)
57
+ }
58
+
59
+ type turnPaging struct{ r *Registry }
60
+
61
+ func (p turnPaging) FetchTurns(ctx context.Context, params ahptypes.FetchTurnsParams) error {
62
+ return p.r.FetchTurns(ctx, params)
63
+ }
64
+
65
+ // Capabilities are what the host serves through these services.
66
+ func (s *Services) Capabilities() host.Capabilities {
67
+ return host.Capabilities{
68
+ Catalogue: liveCatalogue{s}, Sessions: s.Registry, Hydrator: s.Hydrator, TurnPaging: turnPaging{s.Registry},
69
+ }
70
+ }
@@ -0,0 +1,64 @@
1
+ package pi
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+
7
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
10
+ )
11
+
12
+ // ProjectTrustKey is the pre-creation config property that reports project-resource trust.
13
+ const ProjectTrustKey = "projectResources"
14
+
15
+ // SessionConfigOptions configure a [SessionConfigService].
16
+ type SessionConfigOptions struct {
17
+ DefaultWorkingDirectory string
18
+ ProjectTrustPolicy ProjectTrustPolicy
19
+ AgentDir string
20
+ }
21
+
22
+ // SessionConfigService answers resolveSessionConfig: what a session would be like before it is
23
+ // created. The exchange is iterative: the answer changes as the user picks a directory, which is
24
+ // the whole reason the command exists (port of src/pi/session-config.ts).
25
+ type SessionConfigService struct{ opts SessionConfigOptions }
26
+
27
+ // NewSessionConfigService creates the service.
28
+ func NewSessionConfigService(opts SessionConfigOptions) *SessionConfigService {
29
+ return &SessionConfigService{opts: opts}
30
+ }
31
+
32
+ // Resolve implements host.SessionConfigHandler.
33
+ func (s *SessionConfigService) Resolve(_ context.Context, params ahptypes.ResolveSessionConfigParams) (ahptypes.ResolveSessionConfigResult, error) {
34
+ workingDirectory := s.opts.DefaultWorkingDirectory
35
+ if params.WorkingDirectory != nil && *params.WorkingDirectory != "" {
36
+ path, err := wire.FileURIToPath(*params.WorkingDirectory)
37
+ if err != nil {
38
+ return ahptypes.ResolveSessionConfigResult{}, wire.InvalidParams(err.Error())
39
+ }
40
+ workingDirectory = path
41
+ }
42
+ trust, err := ResolveProjectTrust(workingDirectory, s.opts.ProjectTrustPolicy, s.opts.AgentDir)
43
+ if err != nil {
44
+ return ahptypes.ResolveSessionConfigResult{}, err
45
+ }
46
+ description := "Trust-gated project resources (" + trust.Reason + ")."
47
+ if trust.Reason == "no-project-resources" {
48
+ description = "This folder has no trust-gated project resources."
49
+ }
50
+ readOnly := true
51
+ value, _ := json.Marshal(trust.Trusted)
52
+ return ahptypes.ResolveSessionConfigResult{
53
+ Schema: ahptypes.SessionConfigSchema{Type: "object", Properties: map[string]ahptypes.SessionConfigPropertySchema{
54
+ // Host policy, not a per-session choice.
55
+ ProjectTrustKey: {Type: "boolean", Title: "Load project resources", Description: &description, ReadOnly: &readOnly},
56
+ }},
57
+ Values: map[string]json.RawMessage{ProjectTrustKey: value},
58
+ }, nil
59
+ }
60
+
61
+ // Completions implements host.SessionConfigHandler: this host offers no dynamic completions.
62
+ func (s *SessionConfigService) Completions(context.Context, ahptypes.SessionConfigCompletionsParams) (ahptypes.SessionConfigCompletionsResult, error) {
63
+ return ahptypes.SessionConfigCompletionsResult{Items: []ahptypes.SessionConfigValueItem{}}, nil
64
+ }
@@ -0,0 +1,74 @@
1
+ package pi_test
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strconv"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+ )
13
+
14
+ // fixtureSessionDirectory mirrors Pi's default cwd partition solely for synthetic JSONL fixtures
15
+ // (upstream test/support/session-files.ts).
16
+ func fixtureSessionDirectory(t testing.TB, root, cwd string) string {
17
+ t.Helper()
18
+ abs, err := filepath.Abs(cwd)
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ abs = strings.TrimLeft(abs, `/\`)
23
+ partition := "--" + strings.NewReplacer("/", "-", `\`, "-", ":", "-").Replace(abs) + "--"
24
+ dir := filepath.Join(root, partition)
25
+ if err := os.MkdirAll(dir, 0o755); err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ return dir
29
+ }
30
+
31
+ type fakeSession struct {
32
+ cwd string
33
+ firstUserMessage string
34
+ name string
35
+ mtimeSeconds int64 // controls catalogue ordering
36
+ }
37
+
38
+ var entryCounter int
39
+
40
+ // writeFakeSession writes a minimal but genuine Pi session file: a session header followed by
41
+ // entries linked through id/parentId (upstream writeFakeSession).
42
+ func writeFakeSession(t testing.TB, root, id string, o fakeSession) string {
43
+ t.Helper()
44
+ dir := fixtureSessionDirectory(t, root, o.cwd)
45
+ timestamp := time.Unix(o.mtimeSeconds, 0).UTC().Format("2006-01-02T15:04:05.000Z")
46
+ line := func(v map[string]any) string {
47
+ raw, _ := json.Marshal(v)
48
+ return string(raw)
49
+ }
50
+ lines := []string{line(map[string]any{"type": "session", "id": id, "parentId": nil, "timestamp": timestamp, "version": 3, "cwd": o.cwd})}
51
+ var parent any
52
+ push := func(entry map[string]any) {
53
+ entryCounter++
54
+ entryID := fmt.Sprintf("entry-%d", entryCounter)
55
+ entry["id"], entry["parentId"], entry["timestamp"] = entryID, parent, timestamp
56
+ lines = append(lines, line(entry))
57
+ parent = entryID
58
+ }
59
+ if o.firstUserMessage != "" {
60
+ push(map[string]any{"type": "message", "message": map[string]any{"role": "user", "content": o.firstUserMessage, "timestamp": 0}})
61
+ }
62
+ if o.name != "" {
63
+ push(map[string]any{"type": "session_info", "name": o.name})
64
+ }
65
+ path := filepath.Join(dir, strconv.FormatInt(o.mtimeSeconds, 10)+"_"+id+".jsonl")
66
+ if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ at := time.Unix(o.mtimeSeconds, 0)
70
+ if err := os.Chtimes(path, at, at); err != nil {
71
+ t.Fatal(err)
72
+ }
73
+ return path
74
+ }
@@ -0,0 +1,83 @@
1
+ package pi
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+
7
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pisession"
8
+ )
9
+
10
+ // SessionStore is the slice of Pi's session manager the host uses. *pisession.Manager satisfies
11
+ // it; the extension adapter provides one over the live PiG session.
12
+ type SessionStore interface {
13
+ // File is the session file, "" when the session is not persisted; it may not exist yet.
14
+ File() string
15
+ LeafEntry() pisession.Entry
16
+ SessionName() string
17
+ AppendSessionInfo(name string) (string, error)
18
+ // Branch is the path from the root to the leaf.
19
+ Branch(fromID string) []pisession.Entry
20
+ // ContextEntries is the compaction-aware entry list along the current branch.
21
+ ContextEntries() []pisession.Entry
22
+ }
23
+
24
+ // SessionManagerFactory creates the storage for a new session.
25
+ type SessionManagerFactory func(workingDirectory, sessionID string) (SessionStore, error)
26
+
27
+ // InMemoryStorage is a factory for sessions that never touch disk.
28
+ func InMemoryStorage(workingDirectory, sessionID string) (SessionStore, error) {
29
+ return pisession.InMemory(workingDirectory, pisession.Options{ID: sessionID})
30
+ }
31
+
32
+ // PersistentStorage is a factory for sessions written under dir.
33
+ func PersistentStorage(dir string) SessionManagerFactory {
34
+ return func(workingDirectory, sessionID string) (SessionStore, error) {
35
+ return pisession.Create(workingDirectory, dir, pisession.Options{ID: sessionID})
36
+ }
37
+ }
38
+
39
+ // HistorySource is what history serving needs from a live session.
40
+ type HistorySource interface {
41
+ Store() SessionStore
42
+ ID() string
43
+ // Anchor is the recorded truncation anchor of a turn.
44
+ Anchor(turnID string) (string, bool)
45
+ }
46
+
47
+ // TruncationAnchor is the session entry a truncation to turnID would move the agent to ("" turnID
48
+ // clears everything). ok is false when no entry matches: accepting an impossible truncation would
49
+ // shorten the client's view while Pi kept using context the user believes is gone.
50
+ func TruncationAnchor(source HistorySource, turnID *string) (string, bool) {
51
+ key := ClearAllAnchor
52
+ if turnID != nil {
53
+ key = *turnID
54
+ }
55
+ if known, ok := source.Anchor(key); ok && known != "" {
56
+ return known, true
57
+ }
58
+ // A turn this host never ran: recompute from the file, which is where a hydrated session's ids
59
+ // came from in the first place.
60
+ a, ok := RebuildHistoryFromSession(source.Store(), RebuildOptions{TurnIDPrefix: source.ID()}).Anchors[key]
61
+ return a, ok && a != ""
62
+ }
63
+
64
+ // DefaultSessionDir is where Pi keeps the sessions of a working directory: one sub-directory of
65
+ // <agentDir>/sessions per cwd, named after the encoded path (Pi's getDefaultSessionDirPath).
66
+ func DefaultSessionDir(agentDir, cwd string) string {
67
+ abs, err := filepath.Abs(cwd)
68
+ if err != nil {
69
+ abs = cwd
70
+ }
71
+ abs = strings.TrimLeft(abs, `/\`)
72
+ safe := "--" + strings.NewReplacer("/", "-", `\`, "-", ":", "-").Replace(abs) + "--"
73
+ return filepath.Join(agentDir, "sessions", safe)
74
+ }
75
+
76
+ // DefaultStorage keeps new sessions where Pi itself would, under <agentDir>/sessions, and pairs the
77
+ // catalogue root with the same profile so the catalogue and the writer never disagree (port of
78
+ // createDefaultPiSessionStorage).
79
+ func DefaultStorage(agentDir string) (catalogueRoot string, create SessionManagerFactory) {
80
+ return filepath.Join(agentDir, "sessions"), func(workingDirectory, sessionID string) (SessionStore, error) {
81
+ return pisession.Create(workingDirectory, DefaultSessionDir(agentDir, workingDirectory), pisession.Options{ID: sessionID})
82
+ }
83
+ }