@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,29 @@
1
+ import { join } from "node:path";
2
+ import { getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
3
+ import { PiSessionCatalogue } from "./session-catalogue.ts";
4
+
5
+ /** Allocates pi's canonical history manager for a newly created session. */
6
+ export type SessionManagerFactory = (workingDirectory: string, sessionId: string) => SessionManager;
7
+
8
+ /** Paired read/write boundary for pi's durable session corpus. */
9
+ export interface PiSessionStorage {
10
+ readonly catalogue: PiSessionCatalogue;
11
+ readonly createSessionManager: SessionManagerFactory;
12
+ }
13
+
14
+ /** Pi's standard durable, cwd-partitioned session tree. */
15
+ const durableSessionManagerFactory: SessionManagerFactory = (workingDirectory, sessionId) =>
16
+ SessionManager.create(workingDirectory, undefined, { id: sessionId });
17
+
18
+ /**
19
+ * Production storage follows the SDK default: SessionManager resolves each
20
+ * cwd-partitioned write directory, while the catalogue scans the matching
21
+ * `getAgentDir()/sessions` root. Pi's separate session-dir precedence lives in
22
+ * its CLI rather than an exported SDK resolver, so the host does not duplicate it.
23
+ */
24
+ export function createDefaultPiSessionStorage(): PiSessionStorage {
25
+ return {
26
+ catalogue: new PiSessionCatalogue(join(getAgentDir(), "sessions")),
27
+ createSessionManager: durableSessionManagerFactory,
28
+ };
29
+ }
@@ -0,0 +1,18 @@
1
+ /** Shared display-title rules for live, catalogued, and hydrated sessions. */
2
+
3
+ export const NEW_SESSION_TITLE = "New Session";
4
+ const UNTITLED_SESSION_TITLE = "Untitled session";
5
+ const TITLE_FALLBACK_LENGTH = 60;
6
+
7
+ /** pi displays the first user message when no explicit session name exists. */
8
+ export function fallbackSessionTitle(text: string | undefined): string | undefined {
9
+ const collapsed = text?.replace(/\s+/gu, " ").trim() ?? "";
10
+ if (!collapsed) {
11
+ return undefined;
12
+ }
13
+ return collapsed.length > TITLE_FALLBACK_LENGTH ? `${collapsed.slice(0, TITLE_FALLBACK_LENGTH - 1)}…` : collapsed;
14
+ }
15
+
16
+ export function sessionDisplayTitle(name: string | undefined, firstUserMessage: string | undefined): string {
17
+ return name?.trim() || fallbackSessionTitle(firstUserMessage) || UNTITLED_SESSION_TITLE;
18
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Paging older turns into a chat.
3
+ *
4
+ * A hydrated chat shows the window pi itself renders — everything back to the
5
+ * most recent compaction. The conversation before that is still on disk, and
6
+ * without a way to reach it a long, repeatedly-compacted session looks like it
7
+ * only ever had its last few exchanges.
8
+ *
9
+ * `fetchTurns` is that way. The command returns nothing itself: the host
10
+ * dispatches `chat/turnsLoaded`, which prepends the older turns into the same
11
+ * reduced state and updates or clears the cursor.
12
+ *
13
+ * @see https://microsoft.github.io/agent-host-protocol/specification/chat-channel
14
+ */
15
+
16
+ import type { SessionManager } from "@earendil-works/pi-coding-agent";
17
+ import type { Turn } from "@microsoft/agent-host-protocol";
18
+ import { rebuildHistory } from "./history.ts";
19
+
20
+ /** Turns per page. Large enough to be worth a round-trip, small enough to stay cheap. */
21
+ const DEFAULT_PAGE_SIZE = 20;
22
+
23
+ export interface OlderTurnsPage {
24
+ /** Oldest-first, as the action requires. */
25
+ readonly turns: Turn[];
26
+ /** Cursor for the next page older than this one, absent when at the beginning. */
27
+ readonly nextCursor?: string;
28
+ }
29
+
30
+ /**
31
+ * Every turn on the session's current branch, ignoring compaction.
32
+ *
33
+ * `getBranch()` walks root→leaf, so it sees the whole conversation; the visible
34
+ * window comes from `buildContextEntries()`, which starts at the last
35
+ * compaction. Turn ids are derived from the entry that opened each turn, so the
36
+ * two rebuilds agree on ids wherever they overlap — which is also what lets the
37
+ * reducer dedupe a page against what the client already holds.
38
+ */
39
+ function fullBranchTurns(manager: SessionManager, prefix: string): Turn[] {
40
+ return rebuildHistory(manager.getBranch(), { turnIdPrefix: prefix }).turns;
41
+ }
42
+
43
+ /**
44
+ * The cursor to publish for a chat, or `undefined` when nothing older exists.
45
+ *
46
+ * Its presence is the protocol's signal that `turns` is a tail window rather
47
+ * than the whole conversation.
48
+ */
49
+ export function initialTurnsCursor(
50
+ manager: SessionManager,
51
+ prefix: string,
52
+ visible: readonly Turn[],
53
+ ): string | undefined {
54
+ const oldestVisible = visible[0];
55
+ if (!oldestVisible) {
56
+ return undefined;
57
+ }
58
+ const full = fullBranchTurns(manager, prefix);
59
+ const boundary = full.findIndex((turn) => turn.id === oldestVisible.id);
60
+ // `boundary <= 0` means the visible window already starts at the beginning.
61
+ return boundary > 0 ? oldestVisible.id : undefined;
62
+ }
63
+
64
+ /**
65
+ * Loads the page immediately older than `cursor`.
66
+ *
67
+ * The cursor names the oldest turn the client currently holds, so paging is
68
+ * anchored to content rather than to an offset — appending to a session cannot
69
+ * shift it, and a cursor for a turn that has since been truncated away simply
70
+ * finds nothing rather than returning the wrong slice.
71
+ */
72
+ export function loadOlderTurns(
73
+ manager: SessionManager,
74
+ prefix: string,
75
+ cursor: string | undefined,
76
+ limit: number = DEFAULT_PAGE_SIZE,
77
+ ): OlderTurnsPage {
78
+ if (!cursor) {
79
+ return { turns: [] };
80
+ }
81
+ const full = fullBranchTurns(manager, prefix);
82
+ const boundary = full.findIndex((turn) => turn.id === cursor);
83
+ if (boundary <= 0) {
84
+ return { turns: [] };
85
+ }
86
+
87
+ const start = Math.max(0, boundary - Math.max(1, limit));
88
+ const turns = full.slice(start, boundary);
89
+ return {
90
+ turns,
91
+ // More remains only if this page did not reach the beginning.
92
+ ...(start > 0 && turns[0] ? { nextCursor: turns[0].id } : {}),
93
+ };
94
+ }
@@ -0,0 +1,66 @@
1
+ /** Reconstructs the protocol-visible part of a user message stored by pi. */
2
+
3
+ import {
4
+ type Message,
5
+ type MessageAttachment,
6
+ MessageAttachmentKind,
7
+ MessageKind,
8
+ } from "@microsoft/agent-host-protocol";
9
+ import { normalizeImageMimeType } from "./image-mime.ts";
10
+
11
+ interface PiContentBlock {
12
+ readonly type?: unknown;
13
+ readonly text?: unknown;
14
+ readonly data?: unknown;
15
+ readonly mimeType?: unknown;
16
+ }
17
+
18
+ function blocksOf(content: unknown): PiContentBlock[] {
19
+ return Array.isArray(content)
20
+ ? content.filter((block): block is PiContentBlock => typeof block === "object" && block !== null)
21
+ : [];
22
+ }
23
+
24
+ export function textFromPiUserContent(content: unknown): string {
25
+ if (typeof content === "string") {
26
+ return content;
27
+ }
28
+ return blocksOf(content)
29
+ .filter((block) => block.type === "text" && typeof block.text === "string")
30
+ .map((block) => block.text as string)
31
+ .join("");
32
+ }
33
+
34
+ function imageAttachments(content: unknown): MessageAttachment[] {
35
+ const attachments: MessageAttachment[] = [];
36
+ for (const block of blocksOf(content)) {
37
+ if (block.type !== "image" || typeof block.data !== "string" || typeof block.mimeType !== "string") {
38
+ continue;
39
+ }
40
+ const contentType = normalizeImageMimeType(block.mimeType);
41
+ if (!contentType) {
42
+ continue;
43
+ }
44
+ attachments.push({
45
+ type: MessageAttachmentKind.EmbeddedResource,
46
+ label: `Image ${attachments.length + 1}`,
47
+ displayKind: "image",
48
+ data: block.data,
49
+ contentType,
50
+ });
51
+ }
52
+ return attachments;
53
+ }
54
+
55
+ /**
56
+ * pi persists image bytes but not client-side labels, so replay uses stable
57
+ * ordinal labels while preserving the actual image and MIME type.
58
+ */
59
+ export function userMessageFromPiContent(content: unknown): Message {
60
+ const attachments = imageAttachments(content);
61
+ return {
62
+ text: textFromPiUserContent(content),
63
+ origin: { kind: MessageKind.User },
64
+ ...(attachments.length > 0 ? { attachments } : {}),
65
+ };
66
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Protocol-level errors.
3
+ *
4
+ * Handlers throw {@link ProtocolError}; the router turns it into a JSON-RPC
5
+ * error response. Anything else becomes `InternalError` (-32603).
6
+ */
7
+
8
+ import { AhpErrorCodes, JsonRpcErrorCodes } from "@microsoft/agent-host-protocol";
9
+
10
+ export class ProtocolError extends Error {
11
+ readonly code: number;
12
+ readonly data: unknown;
13
+
14
+ constructor(code: number, message: string, data?: unknown) {
15
+ super(message);
16
+ this.name = "ProtocolError";
17
+ this.code = code;
18
+ this.data = data;
19
+ }
20
+
21
+ static invalidParams(message: string, data?: unknown): ProtocolError {
22
+ return new ProtocolError(JsonRpcErrorCodes.InvalidParams, message, data);
23
+ }
24
+
25
+ static methodNotFound(method: string): ProtocolError {
26
+ return new ProtocolError(JsonRpcErrorCodes.MethodNotFound, `Unknown method: ${method}`);
27
+ }
28
+
29
+ static sessionNotFound(channel: string): ProtocolError {
30
+ return new ProtocolError(AhpErrorCodes.SessionNotFound, `Session not found: ${channel}`);
31
+ }
32
+
33
+ static sessionAlreadyExists(channel: string): ProtocolError {
34
+ return new ProtocolError(AhpErrorCodes.SessionAlreadyExists, `Session already exists: ${channel}`);
35
+ }
36
+
37
+ static providerNotFound(provider: string): ProtocolError {
38
+ return new ProtocolError(AhpErrorCodes.ProviderNotFound, `No agent for provider: ${provider}`);
39
+ }
40
+
41
+ static notFound(uri: string): ProtocolError {
42
+ return new ProtocolError(AhpErrorCodes.NotFound, `Not found: ${uri}`);
43
+ }
44
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * JSON-RPC 2.0 wire helpers.
3
+ *
4
+ * AHP is transport-agnostic JSON-RPC 2.0. Every command's and every
5
+ * notification's `params` carries a top-level `channel: URI`, so a receiver can
6
+ * dispatch any message by inspecting `(method, params.channel)` without
7
+ * per-method deserialisation.
8
+ *
9
+ * @see https://microsoft.github.io/agent-host-protocol/specification/overview
10
+ */
11
+
12
+ import type { URI } from "@microsoft/agent-host-protocol";
13
+
14
+ /** A JSON-RPC request: has both `method` and `id`. */
15
+ export interface JsonRpcRequest {
16
+ readonly jsonrpc: "2.0";
17
+ readonly id: number;
18
+ readonly method: string;
19
+ readonly params?: unknown;
20
+ }
21
+
22
+ /** A JSON-RPC notification: has `method` but no `id`. */
23
+ export interface JsonRpcNotification {
24
+ readonly jsonrpc: "2.0";
25
+ readonly method: string;
26
+ readonly params?: unknown;
27
+ }
28
+
29
+ export interface JsonRpcSuccessResponse {
30
+ readonly jsonrpc: "2.0";
31
+ readonly id: number;
32
+ readonly result: unknown;
33
+ }
34
+
35
+ export interface JsonRpcErrorResponse {
36
+ readonly jsonrpc: "2.0";
37
+ readonly id: number;
38
+ readonly error: {
39
+ readonly code: number;
40
+ readonly message: string;
41
+ readonly data?: unknown;
42
+ };
43
+ }
44
+
45
+ export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
46
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
47
+
48
+ function isObject(value: unknown): value is Record<string, unknown> {
49
+ return typeof value === "object" && value !== null;
50
+ }
51
+
52
+ export function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {
53
+ return isObject(message) && typeof message.method === "string" && typeof message.id === "number";
54
+ }
55
+
56
+ export function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {
57
+ return isObject(message) && typeof message.method === "string" && message.id === undefined;
58
+ }
59
+
60
+ /**
61
+ * Reads the universal routing key off a message's params.
62
+ *
63
+ * Returns `undefined` when the params are missing or carry no `channel`, which
64
+ * the caller surfaces as `InvalidParams` — the field is required on every
65
+ * command and notification in the protocol.
66
+ */
67
+ export function readChannel(params: unknown): URI | undefined {
68
+ if (!isObject(params)) {
69
+ return undefined;
70
+ }
71
+ const channel = params.channel;
72
+ return typeof channel === "string" ? channel : undefined;
73
+ }
74
+
75
+ export function successResponse(id: number, result: unknown): JsonRpcSuccessResponse {
76
+ return { jsonrpc: "2.0", id, result };
77
+ }
78
+
79
+ export function errorResponse(id: number, code: number, message: string, data?: unknown): JsonRpcErrorResponse {
80
+ return {
81
+ jsonrpc: "2.0",
82
+ id,
83
+ error: data === undefined ? { code, message } : { code, message, data },
84
+ };
85
+ }
86
+
87
+ export function notification(method: string, params: unknown): JsonRpcNotification {
88
+ return { jsonrpc: "2.0", method, params };
89
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Protocol version negotiation.
3
+ *
4
+ * The client offers `InitializeParams.protocolVersions` most-preferred first;
5
+ * the server picks the first entry it can speak and echoes it back as
6
+ * `InitializeResult.protocolVersion`. If there is no overlap the server MUST
7
+ * return `UnsupportedProtocolVersion` (-32005) rather than a result.
8
+ *
9
+ * @see https://microsoft.github.io/agent-host-protocol/specification/versioning
10
+ */
11
+
12
+ import { AhpErrorCodes, PROTOCOL_VERSION } from "@microsoft/agent-host-protocol";
13
+ import { ProtocolError } from "./errors.ts";
14
+
15
+ /** Generated wire models do not down-convert required fields across minor versions. */
16
+ const HOST_SUPPORTED_VERSIONS: readonly string[] = [PROTOCOL_VERSION];
17
+
18
+ /**
19
+ * Picks the client's most-preferred version that this host also speaks.
20
+ *
21
+ * @throws {ProtocolError} `UnsupportedProtocolVersion` when there is no overlap.
22
+ * The error `data` advertises what the host can speak so the client can
23
+ * decide whether to downgrade.
24
+ */
25
+ export function negotiateProtocolVersion(offered: readonly string[] | undefined): string {
26
+ if (!Array.isArray(offered) || offered.length === 0 || offered.some((version) => typeof version !== "string")) {
27
+ throw ProtocolError.invalidParams("initialize requires a non-empty protocolVersions array of strings");
28
+ }
29
+ for (const candidate of offered) {
30
+ if (HOST_SUPPORTED_VERSIONS.includes(candidate)) {
31
+ return candidate;
32
+ }
33
+ }
34
+ throw new ProtocolError(
35
+ AhpErrorCodes.UnsupportedProtocolVersion,
36
+ `No mutually supported protocol version. Offered: ${offered.join(", ")}`,
37
+ { supportedVersions: [...HOST_SUPPORTED_VERSIONS] },
38
+ );
39
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * WebSocket transport.
3
+ *
4
+ * AHP does not mandate a transport — anything reliable, ordered, bidirectional,
5
+ * and message-framed works. This transport carries one JSON-RPC message per
6
+ * WebSocket frame.
7
+ *
8
+ * Access control to the endpoint itself is a transport concern, handled here
9
+ * during the HTTP upgrade rather than inside the protocol.
10
+ *
11
+ * @see https://microsoft.github.io/agent-host-protocol/specification/transport
12
+ */
13
+
14
+ import { createServer, type Server as HttpServer } from "node:http";
15
+ import { type WebSocket, WebSocketServer } from "ws";
16
+ import type { Transport } from "../core/connection.ts";
17
+ import type { AhpHost } from "../core/host.ts";
18
+ import type { JsonRpcMessage } from "../protocol/jsonrpc.ts";
19
+
20
+ export interface WebSocketTransportOptions {
21
+ readonly host?: string;
22
+ readonly port?: number;
23
+ /** Expected as `?token=` or VS Code's `?tkn=` when set. */
24
+ readonly connectionToken?: string;
25
+ readonly log?: (message: string) => void;
26
+ }
27
+
28
+ class WebSocketClientTransport implements Transport {
29
+ readonly #socket: WebSocket;
30
+ readonly #log: ((message: string) => void) | undefined;
31
+
32
+ constructor(socket: WebSocket, log?: (message: string) => void) {
33
+ this.#socket = socket;
34
+ this.#log = log;
35
+ }
36
+
37
+ send(message: JsonRpcMessage): void {
38
+ if (this.#socket.readyState === this.#socket.OPEN) {
39
+ this.#socket.send(JSON.stringify(message));
40
+ }
41
+ }
42
+
43
+ close(): void {
44
+ this.#socket.close();
45
+ }
46
+
47
+ onMessage(handler: (message: unknown) => void): void {
48
+ this.#socket.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => {
49
+ const text = Array.isArray(data)
50
+ ? Buffer.concat(data).toString("utf8")
51
+ : Buffer.from(data as Buffer).toString("utf8");
52
+ try {
53
+ handler(JSON.parse(text));
54
+ } catch (error) {
55
+ // A frame we cannot parse has no `id`, so there is nobody to answer.
56
+ this.#log?.(`Dropping unparseable frame: ${String(error)}`);
57
+ }
58
+ });
59
+ }
60
+
61
+ onClose(handler: () => void): void {
62
+ this.#socket.on("close", handler);
63
+ }
64
+ }
65
+
66
+ export interface RunningServer {
67
+ readonly host: string;
68
+ readonly port: number;
69
+ close(): Promise<void>;
70
+ }
71
+
72
+ /** Starts a WebSocket server and wires each connection into the host. */
73
+ export async function serveWebSocket(
74
+ ahpHost: AhpHost,
75
+ options: WebSocketTransportOptions = {},
76
+ ): Promise<RunningServer> {
77
+ const bindHost = options.host ?? "127.0.0.1";
78
+ // Port 0 asks the OS for a free port; the caller reads the real one back.
79
+ const bindPort = options.port ?? 0;
80
+
81
+ const httpServer: HttpServer = createServer((_request, response) => {
82
+ response.writeHead(426, { "Content-Type": "text/plain" });
83
+ response.end("Upgrade required");
84
+ });
85
+
86
+ const wss = new WebSocketServer({ noServer: true });
87
+
88
+ httpServer.on("upgrade", (request, socket, head) => {
89
+ if (options.connectionToken) {
90
+ const url = new URL(request.url ?? "/", "http://localhost");
91
+ // VS Code calls a manually configured connection token `tkn`; generic
92
+ // AHP examples use `token`, so a protected direct listener accepts both.
93
+ const presented = url.searchParams.get("token") ?? url.searchParams.get("tkn");
94
+ if (presented !== options.connectionToken) {
95
+ options.log?.("Rejecting upgrade: bad token");
96
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
97
+ socket.destroy();
98
+ return;
99
+ }
100
+ }
101
+ wss.handleUpgrade(request, socket, head, (ws) => {
102
+ wss.emit("connection", ws, request);
103
+ });
104
+ });
105
+
106
+ wss.on("connection", (socket: WebSocket) => {
107
+ ahpHost.accept(new WebSocketClientTransport(socket, options.log));
108
+ });
109
+
110
+ await new Promise<void>((resolve, reject) => {
111
+ httpServer.once("error", reject);
112
+ httpServer.listen(bindPort, bindHost, () => {
113
+ httpServer.removeListener("error", reject);
114
+ resolve();
115
+ });
116
+ });
117
+
118
+ const address = httpServer.address();
119
+ if (address === null || typeof address === "string") {
120
+ throw new Error("Failed to determine the bound address");
121
+ }
122
+
123
+ return {
124
+ host: bindHost,
125
+ port: address.port,
126
+ close: () =>
127
+ new Promise<void>((resolve) => {
128
+ for (const client of wss.clients) {
129
+ client.terminate();
130
+ }
131
+ wss.close(() => {
132
+ httpServer.close(() => resolve());
133
+ });
134
+ }),
135
+ };
136
+ }