@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,533 @@
1
+ package pi
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "sync"
8
+ "sync/atomic"
9
+ "time"
10
+
11
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
12
+
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
15
+ )
16
+
17
+ // ChatDriverOptions configure a [ChatDriver].
18
+ type ChatDriverOptions struct {
19
+ Host *host.Host
20
+ ChatChannel string
21
+ Backend Backend
22
+ WorkingDirectory string
23
+ // RecordTurnAnchor is told which turn finished, so the session can remember the entry the turn
24
+ // ends on (truncation needs it).
25
+ RecordTurnAnchor func(turnID string)
26
+ // AdoptForeignTurns makes a turn that no client started (someone typed a prompt into the
27
+ // session locally) appear in the chat: it is opened from the user message that starts it.
28
+ // Upstream's host owns its agent, so it has no such turns; the extension does not own PiG's.
29
+ AdoptForeignTurns bool
30
+ Log func(format string, args ...any)
31
+ }
32
+
33
+ // ChatDriver connects one chat channel to one [Backend] (port of src/pi/chat-driver.ts).
34
+ //
35
+ // A client's chat/turnStarted reaches the backend as a prompt; the backend's events stream back
36
+ // through a [mapper.TurnMapper] as chat actions. The driver arbitrates everything in between:
37
+ // cancellation (which must settle before anything new is sent), steering, queued messages that
38
+ // start as their own turn once the chat goes idle, and quiescing for disposal.
39
+ //
40
+ // Locking: mu guards fields and is never held across a call into the host or the backend (the
41
+ // host delivers listener callbacks synchronously, and those can re-enter the driver). evMu keeps
42
+ // backend events, which arrive on the backend's goroutine, ordered.
43
+ type ChatDriver struct {
44
+ opts ChatDriverOptions
45
+
46
+ evMu sync.Mutex // serialises agent events
47
+
48
+ mu sync.Mutex
49
+ mapper *mapper.TurnMapper
50
+ turnCancel context.CancelFunc
51
+ turnCtx context.Context
52
+ turnOperation chan struct{} // closed when the prompt call for the running turn returns
53
+ cancellation chan struct{} // closed when the latest cancellation has settled
54
+ cancellationGen int
55
+ persistedInputTurn map[string]bool
56
+ quiesced bool
57
+ pendingSteeringID string
58
+ steeringQueueLen int
59
+ inflight int
60
+ idle *sync.Cond
61
+ unsubscribe func()
62
+ }
63
+
64
+ // NewChatDriver subscribes to the backend and returns the driver.
65
+ func NewChatDriver(opts ChatDriverOptions) *ChatDriver {
66
+ d := &ChatDriver{opts: opts, persistedInputTurn: map[string]bool{}}
67
+ d.idle = sync.NewCond(&d.mu)
68
+ d.unsubscribe = opts.Backend.Subscribe(d.onAgentEvent)
69
+ return d
70
+ }
71
+
72
+ // ChatChannel is the chat this driver serves.
73
+ func (d *ChatDriver) ChatChannel() string { return d.opts.ChatChannel }
74
+
75
+ // Busy reports whether a turn is being mapped.
76
+ func (d *ChatDriver) Busy() bool {
77
+ d.mu.Lock()
78
+ defer d.mu.Unlock()
79
+ return d.mapper != nil
80
+ }
81
+
82
+ func (d *ChatDriver) logf(format string, args ...any) {
83
+ if d.opts.Log != nil {
84
+ d.opts.Log(format, args...)
85
+ }
86
+ }
87
+
88
+ func (d *ChatDriver) dispatch(action ahptypes.StateAction) {
89
+ d.opts.Host.DispatchServerAction(d.opts.ChatChannel, action)
90
+ }
91
+
92
+ func (d *ChatDriver) dispatchAll(actions []ahptypes.StateAction) {
93
+ for _, a := range actions {
94
+ d.dispatch(a)
95
+ }
96
+ }
97
+
98
+ // track runs fn as an in-flight operation that Quiesce waits for.
99
+ func (d *ChatDriver) track(fn func()) {
100
+ d.mu.Lock()
101
+ d.inflight++
102
+ d.mu.Unlock()
103
+ go func() {
104
+ defer func() {
105
+ d.mu.Lock()
106
+ d.inflight--
107
+ d.idle.Broadcast()
108
+ d.mu.Unlock()
109
+ }()
110
+ fn()
111
+ }()
112
+ }
113
+
114
+ func (d *ChatDriver) drainInFlight() {
115
+ d.mu.Lock()
116
+ for d.inflight > 0 {
117
+ d.idle.Wait()
118
+ }
119
+ d.mu.Unlock()
120
+ }
121
+
122
+ // Quiesce stops the chat for disposal: no new work is accepted, the agent is aborted, in-flight
123
+ // operations drain, and an active turn is closed as cancelled. On failure the driver resumes.
124
+ func (d *ChatDriver) Quiesce(ctx context.Context) error {
125
+ d.mu.Lock()
126
+ d.quiesced = true
127
+ cancel := d.turnCancel
128
+ d.mu.Unlock()
129
+ if err := d.opts.Backend.Abort(ctx); err != nil {
130
+ d.Resume()
131
+ return err
132
+ }
133
+ if cancel != nil {
134
+ cancel()
135
+ }
136
+ d.drainInFlight()
137
+ d.finishTurn(mapper.OutcomeCancelled, "")
138
+ return nil
139
+ }
140
+
141
+ // Resume undoes a Quiesce whose disposal did not go through.
142
+ func (d *ChatDriver) Resume() {
143
+ d.mu.Lock()
144
+ if !d.quiesced {
145
+ d.mu.Unlock()
146
+ return
147
+ }
148
+ d.quiesced = false
149
+ d.mu.Unlock()
150
+ d.consumeNextQueuedMessage()
151
+ }
152
+
153
+ // Dispose releases the backend.
154
+ func (d *ChatDriver) Dispose() {
155
+ if d.unsubscribe != nil {
156
+ d.unsubscribe()
157
+ }
158
+ if disposer, ok := d.opts.Backend.(Disposer); ok {
159
+ if err := disposer.Dispose(); err != nil {
160
+ d.logf("backend disposal failed: %v", err)
161
+ }
162
+ }
163
+ }
164
+
165
+ // Truncate asks the backend to move back to an entry (false when it cannot or refuses).
166
+ func (d *ChatDriver) Truncate(ctx context.Context, entryID string) bool {
167
+ t, ok := d.opts.Backend.(Truncater)
168
+ if !ok {
169
+ return false
170
+ }
171
+ var applied bool
172
+ done := make(chan struct{})
173
+ d.track(func() {
174
+ defer close(done)
175
+ var err error
176
+ applied, err = t.Truncate(ctx, entryID)
177
+ if err != nil {
178
+ d.logf("truncate failed: %v", err)
179
+ applied = false
180
+ }
181
+ })
182
+ <-done
183
+ return applied
184
+ }
185
+
186
+ // HandleClientAction reacts to an action a client dispatched on this chat. The action is already
187
+ // reduced; this carries out its side effect.
188
+ func (d *ChatDriver) HandleClientAction(channel string, action ahptypes.StateAction) bool {
189
+ if channel != d.opts.ChatChannel {
190
+ return false
191
+ }
192
+ switch a := action.Value.(type) {
193
+ case *ahptypes.ChatTurnStartedAction:
194
+ d.startTurn(a.TurnId, a.Message)
195
+ case *ahptypes.ChatTurnCancelledAction:
196
+ d.cancelTurn(a.TurnId)
197
+ case *ahptypes.ChatPendingMessageSetAction:
198
+ if a.Kind == ahptypes.PendingMessageKindSteering {
199
+ d.mu.Lock()
200
+ d.pendingSteeringID = a.Id
201
+ ctx := d.turnCtx
202
+ d.mu.Unlock()
203
+ message := a.Message
204
+ d.track(func() { d.steer(ctx, message) })
205
+ } else {
206
+ d.consumeNextQueuedMessage()
207
+ }
208
+ }
209
+ return true
210
+ }
211
+
212
+ func (d *ChatDriver) steer(ctx context.Context, message ahptypes.Message) {
213
+ if ctx == nil {
214
+ ctx = context.Background()
215
+ }
216
+ d.mu.Lock()
217
+ cancellation := d.cancellation
218
+ d.mu.Unlock()
219
+ if cancellation != nil {
220
+ <-cancellation
221
+ }
222
+ if ctx.Err() != nil {
223
+ return
224
+ }
225
+ input, err := messageInput(message)
226
+ if err == nil {
227
+ err = d.opts.Backend.Steer(ctx, input.Text, input.Images)
228
+ }
229
+ if err != nil {
230
+ d.logf("steer failed: %v", err)
231
+ d.clearPendingSteering()
232
+ }
233
+ }
234
+
235
+ func (d *ChatDriver) newMapper(turnID string) *mapper.TurnMapper {
236
+ return mapper.NewTurnMapper(turnID, time.Now().UnixMilli(), mapper.Options{WorkingDirectory: d.opts.WorkingDirectory})
237
+ }
238
+
239
+ func (d *ChatDriver) startTurn(turnID string, message ahptypes.Message) {
240
+ m := d.newMapper(turnID)
241
+ ctx, cancel := context.WithCancel(context.Background())
242
+ d.mu.Lock()
243
+ d.persistedInputTurn = map[string]bool{}
244
+ d.mapper, d.turnCtx, d.turnCancel = m, ctx, cancel
245
+ d.mu.Unlock()
246
+ d.runPrompt(m, message, "prompt", ctx)
247
+ }
248
+
249
+ func (d *ChatDriver) runPrompt(m *mapper.TurnMapper, message ahptypes.Message, label string, ctx context.Context) {
250
+ operation := make(chan struct{})
251
+ d.mu.Lock()
252
+ d.turnOperation = operation
253
+ cancellation := d.cancellation
254
+ d.mu.Unlock()
255
+ d.track(func() {
256
+ defer func() {
257
+ close(operation)
258
+ d.mu.Lock()
259
+ if d.turnOperation == operation {
260
+ d.turnOperation = nil
261
+ }
262
+ d.mu.Unlock()
263
+ }()
264
+ if cancellation != nil {
265
+ <-cancellation
266
+ }
267
+ if !d.stillCurrent(m, ctx) {
268
+ return
269
+ }
270
+ if message.Model != nil {
271
+ if selector, ok := d.opts.Backend.(ModelSelector); ok {
272
+ if err := selector.SelectModel(ctx, *message.Model); err != nil {
273
+ d.logf("selectModel failed: %v", err)
274
+ }
275
+ }
276
+ }
277
+ if !d.stillCurrent(m, ctx) {
278
+ return
279
+ }
280
+ input, err := messageInput(message)
281
+ if err == nil {
282
+ err = d.opts.Backend.Prompt(ctx, input.Text, input.Images)
283
+ }
284
+ if err != nil {
285
+ d.mu.Lock()
286
+ current := d.mapper == m
287
+ d.mu.Unlock()
288
+ if !current {
289
+ return
290
+ }
291
+ d.logf("%s failed: %v", label, err)
292
+ d.finishTurn(mapper.OutcomeError, err.Error())
293
+ }
294
+ })
295
+ }
296
+
297
+ func (d *ChatDriver) stillCurrent(m *mapper.TurnMapper, ctx context.Context) bool {
298
+ d.mu.Lock()
299
+ defer d.mu.Unlock()
300
+ return d.mapper == m && !d.quiesced && ctx.Err() == nil
301
+ }
302
+
303
+ func (d *ChatDriver) onAgentEvent(event mapper.Event) {
304
+ d.evMu.Lock()
305
+ defer d.evMu.Unlock()
306
+ if event["type"] == "queue_update" {
307
+ d.reconcileSteering(event)
308
+ return
309
+ }
310
+ d.mu.Lock()
311
+ m := d.mapper
312
+ d.mu.Unlock()
313
+ if m == nil {
314
+ m = d.adoptForeignTurn(event)
315
+ if m == nil {
316
+ return
317
+ }
318
+ }
319
+ d.dispatchAll(m.Handle(event))
320
+ if event["type"] == "message_end" {
321
+ if msg, _ := event["message"].(map[string]any); msg["role"] == "user" {
322
+ d.mu.Lock()
323
+ d.persistedInputTurn[m.TurnID()] = true
324
+ d.mu.Unlock()
325
+ }
326
+ }
327
+ if m.Finished() {
328
+ if d.opts.RecordTurnAnchor != nil {
329
+ d.opts.RecordTurnAnchor(m.TurnID())
330
+ }
331
+ d.mu.Lock()
332
+ d.persistedInputTurn = map[string]bool{}
333
+ if d.mapper == m {
334
+ d.mapper, d.turnCancel, d.turnCtx = nil, nil, nil
335
+ }
336
+ d.mu.Unlock()
337
+ d.consumeNextQueuedMessage()
338
+ }
339
+ }
340
+
341
+ var foreignTurnCounter atomic.Int64
342
+
343
+ // adoptForeignTurn opens a chat turn for a prompt that did not come from a client: only a user
344
+ // message arriving while no turn is mapped, no cancellation is settling and the chat is not being
345
+ // disposed can start one, so the tail of a cancelled run is never mistaken for a new turn.
346
+ func (d *ChatDriver) adoptForeignTurn(event mapper.Event) *mapper.TurnMapper {
347
+ if !d.opts.AdoptForeignTurns || event["type"] != "message_start" {
348
+ return nil
349
+ }
350
+ message, _ := event["message"].(map[string]any)
351
+ if message == nil || message["role"] != "user" {
352
+ return nil
353
+ }
354
+ d.mu.Lock()
355
+ if d.quiesced || d.cancellation != nil || d.mapper != nil {
356
+ d.mu.Unlock()
357
+ return nil
358
+ }
359
+ turnID := fmt.Sprintf("local-%d-%d", time.Now().UnixMilli(), foreignTurnCounter.Add(1))
360
+ m := d.newMapper(turnID)
361
+ ctx, cancel := context.WithCancel(context.Background())
362
+ d.persistedInputTurn = map[string]bool{}
363
+ d.mapper, d.turnCtx, d.turnCancel = m, ctx, cancel
364
+ d.mu.Unlock()
365
+ d.dispatch(ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
366
+ Type: ahptypes.ActionTypeChatTurnStarted, TurnId: turnID, StartedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
367
+ Message: ahptypes.Message{Text: mapper.TextFromPiUserContent(message["content"]), Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
368
+ }})
369
+ // the run began before its user message was seen
370
+ d.dispatchAll(m.Handle(mapper.Event{"type": "agent_start"}))
371
+ return m
372
+ }
373
+
374
+ func (d *ChatDriver) reconcileSteering(event mapper.Event) {
375
+ steering, _ := event["steering"].([]any)
376
+ d.mu.Lock()
377
+ consumed := len(steering) < d.steeringQueueLen
378
+ d.steeringQueueLen = len(steering)
379
+ d.mu.Unlock()
380
+ if consumed {
381
+ d.clearPendingSteering()
382
+ }
383
+ }
384
+
385
+ func (d *ChatDriver) clearPendingSteering() {
386
+ d.mu.Lock()
387
+ id := d.pendingSteeringID
388
+ d.pendingSteeringID = ""
389
+ d.mu.Unlock()
390
+ if id == "" {
391
+ return
392
+ }
393
+ d.dispatch(ahptypes.StateAction{Value: &ahptypes.ChatPendingMessageRemovedAction{
394
+ Type: ahptypes.ActionTypeChatPendingMessageRemoved, Kind: ahptypes.PendingMessageKindSteering, Id: id,
395
+ }})
396
+ }
397
+
398
+ // finishTurn closes the running turn from the host's side: a failed prompt, or disposal.
399
+ func (d *ChatDriver) finishTurn(outcome mapper.Outcome, message string) {
400
+ d.evMu.Lock()
401
+ d.mu.Lock()
402
+ m := d.mapper
403
+ d.mu.Unlock()
404
+ if m == nil {
405
+ d.evMu.Unlock()
406
+ return
407
+ }
408
+ d.dispatchAll(m.Finish(outcome, message))
409
+ d.evMu.Unlock()
410
+ d.mu.Lock()
411
+ persisted := d.persistedInputTurn[m.TurnID()]
412
+ d.persistedInputTurn = map[string]bool{}
413
+ if d.mapper == m {
414
+ d.mapper, d.turnCancel, d.turnCtx = nil, nil, nil
415
+ }
416
+ d.mu.Unlock()
417
+ if persisted && d.opts.RecordTurnAnchor != nil {
418
+ d.opts.RecordTurnAnchor(m.TurnID())
419
+ }
420
+ d.clearPendingSteering()
421
+ d.consumeNextQueuedMessage()
422
+ }
423
+
424
+ // consumeNextQueuedMessage starts the head of the queue as its own turn, once the chat is idle.
425
+ func (d *ChatDriver) consumeNextQueuedMessage() {
426
+ d.mu.Lock()
427
+ if d.quiesced || d.cancellation != nil || d.mapper != nil {
428
+ d.mu.Unlock()
429
+ return
430
+ }
431
+ state := d.opts.Host.Store().Chat(d.opts.ChatChannel)
432
+ if state == nil || len(state.QueuedMessages) == 0 {
433
+ d.mu.Unlock()
434
+ return
435
+ }
436
+ next := state.QueuedMessages[0]
437
+ turnID := "turn-" + next.Id
438
+ m := d.newMapper(turnID)
439
+ ctx, cancel := context.WithCancel(context.Background())
440
+ d.persistedInputTurn = map[string]bool{}
441
+ d.mapper, d.turnCtx, d.turnCancel = m, ctx, cancel
442
+ d.mu.Unlock()
443
+
444
+ queued := next.Id
445
+ d.dispatch(ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
446
+ Type: ahptypes.ActionTypeChatTurnStarted, TurnId: turnID,
447
+ StartedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), Message: next.Message, QueuedMessageId: &queued,
448
+ }})
449
+ d.runPrompt(m, next.Message, "queued prompt", ctx)
450
+ }
451
+
452
+ func (d *ChatDriver) cancelTurn(turnID string) {
453
+ d.mu.Lock()
454
+ m := d.mapper
455
+ if m != nil && m.TurnID() != turnID {
456
+ d.mu.Unlock()
457
+ return
458
+ }
459
+ turnOperation := d.turnOperation
460
+ inputPersisted := d.persistedInputTurn[turnID]
461
+ d.persistedInputTurn = map[string]bool{}
462
+ if d.turnCancel != nil {
463
+ d.turnCancel()
464
+ }
465
+ d.turnCancel, d.turnCtx, d.mapper = nil, nil, nil
466
+ d.cancellationGen++
467
+ gen := d.cancellationGen
468
+ previous := d.cancellation
469
+ done := make(chan struct{})
470
+ d.cancellation = done
471
+ d.mu.Unlock()
472
+
473
+ d.clearPendingSteering()
474
+ if st := d.opts.Host.Store().Chat(d.opts.ChatChannel); st != nil && st.Activity != nil {
475
+ d.dispatch(ahptypes.StateAction{Value: &ahptypes.ChatActivityChangedAction{Type: ahptypes.ActionTypeChatActivityChanged}})
476
+ }
477
+
478
+ d.track(func() {
479
+ if previous != nil {
480
+ <-previous
481
+ }
482
+ if err := d.opts.Backend.Abort(context.Background()); err != nil {
483
+ d.logf("abort failed: %v", err)
484
+ }
485
+ if turnOperation != nil {
486
+ <-turnOperation
487
+ }
488
+ if inputPersisted && d.opts.RecordTurnAnchor != nil {
489
+ d.opts.RecordTurnAnchor(turnID)
490
+ }
491
+ d.mu.Lock()
492
+ latest := gen == d.cancellationGen
493
+ if latest {
494
+ d.cancellation = nil
495
+ }
496
+ d.mu.Unlock()
497
+ close(done)
498
+ if latest {
499
+ d.consumeNextQueuedMessage()
500
+ }
501
+ })
502
+ }
503
+
504
+ // PublishDefaultSelection tells the client which model and reasoning effort are in effect. There
505
+ // is no protocol field for a "default model", but a client initialises its input from the chat's
506
+ // draft, so seeding the draft's selection is how a host answers.
507
+ func (d *ChatDriver) PublishDefaultSelection() {
508
+ reporter, ok := d.opts.Backend.(SelectionReporter)
509
+ if !ok {
510
+ return
511
+ }
512
+ selection := reporter.CurrentSelection()
513
+ if selection == nil {
514
+ return
515
+ }
516
+ state := d.opts.Host.Store().Chat(d.opts.ChatChannel)
517
+ text := ""
518
+ if state != nil && state.Draft != nil {
519
+ text = state.Draft.Text
520
+ if a, b := jsonOf(state.Draft.Model), jsonOf(selection); a == b {
521
+ return
522
+ }
523
+ }
524
+ d.dispatch(ahptypes.StateAction{Value: &ahptypes.ChatDraftChangedAction{
525
+ Type: ahptypes.ActionTypeChatDraftChanged,
526
+ Draft: &ahptypes.Message{Text: text, Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}, Model: selection},
527
+ }})
528
+ }
529
+
530
+ func jsonOf(v any) string {
531
+ raw, _ := json.Marshal(v)
532
+ return string(raw)
533
+ }