@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,1011 @@
1
+ package ahp
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "sync"
9
+ "sync/atomic"
10
+ "time"
11
+
12
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
13
+ )
14
+
15
+ // ─── Configuration ──────────────────────────────────────────────────────
16
+
17
+ // Config tunes a [Client].
18
+ type Config struct {
19
+ // DefaultRequestTimeout bounds how long a [Client.Request] waits
20
+ // for a response. Zero disables the timeout.
21
+ DefaultRequestTimeout time.Duration
22
+
23
+ // SubscriptionBuffer is the size of each subscription's event
24
+ // channel. Slow consumers that fail to drain within this many
25
+ // messages will drop frames and must resubscribe.
26
+ SubscriptionBuffer int
27
+ }
28
+
29
+ // DefaultConfig returns a [Config] with sensible defaults: 30s request
30
+ // timeout, 256-message subscription buffer.
31
+ func DefaultConfig() Config {
32
+ return Config{
33
+ DefaultRequestTimeout: 30 * time.Second,
34
+ SubscriptionBuffer: 256,
35
+ }
36
+ }
37
+
38
+ // ─── Subscription events ─────────────────────────────────────────────────
39
+
40
+ // SubscriptionEvent is the marker interface implemented by every
41
+ // subscription event variant.
42
+ type SubscriptionEvent interface{ isSubscriptionEvent() }
43
+
44
+ // SubscriptionEventAction is a write-ahead action envelope for the
45
+ // subscription's channel.
46
+ type SubscriptionEventAction struct {
47
+ Envelope ahptypes.ActionEnvelope
48
+ }
49
+
50
+ func (SubscriptionEventAction) isSubscriptionEvent() {}
51
+
52
+ // SubscriptionEventSessionAdded mirrors the `root/sessionAdded`
53
+ // notification.
54
+ type SubscriptionEventSessionAdded struct {
55
+ Params ahptypes.SessionAddedParams
56
+ }
57
+
58
+ func (SubscriptionEventSessionAdded) isSubscriptionEvent() {}
59
+
60
+ // SubscriptionEventSessionRemoved mirrors the `root/sessionRemoved`
61
+ // notification.
62
+ type SubscriptionEventSessionRemoved struct {
63
+ Params ahptypes.SessionRemovedParams
64
+ }
65
+
66
+ func (SubscriptionEventSessionRemoved) isSubscriptionEvent() {}
67
+
68
+ // SubscriptionEventSessionSummaryChanged mirrors the
69
+ // `root/sessionSummaryChanged` notification.
70
+ type SubscriptionEventSessionSummaryChanged struct {
71
+ Params ahptypes.SessionSummaryChangedParams
72
+ }
73
+
74
+ func (SubscriptionEventSessionSummaryChanged) isSubscriptionEvent() {}
75
+
76
+ // SubscriptionEventAuthRequired mirrors the `auth/required`
77
+ // notification.
78
+ type SubscriptionEventAuthRequired struct {
79
+ Params ahptypes.AuthRequiredParams
80
+ }
81
+
82
+ func (SubscriptionEventAuthRequired) isSubscriptionEvent() {}
83
+
84
+ // ClientEvent is a [SubscriptionEvent] tagged with the channel URI it
85
+ // was scoped to. Returned by [Client.Events].
86
+ type ClientEvent struct {
87
+ Channel ahptypes.URI
88
+ Event SubscriptionEvent
89
+ }
90
+
91
+ // ─── Subscription handle ────────────────────────────────────────────────
92
+
93
+ // Subscription is the per-URI fan-out handle returned by
94
+ // [Client.Subscribe] and [Client.AttachSubscription]. Drop the handle
95
+ // by letting [Client.Shutdown] tear it down, or call its own
96
+ // [Subscription.Close] to stop receiving events.
97
+ type Subscription struct {
98
+ uri string
99
+ events chan SubscriptionEvent
100
+ closeMu sync.Mutex
101
+ closed bool
102
+ }
103
+
104
+ // URI returns the channel URI this subscription is bound to.
105
+ func (s *Subscription) URI() string { return s.uri }
106
+
107
+ // Events returns a receive-only channel of events. The channel closes
108
+ // when the [Client] shuts down or the caller invokes
109
+ // [Subscription.Close].
110
+ func (s *Subscription) Events() <-chan SubscriptionEvent { return s.events }
111
+
112
+ // Close stops the subscription locally without notifying the server.
113
+ // Safe to call multiple times.
114
+ func (s *Subscription) Close() {
115
+ s.closeMu.Lock()
116
+ defer s.closeMu.Unlock()
117
+ if s.closed {
118
+ return
119
+ }
120
+ s.closed = true
121
+ close(s.events)
122
+ }
123
+
124
+ func (s *Subscription) trySend(ev SubscriptionEvent) {
125
+ s.closeMu.Lock()
126
+ defer s.closeMu.Unlock()
127
+ if s.closed {
128
+ return
129
+ }
130
+ select {
131
+ case s.events <- ev:
132
+ default:
133
+ }
134
+ }
135
+
136
+ // ─── DispatchHandle ─────────────────────────────────────────────────────
137
+
138
+ // DispatchHandle is the receipt returned by [Client.Dispatch],
139
+ // recording the client-assigned sequence number for the dispatched
140
+ // action.
141
+ type DispatchHandle struct {
142
+ ClientSeq int64
143
+ }
144
+
145
+ // ─── Client ─────────────────────────────────────────────────────────────
146
+
147
+ // pendingResult routes a single in-flight request's outcome to its
148
+ // waiter.
149
+ type pendingResult struct {
150
+ value json.RawMessage
151
+ err *ahptypes.JsonRpcError
152
+ }
153
+
154
+ // outboundMsg is the writer goroutine's input queue payload.
155
+ type outboundMsg struct {
156
+ msg ahptypes.JsonRpcMessage
157
+ // done, if non-nil, receives the result of the send attempt.
158
+ done chan error
159
+ }
160
+
161
+ // EventStream is a top-level fan-in receiver over every inbound event
162
+ // from a [Client]. Returned by [Client.Events].
163
+ type EventStream struct {
164
+ events chan ClientEvent
165
+ closeMu sync.Mutex
166
+ closed bool
167
+ }
168
+
169
+ // Events returns a receive-only channel of every [ClientEvent].
170
+ func (s *EventStream) Events() <-chan ClientEvent { return s.events }
171
+
172
+ // Close stops the stream. Safe to call multiple times.
173
+ func (s *EventStream) Close() {
174
+ s.closeMu.Lock()
175
+ defer s.closeMu.Unlock()
176
+ if s.closed {
177
+ return
178
+ }
179
+ s.closed = true
180
+ close(s.events)
181
+ }
182
+
183
+ func (s *EventStream) trySend(ev ClientEvent) {
184
+ s.closeMu.Lock()
185
+ defer s.closeMu.Unlock()
186
+ if s.closed {
187
+ return
188
+ }
189
+ select {
190
+ case s.events <- ev:
191
+ default:
192
+ }
193
+ }
194
+
195
+ // ServerRequestHandler answers server-initiated JSON-RPC requests.
196
+ //
197
+ // Return a JSON-encoded result on success, or a non-nil JSON-RPC error
198
+ // to send an error response.
199
+ type ServerRequestHandler func(method string, params json.RawMessage) (json.RawMessage, *ahptypes.JsonRpcError)
200
+
201
+ // ResourceRequestHandlers is a typed registry for symmetrical resource requests.
202
+ //
203
+ // Install it with [Client.SetResourceRequestHandlers]. A nil per-method
204
+ // handler is reported as MethodNotFound.
205
+ type ResourceRequestHandlers struct {
206
+ OnResourceRead func(context.Context, ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error)
207
+ OnResourceWrite func(context.Context, ahptypes.ResourceWriteParams) (*ahptypes.ResourceWriteResult, error)
208
+ OnResourceList func(context.Context, ahptypes.ResourceListParams) (*ahptypes.ResourceListResult, error)
209
+ OnResourceCopy func(context.Context, ahptypes.ResourceCopyParams) (*ahptypes.ResourceCopyResult, error)
210
+ OnResourceDelete func(context.Context, ahptypes.ResourceDeleteParams) (*ahptypes.ResourceDeleteResult, error)
211
+ OnResourceMove func(context.Context, ahptypes.ResourceMoveParams) (*ahptypes.ResourceMoveResult, error)
212
+ OnResourceResolve func(context.Context, ahptypes.ResourceResolveParams) (*ahptypes.ResourceResolveResult, error)
213
+ OnResourceMkdir func(context.Context, ahptypes.ResourceMkdirParams) (*ahptypes.ResourceMkdirResult, error)
214
+ OnResourceRequest func(context.Context, ahptypes.ResourceRequestParams) (*ahptypes.ResourceRequestResult, error)
215
+ OnCreateResourceWatch func(context.Context, ahptypes.CreateResourceWatchParams) (*ahptypes.CreateResourceWatchResult, error)
216
+ }
217
+
218
+ // Client is an async JSON-RPC client driving a pluggable [Transport].
219
+ //
220
+ // A Client is created with [Connect] which spawns a background
221
+ // goroutine that pumps inbound frames and dispatches outbound
222
+ // requests. Methods are safe to call from multiple goroutines.
223
+ type Client struct {
224
+ cfg Config
225
+ transport Transport
226
+
227
+ // outbound carries messages to be sent to the writer goroutine.
228
+ outbound chan outboundMsg
229
+
230
+ // pending is the request-correlation map keyed by JSON-RPC id.
231
+ pendingMu sync.Mutex
232
+ pending map[uint64]chan pendingResult
233
+
234
+ // subscriptionsMu guards subscriptions and the all-events
235
+ // fan-out registry.
236
+ subscriptionsMu sync.Mutex
237
+ subscriptions map[string][]*Subscription
238
+ eventListeners []*EventStream
239
+
240
+ serverRequestMu sync.Mutex
241
+ serverRequestHandler ServerRequestHandler
242
+
243
+ nextID atomic.Uint64
244
+ nextClientSeq atomic.Int64
245
+
246
+ // done closes once the client has begun teardown. Subsequent
247
+ // sends fail with [ErrShutdown].
248
+ done chan struct{}
249
+ // closeOnce guards the centralized close path so concurrent
250
+ // shutdowns / transport faults are idempotent.
251
+ closeOnce sync.Once
252
+ closeErr atomic.Value // holds error
253
+ wg sync.WaitGroup
254
+ }
255
+
256
+ // ─── Connect / Shutdown ─────────────────────────────────────────────────
257
+
258
+ // Connect wires a [Transport] up to a new [Client] and starts the
259
+ // background reader/writer goroutines.
260
+ //
261
+ // The Client owns the transport from this point: callers should not
262
+ // invoke [Transport.Send] / [Transport.Recv] directly.
263
+ func Connect(_ context.Context, transport Transport, cfg Config) (*Client, error) {
264
+ if cfg.SubscriptionBuffer <= 0 {
265
+ cfg.SubscriptionBuffer = 256
266
+ }
267
+ c := &Client{
268
+ cfg: cfg,
269
+ transport: transport,
270
+ outbound: make(chan outboundMsg, 64),
271
+ pending: make(map[uint64]chan pendingResult),
272
+ subscriptions: make(map[string][]*Subscription),
273
+ done: make(chan struct{}),
274
+ }
275
+ c.nextID.Store(1)
276
+ c.nextClientSeq.Store(1)
277
+ c.wg.Add(2)
278
+ go c.runReader()
279
+ go c.runWriter()
280
+ return c, nil
281
+ }
282
+
283
+ // Done returns a channel that closes when the client has begun
284
+ // teardown (either via [Client.Shutdown] or a transport failure
285
+ // observed by the background reader/writer).
286
+ func (c *Client) Done() <-chan struct{} { return c.done }
287
+
288
+ // Err returns the first error that triggered teardown, or nil if
289
+ // the client is still running or was shut down cleanly.
290
+ func (c *Client) Err() error {
291
+ if v := c.closeErr.Load(); v != nil {
292
+ if e, ok := v.(error); ok {
293
+ return e
294
+ }
295
+ }
296
+ return nil
297
+ }
298
+
299
+ // Shutdown gracefully tears down the client. In-flight requests
300
+ // resolve with [ErrShutdown]. Subscriptions and event streams are
301
+ // closed. The underlying transport is closed too.
302
+ //
303
+ // Safe to call multiple times.
304
+ func (c *Client) Shutdown(ctx context.Context) error {
305
+ c.shutdownWithError(nil)
306
+ doneCh := make(chan struct{})
307
+ go func() { c.wg.Wait(); close(doneCh) }()
308
+ select {
309
+ case <-doneCh:
310
+ return nil
311
+ case <-ctx.Done():
312
+ return ctx.Err()
313
+ }
314
+ }
315
+
316
+ // shutdownWithError runs the centralized close path exactly once.
317
+ // Reader, writer, and Shutdown all funnel through it so concurrent
318
+ // callers can't race on closing channels.
319
+ func (c *Client) shutdownWithError(err error) {
320
+ c.closeOnce.Do(func() {
321
+ if err != nil {
322
+ c.closeErr.Store(err)
323
+ }
324
+ close(c.done)
325
+
326
+ // Close the transport so any blocked Recv unblocks.
327
+ closeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
328
+ _ = c.transport.Close(closeCtx)
329
+ cancel()
330
+
331
+ // Fail every in-flight request.
332
+ failErr := &ahptypes.JsonRpcError{Code: -32000, Message: "client shut down"}
333
+ if err != nil {
334
+ failErr.Message = fmt.Sprintf("client shut down: %v", err)
335
+ }
336
+ c.pendingMu.Lock()
337
+ for id, ch := range c.pending {
338
+ select {
339
+ case ch <- pendingResult{err: failErr}:
340
+ default:
341
+ }
342
+ close(ch)
343
+ delete(c.pending, id)
344
+ }
345
+ c.pendingMu.Unlock()
346
+
347
+ // Close every subscription and listener so consumers see EOF.
348
+ c.subscriptionsMu.Lock()
349
+ subs := c.subscriptions
350
+ listeners := c.eventListeners
351
+ c.subscriptions = map[string][]*Subscription{}
352
+ c.eventListeners = nil
353
+ c.subscriptionsMu.Unlock()
354
+ for _, list := range subs {
355
+ for _, s := range list {
356
+ s.Close()
357
+ }
358
+ }
359
+ for _, l := range listeners {
360
+ l.Close()
361
+ }
362
+ })
363
+ }
364
+
365
+ // runWriter drains outbound messages and ships them via the transport.
366
+ // Exits when c.done is closed.
367
+ func (c *Client) runWriter() {
368
+ defer c.wg.Done()
369
+ for {
370
+ select {
371
+ case <-c.done:
372
+ return
373
+ case msg, ok := <-c.outbound:
374
+ if !ok {
375
+ return
376
+ }
377
+ wire, err := EncodeMessage(msg.msg)
378
+ if err != nil {
379
+ if msg.done != nil {
380
+ msg.done <- err
381
+ }
382
+ continue
383
+ }
384
+ ctx, cancel := contextWithDone(context.Background(), c.done)
385
+ sendErr := c.transport.Send(ctx, wire)
386
+ cancel()
387
+ if msg.done != nil {
388
+ msg.done <- sendErr
389
+ }
390
+ if sendErr != nil {
391
+ // Transport faulted — trigger global shutdown so all
392
+ // consumers see EOF in bounded time.
393
+ c.shutdownWithError(fmt.Errorf("ahp: transport send: %w", sendErr))
394
+ return
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+ // runReader pumps inbound messages from the transport and dispatches
401
+ // them to the pending map / subscriptions. Exits when the transport
402
+ // reports closed.
403
+ func (c *Client) runReader() {
404
+ defer c.wg.Done()
405
+ for {
406
+ select {
407
+ case <-c.done:
408
+ return
409
+ default:
410
+ }
411
+ ctx, cancel := contextWithDone(context.Background(), c.done)
412
+ msg, err := c.transport.Recv(ctx)
413
+ cancel()
414
+ if err != nil {
415
+ c.shutdownWithError(fmt.Errorf("ahp: transport recv: %w", err))
416
+ return
417
+ }
418
+ parsed, perr := msg.IntoParsed()
419
+ if perr != nil {
420
+ // Skip malformed frames; protocol resync is the server's
421
+ // responsibility.
422
+ continue
423
+ }
424
+ c.dispatch(parsed)
425
+ }
426
+ }
427
+
428
+ // contextWithDone returns a context that's cancelled when either the
429
+ // parent ctx is cancelled or done is closed — without spawning a
430
+ // long-lived goroutine that outlives the call.
431
+ func contextWithDone(parent context.Context, done <-chan struct{}) (context.Context, context.CancelFunc) {
432
+ ctx, cancel := context.WithCancel(parent)
433
+ stop := make(chan struct{})
434
+ go func() {
435
+ select {
436
+ case <-done:
437
+ cancel()
438
+ case <-stop:
439
+ }
440
+ }()
441
+ wrappedCancel := func() {
442
+ close(stop)
443
+ cancel()
444
+ }
445
+ return ctx, wrappedCancel
446
+ }
447
+
448
+ // dispatch routes a parsed message to the matching consumer.
449
+ func (c *Client) dispatch(msg ahptypes.JsonRpcMessage) {
450
+ switch {
451
+ case msg.SuccessResponse != nil:
452
+ c.deliver(msg.SuccessResponse.ID, pendingResult{value: msg.SuccessResponse.Result})
453
+ case msg.ErrorResponse != nil:
454
+ c.deliver(msg.ErrorResponse.ID, pendingResult{err: &msg.ErrorResponse.Error})
455
+ case msg.Notification != nil:
456
+ c.handleNotification(*msg.Notification)
457
+ case msg.Request != nil:
458
+ go c.handleServerRequest(*msg.Request)
459
+ }
460
+ }
461
+
462
+ func (c *Client) deliver(id uint64, r pendingResult) {
463
+ c.pendingMu.Lock()
464
+ ch, ok := c.pending[id]
465
+ if ok {
466
+ delete(c.pending, id)
467
+ }
468
+ c.pendingMu.Unlock()
469
+ if !ok {
470
+ return
471
+ }
472
+ ch <- r
473
+ close(ch)
474
+ }
475
+
476
+ func (c *Client) handleNotification(n ahptypes.JsonRpcNotification) {
477
+ switch n.Method {
478
+ case "action":
479
+ var env ahptypes.ActionEnvelope
480
+ if err := json.Unmarshal(n.Params, &env); err != nil {
481
+ return
482
+ }
483
+ c.fanOut(env.Channel, SubscriptionEventAction{Envelope: env})
484
+ case "root/sessionAdded":
485
+ var p ahptypes.SessionAddedParams
486
+ if err := json.Unmarshal(n.Params, &p); err != nil {
487
+ return
488
+ }
489
+ c.fanOut(p.Channel, SubscriptionEventSessionAdded{Params: p})
490
+ case "root/sessionRemoved":
491
+ var p ahptypes.SessionRemovedParams
492
+ if err := json.Unmarshal(n.Params, &p); err != nil {
493
+ return
494
+ }
495
+ c.fanOut(p.Channel, SubscriptionEventSessionRemoved{Params: p})
496
+ case "root/sessionSummaryChanged":
497
+ var p ahptypes.SessionSummaryChangedParams
498
+ if err := json.Unmarshal(n.Params, &p); err != nil {
499
+ return
500
+ }
501
+ c.fanOut(p.Channel, SubscriptionEventSessionSummaryChanged{Params: p})
502
+ case "auth/required":
503
+ var p ahptypes.AuthRequiredParams
504
+ if err := json.Unmarshal(n.Params, &p); err != nil {
505
+ return
506
+ }
507
+ c.fanOut(p.Channel, SubscriptionEventAuthRequired{Params: p})
508
+ }
509
+ }
510
+
511
+ func (c *Client) fanOut(channel string, ev SubscriptionEvent) {
512
+ c.subscriptionsMu.Lock()
513
+ subs := append([]*Subscription(nil), c.subscriptions[channel]...)
514
+ listeners := append([]*EventStream(nil), c.eventListeners...)
515
+ c.subscriptionsMu.Unlock()
516
+ for _, s := range subs {
517
+ s.trySend(ev)
518
+ }
519
+ for _, l := range listeners {
520
+ l.trySend(ClientEvent{Channel: channel, Event: ev})
521
+ }
522
+ }
523
+
524
+ func methodNotFound(method string) *ahptypes.JsonRpcError {
525
+ return &ahptypes.JsonRpcError{
526
+ Code: ahptypes.ErrorCodeMethodNotFound,
527
+ Message: fmt.Sprintf("no handler for server method %q", method),
528
+ }
529
+ }
530
+
531
+ func errorFromHandler(err error) *ahptypes.JsonRpcError {
532
+ var rpc *RPCError
533
+ if errors.As(err, &rpc) {
534
+ return &ahptypes.JsonRpcError{Code: rpc.Code, Message: rpc.Message, Data: rpc.Data}
535
+ }
536
+ return &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInternalError, Message: err.Error()}
537
+ }
538
+
539
+ func runResourceHandler[P any, R any](method string, params json.RawMessage, handler func(context.Context, P) (*R, error)) (json.RawMessage, *ahptypes.JsonRpcError) {
540
+ if handler == nil {
541
+ return nil, methodNotFound(method)
542
+ }
543
+ var decoded P
544
+ if err := json.Unmarshal(params, &decoded); err != nil {
545
+ return nil, &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInvalidParams, Message: err.Error()}
546
+ }
547
+ result, err := handler(context.Background(), decoded)
548
+ if err != nil {
549
+ return nil, errorFromHandler(err)
550
+ }
551
+ if result == nil {
552
+ return json.RawMessage("{}"), nil
553
+ }
554
+ raw, err := json.Marshal(result)
555
+ if err != nil {
556
+ return nil, &ahptypes.JsonRpcError{Code: ahptypes.ErrorCodeInternalError, Message: err.Error()}
557
+ }
558
+ return raw, nil
559
+ }
560
+
561
+ // SetServerRequestHandler installs the generic handler for server-initiated
562
+ // JSON-RPC requests.
563
+ func (c *Client) SetServerRequestHandler(h ServerRequestHandler) {
564
+ c.serverRequestMu.Lock()
565
+ defer c.serverRequestMu.Unlock()
566
+ c.serverRequestHandler = h
567
+ }
568
+
569
+ // ClearServerRequestHandler removes the handler for server-initiated requests.
570
+ func (c *Client) ClearServerRequestHandler() {
571
+ c.SetServerRequestHandler(nil)
572
+ }
573
+
574
+ // SetResourceRequestHandlers installs typed handlers for symmetrical resource
575
+ // requests.
576
+ func (c *Client) SetResourceRequestHandlers(h ResourceRequestHandlers) {
577
+ c.SetServerRequestHandler(func(method string, params json.RawMessage) (json.RawMessage, *ahptypes.JsonRpcError) {
578
+ switch method {
579
+ case "resourceRead":
580
+ return runResourceHandler(method, params, h.OnResourceRead)
581
+ case "resourceWrite":
582
+ return runResourceHandler(method, params, h.OnResourceWrite)
583
+ case "resourceList":
584
+ return runResourceHandler(method, params, h.OnResourceList)
585
+ case "resourceCopy":
586
+ return runResourceHandler(method, params, h.OnResourceCopy)
587
+ case "resourceDelete":
588
+ return runResourceHandler(method, params, h.OnResourceDelete)
589
+ case "resourceMove":
590
+ return runResourceHandler(method, params, h.OnResourceMove)
591
+ case "resourceResolve":
592
+ return runResourceHandler(method, params, h.OnResourceResolve)
593
+ case "resourceMkdir":
594
+ return runResourceHandler(method, params, h.OnResourceMkdir)
595
+ case "resourceRequest":
596
+ return runResourceHandler(method, params, h.OnResourceRequest)
597
+ case "createResourceWatch":
598
+ return runResourceHandler(method, params, h.OnCreateResourceWatch)
599
+ default:
600
+ return nil, methodNotFound(method)
601
+ }
602
+ })
603
+ }
604
+
605
+ func (c *Client) handleServerRequest(req ahptypes.JsonRpcRequest) {
606
+ c.serverRequestMu.Lock()
607
+ handler := c.serverRequestHandler
608
+ c.serverRequestMu.Unlock()
609
+
610
+ var response ahptypes.JsonRpcMessage
611
+ if handler == nil {
612
+ response = ahptypes.JsonRpcMessage{ErrorResponse: &ahptypes.JsonRpcErrorResponse{
613
+ JsonRpc: ahptypes.JsonRpcV2,
614
+ ID: req.ID,
615
+ Error: *methodNotFound(req.Method),
616
+ }}
617
+ } else if result, rpcErr := handler(req.Method, req.Params); rpcErr != nil {
618
+ response = ahptypes.JsonRpcMessage{ErrorResponse: &ahptypes.JsonRpcErrorResponse{
619
+ JsonRpc: ahptypes.JsonRpcV2,
620
+ ID: req.ID,
621
+ Error: *rpcErr,
622
+ }}
623
+ } else {
624
+ if len(result) == 0 {
625
+ result = json.RawMessage("null")
626
+ }
627
+ response = ahptypes.JsonRpcMessage{SuccessResponse: &ahptypes.JsonRpcSuccessResponse{
628
+ JsonRpc: ahptypes.JsonRpcV2,
629
+ ID: req.ID,
630
+ Result: result,
631
+ }}
632
+ }
633
+ _ = c.send(context.Background(), response)
634
+ }
635
+
636
+ // ─── Request / Notify ───────────────────────────────────────────────────
637
+
638
+ // Request sends a JSON-RPC request and decodes the response into out.
639
+ // If out is nil, the result is discarded.
640
+ func (c *Client) Request(ctx context.Context, method string, params any, out any) error {
641
+ select {
642
+ case <-c.done:
643
+ return ErrShutdown
644
+ default:
645
+ }
646
+ rawParams, err := encodeParams(params)
647
+ if err != nil {
648
+ return err
649
+ }
650
+ id := c.nextID.Add(1) - 1
651
+ resultCh := make(chan pendingResult, 1)
652
+
653
+ c.pendingMu.Lock()
654
+ c.pending[id] = resultCh
655
+ c.pendingMu.Unlock()
656
+
657
+ req := ahptypes.JsonRpcMessage{Request: &ahptypes.JsonRpcRequest{
658
+ JsonRpc: ahptypes.JsonRpcV2,
659
+ ID: id,
660
+ Method: method,
661
+ Params: rawParams,
662
+ }}
663
+ if err := c.send(ctx, req); err != nil {
664
+ c.pendingMu.Lock()
665
+ delete(c.pending, id)
666
+ c.pendingMu.Unlock()
667
+ return err
668
+ }
669
+
670
+ // Apply the configured default timeout if no deadline is set.
671
+ if c.cfg.DefaultRequestTimeout > 0 {
672
+ if _, ok := ctx.Deadline(); !ok {
673
+ var cancel context.CancelFunc
674
+ ctx, cancel = context.WithTimeout(ctx, c.cfg.DefaultRequestTimeout)
675
+ defer cancel()
676
+ }
677
+ }
678
+
679
+ select {
680
+ case r := <-resultCh:
681
+ if r.err != nil {
682
+ return &RPCError{Code: r.err.Code, Message: r.err.Message, Data: r.err.Data}
683
+ }
684
+ if out != nil && len(r.value) > 0 && string(r.value) != "null" {
685
+ if err := json.Unmarshal(r.value, out); err != nil {
686
+ return fmt.Errorf("ahp: decode response for %q: %w", method, err)
687
+ }
688
+ }
689
+ return nil
690
+ case <-ctx.Done():
691
+ c.pendingMu.Lock()
692
+ delete(c.pending, id)
693
+ c.pendingMu.Unlock()
694
+ return ctx.Err()
695
+ case <-c.done:
696
+ return ErrShutdown
697
+ }
698
+ }
699
+
700
+ // Notify sends a JSON-RPC notification (fire-and-forget).
701
+ func (c *Client) Notify(ctx context.Context, method string, params any) error {
702
+ select {
703
+ case <-c.done:
704
+ return ErrShutdown
705
+ default:
706
+ }
707
+ rawParams, err := encodeParams(params)
708
+ if err != nil {
709
+ return err
710
+ }
711
+ msg := ahptypes.JsonRpcMessage{Notification: &ahptypes.JsonRpcNotification{
712
+ JsonRpc: ahptypes.JsonRpcV2,
713
+ Method: method,
714
+ Params: rawParams,
715
+ }}
716
+ return c.send(ctx, msg)
717
+ }
718
+
719
+ func (c *Client) send(ctx context.Context, msg ahptypes.JsonRpcMessage) error {
720
+ done := make(chan error, 1)
721
+ select {
722
+ case c.outbound <- outboundMsg{msg: msg, done: done}:
723
+ case <-ctx.Done():
724
+ return ctx.Err()
725
+ case <-c.done:
726
+ return ErrShutdown
727
+ }
728
+ select {
729
+ case err := <-done:
730
+ if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
731
+ return err
732
+ }
733
+ return err
734
+ case <-ctx.Done():
735
+ return ctx.Err()
736
+ case <-c.done:
737
+ return ErrShutdown
738
+ }
739
+ }
740
+
741
+ func encodeParams(params any) (json.RawMessage, error) {
742
+ if params == nil {
743
+ return nil, nil
744
+ }
745
+ b, err := json.Marshal(params)
746
+ if err != nil {
747
+ return nil, fmt.Errorf("ahp: encode params: %w", err)
748
+ }
749
+ if string(b) == "null" {
750
+ return nil, nil
751
+ }
752
+ return b, nil
753
+ }
754
+
755
+ // ─── Protocol surface ───────────────────────────────────────────────────
756
+
757
+ // Initialize issues the `initialize` handshake.
758
+ //
759
+ // protocolVersions is the list of protocol versions the client is
760
+ // willing to speak, ordered most-preferred-first. Pass
761
+ // [ahptypes.SupportedProtocolVersions]() to advertise every version
762
+ // this generated source knows about. initialSubscriptions may be nil
763
+ // or empty.
764
+ func (c *Client) Initialize(ctx context.Context, clientID string, protocolVersions []string, initialSubscriptions []string) (*ahptypes.InitializeResult, error) {
765
+ params := ahptypes.InitializeParams{
766
+ Channel: ahptypes.RootResourceURI,
767
+ ProtocolVersions: protocolVersions,
768
+ ClientId: clientID,
769
+ }
770
+ if len(initialSubscriptions) > 0 {
771
+ params.InitialSubscriptions = initialSubscriptions
772
+ }
773
+ var out ahptypes.InitializeResult
774
+ if err := c.Request(ctx, "initialize", params, &out); err != nil {
775
+ return nil, err
776
+ }
777
+ return &out, nil
778
+ }
779
+
780
+ // Reconnect re-establishes a dropped connection with the server's
781
+ // `reconnect` flow.
782
+ func (c *Client) Reconnect(ctx context.Context, clientID string, lastSeenServerSeq int64, subscriptions []string) (*ahptypes.ReconnectResult, error) {
783
+ params := ahptypes.ReconnectParams{
784
+ Channel: ahptypes.RootResourceURI,
785
+ ClientId: clientID,
786
+ LastSeenServerSeq: lastSeenServerSeq,
787
+ Subscriptions: subscriptions,
788
+ }
789
+ var out ahptypes.ReconnectResult
790
+ if err := c.Request(ctx, "reconnect", params, &out); err != nil {
791
+ return nil, err
792
+ }
793
+ return &out, nil
794
+ }
795
+
796
+ // Ping sends a protocol-level `ping` request to verify the connection is
797
+ // still alive and keep it from being closed by idle-timeout intermediaries
798
+ // (proxies, load balancers, etc.). It is a connection-level command scoped to
799
+ // the root channel and carries no payload in either direction — the response
800
+ // itself is the signal. The server responds regardless of whether Initialize
801
+ // has completed or any subscriptions are held.
802
+ func (c *Client) Ping(ctx context.Context) error {
803
+ params := struct {
804
+ Channel ahptypes.URI `json:"channel"`
805
+ }{Channel: ahptypes.RootResourceURI}
806
+ return c.Request(ctx, "ping", params, nil)
807
+ }
808
+
809
+ // Subscribe sends a `subscribe` request and returns the initial snapshot
810
+ // together with a per-URI [Subscription] handle.
811
+ func (c *Client) Subscribe(ctx context.Context, uri string) (*ahptypes.SubscribeResult, *Subscription, error) {
812
+ return c.SubscribeWithDelivery(ctx, uri, nil)
813
+ }
814
+
815
+ // SubscribeWithDelivery sends a `subscribe` request with advisory delivery
816
+ // preferences and returns the initial snapshot together with a per-URI
817
+ // [Subscription] handle.
818
+ func (c *Client) SubscribeWithDelivery(ctx context.Context, uri string, delivery *ahptypes.SubscriptionDeliveryOptions) (*ahptypes.SubscribeResult, *Subscription, error) {
819
+ sub := c.AttachSubscription(uri)
820
+ var out ahptypes.SubscribeResult
821
+ if err := c.Request(ctx, "subscribe", ahptypes.SubscribeParams{Channel: uri, Delivery: delivery}, &out); err != nil {
822
+ sub.Close()
823
+ return nil, nil, err
824
+ }
825
+ return &out, sub, nil
826
+ }
827
+
828
+ // AttachSubscription returns a local [Subscription] for uri without
829
+ // sending a `subscribe` request. Useful when uri was included in
830
+ // `initialSubscriptions` during [Client.Initialize].
831
+ func (c *Client) AttachSubscription(uri string) *Subscription {
832
+ c.subscriptionsMu.Lock()
833
+ defer c.subscriptionsMu.Unlock()
834
+ sub := &Subscription{uri: uri, events: make(chan SubscriptionEvent, c.cfg.SubscriptionBuffer)}
835
+ c.subscriptions[uri] = append(c.subscriptions[uri], sub)
836
+ return sub
837
+ }
838
+
839
+ // Unsubscribe sends an `unsubscribe` notification and drops every
840
+ // local [Subscription] for uri.
841
+ func (c *Client) Unsubscribe(ctx context.Context, uri string) error {
842
+ c.subscriptionsMu.Lock()
843
+ subs := c.subscriptions[uri]
844
+ delete(c.subscriptions, uri)
845
+ c.subscriptionsMu.Unlock()
846
+ for _, s := range subs {
847
+ s.Close()
848
+ }
849
+ return c.Notify(ctx, "unsubscribe", ahptypes.UnsubscribeParams{Channel: uri})
850
+ }
851
+
852
+ // Dispatch fires a write-ahead `dispatchAction` notification with a
853
+ // client-assigned sequence number.
854
+ func (c *Client) Dispatch(ctx context.Context, channel string, action ahptypes.StateAction) (DispatchHandle, error) {
855
+ seq := c.nextClientSeq.Add(1) - 1
856
+ err := c.Notify(ctx, "dispatchAction", ahptypes.DispatchActionParams{
857
+ Channel: channel,
858
+ ClientSeq: seq,
859
+ Action: action,
860
+ })
861
+ if err != nil {
862
+ return DispatchHandle{}, err
863
+ }
864
+ return DispatchHandle{ClientSeq: seq}, nil
865
+ }
866
+
867
+ // ResourceRead reads a resource's content (`resourceRead`). It targets the
868
+ // root channel; any Channel set on params is overwritten.
869
+ func (c *Client) ResourceRead(ctx context.Context, params ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error) {
870
+ params.Channel = ahptypes.RootResourceURI
871
+ var out ahptypes.ResourceReadResult
872
+ if err := c.Request(ctx, "resourceRead", params, &out); err != nil {
873
+ return nil, err
874
+ }
875
+ return &out, nil
876
+ }
877
+
878
+ // ResourceWrite writes resource content (`resourceWrite`). It targets the root
879
+ // channel; any Channel set on params is overwritten.
880
+ func (c *Client) ResourceWrite(ctx context.Context, params ahptypes.ResourceWriteParams) (*ahptypes.ResourceWriteResult, error) {
881
+ params.Channel = ahptypes.RootResourceURI
882
+ var out ahptypes.ResourceWriteResult
883
+ if err := c.Request(ctx, "resourceWrite", params, &out); err != nil {
884
+ return nil, err
885
+ }
886
+ return &out, nil
887
+ }
888
+
889
+ // ResourceList lists directory entries (`resourceList`). It targets the root
890
+ // channel; any Channel set on params is overwritten.
891
+ func (c *Client) ResourceList(ctx context.Context, params ahptypes.ResourceListParams) (*ahptypes.ResourceListResult, error) {
892
+ params.Channel = ahptypes.RootResourceURI
893
+ var out ahptypes.ResourceListResult
894
+ if err := c.Request(ctx, "resourceList", params, &out); err != nil {
895
+ return nil, err
896
+ }
897
+ return &out, nil
898
+ }
899
+
900
+ // ResourceCopy copies a resource (`resourceCopy`). It targets the root channel;
901
+ // any Channel set on params is overwritten.
902
+ func (c *Client) ResourceCopy(ctx context.Context, params ahptypes.ResourceCopyParams) (*ahptypes.ResourceCopyResult, error) {
903
+ params.Channel = ahptypes.RootResourceURI
904
+ var out ahptypes.ResourceCopyResult
905
+ if err := c.Request(ctx, "resourceCopy", params, &out); err != nil {
906
+ return nil, err
907
+ }
908
+ return &out, nil
909
+ }
910
+
911
+ // ResourceDelete deletes a resource (`resourceDelete`). It targets the root
912
+ // channel; any Channel set on params is overwritten.
913
+ func (c *Client) ResourceDelete(ctx context.Context, params ahptypes.ResourceDeleteParams) (*ahptypes.ResourceDeleteResult, error) {
914
+ params.Channel = ahptypes.RootResourceURI
915
+ var out ahptypes.ResourceDeleteResult
916
+ if err := c.Request(ctx, "resourceDelete", params, &out); err != nil {
917
+ return nil, err
918
+ }
919
+ return &out, nil
920
+ }
921
+
922
+ // ResourceMove moves a resource (`resourceMove`). It targets the root channel;
923
+ // any Channel set on params is overwritten.
924
+ func (c *Client) ResourceMove(ctx context.Context, params ahptypes.ResourceMoveParams) (*ahptypes.ResourceMoveResult, error) {
925
+ params.Channel = ahptypes.RootResourceURI
926
+ var out ahptypes.ResourceMoveResult
927
+ if err := c.Request(ctx, "resourceMove", params, &out); err != nil {
928
+ return nil, err
929
+ }
930
+ return &out, nil
931
+ }
932
+
933
+ // ResourceResolve resolves resource metadata (`resourceResolve`). It targets
934
+ // the root channel; any Channel set on params is overwritten.
935
+ func (c *Client) ResourceResolve(ctx context.Context, params ahptypes.ResourceResolveParams) (*ahptypes.ResourceResolveResult, error) {
936
+ params.Channel = ahptypes.RootResourceURI
937
+ var out ahptypes.ResourceResolveResult
938
+ if err := c.Request(ctx, "resourceResolve", params, &out); err != nil {
939
+ return nil, err
940
+ }
941
+ return &out, nil
942
+ }
943
+
944
+ // ResourceMkdir creates a directory (`resourceMkdir`). It targets the root
945
+ // channel; any Channel set on params is overwritten.
946
+ func (c *Client) ResourceMkdir(ctx context.Context, params ahptypes.ResourceMkdirParams) (*ahptypes.ResourceMkdirResult, error) {
947
+ params.Channel = ahptypes.RootResourceURI
948
+ var out ahptypes.ResourceMkdirResult
949
+ if err := c.Request(ctx, "resourceMkdir", params, &out); err != nil {
950
+ return nil, err
951
+ }
952
+ return &out, nil
953
+ }
954
+
955
+ // ResourceRequest requests resource access (`resourceRequest`). It targets the
956
+ // root channel; any Channel set on params is overwritten.
957
+ func (c *Client) ResourceRequest(ctx context.Context, params ahptypes.ResourceRequestParams) (*ahptypes.ResourceRequestResult, error) {
958
+ params.Channel = ahptypes.RootResourceURI
959
+ var out ahptypes.ResourceRequestResult
960
+ if err := c.Request(ctx, "resourceRequest", params, &out); err != nil {
961
+ return nil, err
962
+ }
963
+ return &out, nil
964
+ }
965
+
966
+ // CreateResourceWatch creates a resource watcher (`createResourceWatch`). It
967
+ // targets the root channel; any Channel set on params is overwritten.
968
+ func (c *Client) CreateResourceWatch(ctx context.Context, params ahptypes.CreateResourceWatchParams) (*ahptypes.CreateResourceWatchResult, error) {
969
+ params.Channel = ahptypes.RootResourceURI
970
+ var out ahptypes.CreateResourceWatchResult
971
+ if err := c.Request(ctx, "createResourceWatch", params, &out); err != nil {
972
+ return nil, err
973
+ }
974
+ return &out, nil
975
+ }
976
+
977
+ // Completions requests inline completion items for a partially-typed input
978
+ // (`completions`), e.g. to power `@`-mention pickers. Unlike the resource
979
+ // wrappers, the caller-supplied Channel (the chat URI the completion is scoped
980
+ // to) is preserved. Debounce calls to avoid flooding the server on every
981
+ // keystroke.
982
+ func (c *Client) Completions(ctx context.Context, params ahptypes.CompletionsParams) (*ahptypes.CompletionsResult, error) {
983
+ var out ahptypes.CompletionsResult
984
+ if err := c.Request(ctx, "completions", params, &out); err != nil {
985
+ return nil, err
986
+ }
987
+ return &out, nil
988
+ }
989
+
990
+ // SessionConfigCompletions queries the server for allowed values of a dynamic
991
+ // session config property (`sessionConfigCompletions`). It targets the root
992
+ // channel; any Channel set on params is overwritten.
993
+ func (c *Client) SessionConfigCompletions(ctx context.Context, params ahptypes.SessionConfigCompletionsParams) (*ahptypes.SessionConfigCompletionsResult, error) {
994
+ params.Channel = ahptypes.RootResourceURI
995
+ var out ahptypes.SessionConfigCompletionsResult
996
+ if err := c.Request(ctx, "sessionConfigCompletions", params, &out); err != nil {
997
+ return nil, err
998
+ }
999
+ return &out, nil
1000
+ }
1001
+
1002
+ // Events returns a new top-level [EventStream] that receives every
1003
+ // inbound event from this client, tagged with the channel URI it was
1004
+ // scoped to. Multiple streams may exist concurrently.
1005
+ func (c *Client) Events() *EventStream {
1006
+ c.subscriptionsMu.Lock()
1007
+ defer c.subscriptionsMu.Unlock()
1008
+ s := &EventStream{events: make(chan ClientEvent, c.cfg.SubscriptionBuffer)}
1009
+ c.eventListeners = append(c.eventListeners, s)
1010
+ return s
1011
+ }