@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,600 @@
1
+ package mapper
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strconv"
6
+ "strings"
7
+ "time"
8
+
9
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
10
+ )
11
+
12
+ // Event is one pi agent event, decoded JSON.
13
+ type Event = map[string]any
14
+
15
+ // Outcome is how a turn ended.
16
+ type Outcome string
17
+
18
+ const (
19
+ // OutcomeDefault lets the mapper decide from what it saw.
20
+ OutcomeDefault Outcome = ""
21
+ OutcomeComplete Outcome = "complete"
22
+ OutcomeCancelled Outcome = "cancelled"
23
+ OutcomeError Outcome = "error"
24
+ )
25
+
26
+ // Options configure a TurnMapper.
27
+ type Options struct {
28
+ // WorkingDirectory renders tool paths relative to it, so activity reads as `Reading src/x.ts`.
29
+ WorkingDirectory string
30
+ // Now is the clock (defaults to time.Now); tests pin it.
31
+ Now func() time.Time
32
+ }
33
+
34
+ type openPart struct {
35
+ partID string
36
+ kind ahptypes.ResponsePartKind
37
+ created bool
38
+ }
39
+
40
+ // TurnMapper maps one AHP turn (port of the TurnMapper class of src/pi/event-mapper.ts).
41
+ //
42
+ // Three structural mismatches it bridges:
43
+ //
44
+ // 1. Turn granularity. pi nests an agent run (agent_start..agent_end), a pi turn (one assistant
45
+ // message) and content blocks; AHP has a turn (one user message through to completion) and
46
+ // its response parts. One AHP turn therefore spans everything from prompt() to agent_settled,
47
+ // which may hold several agent runs (auto-retry, post-compaction retry). Mapping agent_end to
48
+ // chat/turnComplete would close the turn early and the reducer would drop every later action.
49
+ // 2. Part identity. contentIndex restarts at 0 for every assistant message and the stream has no
50
+ // message id, so parts are keyed `<turnId>:<message ordinal>:<contentIndex>`.
51
+ // 3. Part creation. chat/delta appends to a part that must already exist (an unknown partId is a
52
+ // silent no-op), so parts are created lazily, on the first content that lands in them.
53
+ //
54
+ // A mapper is created when a turn starts and discarded when it ends. It is not safe for
55
+ // concurrent use; the caller serialises Handle and Finish.
56
+ type TurnMapper struct {
57
+ turnID string // the turn being mapped; changes when a message is injected mid-run
58
+ rootTurnID string
59
+ turnStartedAt int64 // ms since the epoch
60
+ injectedCount int
61
+
62
+ messageOrdinal int
63
+ parts map[int]*openPart
64
+ toolCallsByIdx map[int]string
65
+ liveToolCalls map[string]string // toolCallId -> how the call reads once it has finished
66
+ outcome Outcome
67
+ errorMessage *string
68
+ finished bool
69
+
70
+ opts Options
71
+ activity *string
72
+ }
73
+
74
+ // NewTurnMapper creates a mapper for a turn that started at startedAtMs (ms since the epoch).
75
+ func NewTurnMapper(turnID string, startedAtMs int64, opts ...Options) *TurnMapper {
76
+ m := &TurnMapper{
77
+ turnID: turnID, rootTurnID: turnID, turnStartedAt: startedAtMs,
78
+ messageOrdinal: -1, parts: map[int]*openPart{}, toolCallsByIdx: map[int]string{},
79
+ liveToolCalls: map[string]string{}, outcome: OutcomeComplete,
80
+ }
81
+ if len(opts) > 0 {
82
+ m.opts = opts[0]
83
+ }
84
+ if m.opts.Now == nil {
85
+ m.opts.Now = time.Now
86
+ }
87
+ return m
88
+ }
89
+
90
+ // TurnID is the turn currently being mapped.
91
+ func (m *TurnMapper) TurnID() string { return m.turnID }
92
+
93
+ // Finished reports whether the turn has been closed.
94
+ func (m *TurnMapper) Finished() bool { return m.finished }
95
+
96
+ func (m *TurnMapper) nowMs() int64 { return m.opts.Now().UnixMilli() }
97
+
98
+ // Handle translates one pi event into zero or more chat actions.
99
+ func (m *TurnMapper) Handle(event Event) []ahptypes.StateAction {
100
+ if m.finished {
101
+ return nil
102
+ }
103
+ typ, _ := event["type"].(string)
104
+ switch typ {
105
+ case "message_start":
106
+ return m.onMessageStart(event)
107
+ case "message_update":
108
+ return m.onDelta(asMap(event["assistantMessageEvent"]))
109
+ case "message_end":
110
+ return m.onMessageEnd(event)
111
+ case "tool_execution_start":
112
+ // The tool is about to run: say which one, and with what.
113
+ name, _ := event["toolName"].(string)
114
+ return m.setActivity(strPtr(DescribeToolCall(name, event["args"], m.opts.WorkingDirectory)))
115
+ case "tool_execution_update":
116
+ return m.onToolUpdate(event)
117
+ case "tool_execution_end":
118
+ out := m.onToolEnd(event)
119
+ // Back to the model. Kept out of onToolEnd, which bails early for a call it never saw
120
+ // start: activity is display state and should not depend on that bookkeeping.
121
+ return append(out, m.setActivity(strPtr(ThinkingActivity))...)
122
+ case "agent_settled":
123
+ return m.Finish(OutcomeDefault, "")
124
+ case "agent_start":
125
+ // A run beginning, or resuming after a retry.
126
+ return m.setActivity(strPtr(ThinkingActivity))
127
+ default:
128
+ // agent_end / turn_start / turn_end carry no protocol-visible state of their own: a retry
129
+ // re-enters agent_start inside the same AHP turn, and pi's turn boundaries are just
130
+ // groupings of response parts.
131
+ return nil
132
+ }
133
+ }
134
+
135
+ func asMap(v any) map[string]any { m, _ := v.(map[string]any); return m }
136
+
137
+ // ── Assistant messages ──────────────────────────────────────────────────
138
+
139
+ func (m *TurnMapper) onMessageStart(event Event) []ahptypes.StateAction {
140
+ message := asMap(event["message"])
141
+ switch message["role"] {
142
+ case "user":
143
+ return m.onInjectedMessage(message)
144
+ case "assistant":
145
+ // A new assistant message restarts contentIndex at 0, so the part map must not leak.
146
+ m.messageOrdinal++
147
+ m.parts = map[int]*openPart{}
148
+ m.toolCallsByIdx = map[int]string{}
149
+ }
150
+ return nil
151
+ }
152
+
153
+ // setActivity emits an activity change, skipping no-ops: a turn fires many events that map to
154
+ // the same description, and re-sending it would burn a serverSeq per token.
155
+ func (m *TurnMapper) setActivity(activity *string) []ahptypes.StateAction {
156
+ if (m.activity == nil && activity == nil) || (m.activity != nil && activity != nil && *m.activity == *activity) {
157
+ return nil
158
+ }
159
+ m.activity = activity
160
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatActivityChangedAction{Type: ahptypes.ActionTypeChatActivityChanged, Activity: activity}}}
161
+ }
162
+
163
+ func (m *TurnMapper) onMessageEnd(event Event) []ahptypes.StateAction {
164
+ message := asMap(event["message"])
165
+ if message["role"] != "assistant" {
166
+ return nil
167
+ }
168
+ // A run cancelled or failed before any content streamed produces no `error` delta: the outcome
169
+ // shows up only as the finished message's stopReason. Reading it here keeps such a turn from
170
+ // being reported as a normal completion.
171
+ switch message["stopReason"] {
172
+ case "aborted":
173
+ m.outcome = OutcomeCancelled
174
+ m.errorMessageIfUnset(message["errorMessage"])
175
+ case "error":
176
+ m.outcome = OutcomeError
177
+ m.errorMessageIfUnset(message["errorMessage"])
178
+ }
179
+ model, _ := message["model"].(string)
180
+ if usage := toUsageInfo(asMap(message["usage"]), model); usage != nil {
181
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatUsageAction{Type: ahptypes.ActionTypeChatUsage, TurnId: m.turnID, Usage: *usage}}}
182
+ }
183
+ return nil
184
+ }
185
+
186
+ // errorMessageIfUnset is `this.#errorMessage ??= value`.
187
+ func (m *TurnMapper) errorMessageIfUnset(v any) {
188
+ if m.errorMessage == nil {
189
+ if s, ok := v.(string); ok {
190
+ m.errorMessage = &s
191
+ }
192
+ }
193
+ }
194
+
195
+ func toUsageInfo(usage map[string]any, model string) *ahptypes.UsageInfo {
196
+ if usage == nil {
197
+ return nil
198
+ }
199
+ // UsageInfo only has slots for input/output/cacheRead, but pi reports more (cache writes, a
200
+ // reasoning-token breakdown, computed cost). Dropping them would lose the numbers a client
201
+ // needs to show what a turn cost, so the remainder rides in `_meta`, the protocol's place for
202
+ // provider-specific extras.
203
+ extra := map[string]json.RawMessage{}
204
+ put := func(key string, v any) {
205
+ if raw, err := json.Marshal(v); err == nil {
206
+ extra[key] = raw
207
+ }
208
+ }
209
+ if v, ok := usage["cacheWrite"]; ok && v != nil {
210
+ put("cacheWriteTokens", v)
211
+ }
212
+ if v, ok := usage["reasoning"]; ok && v != nil {
213
+ put("reasoningTokens", v)
214
+ }
215
+ if v, ok := usage["totalTokens"]; ok && v != nil {
216
+ put("totalTokens", v)
217
+ }
218
+ if v, ok := usage["cost"]; ok && v != nil {
219
+ put("cost", v)
220
+ }
221
+ info := &ahptypes.UsageInfo{}
222
+ if f, ok := num(usage["input"]); ok {
223
+ info.InputTokens = i64Ptr(int64(f))
224
+ }
225
+ if f, ok := num(usage["output"]); ok {
226
+ info.OutputTokens = i64Ptr(int64(f))
227
+ }
228
+ if f, ok := num(usage["cacheRead"]); ok {
229
+ info.CacheReadTokens = i64Ptr(int64(f))
230
+ }
231
+ if model != "" {
232
+ info.Model = &model
233
+ }
234
+ if len(extra) > 0 {
235
+ info.Meta = extra
236
+ }
237
+ return info
238
+ }
239
+
240
+ // onInjectedMessage handles a user message pi injects part-way through a run.
241
+ //
242
+ // pi delivers a steering message as an ordinary user message inside the running turn and stores
243
+ // it in the session file as an ordinary user message too, with nothing to mark it as steering. So
244
+ // when the conversation is rebuilt from disk it necessarily becomes its own turn, because a user
245
+ // message is what starts one. The live path does the same, or the conversation would render one
246
+ // way while running and another after a reload: the turn in flight is closed and a fresh one
247
+ // opened around the injected message.
248
+ //
249
+ // The first user message of a run is the prompt itself, which the client already opened the turn
250
+ // with, so only later ones split.
251
+ func (m *TurnMapper) onInjectedMessage(message map[string]any) []ahptypes.StateAction {
252
+ if m.messageOrdinal < 0 {
253
+ return nil // nothing has streamed yet: this is the run's own prompt
254
+ }
255
+ injected := UserMessageFromPiContent(message["content"])
256
+ completed := m.closeCurrentTurn()
257
+
258
+ m.injectedCount++
259
+ m.turnID = m.rootTurnID + "#" + strconv.Itoa(m.injectedCount)
260
+ m.turnStartedAt = m.nowMs()
261
+ m.messageOrdinal = -1
262
+ m.parts = map[int]*openPart{}
263
+ m.toolCallsByIdx = map[int]string{}
264
+
265
+ return append(completed, ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
266
+ Type: ahptypes.ActionTypeChatTurnStarted, TurnId: m.turnID,
267
+ StartedAt: isoTime(m.turnStartedAt), Message: injected,
268
+ }})
269
+ }
270
+
271
+ func (m *TurnMapper) closeCurrentTurn() []ahptypes.StateAction {
272
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatTurnCompleteAction{
273
+ Type: ahptypes.ActionTypeChatTurnComplete, TurnId: m.turnID, Duration: m.duration(),
274
+ }}}
275
+ }
276
+
277
+ func (m *TurnMapper) duration() int64 {
278
+ if d := m.nowMs() - m.turnStartedAt; d > 0 {
279
+ return d
280
+ }
281
+ return 0
282
+ }
283
+
284
+ // isoTime is Date.toISOString.
285
+ func isoTime(ms int64) string { return time.UnixMilli(ms).UTC().Format("2006-01-02T15:04:05.000Z") }
286
+
287
+ // ── Streaming content ───────────────────────────────────────────────────
288
+
289
+ func (m *TurnMapper) onDelta(delta map[string]any) []ahptypes.StateAction {
290
+ typ, _ := delta["type"].(string)
291
+ switch typ {
292
+ case "text_delta":
293
+ return m.appendText(delta, ahptypes.ResponsePartKindMarkdown, false)
294
+ case "thinking_delta":
295
+ return m.appendText(delta, ahptypes.ResponsePartKindReasoning, false)
296
+ case "text_end":
297
+ return m.closeBlock(delta, ahptypes.ResponsePartKindMarkdown)
298
+ case "thinking_end":
299
+ return m.closeBlock(delta, ahptypes.ResponsePartKindReasoning)
300
+ case "toolcall_start":
301
+ return m.onToolCallStart(delta)
302
+ case "toolcall_delta":
303
+ return m.onToolCallDelta(delta)
304
+ case "toolcall_end":
305
+ return m.onToolCallReady(delta)
306
+ case "error":
307
+ // Recorded, not emitted: a mid-stream error may still be retried inside the same AHP turn,
308
+ // so only agent_settled decides the turn's outcome.
309
+ if dig(delta, "error", "stopReason") == "aborted" {
310
+ m.outcome = OutcomeCancelled
311
+ } else {
312
+ m.outcome = OutcomeError
313
+ }
314
+ m.errorMessage = nil
315
+ if s, ok := digStr(delta, "error", "errorMessage"); ok {
316
+ m.errorMessage = &s
317
+ }
318
+ return nil
319
+ default:
320
+ // `*_start` / `start` / `done` need no action: parts are created lazily by the first delta.
321
+ return nil
322
+ }
323
+ }
324
+
325
+ // closeBlock handles the end of a text or reasoning block. Normally a no-op, because the deltas
326
+ // already built the part. A provider that does not stream a block incrementally emits only
327
+ // `*_end` with the finished content; without this the content would be silently dropped.
328
+ func (m *TurnMapper) closeBlock(delta map[string]any, kind ahptypes.ResponsePartKind) []ahptypes.StateAction {
329
+ idx := intOr(delta["contentIndex"], 0)
330
+ if p := m.parts[idx]; p != nil && p.created {
331
+ return nil
332
+ }
333
+ content, _ := delta["content"].(string)
334
+ return m.appendTextValue(idx, content, kind)
335
+ }
336
+
337
+ func (m *TurnMapper) partID(idx int) string {
338
+ return m.turnID + ":" + strconv.Itoa(m.messageOrdinal) + ":" + strconv.Itoa(idx)
339
+ }
340
+
341
+ func (m *TurnMapper) appendText(delta map[string]any, kind ahptypes.ResponsePartKind, _ bool) []ahptypes.StateAction {
342
+ text, _ := delta["delta"].(string)
343
+ return m.appendTextValue(intOr(delta["contentIndex"], 0), text, kind)
344
+ }
345
+
346
+ // appendTextValue appends streamed text, creating the part on first use. The first chunk rides
347
+ // along on chat/responsePart rather than a separate chat/delta, which halves the actions for
348
+ // short responses without changing the reduced state.
349
+ func (m *TurnMapper) appendTextValue(idx int, text string, kind ahptypes.ResponsePartKind) []ahptypes.StateAction {
350
+ if len(text) == 0 {
351
+ return nil
352
+ }
353
+ out := m.setActivity(strPtr(RespondingActivity))
354
+ part := m.parts[idx]
355
+ if part == nil {
356
+ part = &openPart{partID: m.partID(idx), kind: kind}
357
+ m.parts[idx] = part
358
+ }
359
+ if !part.created {
360
+ part.created = true
361
+ var rp ahptypes.ResponsePart
362
+ if kind == ahptypes.ResponsePartKindMarkdown {
363
+ rp = ahptypes.ResponsePart{Value: &ahptypes.MarkdownResponsePart{Kind: kind, Id: part.partID, Content: text}}
364
+ } else {
365
+ rp = ahptypes.ResponsePart{Value: &ahptypes.ReasoningResponsePart{Kind: kind, Id: part.partID, Content: text}}
366
+ }
367
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatResponsePartAction{Type: ahptypes.ActionTypeChatResponsePart, TurnId: m.turnID, Part: rp}})
368
+ }
369
+ if kind == ahptypes.ResponsePartKindMarkdown {
370
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatDeltaAction{Type: ahptypes.ActionTypeChatDelta, TurnId: m.turnID, PartId: part.partID, Content: text}})
371
+ }
372
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatReasoningAction{Type: ahptypes.ActionTypeChatReasoning, TurnId: m.turnID, PartId: part.partID, Content: text}})
373
+ }
374
+
375
+ // ── Tool calls ──────────────────────────────────────────────────────────
376
+
377
+ func (m *TurnMapper) onToolCallStart(delta map[string]any) []ahptypes.StateAction {
378
+ idx := intOr(delta["contentIndex"], 0)
379
+ block := asMap(dig(delta, "partial", "content", idx))
380
+ id, _ := block["id"].(string)
381
+ if id == "" {
382
+ // Without an id there is nothing to correlate later; wait for the next delta, which
383
+ // carries the resolved block.
384
+ return nil
385
+ }
386
+ m.toolCallsByIdx[idx] = id
387
+ name := "tool"
388
+ if s, ok := block["name"].(string); ok {
389
+ name = s
390
+ }
391
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallStartAction{
392
+ Type: ahptypes.ActionTypeChatToolCallStart, TurnId: m.turnID, ToolCallId: id, ToolName: name, DisplayName: name,
393
+ }}}
394
+ }
395
+
396
+ func (m *TurnMapper) onToolCallDelta(delta map[string]any) []ahptypes.StateAction {
397
+ idx := intOr(delta["contentIndex"], 0)
398
+ content, _ := delta["delta"].(string)
399
+ deltaAction := func(id string) ahptypes.StateAction {
400
+ return ahptypes.StateAction{Value: &ahptypes.ChatToolCallDeltaAction{Type: ahptypes.ActionTypeChatToolCallDelta, TurnId: m.turnID, ToolCallId: id, Content: &content}}
401
+ }
402
+ if id, ok := m.toolCallsByIdx[idx]; ok && id != "" {
403
+ return []ahptypes.StateAction{deltaAction(id)}
404
+ }
405
+ // The id may only have materialised after toolcall_start; recover from the partial message
406
+ // rather than dropping the call.
407
+ started := m.onToolCallStart(delta)
408
+ id := m.toolCallsByIdx[idx]
409
+ if id == "" {
410
+ return nil
411
+ }
412
+ return append(started, deltaAction(id))
413
+ }
414
+
415
+ // onToolCallReady: parameters are complete. `confirmed: 'setting'` sends the call straight to
416
+ // running, skipping pending-confirmation. That is a first-class path in the protocol and the
417
+ // honest one here: pi has no permission system, so there is nothing for a client to approve.
418
+ // Because the call never enters pending-confirmation a conforming client renders no approve/deny
419
+ // UI; cancelling the whole turn stays available and maps to abort().
420
+ func (m *TurnMapper) onToolCallReady(delta map[string]any) []ahptypes.StateAction {
421
+ idx := intOr(delta["contentIndex"], 0)
422
+ toolCall := asMap(delta["toolCall"])
423
+ id, hasID := toolCall["id"].(string)
424
+ if !hasID {
425
+ id = m.toolCallsByIdx[idx]
426
+ }
427
+ if id == "" {
428
+ return nil
429
+ }
430
+ m.toolCallsByIdx[idx] = id
431
+ name := "tool"
432
+ if s, ok := toolCall["name"].(string); ok {
433
+ name = s
434
+ }
435
+ args := toolCall["arguments"]
436
+ cwd := m.opts.WorkingDirectory
437
+ description := DescribeToolCall(name, args, cwd)
438
+ m.liveToolCalls[id] = DescribeFinishedToolCall(name, args, cwd)
439
+ confirmed := ahptypes.ToolCallConfirmationReasonSetting
440
+ ready := &ahptypes.ChatToolCallReadyAction{
441
+ Type: ahptypes.ActionTypeChatToolCallReady, TurnId: m.turnID, ToolCallId: id,
442
+ // Rendered while the call runs, and the same phrasing the activity indicator uses, so the
443
+ // two do not describe one call differently.
444
+ InvocationMessage: ahptypes.NewStringOrMarkdownPlain(description),
445
+ Confirmed: &confirmed,
446
+ }
447
+ if input, ok := ToolInputFor(name, args); ok {
448
+ ready.ToolInput = &ahptypes.ToolInput{Inline: &input}
449
+ }
450
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: ready}}
451
+ }
452
+
453
+ func (m *TurnMapper) onToolUpdate(event Event) []ahptypes.StateAction {
454
+ id, _ := event["toolCallId"].(string)
455
+ if _, live := m.liveToolCalls[id]; !live {
456
+ return nil
457
+ }
458
+ content := toolResultContent(event["partialResult"])
459
+ if content == nil {
460
+ return nil
461
+ }
462
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallContentChangedAction{
463
+ Type: ahptypes.ActionTypeChatToolCallContentChanged, TurnId: m.turnID, ToolCallId: id, Content: content,
464
+ }}}
465
+ }
466
+
467
+ func textContent(text string) []ahptypes.ToolResultContent {
468
+ return []ahptypes.ToolResultContent{{Value: &ahptypes.ToolResultTextContent{Type: ahptypes.ToolResultContentTypeText, Text: text}}}
469
+ }
470
+
471
+ // toolResultContent flattens pi's tool result content blocks into the protocol's text blocks.
472
+ func toolResultContent(result any) []ahptypes.ToolResultContent {
473
+ blocks, ok := dig(result, "content").([]any)
474
+ if !ok {
475
+ return nil
476
+ }
477
+ var texts []string
478
+ for _, b := range blocks {
479
+ if bm := asMap(b); bm["type"] == "text" {
480
+ if s, ok := bm["text"].(string); ok && s != "" {
481
+ texts = append(texts, s)
482
+ }
483
+ }
484
+ }
485
+ if len(texts) == 0 {
486
+ return nil
487
+ }
488
+ return textContent(strings.Join(texts, "\n"))
489
+ }
490
+
491
+ // editPatch is the unified diff pi computes for an edit, if the result carries one. `edit` is the
492
+ // only built-in tool that reports structured details; its text says how many blocks were replaced
493
+ // (a count a client cannot render or check), and the patch beside it is the part worth showing.
494
+ func editPatch(result any) (string, bool) {
495
+ s, ok := digStr(result, "details", "patch")
496
+ return s, ok && s != ""
497
+ }
498
+
499
+ // textOf is the text a tool reported, as a client would render it.
500
+ func textOf(content []ahptypes.ToolResultContent) string {
501
+ var sb strings.Builder
502
+ for _, c := range content {
503
+ if t, ok := c.Value.(*ahptypes.ToolResultTextContent); ok {
504
+ sb.WriteString(t.Text)
505
+ }
506
+ }
507
+ return trimJS(sb.String())
508
+ }
509
+
510
+ func (m *TurnMapper) onToolEnd(event Event) []ahptypes.StateAction {
511
+ id, _ := event["toolCallId"].(string)
512
+ finished, live := m.liveToolCalls[id]
513
+ if !live {
514
+ return nil
515
+ }
516
+ delete(m.liveToolCalls, id)
517
+ toolName, _ := event["toolName"].(string)
518
+ success := event["isError"] != true
519
+ var content []ahptypes.ToolResultContent
520
+ if patch, ok := editPatch(event["result"]); ok {
521
+ content = textContent(patch)
522
+ } else {
523
+ content = toolResultContent(event["result"])
524
+ }
525
+ // A failed tool has already said why: an ENOENT naming the path it could not open, a non-zero
526
+ // exit with its stderr. That text belongs in error.message, where a client looks.
527
+ reported := textOf(content)
528
+ var past string
529
+ switch {
530
+ case success && live && finished != "":
531
+ past = finished
532
+ case success:
533
+ past = "Ran " + toolName
534
+ case finished != "":
535
+ past = finished + " failed"
536
+ default:
537
+ past = toolName + " failed"
538
+ }
539
+ result := ahptypes.ToolCallResult{Success: success, PastTenseMessage: ahptypes.NewStringOrMarkdownPlain(past), Content: content}
540
+ if !success {
541
+ message := reported
542
+ if message == "" {
543
+ message = toolName + " failed"
544
+ }
545
+ // ToolCallResult.error is its own shape (message + code), not ErrorInfo.
546
+ raw, _ := json.Marshal(map[string]string{"message": message})
547
+ rm := json.RawMessage(raw)
548
+ result.Error = &rm
549
+ }
550
+ return []ahptypes.StateAction{ahptypes.StateAction{Value: &ahptypes.ChatToolCallCompleteAction{
551
+ Type: ahptypes.ActionTypeChatToolCallComplete, TurnId: m.turnID, ToolCallId: id, Result: result,
552
+ }}}
553
+ }
554
+
555
+ // ── Turn termination ────────────────────────────────────────────────────
556
+
557
+ // Finish closes the turn. It is also called directly when a turn ends without agent_settled: an
558
+ // extension command that never reaches the model, or a failed prompt(). Without it the turn would
559
+ // stay active forever and the session would sit at InProgress. outcome "" lets the mapper decide;
560
+ // message "" means no explicit error message.
561
+ func (m *TurnMapper) Finish(outcome Outcome, message string) []ahptypes.StateAction {
562
+ if m.finished {
563
+ return nil
564
+ }
565
+ m.finished = true
566
+ // Nothing is happening once the turn is over; a stale description would leave the client
567
+ // showing "Reading ..." against an idle chat.
568
+ out := m.setActivity(nil)
569
+ resolved := outcome
570
+ if resolved == OutcomeDefault {
571
+ resolved = m.outcome
572
+ }
573
+ duration := m.duration()
574
+ switch resolved {
575
+ case OutcomeCancelled:
576
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatTurnCancelledAction{Type: ahptypes.ActionTypeChatTurnCancelled, TurnId: m.turnID, Duration: duration}})
577
+ case OutcomeError:
578
+ msg := "The agent run failed"
579
+ switch {
580
+ case message != "":
581
+ msg = message
582
+ case m.errorMessage != nil:
583
+ msg = *m.errorMessage
584
+ }
585
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatErrorAction{
586
+ Type: ahptypes.ActionTypeChatError, TurnId: m.turnID, Duration: duration,
587
+ Part: ahptypes.ErrorResponsePart{Kind: ahptypes.ResponsePartKindError, Error: ahptypes.ErrorInfo{ErrorType: "agentRunFailed", Message: msg}},
588
+ }})
589
+ default:
590
+ return append(out, ahptypes.StateAction{Value: &ahptypes.ChatTurnCompleteAction{Type: ahptypes.ActionTypeChatTurnComplete, TurnId: m.turnID, Duration: duration}})
591
+ }
592
+ }
593
+
594
+ // UserTurnStarted builds the chat/turnStarted action for a user message.
595
+ func UserTurnStarted(turnID, text, startedAt string) ahptypes.StateAction {
596
+ return ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
597
+ Type: ahptypes.ActionTypeChatTurnStarted, TurnId: turnID, StartedAt: startedAt,
598
+ Message: ahptypes.Message{Text: text, Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
599
+ }}
600
+ }