@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,1939 @@
1
+ package ahp
2
+
3
+ import (
4
+ "encoding/json"
5
+ "time"
6
+
7
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
8
+ )
9
+
10
+ // ─── ReduceOutcome ─────────────────────────────────────────────────────
11
+
12
+ // ReduceOutcome reports what happened when a reducer was asked to
13
+ // apply an action.
14
+ type ReduceOutcome int
15
+
16
+ const (
17
+ // ReduceOutcomeApplied indicates the action was applied and the
18
+ // state was mutated.
19
+ ReduceOutcomeApplied ReduceOutcome = iota
20
+ // ReduceOutcomeNoOp indicates the action was recognized but had no
21
+ // effect against this state (e.g. an unknown turn id).
22
+ ReduceOutcomeNoOp
23
+ // ReduceOutcomeOutOfScope indicates the action targets a different
24
+ // scope (e.g. a session action passed to the root reducer).
25
+ ReduceOutcomeOutOfScope
26
+ )
27
+
28
+ func addMillisecondsToTimestamp(timestamp string, duration int64) string {
29
+ start, err := time.Parse(time.RFC3339Nano, timestamp)
30
+ if err != nil {
31
+ panic("invalid ISO 8601 timestamp: " + timestamp)
32
+ }
33
+ delta := time.Duration(duration) * time.Millisecond
34
+ if int64(delta/time.Millisecond) != duration {
35
+ panic("timestamp duration overflows time.Duration")
36
+ }
37
+ return start.Add(delta).UTC().Format("2006-01-02T15:04:05.000Z")
38
+ }
39
+
40
+ // ─── Status helpers ────────────────────────────────────────────────────
41
+
42
+ // statusActivityMask covers the mutually-exclusive activity bits
43
+ // (bits 0–4) of [ahptypes.SessionStatus].
44
+ const statusActivityMask ahptypes.SessionStatus = (1 << 5) - 1
45
+
46
+ func withStatusFlag(status, flag ahptypes.SessionStatus, set bool) ahptypes.SessionStatus {
47
+ if set {
48
+ return status | flag
49
+ }
50
+ return status &^ flag
51
+ }
52
+
53
+ // awaitsUser reports whether an entry blocks on the *user*.
54
+ //
55
+ // SessionInputRequestKindToolClientExecution is work delegated to a client, not
56
+ // a prompt: the call has already cleared its confirmation gate and is simply
57
+ // running somewhere else. Counting it would report a session as awaiting the
58
+ // user for the entire duration of every client tool call.
59
+ func awaitsUser(request ahptypes.SessionInputRequest) bool {
60
+ _, isClientExecution := request.Value.(*ahptypes.SessionToolClientExecutionRequest)
61
+ return !isClientExecution
62
+ }
63
+
64
+ // withInputNeededStatus reflects the session-level input queue into the activity
65
+ // bits of status. A queue holding any user-blocking entry promotes the activity
66
+ // to InputNeeded; draining those entries clears the input-needed-specific bit.
67
+ // Because InputNeeded implies InProgress, an unblocked turn falls back to
68
+ // InProgress while an already-idle session stays idle. Orthogonal flags
69
+ // (IsRead / IsArchived) are preserved.
70
+ func withInputNeededStatus(status ahptypes.SessionStatus, inputNeeded []ahptypes.SessionInputRequest) ahptypes.SessionStatus {
71
+ for _, request := range inputNeeded {
72
+ if awaitsUser(request) {
73
+ return (status &^ statusActivityMask) | ahptypes.SessionStatusInputNeeded
74
+ }
75
+ }
76
+ return status &^ (ahptypes.SessionStatusInputNeeded &^ ahptypes.SessionStatusInProgress)
77
+ }
78
+
79
+ // ─── Tool-call helpers ─────────────────────────────────────────────────
80
+
81
+ // toolCallCommon carries the fields shared by every concrete
82
+ // [ahptypes.ToolCallState] variant.
83
+ type toolCallCommon struct {
84
+ id string
85
+ name string
86
+ displayName string
87
+ intention *string
88
+ toolInput *ahptypes.ToolInput
89
+ contributor *ahptypes.ToolCallContributor
90
+ meta ahptypes.JSONObject
91
+ }
92
+
93
+ func toolCallMeta(tc ahptypes.ToolCallState) toolCallCommon {
94
+ switch v := tc.Value.(type) {
95
+ case *ahptypes.ToolCallStreamingState:
96
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, nil, v.Contributor, v.Meta}
97
+ case *ahptypes.ToolCallPendingConfirmationState:
98
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
99
+ case *ahptypes.ToolCallRunningState:
100
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
101
+ case *ahptypes.ToolCallAuthRequiredState:
102
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
103
+ case *ahptypes.ToolCallPendingResultConfirmationState:
104
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
105
+ case *ahptypes.ToolCallCompletedState:
106
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
107
+ case *ahptypes.ToolCallCancelledState:
108
+ return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta}
109
+ }
110
+ return toolCallCommon{}
111
+ }
112
+
113
+ // toolCallInvocationMessage pulls the invocation message from a non-terminal
114
+ // tool-call variant.
115
+ func toolCallInvocationMessage(tc ahptypes.ToolCallState) ahptypes.StringOrMarkdown {
116
+ switch v := tc.Value.(type) {
117
+ case *ahptypes.ToolCallStreamingState:
118
+ var im ahptypes.StringOrMarkdown
119
+ if v.InvocationMessage != nil {
120
+ im = *v.InvocationMessage
121
+ }
122
+ return im
123
+ case *ahptypes.ToolCallPendingConfirmationState:
124
+ return v.InvocationMessage
125
+ case *ahptypes.ToolCallRunningState:
126
+ return v.InvocationMessage
127
+ case *ahptypes.ToolCallAuthRequiredState:
128
+ return v.InvocationMessage
129
+ case *ahptypes.ToolCallPendingResultConfirmationState:
130
+ return v.InvocationMessage
131
+ }
132
+ return ahptypes.StringOrMarkdown{}
133
+ }
134
+
135
+ func toolCallID(tc ahptypes.ToolCallState) string {
136
+ return toolCallMeta(tc).id
137
+ }
138
+
139
+ // hasBlockingToolCall reports whether the active turn has any tool call blocked
140
+ // on confirmation or MCP authentication.
141
+ func hasBlockingToolCall(state *ahptypes.ChatState) bool {
142
+ if state.ActiveTurn == nil {
143
+ return false
144
+ }
145
+ for _, part := range state.ActiveTurn.ResponseParts {
146
+ tc, ok := part.Value.(*ahptypes.ToolCallResponsePart)
147
+ if !ok {
148
+ continue
149
+ }
150
+ switch tc.ToolCall.Value.(type) {
151
+ case *ahptypes.ToolCallPendingConfirmationState,
152
+ *ahptypes.ToolCallAuthRequiredState,
153
+ *ahptypes.ToolCallPendingResultConfirmationState:
154
+ return true
155
+ }
156
+ }
157
+ return false
158
+ }
159
+
160
+ func hasOpenInputRequest(state *ahptypes.ChatState) bool {
161
+ if state.ActiveTurn == nil {
162
+ return false
163
+ }
164
+ for _, part := range state.ActiveTurn.ResponseParts {
165
+ input, ok := part.Value.(*ahptypes.InputRequestResponsePart)
166
+ if ok && input.Response == nil {
167
+ return true
168
+ }
169
+ }
170
+ return false
171
+ }
172
+
173
+ func hasResumableError(turn *ahptypes.Turn) bool {
174
+ if len(turn.ResponseParts) == 0 {
175
+ return false
176
+ }
177
+ part, ok := turn.ResponseParts[len(turn.ResponseParts)-1].Value.(*ahptypes.ErrorResponsePart)
178
+ return ok && part.Resumable != nil && *part.Resumable
179
+ }
180
+
181
+ func summaryStatus(state *ahptypes.ChatState, terminal *ahptypes.SessionStatus) ahptypes.SessionStatus {
182
+ var activity ahptypes.SessionStatus
183
+ switch {
184
+ case terminal != nil:
185
+ activity = *terminal
186
+ case hasOpenInputRequest(state) || hasBlockingToolCall(state):
187
+ activity = ahptypes.SessionStatusInputNeeded
188
+ case state.ActiveTurn != nil:
189
+ activity = ahptypes.SessionStatusInProgress
190
+ default:
191
+ activity = ahptypes.SessionStatusIdle
192
+ }
193
+ return (state.Status &^ statusActivityMask) | activity
194
+ }
195
+
196
+ func refreshSummaryStatus(state *ahptypes.ChatState) {
197
+ state.Status = summaryStatus(state, nil)
198
+ }
199
+
200
+ // ─── Active-turn helpers ───────────────────────────────────────────────
201
+
202
+ func endTurn(state *ahptypes.ChatState, turnID string, duration int64, turnState ahptypes.TurnState, terminalStatus *ahptypes.SessionStatus, errorPart *ahptypes.ErrorResponsePart) ReduceOutcome {
203
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID {
204
+ return ReduceOutcomeNoOp
205
+ }
206
+ active := state.ActiveTurn
207
+ state.ActiveTurn = nil
208
+
209
+ parts := make([]ahptypes.ResponsePart, 0, len(active.ResponseParts))
210
+ for _, part := range active.ResponseParts {
211
+ tc, ok := part.Value.(*ahptypes.ToolCallResponsePart)
212
+ if !ok {
213
+ parts = append(parts, part)
214
+ continue
215
+ }
216
+ switch tc.ToolCall.Value.(type) {
217
+ case *ahptypes.ToolCallCompletedState, *ahptypes.ToolCallCancelledState:
218
+ parts = append(parts, part)
219
+ continue
220
+ }
221
+ common := toolCallMeta(tc.ToolCall)
222
+ invocation := toolCallInvocationMessage(tc.ToolCall)
223
+ cancelled := &ahptypes.ToolCallCancelledState{
224
+ Status: ahptypes.ToolCallStatusCancelled,
225
+ ToolCallId: common.id,
226
+ ToolName: common.name,
227
+ DisplayName: common.displayName,
228
+ Intention: common.intention,
229
+ ToolInput: common.toolInput,
230
+ Contributor: common.contributor,
231
+ Meta: common.meta,
232
+ InvocationMessage: invocation,
233
+ Reason: ahptypes.ToolCallCancellationReasonSkipped,
234
+ }
235
+ parts = append(parts, ahptypes.ResponsePart{Value: &ahptypes.ToolCallResponsePart{
236
+ Kind: ahptypes.ResponsePartKindToolCall,
237
+ ToolCall: ahptypes.ToolCallState{Value: cancelled},
238
+ }})
239
+ }
240
+ if errorPart != nil {
241
+ parts = append(parts, ahptypes.ResponsePart{Value: errorPart})
242
+ }
243
+
244
+ // Defensive clamp: duration is producer-supplied and opaque to this
245
+ // reducer, but a negative value would be nonsensical to display.
246
+ if duration < 0 {
247
+ duration = 0
248
+ }
249
+ startedAt := active.StartedAt
250
+ turn := ahptypes.Turn{
251
+ Id: active.Id,
252
+ StartedAt: &startedAt,
253
+ Duration: &duration,
254
+ Message: active.Message,
255
+ ResponseParts: parts,
256
+ Usage: active.Usage,
257
+ State: turnState,
258
+ }
259
+
260
+ state.Turns = append(state.Turns, turn)
261
+ state.ModifiedAt = addMillisecondsToTimestamp(active.StartedAt, duration)
262
+ state.Status = summaryStatus(state, terminalStatus)
263
+ return ReduceOutcomeApplied
264
+ }
265
+
266
+ func upsertInputRequest(state *ahptypes.ChatState, req ahptypes.ChatInputRequest) ReduceOutcome {
267
+ if state.ActiveTurn == nil {
268
+ return ReduceOutcomeNoOp
269
+ }
270
+ parts := state.ActiveTurn.ResponseParts
271
+ for i := range parts {
272
+ input, ok := parts[i].Value.(*ahptypes.InputRequestResponsePart)
273
+ if ok && input.Response == nil && input.Request.Id == req.Id {
274
+ if req.Answers == nil {
275
+ req.Answers = input.Request.Answers
276
+ }
277
+ parts[i].Value = &ahptypes.InputRequestResponsePart{
278
+ Kind: ahptypes.ResponsePartKindInputRequest,
279
+ Request: req,
280
+ }
281
+ state.ActiveTurn.ResponseParts = parts
282
+ state.Status = summaryStatus(state, nil)
283
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false)
284
+ return ReduceOutcomeApplied
285
+ }
286
+ }
287
+ state.ActiveTurn.ResponseParts = append(parts, ahptypes.ResponsePart{
288
+ Value: &ahptypes.InputRequestResponsePart{
289
+ Kind: ahptypes.ResponsePartKindInputRequest,
290
+ Request: req,
291
+ },
292
+ })
293
+ state.Status = summaryStatus(state, nil)
294
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false)
295
+ return ReduceOutcomeApplied
296
+ }
297
+
298
+ // ─── Customization helpers ─────────────────────────────────────────────
299
+
300
+ func customizationID(c ahptypes.Customization) (string, bool) {
301
+ switch v := c.Value.(type) {
302
+ case *ahptypes.PluginCustomization:
303
+ return v.Id, true
304
+ case *ahptypes.DirectoryCustomization:
305
+ return v.Id, true
306
+ case *ahptypes.McpServerCustomization:
307
+ return v.Id, true
308
+ }
309
+ return "", false
310
+ }
311
+
312
+ func sessionInputRequestID(r ahptypes.SessionInputRequest) (string, bool) {
313
+ switch v := r.Value.(type) {
314
+ case *ahptypes.SessionChatInputRequest:
315
+ return v.Id, true
316
+ case *ahptypes.SessionToolConfirmationRequest:
317
+ return v.Id, true
318
+ case *ahptypes.SessionToolClientExecutionRequest:
319
+ return v.Id, true
320
+ case *ahptypes.SessionToolAuthenticationRequest:
321
+ return v.Id, true
322
+ }
323
+ return "", false
324
+ }
325
+
326
+ func childCustomizationID(c ahptypes.ChildCustomization) (string, bool) {
327
+ switch v := c.Value.(type) {
328
+ case *ahptypes.AgentCustomization:
329
+ return v.Id, true
330
+ case *ahptypes.SkillCustomization:
331
+ return v.Id, true
332
+ case *ahptypes.PromptCustomization:
333
+ return v.Id, true
334
+ case *ahptypes.RuleCustomization:
335
+ return v.Id, true
336
+ case *ahptypes.HookCustomization:
337
+ return v.Id, true
338
+ case *ahptypes.McpServerCustomization:
339
+ return v.Id, true
340
+ }
341
+ return "", false
342
+ }
343
+
344
+ func containerChildren(c *ahptypes.Customization) *[]ahptypes.ChildCustomization {
345
+ switch v := c.Value.(type) {
346
+ case *ahptypes.PluginCustomization:
347
+ return &v.Children
348
+ case *ahptypes.DirectoryCustomization:
349
+ return &v.Children
350
+ }
351
+ return nil
352
+ }
353
+
354
+ func effectiveEnablement(enablement []ahptypes.CustomizationEnablement) bool {
355
+ if len(enablement) == 0 {
356
+ return true
357
+ }
358
+ switch decision := enablement[0].Value.(type) {
359
+ case *ahptypes.CustomizationEnablementGlobal:
360
+ return decision.Enabled
361
+ case *ahptypes.CustomizationEnablementWorkspace:
362
+ return decision.Enabled
363
+ case *ahptypes.CustomizationEnablementSession:
364
+ return decision.Enabled
365
+ default:
366
+ return true
367
+ }
368
+ }
369
+
370
+ func applyContainerEnablement(c *ahptypes.Customization, enablement []ahptypes.CustomizationEnablement) {
371
+ enabled := effectiveEnablement(enablement)
372
+ provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...)
373
+ switch v := c.Value.(type) {
374
+ case *ahptypes.PluginCustomization:
375
+ v.Enablement = provenance
376
+ case *ahptypes.DirectoryCustomization:
377
+ v.Enabled = enabled
378
+ case *ahptypes.McpServerCustomization:
379
+ v.Enablement = provenance
380
+ }
381
+ }
382
+
383
+ func applyChildEnablement(c *ahptypes.ChildCustomization, enablement []ahptypes.CustomizationEnablement) {
384
+ enabled := effectiveEnablement(enablement)
385
+ provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...)
386
+ switch v := c.Value.(type) {
387
+ case *ahptypes.AgentCustomization:
388
+ v.Enabled = &enabled
389
+ case *ahptypes.SkillCustomization:
390
+ v.Enabled = &enabled
391
+ case *ahptypes.PromptCustomization:
392
+ v.Enabled = &enabled
393
+ case *ahptypes.RuleCustomization:
394
+ v.Enabled = &enabled
395
+ case *ahptypes.HookCustomization:
396
+ v.Enabled = &enabled
397
+ case *ahptypes.McpServerCustomization:
398
+ v.Enablement = provenance
399
+ }
400
+ }
401
+
402
+ func applyToggle(list []ahptypes.Customization, id string, enablement []ahptypes.CustomizationEnablement) bool {
403
+ for i := range list {
404
+ got, ok := customizationID(list[i])
405
+ if ok && got == id {
406
+ applyContainerEnablement(&list[i], enablement)
407
+ return true
408
+ }
409
+ }
410
+ for i := range list {
411
+ children := containerChildren(&list[i])
412
+ if children == nil {
413
+ continue
414
+ }
415
+ for j := range *children {
416
+ got, ok := childCustomizationID((*children)[j])
417
+ if ok && got == id {
418
+ applyChildEnablement(&(*children)[j], enablement)
419
+ return true
420
+ }
421
+ }
422
+ }
423
+ return false
424
+ }
425
+
426
+ // ─── Active-turn mutation helpers ──────────────────────────────────────
427
+
428
+ func updateToolCall(state *ahptypes.ChatState, turnID, targetToolCallID string, updater func(ahptypes.ToolCallState) ahptypes.ToolCallState) ReduceOutcome {
429
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID {
430
+ return ReduceOutcomeNoOp
431
+ }
432
+ for i := range state.ActiveTurn.ResponseParts {
433
+ tc, ok := state.ActiveTurn.ResponseParts[i].Value.(*ahptypes.ToolCallResponsePart)
434
+ if !ok {
435
+ continue
436
+ }
437
+ if toolCallID(tc.ToolCall) == targetToolCallID {
438
+ tc.ToolCall = updater(tc.ToolCall)
439
+ return ReduceOutcomeApplied
440
+ }
441
+ }
442
+ return ReduceOutcomeNoOp
443
+ }
444
+
445
+ func updateResponsePart(state *ahptypes.ChatState, turnID, partID string, updater func(*ahptypes.ResponsePart)) ReduceOutcome {
446
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID {
447
+ return ReduceOutcomeNoOp
448
+ }
449
+
450
+ for i := range state.ActiveTurn.ResponseParts {
451
+ part := &state.ActiveTurn.ResponseParts[i]
452
+ var id string
453
+ switch v := part.Value.(type) {
454
+ case *ahptypes.ToolCallResponsePart:
455
+ id = toolCallID(v.ToolCall)
456
+ case *ahptypes.MarkdownResponsePart:
457
+ id = v.Id
458
+ case *ahptypes.ReasoningResponsePart:
459
+ id = v.Id
460
+ }
461
+ if id != "" && id == partID {
462
+ updater(part)
463
+ return ReduceOutcomeApplied
464
+ }
465
+ }
466
+ return ReduceOutcomeNoOp
467
+ }
468
+
469
+ // ─── Root Reducer ──────────────────────────────────────────────────────
470
+
471
+ // ApplyActionToRoot applies action to the [ahptypes.RootState] in
472
+ // place. Returns [ReduceOutcomeOutOfScope] for actions that target a
473
+ // different state tree.
474
+ func ApplyActionToRoot(state *ahptypes.RootState, action ahptypes.StateAction) ReduceOutcome {
475
+ switch a := action.Value.(type) {
476
+ case *ahptypes.RootAgentsChangedAction:
477
+ state.Agents = append([]ahptypes.AgentInfo(nil), a.Agents...)
478
+ return ReduceOutcomeApplied
479
+ case *ahptypes.RootActiveSessionsChangedAction:
480
+ v := a.ActiveSessions
481
+ state.ActiveSessions = &v
482
+ return ReduceOutcomeApplied
483
+ case *ahptypes.RootTerminalsChangedAction:
484
+ state.Terminals = append([]ahptypes.TerminalInfo(nil), a.Terminals...)
485
+ return ReduceOutcomeApplied
486
+ case *ahptypes.RootConfigChangedAction:
487
+ if state.Config == nil {
488
+ return ReduceOutcomeNoOp
489
+ }
490
+ replace := a.Replace != nil && *a.Replace
491
+ if replace {
492
+ state.Config.Values = make(map[string]json.RawMessage, len(a.Config))
493
+ } else if state.Config.Values == nil {
494
+ state.Config.Values = make(map[string]json.RawMessage, len(a.Config))
495
+ }
496
+ for k, v := range a.Config {
497
+ state.Config.Values[k] = v
498
+ }
499
+ return ReduceOutcomeApplied
500
+ }
501
+ return ReduceOutcomeOutOfScope
502
+ }
503
+
504
+ // ─── Chat Reducer ──────────────────────────────────────────────────────
505
+
506
+ // ApplyActionToChat applies action to the [ahptypes.ChatState] in
507
+ // place. Returns [ReduceOutcomeOutOfScope] for actions that target a
508
+ // different state tree.
509
+ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) ReduceOutcome {
510
+ switch a := action.Value.(type) {
511
+ case *ahptypes.ChatTurnStartedAction:
512
+ return applyTurnStarted(state, a)
513
+ case *ahptypes.ChatDeltaAction:
514
+ return updateResponsePart(state, a.TurnId, a.PartId, func(p *ahptypes.ResponsePart) {
515
+ if m, ok := p.Value.(*ahptypes.MarkdownResponsePart); ok {
516
+ m.Content += a.Content
517
+ }
518
+ })
519
+ case *ahptypes.ChatResponsePartAction:
520
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId {
521
+ return ReduceOutcomeNoOp
522
+ }
523
+ if _, ok := a.Part.Value.(*ahptypes.ErrorResponsePart); ok {
524
+ return ReduceOutcomeNoOp
525
+ }
526
+ state.ActiveTurn.ResponseParts = append(state.ActiveTurn.ResponseParts, a.Part)
527
+ return ReduceOutcomeApplied
528
+ case *ahptypes.ChatTurnCompleteAction:
529
+ return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateComplete, nil, nil)
530
+ case *ahptypes.ChatTurnCancelledAction:
531
+ return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateCancelled, nil, nil)
532
+ case *ahptypes.ChatErrorAction:
533
+ errStatus := ahptypes.SessionStatusError
534
+ return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateError, &errStatus, &a.Part)
535
+ case *ahptypes.ChatTurnResumeAction:
536
+ if state.ActiveTurn != nil || len(state.Turns) == 0 {
537
+ return ReduceOutcomeNoOp
538
+ }
539
+ turnIndex := len(state.Turns) - 1
540
+ turn := state.Turns[turnIndex]
541
+ if turn.Id != a.TurnId || turn.State != ahptypes.TurnStateError || !hasResumableError(&turn) {
542
+ return ReduceOutcomeNoOp
543
+ }
544
+
545
+ startedAt := state.ModifiedAt
546
+ if turn.StartedAt != nil {
547
+ startedAt = *turn.StartedAt
548
+ }
549
+ state.Turns = state.Turns[:turnIndex]
550
+ state.ActiveTurn = &ahptypes.ActiveTurn{
551
+ Id: turn.Id,
552
+ StartedAt: startedAt,
553
+ Message: turn.Message,
554
+ ResponseParts: turn.ResponseParts,
555
+ Usage: turn.Usage,
556
+ }
557
+ state.Status = withStatusFlag(summaryStatus(state, nil), ahptypes.SessionStatusIsRead, false)
558
+ return ReduceOutcomeApplied
559
+ case *ahptypes.ChatActivityChangedAction:
560
+ state.Activity = a.Activity
561
+ return ReduceOutcomeApplied
562
+ case *ahptypes.ChatChangesetsChangedAction:
563
+ if a.Changesets == nil {
564
+ state.Changesets = nil
565
+ } else {
566
+ state.Changesets = append([]ahptypes.Changeset(nil), a.Changesets...)
567
+ }
568
+ return ReduceOutcomeApplied
569
+ case *ahptypes.ChatWorkingDirectorySetAction:
570
+ for _, d := range state.WorkingDirectories {
571
+ if d == a.Directory {
572
+ return ReduceOutcomeNoOp
573
+ }
574
+ }
575
+ state.WorkingDirectories = append(state.WorkingDirectories, a.Directory)
576
+ return ReduceOutcomeApplied
577
+ case *ahptypes.ChatWorkingDirectoryRemovedAction:
578
+ for i := range state.WorkingDirectories {
579
+ if state.WorkingDirectories[i] == a.Directory {
580
+ state.WorkingDirectories = append(state.WorkingDirectories[:i], state.WorkingDirectories[i+1:]...)
581
+ return ReduceOutcomeApplied
582
+ }
583
+ }
584
+ return ReduceOutcomeNoOp
585
+ case *ahptypes.ChatToolCallStartAction:
586
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId {
587
+ return ReduceOutcomeNoOp
588
+ }
589
+ state.ActiveTurn.ResponseParts = append(state.ActiveTurn.ResponseParts, ahptypes.ResponsePart{Value: &ahptypes.ToolCallResponsePart{
590
+ Kind: ahptypes.ResponsePartKindToolCall,
591
+ ToolCall: ahptypes.ToolCallState{Value: &ahptypes.ToolCallStreamingState{
592
+ Status: ahptypes.ToolCallStatusStreaming,
593
+ ToolCallId: a.ToolCallId,
594
+ ToolName: a.ToolName,
595
+ DisplayName: a.DisplayName,
596
+ Intention: a.Intention,
597
+ Contributor: a.Contributor,
598
+ Meta: a.Meta,
599
+ }},
600
+ }})
601
+ return ReduceOutcomeApplied
602
+ case *ahptypes.ChatToolCallDeltaAction:
603
+ return applyToolCallDelta(state, a)
604
+ case *ahptypes.ChatToolCallReadyAction:
605
+ res := applyToolCallReady(state, a)
606
+ if res == ReduceOutcomeApplied {
607
+ refreshSummaryStatus(state)
608
+ }
609
+ return res
610
+ case *ahptypes.ChatToolCallConfirmedAction:
611
+ res := applyToolCallConfirmed(state, a)
612
+ if res == ReduceOutcomeApplied {
613
+ refreshSummaryStatus(state)
614
+ }
615
+ return res
616
+ case *ahptypes.ChatToolCallCompleteAction:
617
+ res := applyToolCallComplete(state, a)
618
+ if res == ReduceOutcomeApplied {
619
+ refreshSummaryStatus(state)
620
+ }
621
+ return res
622
+ case *ahptypes.ChatToolCallResultConfirmedAction:
623
+ res := applyToolCallResultConfirmed(state, a)
624
+ if res == ReduceOutcomeApplied {
625
+ refreshSummaryStatus(state)
626
+ }
627
+ return res
628
+ case *ahptypes.ChatToolCallContentChangedAction:
629
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
630
+ if r, ok := tc.Value.(*ahptypes.ToolCallRunningState); ok {
631
+ if a.Meta != nil {
632
+ r.Meta = a.Meta
633
+ }
634
+ r.Content = append([]ahptypes.ToolResultContent(nil), a.Content...)
635
+ }
636
+ return tc
637
+ })
638
+ case *ahptypes.ChatToolCallAuthRequiredAction:
639
+ res := applyToolCallAuthRequired(state, a)
640
+ if res == ReduceOutcomeApplied {
641
+ refreshSummaryStatus(state)
642
+ }
643
+ return res
644
+ case *ahptypes.ChatToolCallAuthResolvedAction:
645
+ res := applyToolCallAuthResolved(state, a)
646
+ if res == ReduceOutcomeApplied {
647
+ refreshSummaryStatus(state)
648
+ }
649
+ return res
650
+ case *ahptypes.ChatUsageAction:
651
+ if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId {
652
+ return ReduceOutcomeNoOp
653
+ }
654
+ usage := a.Usage
655
+ state.ActiveTurn.Usage = &usage
656
+ return ReduceOutcomeApplied
657
+ case *ahptypes.ChatReasoningAction:
658
+ return updateResponsePart(state, a.TurnId, a.PartId, func(p *ahptypes.ResponsePart) {
659
+ if r, ok := p.Value.(*ahptypes.ReasoningResponsePart); ok {
660
+ r.Content += a.Content
661
+ }
662
+ })
663
+ case *ahptypes.ChatTruncatedAction:
664
+ return applyTruncated(state, a.TurnId)
665
+ case *ahptypes.ChatTurnsLoadedAction:
666
+ existingIds := make(map[string]struct{}, len(state.Turns))
667
+ for _, turn := range state.Turns {
668
+ existingIds[turn.Id] = struct{}{}
669
+ }
670
+ olderTurns := make([]ahptypes.Turn, 0, len(a.Turns)+len(state.Turns))
671
+ for _, turn := range a.Turns {
672
+ if _, ok := existingIds[turn.Id]; !ok {
673
+ olderTurns = append(olderTurns, turn)
674
+ }
675
+ }
676
+ state.Turns = append(olderTurns, state.Turns...)
677
+ state.TurnsNextCursor = a.TurnsNextCursor
678
+ return ReduceOutcomeApplied
679
+ case *ahptypes.ChatInputRequestedAction:
680
+ return upsertInputRequest(state, a.Request)
681
+ case *ahptypes.ChatInputAnswerChangedAction:
682
+ return applyInputAnswerChanged(state, a)
683
+ case *ahptypes.ChatInputCompletedAction:
684
+ if state.ActiveTurn == nil {
685
+ return ReduceOutcomeNoOp
686
+ }
687
+ for i := range state.ActiveTurn.ResponseParts {
688
+ input, ok := state.ActiveTurn.ResponseParts[i].Value.(*ahptypes.InputRequestResponsePart)
689
+ if !ok || input.Response != nil || input.Request.Id != a.RequestId {
690
+ continue
691
+ }
692
+ finalAnswers := map[string]ahptypes.ChatInputAnswer{}
693
+ for k, v := range input.Request.Answers {
694
+ finalAnswers[k] = v
695
+ }
696
+ for k, v := range a.Answers {
697
+ finalAnswers[k] = v
698
+ }
699
+ request := input.Request
700
+ if len(finalAnswers) == 0 {
701
+ request.Answers = nil
702
+ } else {
703
+ request.Answers = finalAnswers
704
+ }
705
+ response := a.Response
706
+ state.ActiveTurn.ResponseParts[i].Value = &ahptypes.InputRequestResponsePart{
707
+ Kind: ahptypes.ResponsePartKindInputRequest,
708
+ Request: request,
709
+ Response: &response,
710
+ }
711
+ refreshSummaryStatus(state)
712
+ return ReduceOutcomeApplied
713
+ }
714
+ return ReduceOutcomeNoOp
715
+ case *ahptypes.ChatPendingMessageSetAction:
716
+ entry := ahptypes.PendingMessage{Id: a.Id, Message: a.Message}
717
+ switch a.Kind {
718
+ case ahptypes.PendingMessageKindSteering:
719
+ state.SteeringMessage = &entry
720
+ case ahptypes.PendingMessageKindQueued:
721
+ list := state.QueuedMessages
722
+ idx := -1
723
+ for i := range list {
724
+ if list[i].Id == entry.Id {
725
+ idx = i
726
+ break
727
+ }
728
+ }
729
+ if idx >= 0 {
730
+ list[idx] = entry
731
+ } else {
732
+ list = append(list, entry)
733
+ }
734
+ state.QueuedMessages = list
735
+ }
736
+ return ReduceOutcomeApplied
737
+ case *ahptypes.ChatPendingMessageRemovedAction:
738
+ switch a.Kind {
739
+ case ahptypes.PendingMessageKindSteering:
740
+ if state.SteeringMessage != nil && state.SteeringMessage.Id == a.Id {
741
+ state.SteeringMessage = nil
742
+ return ReduceOutcomeApplied
743
+ }
744
+ return ReduceOutcomeNoOp
745
+ case ahptypes.PendingMessageKindQueued:
746
+ list := state.QueuedMessages
747
+ if list == nil {
748
+ return ReduceOutcomeNoOp
749
+ }
750
+ next := list[:0]
751
+ removed := false
752
+ for _, m := range list {
753
+ if m.Id == a.Id {
754
+ removed = true
755
+ continue
756
+ }
757
+ next = append(next, m)
758
+ }
759
+ if !removed {
760
+ return ReduceOutcomeNoOp
761
+ }
762
+ if len(next) == 0 {
763
+ state.QueuedMessages = nil
764
+ } else {
765
+ state.QueuedMessages = next
766
+ }
767
+ return ReduceOutcomeApplied
768
+ }
769
+ return ReduceOutcomeNoOp
770
+ case *ahptypes.ChatQueuedMessagesReorderedAction:
771
+ if state.QueuedMessages == nil {
772
+ return ReduceOutcomeNoOp
773
+ }
774
+ byID := make(map[string]ahptypes.PendingMessage, len(state.QueuedMessages))
775
+ for _, m := range state.QueuedMessages {
776
+ byID[m.Id] = m
777
+ }
778
+ reordered := make([]ahptypes.PendingMessage, 0, len(byID))
779
+ seen := make(map[string]struct{}, len(byID))
780
+ for _, id := range a.Order {
781
+ if msg, ok := byID[id]; ok {
782
+ if _, dup := seen[id]; !dup {
783
+ seen[id] = struct{}{}
784
+ reordered = append(reordered, msg)
785
+ }
786
+ }
787
+ }
788
+ // Append messages absent from `order`, preserving their original
789
+ // relative order in state.QueuedMessages (mirrors the canonical
790
+ // TypeScript reducer in types/channels-session/reducer.ts).
791
+ for _, m := range state.QueuedMessages {
792
+ if _, in := seen[m.Id]; !in {
793
+ reordered = append(reordered, m)
794
+ }
795
+ }
796
+ state.QueuedMessages = reordered
797
+ return ReduceOutcomeApplied
798
+ case *ahptypes.ChatDraftChangedAction:
799
+ state.Draft = a.Draft
800
+ return ReduceOutcomeApplied
801
+ case *ahptypes.ChatIsArchivedChangedAction:
802
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsArchived, a.IsArchived)
803
+ return ReduceOutcomeApplied
804
+ }
805
+ return ReduceOutcomeOutOfScope
806
+ }
807
+
808
+ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.PartialChatSummary) {
809
+ if changes.Title != nil {
810
+ summary.Title = *changes.Title
811
+ }
812
+ if changes.Status != nil {
813
+ summary.Status = *changes.Status
814
+ }
815
+ if changes.Activity != nil {
816
+ summary.Activity = changes.Activity
817
+ }
818
+ if changes.ModifiedAt != nil {
819
+ summary.ModifiedAt = *changes.ModifiedAt
820
+ }
821
+ if changes.Origin != nil {
822
+ summary.Origin = changes.Origin
823
+ }
824
+ if changes.WorkingDirectories != nil {
825
+ summary.WorkingDirectories = changes.WorkingDirectories
826
+ }
827
+ }
828
+
829
+ // ─── Session Reducer ───────────────────────────────────────────────────
830
+
831
+ // ApplyActionToSession applies action to the [ahptypes.SessionState]
832
+ // in place. Returns [ReduceOutcomeOutOfScope] for actions that target
833
+ // a different state tree.
834
+ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAction) ReduceOutcome {
835
+ switch a := action.Value.(type) {
836
+ case *ahptypes.SessionReadyAction:
837
+ state.Lifecycle = ahptypes.SessionLifecycleReady
838
+ return ReduceOutcomeApplied
839
+ case *ahptypes.SessionCreationFailedAction:
840
+ state.Lifecycle = ahptypes.SessionLifecycleFailed
841
+ errCopy := a.Error
842
+ state.CreationError = &errCopy
843
+ return ReduceOutcomeApplied
844
+ case *ahptypes.SessionChatAddedAction:
845
+ for i := range state.Chats {
846
+ if state.Chats[i].Resource == a.Summary.Resource {
847
+ state.Chats[i] = a.Summary
848
+ return ReduceOutcomeApplied
849
+ }
850
+ }
851
+ state.Chats = append(state.Chats, a.Summary)
852
+ return ReduceOutcomeApplied
853
+ case *ahptypes.SessionChatRemovedAction:
854
+ for i := range state.Chats {
855
+ if state.Chats[i].Resource == a.Chat {
856
+ state.Chats = append(state.Chats[:i], state.Chats[i+1:]...)
857
+ if state.DefaultChat != nil && *state.DefaultChat == a.Chat {
858
+ state.DefaultChat = nil
859
+ }
860
+ return ReduceOutcomeApplied
861
+ }
862
+ }
863
+ return ReduceOutcomeNoOp
864
+ case *ahptypes.SessionChatUpdatedAction:
865
+ for i := range state.Chats {
866
+ if state.Chats[i].Resource == a.Chat {
867
+ mergeChatSummaryPartial(&state.Chats[i], a.Changes)
868
+ return ReduceOutcomeApplied
869
+ }
870
+ }
871
+ return ReduceOutcomeNoOp
872
+ case *ahptypes.SessionDefaultChatChangedAction:
873
+ state.DefaultChat = a.DefaultChat
874
+ return ReduceOutcomeApplied
875
+ case *ahptypes.SessionTitleChangedAction:
876
+ state.Title = a.Title
877
+ return ReduceOutcomeApplied
878
+ case *ahptypes.SessionIsReadChangedAction:
879
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, a.IsRead)
880
+ return ReduceOutcomeApplied
881
+ case *ahptypes.SessionIsArchivedChangedAction:
882
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsArchived, a.IsArchived)
883
+ return ReduceOutcomeApplied
884
+ case *ahptypes.SessionActivityChangedAction:
885
+ state.Activity = a.Activity
886
+ return ReduceOutcomeApplied
887
+ case *ahptypes.SessionChangesetsChangedAction:
888
+ if a.Changesets == nil {
889
+ state.Changesets = nil
890
+ } else {
891
+ state.Changesets = append([]ahptypes.Changeset(nil), a.Changesets...)
892
+ }
893
+ return ReduceOutcomeApplied
894
+ case *ahptypes.SessionConfigChangedAction:
895
+ if state.Config == nil {
896
+ return ReduceOutcomeNoOp
897
+ }
898
+ replace := a.Replace != nil && *a.Replace
899
+ if replace {
900
+ state.Config.Values = make(map[string]json.RawMessage, len(a.Config))
901
+ } else if state.Config.Values == nil {
902
+ state.Config.Values = make(map[string]json.RawMessage, len(a.Config))
903
+ }
904
+ for k, v := range a.Config {
905
+ state.Config.Values[k] = v
906
+ }
907
+ return ReduceOutcomeApplied
908
+ case *ahptypes.SessionMetaChangedAction:
909
+ state.Meta = a.Meta
910
+ return ReduceOutcomeApplied
911
+ case *ahptypes.SessionServerToolsChangedAction:
912
+ state.ServerTools = append([]ahptypes.ToolDefinition(nil), a.Tools...)
913
+ return ReduceOutcomeApplied
914
+ case *ahptypes.SessionActiveClientSetAction:
915
+ for i := range state.ActiveClients {
916
+ if state.ActiveClients[i].ClientId == a.ActiveClient.ClientId {
917
+ state.ActiveClients[i] = a.ActiveClient
918
+ return ReduceOutcomeApplied
919
+ }
920
+ }
921
+ state.ActiveClients = append(state.ActiveClients, a.ActiveClient)
922
+ return ReduceOutcomeApplied
923
+ case *ahptypes.SessionActiveClientRemovedAction:
924
+ for i := range state.ActiveClients {
925
+ if state.ActiveClients[i].ClientId == a.ClientId {
926
+ state.ActiveClients = append(state.ActiveClients[:i], state.ActiveClients[i+1:]...)
927
+ return ReduceOutcomeApplied
928
+ }
929
+ }
930
+ return ReduceOutcomeNoOp
931
+ case *ahptypes.SessionWorkingDirectorySetAction:
932
+ for _, d := range state.WorkingDirectories {
933
+ if d == a.Directory {
934
+ return ReduceOutcomeNoOp
935
+ }
936
+ }
937
+ state.WorkingDirectories = append(state.WorkingDirectories, a.Directory)
938
+ return ReduceOutcomeApplied
939
+ case *ahptypes.SessionWorkingDirectoryRemovedAction:
940
+ for i := range state.WorkingDirectories {
941
+ if state.WorkingDirectories[i] == a.Directory {
942
+ state.WorkingDirectories = append(state.WorkingDirectories[:i], state.WorkingDirectories[i+1:]...)
943
+ return ReduceOutcomeApplied
944
+ }
945
+ }
946
+ return ReduceOutcomeNoOp
947
+ case *ahptypes.SessionWorkingDirectoryReplacedAction:
948
+ idx := -1
949
+ for i, directory := range state.WorkingDirectories {
950
+ if directory == a.Directory {
951
+ idx = i
952
+ break
953
+ }
954
+ }
955
+ if idx < 0 {
956
+ return ReduceOutcomeNoOp
957
+ }
958
+ for i := 0; i < idx; i++ {
959
+ if state.WorkingDirectories[i] == a.Replacement {
960
+ state.WorkingDirectories = append(state.WorkingDirectories[:idx], state.WorkingDirectories[idx+1:]...)
961
+ return ReduceOutcomeApplied
962
+ }
963
+ }
964
+ updated := make([]ahptypes.URI, 0, len(state.WorkingDirectories))
965
+ for i, directory := range state.WorkingDirectories {
966
+ if i == idx {
967
+ updated = append(updated, a.Replacement)
968
+ } else if directory != a.Replacement {
969
+ updated = append(updated, directory)
970
+ }
971
+ }
972
+ state.WorkingDirectories = updated
973
+ return ReduceOutcomeApplied
974
+ case *ahptypes.SessionInputNeededSetAction:
975
+ id, ok := sessionInputRequestID(a.Request)
976
+ if !ok {
977
+ return ReduceOutcomeNoOp
978
+ }
979
+ for i := range state.InputNeeded {
980
+ if got, ok := sessionInputRequestID(state.InputNeeded[i]); ok && got == id {
981
+ state.InputNeeded[i] = a.Request
982
+ state.Status = withInputNeededStatus(state.Status, state.InputNeeded)
983
+ return ReduceOutcomeApplied
984
+ }
985
+ }
986
+ state.InputNeeded = append(state.InputNeeded, a.Request)
987
+ state.Status = withInputNeededStatus(state.Status, state.InputNeeded)
988
+ return ReduceOutcomeApplied
989
+ case *ahptypes.SessionInputNeededRemovedAction:
990
+ for i := range state.InputNeeded {
991
+ if got, ok := sessionInputRequestID(state.InputNeeded[i]); ok && got == a.Id {
992
+ state.InputNeeded = append(state.InputNeeded[:i], state.InputNeeded[i+1:]...)
993
+ if len(state.InputNeeded) == 0 {
994
+ state.InputNeeded = nil
995
+ }
996
+ state.Status = withInputNeededStatus(state.Status, state.InputNeeded)
997
+ return ReduceOutcomeApplied
998
+ }
999
+ }
1000
+ return ReduceOutcomeNoOp
1001
+ case *ahptypes.SessionCustomizationsChangedAction:
1002
+ state.Customizations = append([]ahptypes.Customization(nil), a.Customizations...)
1003
+ return ReduceOutcomeApplied
1004
+ case *ahptypes.SessionCustomizationToggledAction:
1005
+ if state.Customizations == nil {
1006
+ return ReduceOutcomeNoOp
1007
+ }
1008
+ if applyToggle(state.Customizations, a.Id, a.Enablement) {
1009
+ return ReduceOutcomeApplied
1010
+ }
1011
+ return ReduceOutcomeNoOp
1012
+ case *ahptypes.SessionCustomizationUpdatedAction:
1013
+ actionID, ok := customizationID(a.Customization)
1014
+ if !ok {
1015
+ return ReduceOutcomeNoOp
1016
+ }
1017
+ list := state.Customizations
1018
+ idx := -1
1019
+ for i := range list {
1020
+ if got, ok := customizationID(list[i]); ok && got == actionID {
1021
+ idx = i
1022
+ break
1023
+ }
1024
+ }
1025
+ if idx >= 0 {
1026
+ list[idx] = a.Customization
1027
+ } else {
1028
+ list = append(list, a.Customization)
1029
+ }
1030
+ state.Customizations = list
1031
+ return ReduceOutcomeApplied
1032
+ case *ahptypes.SessionCustomizationRemovedAction:
1033
+ if state.Customizations == nil {
1034
+ return ReduceOutcomeNoOp
1035
+ }
1036
+ list := state.Customizations
1037
+ for i := range list {
1038
+ if got, ok := customizationID(list[i]); ok && got == a.Id {
1039
+ state.Customizations = append(list[:i], list[i+1:]...)
1040
+ return ReduceOutcomeApplied
1041
+ }
1042
+ }
1043
+ for i := range list {
1044
+ children := containerChildren(&list[i])
1045
+ if children == nil {
1046
+ continue
1047
+ }
1048
+ for j := range *children {
1049
+ if got, ok := childCustomizationID((*children)[j]); ok && got == a.Id {
1050
+ *children = append((*children)[:j], (*children)[j+1:]...)
1051
+ return ReduceOutcomeApplied
1052
+ }
1053
+ }
1054
+ }
1055
+ return ReduceOutcomeNoOp
1056
+ case *ahptypes.SessionMcpServerStateChangedAction:
1057
+ return updateMcpServerCustomizationState(state, a.Id, a.State, a.Channel)
1058
+ case *ahptypes.SessionMcpServerStartRequestedAction:
1059
+ return updateMcpServerCustomizationState(state, a.Id, ahptypes.McpServerState{Value: &ahptypes.McpServerStartingState{
1060
+ Kind: ahptypes.McpServerStatusStarting,
1061
+ }}, nil)
1062
+ case *ahptypes.SessionMcpServerBackgroundRequestedAction:
1063
+ mcp := findMcpServerCustomization(state, a.Id)
1064
+ if mcp == nil {
1065
+ return ReduceOutcomeNoOp
1066
+ }
1067
+ starting, ok := mcp.State.Value.(*ahptypes.McpServerStartingState)
1068
+ if !ok || starting.Blocking == nil || !*starting.Blocking {
1069
+ return ReduceOutcomeNoOp
1070
+ }
1071
+ blocking := false
1072
+ next := *starting
1073
+ next.Blocking = &blocking
1074
+ mcp.State = ahptypes.McpServerState{Value: &next}
1075
+ return ReduceOutcomeApplied
1076
+ case *ahptypes.SessionMcpServerStopRequestedAction:
1077
+ return updateMcpServerCustomizationState(state, a.Id, ahptypes.McpServerState{Value: &ahptypes.McpServerStoppedState{
1078
+ Kind: ahptypes.McpServerStatusStopped,
1079
+ }}, nil)
1080
+ }
1081
+ return ReduceOutcomeOutOfScope
1082
+ }
1083
+
1084
+ func applyTurnStarted(state *ahptypes.ChatState, a *ahptypes.ChatTurnStartedAction) ReduceOutcome {
1085
+ state.ActiveTurn = &ahptypes.ActiveTurn{
1086
+ Id: a.TurnId,
1087
+ StartedAt: a.StartedAt,
1088
+ Message: a.Message,
1089
+ ResponseParts: []ahptypes.ResponsePart{},
1090
+ }
1091
+ state.Status = summaryStatus(state, nil)
1092
+ state.ModifiedAt = a.StartedAt
1093
+ state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false)
1094
+
1095
+ if a.QueuedMessageId != nil {
1096
+ qmid := *a.QueuedMessageId
1097
+ if state.SteeringMessage != nil && state.SteeringMessage.Id == qmid {
1098
+ state.SteeringMessage = nil
1099
+ }
1100
+ if state.QueuedMessages != nil {
1101
+ next := state.QueuedMessages[:0]
1102
+ for _, m := range state.QueuedMessages {
1103
+ if m.Id == qmid {
1104
+ continue
1105
+ }
1106
+ next = append(next, m)
1107
+ }
1108
+ if len(next) == 0 {
1109
+ state.QueuedMessages = nil
1110
+ } else {
1111
+ state.QueuedMessages = next
1112
+ }
1113
+ }
1114
+ }
1115
+ return ReduceOutcomeApplied
1116
+ }
1117
+
1118
+ func applyToolCallDelta(state *ahptypes.ChatState, a *ahptypes.ChatToolCallDeltaAction) ReduceOutcome {
1119
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1120
+ s, ok := tc.Value.(*ahptypes.ToolCallStreamingState)
1121
+ if !ok {
1122
+ return tc
1123
+ }
1124
+ if a.Meta != nil {
1125
+ s.Meta = a.Meta
1126
+ }
1127
+ if a.Content != nil {
1128
+ partial := ""
1129
+ if s.PartialInput != nil {
1130
+ partial = *s.PartialInput
1131
+ }
1132
+ partial += *a.Content
1133
+ s.PartialInput = &partial
1134
+ }
1135
+ if a.InvocationMessage != nil {
1136
+ invocationMessage := *a.InvocationMessage
1137
+ s.InvocationMessage = &invocationMessage
1138
+ }
1139
+ return tc
1140
+ })
1141
+ }
1142
+
1143
+ func refineToolCallContributor(current, next *ahptypes.ToolCallContributor) *ahptypes.ToolCallContributor {
1144
+ if next == nil {
1145
+ return current
1146
+ }
1147
+ if current != nil {
1148
+ if currentClient, ok := current.Value.(*ahptypes.ToolCallClientContributor); ok {
1149
+ if nextClient, ok := next.Value.(*ahptypes.ToolCallClientContributor); ok && nextClient.ClientId == currentClient.ClientId {
1150
+ return next
1151
+ }
1152
+ return current
1153
+ }
1154
+ }
1155
+ if _, ok := next.Value.(*ahptypes.ToolCallClientContributor); ok {
1156
+ return current
1157
+ }
1158
+ return next
1159
+ }
1160
+
1161
+ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReadyAction) ReduceOutcome {
1162
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1163
+ common := toolCallMeta(tc)
1164
+ if a.Intention != nil {
1165
+ common.intention = a.Intention
1166
+ }
1167
+ if a.ToolInput != nil {
1168
+ common.toolInput = a.ToolInput
1169
+ }
1170
+ common.contributor = refineToolCallContributor(common.contributor, a.Contributor)
1171
+ if a.Meta != nil {
1172
+ common.meta = a.Meta
1173
+ }
1174
+ var pending *ahptypes.ToolCallPendingConfirmationState
1175
+ if value, ok := tc.Value.(*ahptypes.ToolCallPendingConfirmationState); ok {
1176
+ pending = value
1177
+ }
1178
+ switch tc.Value.(type) {
1179
+ case *ahptypes.ToolCallStreamingState,
1180
+ *ahptypes.ToolCallRunningState,
1181
+ *ahptypes.ToolCallPendingConfirmationState:
1182
+ if a.Confirmed != nil {
1183
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallRunningState{
1184
+ Status: ahptypes.ToolCallStatusRunning,
1185
+ ToolCallId: common.id,
1186
+ ToolName: common.name,
1187
+ DisplayName: common.displayName,
1188
+ Intention: common.intention,
1189
+ ToolInput: common.toolInput,
1190
+ Contributor: common.contributor,
1191
+ Meta: common.meta,
1192
+ InvocationMessage: a.InvocationMessage,
1193
+ Confirmed: *a.Confirmed,
1194
+ }}
1195
+ }
1196
+ next := &ahptypes.ToolCallPendingConfirmationState{
1197
+ Status: ahptypes.ToolCallStatusPendingConfirmation,
1198
+ ToolCallId: common.id,
1199
+ ToolName: common.name,
1200
+ DisplayName: common.displayName,
1201
+ Intention: common.intention,
1202
+ ToolInput: common.toolInput,
1203
+ Contributor: common.contributor,
1204
+ Meta: common.meta,
1205
+ InvocationMessage: a.InvocationMessage,
1206
+ ConfirmationTitle: a.ConfirmationTitle,
1207
+ RiskAssessment: a.RiskAssessment,
1208
+ Edits: a.Edits,
1209
+ Editable: a.Editable,
1210
+ Options: a.Options,
1211
+ }
1212
+ if pending != nil {
1213
+ if next.ConfirmationTitle == nil {
1214
+ next.ConfirmationTitle = pending.ConfirmationTitle
1215
+ }
1216
+ if next.RiskAssessment == nil {
1217
+ next.RiskAssessment = pending.RiskAssessment
1218
+ }
1219
+ if next.Edits == nil {
1220
+ next.Edits = pending.Edits
1221
+ }
1222
+ if next.Editable == nil {
1223
+ next.Editable = pending.Editable
1224
+ }
1225
+ if next.Options == nil {
1226
+ next.Options = pending.Options
1227
+ }
1228
+ }
1229
+ return ahptypes.ToolCallState{Value: next}
1230
+ }
1231
+ return tc
1232
+ })
1233
+ }
1234
+
1235
+ func resolveSelectedOption(options []ahptypes.ConfirmationOption, id *string) *ahptypes.ConfirmationOption {
1236
+ if id == nil {
1237
+ return nil
1238
+ }
1239
+ for i := range options {
1240
+ if options[i].Id == *id {
1241
+ opt := options[i]
1242
+ return &opt
1243
+ }
1244
+ }
1245
+ return nil
1246
+ }
1247
+
1248
+ func applyToolCallConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallConfirmedAction) ReduceOutcome {
1249
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1250
+ s, ok := tc.Value.(*ahptypes.ToolCallPendingConfirmationState)
1251
+ if !ok {
1252
+ return tc
1253
+ }
1254
+ selected := resolveSelectedOption(s.Options, a.SelectedOptionId)
1255
+ if a.Approved {
1256
+ toolInput := s.ToolInput
1257
+ if a.EditedToolInput != nil && toolInput != nil && toolInput.Inline != nil {
1258
+ inline := *a.EditedToolInput
1259
+ toolInput = &ahptypes.ToolInput{Inline: &inline}
1260
+ }
1261
+ meta := s.Meta
1262
+ if a.Meta != nil {
1263
+ meta = a.Meta
1264
+ }
1265
+ confirmed := ahptypes.ToolCallConfirmationReasonNotNeeded
1266
+ if a.Confirmed != nil {
1267
+ confirmed = *a.Confirmed
1268
+ }
1269
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallRunningState{
1270
+ Status: ahptypes.ToolCallStatusRunning,
1271
+ ToolCallId: s.ToolCallId,
1272
+ ToolName: s.ToolName,
1273
+ DisplayName: s.DisplayName,
1274
+ Intention: s.Intention,
1275
+ ToolInput: toolInput,
1276
+ Contributor: s.Contributor,
1277
+ Meta: meta,
1278
+ InvocationMessage: s.InvocationMessage,
1279
+ Confirmed: confirmed,
1280
+ SelectedOption: selected,
1281
+ }}
1282
+ }
1283
+ reason := ahptypes.ToolCallCancellationReasonDenied
1284
+ if a.Reason != nil {
1285
+ reason = *a.Reason
1286
+ }
1287
+ meta := s.Meta
1288
+ if a.Meta != nil {
1289
+ meta = a.Meta
1290
+ }
1291
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCancelledState{
1292
+ Status: ahptypes.ToolCallStatusCancelled,
1293
+ ToolCallId: s.ToolCallId,
1294
+ ToolName: s.ToolName,
1295
+ DisplayName: s.DisplayName,
1296
+ Intention: s.Intention,
1297
+ ToolInput: s.ToolInput,
1298
+ Contributor: s.Contributor,
1299
+ Meta: meta,
1300
+ InvocationMessage: s.InvocationMessage,
1301
+ Reason: reason,
1302
+ ReasonMessage: a.ReasonMessage,
1303
+ UserSuggestion: a.UserSuggestion,
1304
+ SelectedOption: selected,
1305
+ }}
1306
+ })
1307
+ }
1308
+
1309
+ func applyToolCallComplete(state *ahptypes.ChatState, a *ahptypes.ChatToolCallCompleteAction) ReduceOutcome {
1310
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1311
+ common := toolCallMeta(tc)
1312
+ if a.Meta != nil {
1313
+ common.meta = a.Meta
1314
+ }
1315
+ var (
1316
+ invocation ahptypes.StringOrMarkdown
1317
+ toolInput *ahptypes.ToolInput
1318
+ confirmed = ahptypes.ToolCallConfirmationReasonNotNeeded
1319
+ selectedOption *ahptypes.ConfirmationOption
1320
+ preAuthContent []ahptypes.ToolResultContent
1321
+ fromAuthRequired bool
1322
+ )
1323
+ switch v := tc.Value.(type) {
1324
+ case *ahptypes.ToolCallRunningState:
1325
+ invocation = v.InvocationMessage
1326
+ toolInput = v.ToolInput
1327
+ confirmed = v.Confirmed
1328
+ selectedOption = v.SelectedOption
1329
+ case *ahptypes.ToolCallPendingConfirmationState:
1330
+ invocation = v.InvocationMessage
1331
+ toolInput = v.ToolInput
1332
+ case *ahptypes.ToolCallAuthRequiredState:
1333
+ // A client MAY cancel an auth-required MCP tool call by dispatching a
1334
+ // failed completion instead of authenticating. A successful completion
1335
+ // is invalid from auth-required — execution never resumed after the
1336
+ // challenge — and is ignored, leaving the call in auth-required. Any
1337
+ // partial content produced before the call paused for auth is
1338
+ // preserved unless the dispatched result supplies its own content.
1339
+ if a.Result.Success {
1340
+ return tc
1341
+ }
1342
+ invocation = v.InvocationMessage
1343
+ toolInput = v.ToolInput
1344
+ confirmed = v.Confirmed
1345
+ selectedOption = v.SelectedOption
1346
+ preAuthContent = v.Content
1347
+ fromAuthRequired = true
1348
+ default:
1349
+ return tc
1350
+ }
1351
+ content := a.Result.Content
1352
+ if content == nil {
1353
+ content = preAuthContent
1354
+ }
1355
+ // Cancelling from auth-required always completes terminally: the
1356
+ // pending auth challenge isn't a "pending result" the client can
1357
+ // review, so requiresResultConfirmation is ignored for this path —
1358
+ // it must never enter pending-result-confirmation.
1359
+ requiresResultConfirmation := a.RequiresResultConfirmation != nil && *a.RequiresResultConfirmation && !fromAuthRequired
1360
+ if requiresResultConfirmation {
1361
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallPendingResultConfirmationState{
1362
+ Status: ahptypes.ToolCallStatusPendingResultConfirmation,
1363
+ ToolCallId: common.id,
1364
+ ToolName: common.name,
1365
+ DisplayName: common.displayName,
1366
+ Intention: common.intention,
1367
+ ToolInput: toolInput,
1368
+ Contributor: common.contributor,
1369
+ Meta: common.meta,
1370
+ InvocationMessage: invocation,
1371
+ Success: a.Result.Success,
1372
+ PastTenseMessage: a.Result.PastTenseMessage,
1373
+ Content: append([]ahptypes.ToolResultContent(nil), content...),
1374
+ StructuredContent: a.Result.StructuredContent,
1375
+ Error: a.Result.Error,
1376
+ Confirmed: confirmed,
1377
+ SelectedOption: selectedOption,
1378
+ }}
1379
+ }
1380
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCompletedState{
1381
+ Status: ahptypes.ToolCallStatusCompleted,
1382
+ ToolCallId: common.id,
1383
+ ToolName: common.name,
1384
+ DisplayName: common.displayName,
1385
+ Intention: common.intention,
1386
+ ToolInput: toolInput,
1387
+ Contributor: common.contributor,
1388
+ Meta: common.meta,
1389
+ InvocationMessage: invocation,
1390
+ Success: a.Result.Success,
1391
+ PastTenseMessage: a.Result.PastTenseMessage,
1392
+ Content: append([]ahptypes.ToolResultContent(nil), content...),
1393
+ StructuredContent: a.Result.StructuredContent,
1394
+ Error: a.Result.Error,
1395
+ Confirmed: confirmed,
1396
+ SelectedOption: selectedOption,
1397
+ }}
1398
+ })
1399
+ }
1400
+
1401
+ func applyToolCallResultConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallResultConfirmedAction) ReduceOutcome {
1402
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1403
+ s, ok := tc.Value.(*ahptypes.ToolCallPendingResultConfirmationState)
1404
+ if !ok {
1405
+ return tc
1406
+ }
1407
+ if a.Approved {
1408
+ meta := s.Meta
1409
+ if a.Meta != nil {
1410
+ meta = a.Meta
1411
+ }
1412
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCompletedState{
1413
+ Status: ahptypes.ToolCallStatusCompleted,
1414
+ ToolCallId: s.ToolCallId,
1415
+ ToolName: s.ToolName,
1416
+ DisplayName: s.DisplayName,
1417
+ Intention: s.Intention,
1418
+ ToolInput: s.ToolInput,
1419
+ Contributor: s.Contributor,
1420
+ Meta: meta,
1421
+ InvocationMessage: s.InvocationMessage,
1422
+ Success: s.Success,
1423
+ PastTenseMessage: s.PastTenseMessage,
1424
+ Content: s.Content,
1425
+ StructuredContent: s.StructuredContent,
1426
+ Error: s.Error,
1427
+ Confirmed: s.Confirmed,
1428
+ SelectedOption: s.SelectedOption,
1429
+ }}
1430
+ }
1431
+ meta := s.Meta
1432
+ if a.Meta != nil {
1433
+ meta = a.Meta
1434
+ }
1435
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCancelledState{
1436
+ Status: ahptypes.ToolCallStatusCancelled,
1437
+ ToolCallId: s.ToolCallId,
1438
+ ToolName: s.ToolName,
1439
+ DisplayName: s.DisplayName,
1440
+ Intention: s.Intention,
1441
+ ToolInput: s.ToolInput,
1442
+ Contributor: s.Contributor,
1443
+ Meta: meta,
1444
+ InvocationMessage: s.InvocationMessage,
1445
+ Reason: ahptypes.ToolCallCancellationReasonResultDenied,
1446
+ SelectedOption: s.SelectedOption,
1447
+ }}
1448
+ })
1449
+ }
1450
+
1451
+ func applyToolCallAuthRequired(state *ahptypes.ChatState, a *ahptypes.ChatToolCallAuthRequiredAction) ReduceOutcome {
1452
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1453
+ s, ok := tc.Value.(*ahptypes.ToolCallRunningState)
1454
+ if !ok {
1455
+ return tc
1456
+ }
1457
+ if s.Contributor == nil {
1458
+ return tc
1459
+ }
1460
+ if _, ok := s.Contributor.Value.(*ahptypes.ToolCallMcpContributor); !ok {
1461
+ return tc
1462
+ }
1463
+ meta := s.Meta
1464
+ if a.Meta != nil {
1465
+ meta = a.Meta
1466
+ }
1467
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallAuthRequiredState{
1468
+ Status: ahptypes.ToolCallStatusAuthRequired,
1469
+ ToolCallId: s.ToolCallId,
1470
+ ToolName: s.ToolName,
1471
+ DisplayName: s.DisplayName,
1472
+ Intention: s.Intention,
1473
+ ToolInput: s.ToolInput,
1474
+ Contributor: s.Contributor,
1475
+ Meta: meta,
1476
+ InvocationMessage: s.InvocationMessage,
1477
+ Confirmed: s.Confirmed,
1478
+ SelectedOption: s.SelectedOption,
1479
+ Auth: a.Auth,
1480
+ Content: append([]ahptypes.ToolResultContent(nil), s.Content...),
1481
+ }}
1482
+ })
1483
+ }
1484
+
1485
+ func applyToolCallAuthResolved(state *ahptypes.ChatState, a *ahptypes.ChatToolCallAuthResolvedAction) ReduceOutcome {
1486
+ return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState {
1487
+ s, ok := tc.Value.(*ahptypes.ToolCallAuthRequiredState)
1488
+ if !ok {
1489
+ return tc
1490
+ }
1491
+ meta := s.Meta
1492
+ if a.Meta != nil {
1493
+ meta = a.Meta
1494
+ }
1495
+ return ahptypes.ToolCallState{Value: &ahptypes.ToolCallRunningState{
1496
+ Status: ahptypes.ToolCallStatusRunning,
1497
+ ToolCallId: s.ToolCallId,
1498
+ ToolName: s.ToolName,
1499
+ DisplayName: s.DisplayName,
1500
+ Intention: s.Intention,
1501
+ ToolInput: s.ToolInput,
1502
+ Contributor: s.Contributor,
1503
+ Meta: meta,
1504
+ InvocationMessage: s.InvocationMessage,
1505
+ Confirmed: s.Confirmed,
1506
+ SelectedOption: s.SelectedOption,
1507
+ Content: append([]ahptypes.ToolResultContent(nil), s.Content...),
1508
+ }}
1509
+ })
1510
+ }
1511
+
1512
+ func updateMcpServerCustomizationState(state *ahptypes.SessionState, id string, nextState ahptypes.McpServerState, channel *ahptypes.URI) ReduceOutcome {
1513
+ mcp := findMcpServerCustomization(state, id)
1514
+ if mcp == nil {
1515
+ return ReduceOutcomeNoOp
1516
+ }
1517
+ mcp.State = nextState
1518
+ mcp.Channel = channel
1519
+ return ReduceOutcomeApplied
1520
+ }
1521
+
1522
+ // findMcpServerCustomization locates the McpServerCustomization with the given
1523
+ // id, searching the top-level list first and then every container's children.
1524
+ // Returns nil when no entry matches or the id targets a non-MCP customization.
1525
+ func findMcpServerCustomization(state *ahptypes.SessionState, id string) *ahptypes.McpServerCustomization {
1526
+ list := state.Customizations
1527
+ for i := range list {
1528
+ got, ok := customizationID(list[i])
1529
+ if !ok || got != id {
1530
+ continue
1531
+ }
1532
+ mcp, _ := list[i].Value.(*ahptypes.McpServerCustomization)
1533
+ return mcp
1534
+ }
1535
+ for i := range list {
1536
+ children := containerChildren(&list[i])
1537
+ if children == nil {
1538
+ continue
1539
+ }
1540
+ for j := range *children {
1541
+ got, ok := childCustomizationID((*children)[j])
1542
+ if !ok || got != id {
1543
+ continue
1544
+ }
1545
+ mcp, _ := (*children)[j].Value.(*ahptypes.McpServerCustomization)
1546
+ return mcp
1547
+ }
1548
+ }
1549
+ return nil
1550
+ }
1551
+
1552
+ func applyTruncated(state *ahptypes.ChatState, turnID *string) ReduceOutcome {
1553
+ if turnID == nil {
1554
+ state.Turns = []ahptypes.Turn{}
1555
+ state.TurnsNextCursor = nil
1556
+ } else {
1557
+ idx := -1
1558
+ for i := range state.Turns {
1559
+ if state.Turns[i].Id == *turnID {
1560
+ idx = i
1561
+ break
1562
+ }
1563
+ }
1564
+ if idx < 0 {
1565
+ return ReduceOutcomeNoOp
1566
+ }
1567
+ state.Turns = state.Turns[:idx+1]
1568
+ }
1569
+ state.ActiveTurn = nil
1570
+ state.Status = summaryStatus(state, nil)
1571
+ return ReduceOutcomeApplied
1572
+ }
1573
+
1574
+ func applyInputAnswerChanged(state *ahptypes.ChatState, a *ahptypes.ChatInputAnswerChangedAction) ReduceOutcome {
1575
+ if state.ActiveTurn == nil {
1576
+ return ReduceOutcomeNoOp
1577
+ }
1578
+ for i := range state.ActiveTurn.ResponseParts {
1579
+ input, ok := state.ActiveTurn.ResponseParts[i].Value.(*ahptypes.InputRequestResponsePart)
1580
+ if !ok || input.Response != nil || input.Request.Id != a.RequestId {
1581
+ continue
1582
+ }
1583
+ req := &input.Request
1584
+ if req.Answers == nil {
1585
+ req.Answers = make(map[string]ahptypes.ChatInputAnswer)
1586
+ }
1587
+ if a.Answer == nil {
1588
+ delete(req.Answers, a.QuestionId)
1589
+ } else {
1590
+ req.Answers[a.QuestionId] = *a.Answer
1591
+ }
1592
+ if len(req.Answers) == 0 {
1593
+ req.Answers = nil
1594
+ }
1595
+ return ReduceOutcomeApplied
1596
+ }
1597
+ return ReduceOutcomeNoOp
1598
+ }
1599
+
1600
+ // ─── Terminal Reducer ──────────────────────────────────────────────────
1601
+
1602
+ // ApplyActionToTerminal applies action to the [ahptypes.TerminalState]
1603
+ // in place. Returns [ReduceOutcomeOutOfScope] for actions that target a
1604
+ // different state tree.
1605
+ func ApplyActionToTerminal(state *ahptypes.TerminalState, action ahptypes.StateAction) ReduceOutcome {
1606
+ switch a := action.Value.(type) {
1607
+ case *ahptypes.TerminalDataAction:
1608
+ appendTerminalData(state, a.Data)
1609
+ return ReduceOutcomeApplied
1610
+ case *ahptypes.TerminalInputAction:
1611
+ return ReduceOutcomeNoOp
1612
+ case *ahptypes.TerminalResizedAction:
1613
+ cols := a.Cols
1614
+ rows := a.Rows
1615
+ state.Cols = &cols
1616
+ state.Rows = &rows
1617
+ return ReduceOutcomeApplied
1618
+ case *ahptypes.TerminalClaimedAction:
1619
+ state.Claim = a.Claim
1620
+ return ReduceOutcomeApplied
1621
+ case *ahptypes.TerminalTitleChangedAction:
1622
+ state.Title = a.Title
1623
+ return ReduceOutcomeApplied
1624
+ case *ahptypes.TerminalCwdChangedAction:
1625
+ cwd := a.Cwd
1626
+ state.Cwd = &cwd
1627
+ return ReduceOutcomeApplied
1628
+ case *ahptypes.TerminalExitedAction:
1629
+ state.Lifecycle = ahptypes.TerminalLifecycleState{Value: &ahptypes.TerminalExitedLifecycleState{
1630
+ Status: ahptypes.TerminalLifecycleStatusExited,
1631
+ ExitCode: a.ExitCode,
1632
+ }}
1633
+ return ReduceOutcomeApplied
1634
+ case *ahptypes.TerminalClearedAction:
1635
+ state.Content = []ahptypes.TerminalContentPart{}
1636
+ return ReduceOutcomeApplied
1637
+ case *ahptypes.TerminalCommandDetectionAvailableAction:
1638
+ t := true
1639
+ state.SupportsCommandDetection = &t
1640
+ return ReduceOutcomeApplied
1641
+ case *ahptypes.TerminalCommandExecutedAction:
1642
+ state.Content = append(state.Content, ahptypes.TerminalContentPart{Value: &ahptypes.TerminalCommandPart{
1643
+ Type: "command",
1644
+ CommandId: a.CommandId,
1645
+ CommandLine: a.CommandLine,
1646
+ Timestamp: a.Timestamp,
1647
+ IsComplete: false,
1648
+ }})
1649
+ t := true
1650
+ state.SupportsCommandDetection = &t
1651
+ return ReduceOutcomeApplied
1652
+ case *ahptypes.TerminalCommandFinishedAction:
1653
+ for i := range state.Content {
1654
+ c, ok := state.Content[i].Value.(*ahptypes.TerminalCommandPart)
1655
+ if !ok {
1656
+ continue
1657
+ }
1658
+ if c.CommandId == a.CommandId {
1659
+ c.IsComplete = true
1660
+ c.ExitCode = a.ExitCode
1661
+ c.DurationMs = a.DurationMs
1662
+ return ReduceOutcomeApplied
1663
+ }
1664
+ }
1665
+ return ReduceOutcomeNoOp
1666
+ }
1667
+ return ReduceOutcomeOutOfScope
1668
+ }
1669
+
1670
+ func appendTerminalData(state *ahptypes.TerminalState, data string) {
1671
+ n := len(state.Content)
1672
+ if n > 0 {
1673
+ switch tail := state.Content[n-1].Value.(type) {
1674
+ case *ahptypes.TerminalCommandPart:
1675
+ if !tail.IsComplete {
1676
+ tail.Output += data
1677
+ return
1678
+ }
1679
+ case *ahptypes.TerminalUnclassifiedPart:
1680
+ tail.Value += data
1681
+ return
1682
+ }
1683
+ }
1684
+ state.Content = append(state.Content, ahptypes.TerminalContentPart{Value: &ahptypes.TerminalUnclassifiedPart{
1685
+ Type: "unclassified",
1686
+ Value: data,
1687
+ }})
1688
+ }
1689
+
1690
+ // ─── Changeset Reducer ─────────────────────────────────────────────────
1691
+
1692
+ // ApplyActionToChangeset applies action to the [ahptypes.ChangesetState]
1693
+ // in place. Returns [ReduceOutcomeOutOfScope] for actions that target a
1694
+ // different state tree.
1695
+ func ApplyActionToChangeset(state *ahptypes.ChangesetState, action ahptypes.StateAction) ReduceOutcome {
1696
+ switch a := action.Value.(type) {
1697
+ case *ahptypes.ChangesetStatusChangedAction:
1698
+ state.Status = a.Status
1699
+ if a.Status == ahptypes.ChangesetStatusError {
1700
+ state.Error = a.Error
1701
+ } else {
1702
+ state.Error = nil
1703
+ }
1704
+ return ReduceOutcomeApplied
1705
+
1706
+ case *ahptypes.ChangesetFileSetAction:
1707
+ for i := range state.Files {
1708
+ if state.Files[i].Id == a.File.Id {
1709
+ state.Files[i] = a.File
1710
+ return ReduceOutcomeApplied
1711
+ }
1712
+ }
1713
+ state.Files = append(state.Files, a.File)
1714
+ return ReduceOutcomeApplied
1715
+
1716
+ case *ahptypes.ChangesetFileRemovedAction:
1717
+ for i := range state.Files {
1718
+ if state.Files[i].Id == a.FileId {
1719
+ state.Files = append(state.Files[:i], state.Files[i+1:]...)
1720
+ return ReduceOutcomeApplied
1721
+ }
1722
+ }
1723
+ return ReduceOutcomeNoOp
1724
+
1725
+ case *ahptypes.ChangesetFilesReviewChangedAction:
1726
+ ids := make(map[string]struct{}, len(a.Files))
1727
+ for _, id := range a.Files {
1728
+ ids[id] = struct{}{}
1729
+ }
1730
+ changed := false
1731
+ for i := range state.Files {
1732
+ if _, ok := ids[state.Files[i].Id]; !ok {
1733
+ continue
1734
+ }
1735
+ if state.Files[i].Reviewed != nil && *state.Files[i].Reviewed == a.Reviewed {
1736
+ continue
1737
+ }
1738
+ reviewed := a.Reviewed
1739
+ state.Files[i].Reviewed = &reviewed
1740
+ changed = true
1741
+ }
1742
+ if !changed {
1743
+ return ReduceOutcomeNoOp
1744
+ }
1745
+ return ReduceOutcomeApplied
1746
+
1747
+ case *ahptypes.ChangesetContentChangedAction:
1748
+ state.Files = a.Files
1749
+ if a.Operations != nil {
1750
+ state.Operations = a.Operations
1751
+ }
1752
+ return ReduceOutcomeApplied
1753
+
1754
+ case *ahptypes.ChangesetOperationsChangedAction:
1755
+ state.Operations = a.Operations
1756
+ return ReduceOutcomeApplied
1757
+
1758
+ case *ahptypes.ChangesetOperationStatusChangedAction:
1759
+ for i := range state.Operations {
1760
+ if state.Operations[i].Id == a.OperationId {
1761
+ state.Operations[i].Status = a.Status
1762
+ if a.Status == ahptypes.ChangesetOperationStatusError {
1763
+ state.Operations[i].Error = a.Error
1764
+ } else {
1765
+ state.Operations[i].Error = nil
1766
+ }
1767
+ return ReduceOutcomeApplied
1768
+ }
1769
+ }
1770
+ return ReduceOutcomeNoOp
1771
+
1772
+ case *ahptypes.ChangesetClearedAction:
1773
+ if len(state.Files) == 0 {
1774
+ return ReduceOutcomeNoOp
1775
+ }
1776
+ state.Files = []ahptypes.ChangesetFile{}
1777
+ return ReduceOutcomeApplied
1778
+ }
1779
+ return ReduceOutcomeOutOfScope
1780
+ }
1781
+
1782
+ // ─── Annotations Reducer ──────────────────────────────────────────────
1783
+
1784
+ // ApplyActionToAnnotations applies action to the [ahptypes.AnnotationsState]
1785
+ // in place. Returns [ReduceOutcomeOutOfScope] for actions that target a
1786
+ // different state tree.
1787
+ func ApplyActionToAnnotations(state *ahptypes.AnnotationsState, action ahptypes.StateAction) ReduceOutcome {
1788
+ switch a := action.Value.(type) {
1789
+ case *ahptypes.AnnotationsSetAction:
1790
+ for i := range state.Annotations {
1791
+ if state.Annotations[i].Id == a.Annotation.Id {
1792
+ state.Annotations[i] = a.Annotation
1793
+ return ReduceOutcomeApplied
1794
+ }
1795
+ }
1796
+ state.Annotations = append(state.Annotations, a.Annotation)
1797
+ return ReduceOutcomeApplied
1798
+
1799
+ case *ahptypes.AnnotationsUpdatedAction:
1800
+ for i := range state.Annotations {
1801
+ if state.Annotations[i].Id == a.AnnotationId {
1802
+ if a.Origin != nil {
1803
+ state.Annotations[i].Origin = *a.Origin
1804
+ }
1805
+ if a.Resource != nil {
1806
+ state.Annotations[i].Resource = *a.Resource
1807
+ }
1808
+ if a.Range != nil {
1809
+ state.Annotations[i].Range = a.Range
1810
+ }
1811
+ if a.Resolved != nil {
1812
+ state.Annotations[i].Resolved = *a.Resolved
1813
+ }
1814
+ return ReduceOutcomeApplied
1815
+ }
1816
+ }
1817
+ return ReduceOutcomeNoOp
1818
+
1819
+ case *ahptypes.AnnotationsRemovedAction:
1820
+ for i := range state.Annotations {
1821
+ if state.Annotations[i].Id == a.AnnotationId {
1822
+ state.Annotations = append(state.Annotations[:i], state.Annotations[i+1:]...)
1823
+ return ReduceOutcomeApplied
1824
+ }
1825
+ }
1826
+ return ReduceOutcomeNoOp
1827
+
1828
+ case *ahptypes.AnnotationsEntrySetAction:
1829
+ for i := range state.Annotations {
1830
+ if state.Annotations[i].Id != a.AnnotationId {
1831
+ continue
1832
+ }
1833
+ entries := state.Annotations[i].Entries
1834
+ for j := range entries {
1835
+ if entries[j].Id == a.Entry.Id {
1836
+ state.Annotations[i].Entries[j] = a.Entry
1837
+ return ReduceOutcomeApplied
1838
+ }
1839
+ }
1840
+ state.Annotations[i].Entries = append(state.Annotations[i].Entries, a.Entry)
1841
+ return ReduceOutcomeApplied
1842
+ }
1843
+ return ReduceOutcomeNoOp
1844
+
1845
+ case *ahptypes.AnnotationsEntryRemovedAction:
1846
+ for i := range state.Annotations {
1847
+ if state.Annotations[i].Id != a.AnnotationId {
1848
+ continue
1849
+ }
1850
+ entries := state.Annotations[i].Entries
1851
+ for j := range entries {
1852
+ if entries[j].Id == a.EntryId {
1853
+ state.Annotations[i].Entries = append(entries[:j], entries[j+1:]...)
1854
+ return ReduceOutcomeApplied
1855
+ }
1856
+ }
1857
+ return ReduceOutcomeNoOp
1858
+ }
1859
+ return ReduceOutcomeNoOp
1860
+ }
1861
+ return ReduceOutcomeOutOfScope
1862
+ }
1863
+
1864
+ // ─── Resource-Watch Reducer ────────────────────────────────────────────
1865
+
1866
+ // ApplyActionToResourceWatch applies action to the
1867
+ // [ahptypes.ResourceWatchState] in place. The state captures only the
1868
+ // watch descriptor set at subscription time; change events delivered
1869
+ // via [ahptypes.ResourceWatchChangedAction] are pass-through and leave
1870
+ // the state unchanged. Returns [ReduceOutcomeOutOfScope] for actions
1871
+ // that target a different state tree.
1872
+ func ApplyActionToResourceWatch(state *ahptypes.ResourceWatchState, action ahptypes.StateAction) ReduceOutcome {
1873
+ switch action.Value.(type) {
1874
+ case *ahptypes.ResourceWatchChangedAction:
1875
+ return ReduceOutcomeNoOp
1876
+ }
1877
+ return ReduceOutcomeOutOfScope
1878
+ }
1879
+
1880
+ // ApplyActionToAutomation applies an action to automation catalogue state.
1881
+ func ApplyActionToAutomation(state *ahptypes.AutomationState, action ahptypes.StateAction) ReduceOutcome {
1882
+ switch a := action.Value.(type) {
1883
+ case *ahptypes.AutomationCreateRequestedAction, *ahptypes.AutomationUpdateRequestedAction:
1884
+ return ReduceOutcomeNoOp
1885
+ case *ahptypes.AutomationSetAction:
1886
+ for i := range state.Entries {
1887
+ if state.Entries[i].Resource == a.Automation.Resource {
1888
+ state.Entries[i] = a.Automation
1889
+ return ReduceOutcomeApplied
1890
+ }
1891
+ }
1892
+ state.Entries = append(state.Entries, a.Automation)
1893
+ return ReduceOutcomeApplied
1894
+ case *ahptypes.AutomationRemovedAction:
1895
+ for i := range state.Entries {
1896
+ if state.Entries[i].Resource == a.Resource {
1897
+ state.Entries = append(state.Entries[:i], state.Entries[i+1:]...)
1898
+ return ReduceOutcomeApplied
1899
+ }
1900
+ }
1901
+ return ReduceOutcomeNoOp
1902
+ }
1903
+ return ReduceOutcomeOutOfScope
1904
+ }
1905
+
1906
+ // ApplyActionToAutomationRun applies an action to automation-run state.
1907
+ func ApplyActionToAutomationRun(state *ahptypes.AutomationRunState, action ahptypes.StateAction) ReduceOutcome {
1908
+ switch a := action.Value.(type) {
1909
+ case *ahptypes.AutomationRunLifecycleChangedAction:
1910
+ state.Lifecycle = a.Lifecycle
1911
+ return ReduceOutcomeApplied
1912
+ case *ahptypes.AutomationRunSessionSetAction:
1913
+ for _, session := range state.Sessions {
1914
+ if session == a.Session {
1915
+ return ReduceOutcomeNoOp
1916
+ }
1917
+ }
1918
+ state.Sessions = append(state.Sessions, a.Session)
1919
+ return ReduceOutcomeApplied
1920
+ case *ahptypes.AutomationRunSessionRemovedAction:
1921
+ for i, session := range state.Sessions {
1922
+ if session != a.Session {
1923
+ continue
1924
+ }
1925
+ state.Sessions = append(state.Sessions[:i], state.Sessions[i+1:]...)
1926
+ if state.PrimarySession != nil && *state.PrimarySession == a.Session {
1927
+ state.PrimarySession = nil
1928
+ }
1929
+ return ReduceOutcomeApplied
1930
+ }
1931
+ return ReduceOutcomeNoOp
1932
+ case *ahptypes.AutomationRunPrimarySessionChangedAction:
1933
+ state.PrimarySession = a.PrimarySession
1934
+ return ReduceOutcomeApplied
1935
+ case *ahptypes.AutomationRunCancelRequestedAction:
1936
+ return ReduceOutcomeNoOp
1937
+ }
1938
+ return ReduceOutcomeOutOfScope
1939
+ }