@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,305 @@
1
+ /**
2
+ * Rebuilds chat history from a pi session file.
3
+ *
4
+ * A client lists sessions and then subscribes to one. Everything the host has
5
+ * seen live is already in memory, but a session from the catalogue exists only
6
+ * on disk — so subscribing has to reconstruct its state, or the client opens an
7
+ * empty conversation.
8
+ *
9
+ * The window matches what pi itself renders. `buildContextEntries()` starts at
10
+ * the most recent compaction (`session-manager.ts` builds
11
+ * `[compaction, …kept entries, …later entries]`), which is exactly the transcript
12
+ * pi's own TUI shows on resume. Reconstructing more would show the user history
13
+ * their agent has already forgotten.
14
+ *
15
+ * This is a *second* mapper, distinct from the streaming one: it works from
16
+ * finished messages rather than deltas, so there is no partial state, no
17
+ * ordering to preserve, and every tool call already has its result.
18
+ */
19
+
20
+ import type { SessionEntry, SessionManager } from "@earendil-works/pi-coding-agent";
21
+ import {
22
+ type Message,
23
+ MessageKind,
24
+ type ResponsePart,
25
+ ResponsePartKind,
26
+ ToolCallStatus,
27
+ type ToolResultContent,
28
+ ToolResultContentType,
29
+ type Turn,
30
+ TurnState,
31
+ type UsageInfo,
32
+ } from "@microsoft/agent-host-protocol";
33
+ import { userMessageFromPiContent } from "./user-message.ts";
34
+
35
+ interface ContentBlock {
36
+ readonly type: string;
37
+ readonly text?: string;
38
+ readonly thinking?: string;
39
+ readonly id?: string;
40
+ readonly name?: string;
41
+ readonly arguments?: unknown;
42
+ }
43
+
44
+ interface StoredMessage {
45
+ readonly role?: string;
46
+ readonly content?: string | ContentBlock[];
47
+ readonly toolCallId?: string;
48
+ readonly toolName?: string;
49
+ readonly isError?: boolean;
50
+ readonly usage?: { input?: number; output?: number; cacheRead?: number };
51
+ readonly model?: string;
52
+ readonly command?: string;
53
+ readonly output?: string;
54
+ }
55
+
56
+ function blocksOf(message: StoredMessage): ContentBlock[] {
57
+ if (typeof message.content === "string") {
58
+ return [{ type: "text", text: message.content }];
59
+ }
60
+ return Array.isArray(message.content) ? message.content : [];
61
+ }
62
+
63
+ function plainText(message: StoredMessage): string {
64
+ return blocksOf(message)
65
+ .filter((block) => block.type === "text" && typeof block.text === "string")
66
+ .map((block) => block.text as string)
67
+ .join("");
68
+ }
69
+
70
+ function toUsage(message: StoredMessage): UsageInfo | undefined {
71
+ const usage = message.usage;
72
+ if (!usage) {
73
+ return undefined;
74
+ }
75
+ return {
76
+ ...(usage.input !== undefined ? { inputTokens: usage.input } : {}),
77
+ ...(usage.output !== undefined ? { outputTokens: usage.output } : {}),
78
+ ...(usage.cacheRead !== undefined ? { cacheReadTokens: usage.cacheRead } : {}),
79
+ ...(message.model ? { model: message.model } : {}),
80
+ };
81
+ }
82
+
83
+ function textContent(text: string): ToolResultContent[] {
84
+ return [{ type: ToolResultContentType.Text, text }];
85
+ }
86
+
87
+ /** A turn under construction, before its tool results have been matched up. */
88
+ interface PendingTurn {
89
+ readonly id: string;
90
+ readonly startedAt: string;
91
+ readonly message: Message;
92
+ readonly parts: ResponsePart[];
93
+ usage: UsageInfo | undefined;
94
+ }
95
+
96
+ export interface RebuildOptions {
97
+ /** Prefix for generated turn ids; only needs to be stable within a chat. */
98
+ readonly turnIdPrefix?: string;
99
+ }
100
+
101
+ export interface RebuiltHistory {
102
+ readonly turns: Turn[];
103
+ /**
104
+ * Turn id → the session entry that turn *ends* on.
105
+ *
106
+ * This is the anchor truncation needs. `chat/truncated` keeps turns up to
107
+ * and including the named one, and pi's `navigateTree` is inclusive for a
108
+ * non-user entry — so pointing it at a turn's last entry expresses exactly
109
+ * the protocol's meaning.
110
+ *
111
+ * The first user entry is recorded under the empty key: navigating to it
112
+ * lands the leaf on its parent, which is `null`, which is how "clear
113
+ * everything" is expressed.
114
+ */
115
+ readonly anchors: ReadonlyMap<string, string>;
116
+ }
117
+
118
+ /** Key under which the whole-conversation anchor is stored. */
119
+ export const CLEAR_ALL_ANCHOR = "";
120
+
121
+ /**
122
+ * Converts session entries into completed turns.
123
+ *
124
+ * Turn boundaries come from user messages: each one opens a turn that absorbs
125
+ * every assistant message and tool result until the next. That mirrors the live
126
+ * mapper, where a turn spans everything from one prompt to `agent_settled`.
127
+ */
128
+ function rebuildTurns(entries: readonly SessionEntry[], options: RebuildOptions = {}): Turn[] {
129
+ return rebuildHistory(entries, options).turns;
130
+ }
131
+
132
+ /** As {@link rebuildTurns}, but also reporting each turn's truncation anchor. */
133
+ export function rebuildHistory(entries: readonly SessionEntry[], options: RebuildOptions = {}): RebuiltHistory {
134
+ const prefix = options.turnIdPrefix ?? "turn";
135
+ const turns: Turn[] = [];
136
+ const anchors = new Map<string, string>();
137
+ /** The entry most recently folded into the turn being built. */
138
+ let lastEntryId: string | undefined;
139
+ let current: PendingTurn | undefined;
140
+ /** toolCallId → index in `current.parts`, so a result can find its call. */
141
+ let toolCallParts = new Map<string, number>();
142
+
143
+ const closeTurn = (): void => {
144
+ if (!current) {
145
+ return;
146
+ }
147
+ if (lastEntryId) {
148
+ anchors.set(current.id, lastEntryId);
149
+ }
150
+ turns.push({
151
+ id: current.id,
152
+ startedAt: current.startedAt,
153
+ message: current.message,
154
+ responseParts: current.parts,
155
+ usage: current.usage,
156
+ state: TurnState.Complete,
157
+ });
158
+ current = undefined;
159
+ toolCallParts = new Map();
160
+ };
161
+
162
+ const openTurn = (id: string, startedAt: string, message: Message): void => {
163
+ closeTurn();
164
+ current = { id, startedAt, message, parts: [], usage: undefined };
165
+ };
166
+
167
+ for (const entry of entries) {
168
+ if (entry.type === "message" && (entry.message as unknown as StoredMessage).role === "user") {
169
+ anchors.set(CLEAR_ALL_ANCHOR, anchors.get(CLEAR_ALL_ANCHOR) ?? entry.id);
170
+ }
171
+
172
+ if (entry.type === "compaction") {
173
+ // The summary replaces everything before it. Surfacing it as a turn
174
+ // of its own tells the user why the history starts where it does.
175
+ closeTurn();
176
+ turns.push({
177
+ id: `${prefix}-compaction-${entry.id}`,
178
+ startedAt: entry.timestamp,
179
+ message: { text: "", origin: { kind: MessageKind.User } },
180
+ responseParts: [
181
+ {
182
+ kind: ResponsePartKind.SystemNotification,
183
+ content: `Conversation compacted.\n\n${entry.summary}`,
184
+ },
185
+ ],
186
+ usage: undefined,
187
+ state: TurnState.Complete,
188
+ });
189
+ continue;
190
+ }
191
+
192
+ if (entry.type !== "message") {
193
+ // Model / thinking-level changes and labels carry no transcript.
194
+ continue;
195
+ }
196
+
197
+ const message = entry.message as unknown as StoredMessage;
198
+ switch (message.role) {
199
+ case "user": {
200
+ openTurn(`${prefix}-${entry.id}`, entry.timestamp, userMessageFromPiContent(message.content));
201
+ break;
202
+ }
203
+
204
+ case "assistant": {
205
+ if (!current) {
206
+ // An assistant message with no preceding user message (a
207
+ // truncated file, or history that starts mid-turn). Give it a
208
+ // turn so the content is not dropped.
209
+ openTurn(`${prefix}-${entry.id}`, entry.timestamp, userMessageFromPiContent(""));
210
+ }
211
+ const turn = current as PendingTurn;
212
+ let index = 0;
213
+ for (const block of blocksOf(message)) {
214
+ if (block.type === "text" && block.text) {
215
+ turn.parts.push({
216
+ kind: ResponsePartKind.Markdown,
217
+ id: `${turn.id}:${entry.id}:${index}`,
218
+ content: block.text,
219
+ });
220
+ } else if (block.type === "thinking" && block.thinking) {
221
+ turn.parts.push({
222
+ kind: ResponsePartKind.Reasoning,
223
+ id: `${turn.id}:${entry.id}:${index}`,
224
+ content: block.thinking,
225
+ });
226
+ } else if (block.type === "toolCall" && block.id) {
227
+ toolCallParts.set(block.id, turn.parts.length);
228
+ turn.parts.push({
229
+ kind: ResponsePartKind.ToolCall,
230
+ toolCall: {
231
+ status: ToolCallStatus.Running,
232
+ toolCallId: block.id,
233
+ toolName: block.name ?? "tool",
234
+ displayName: block.name ?? "tool",
235
+ invocationMessage: block.name ?? "tool",
236
+ toolInput: block.arguments === undefined ? undefined : JSON.stringify(block.arguments),
237
+ confirmed: "setting",
238
+ } as never,
239
+ });
240
+ }
241
+ index += 1;
242
+ }
243
+ turn.usage = toUsage(message) ?? turn.usage;
244
+ break;
245
+ }
246
+
247
+ case "toolResult": {
248
+ const partIndex = message.toolCallId ? toolCallParts.get(message.toolCallId) : undefined;
249
+ if (!current || partIndex === undefined) {
250
+ break;
251
+ }
252
+ const existing = current.parts[partIndex] as { toolCall: Record<string, unknown> } | undefined;
253
+ if (!existing) {
254
+ break;
255
+ }
256
+ const text = plainText(message);
257
+ const success = message.isError !== true;
258
+ current.parts[partIndex] = {
259
+ kind: ResponsePartKind.ToolCall,
260
+ toolCall: {
261
+ ...existing.toolCall,
262
+ status: ToolCallStatus.Completed,
263
+ success,
264
+ pastTenseMessage: success ? `Ran ${message.toolName ?? "tool"}` : `${message.toolName ?? "tool"} failed`,
265
+ ...(text ? { content: textContent(text) } : {}),
266
+ },
267
+ } as ResponsePart;
268
+ break;
269
+ }
270
+
271
+ case "bashExecution": {
272
+ // pi records `!command` runs as their own message role; they are
273
+ // part of the transcript even though no model produced them.
274
+ if (!current) {
275
+ openTurn(`${prefix}-${entry.id}`, entry.timestamp, userMessageFromPiContent(`!${message.command ?? ""}`));
276
+ }
277
+ (current as PendingTurn).parts.push({
278
+ kind: ResponsePartKind.SystemNotification,
279
+ content: `\`${message.command ?? ""}\`\n\n${message.output ?? ""}`,
280
+ });
281
+ break;
282
+ }
283
+
284
+ default:
285
+ break;
286
+ }
287
+
288
+ // Recorded after the entry has been folded in, so a user message that
289
+ // opens a new turn does not become the previous turn's anchor.
290
+ lastEntryId = entry.id;
291
+ }
292
+
293
+ closeTurn();
294
+ return { turns, anchors };
295
+ }
296
+
297
+ /** Rebuilds the turns of a session file, using the same window pi's TUI shows. */
298
+ export function rebuildTurnsFromSession(manager: SessionManager, options: RebuildOptions = {}): Turn[] {
299
+ return rebuildTurns(manager.buildContextEntries(), options);
300
+ }
301
+
302
+ /** As {@link rebuildTurnsFromSession}, with truncation anchors. */
303
+ export function rebuildHistoryFromSession(manager: SessionManager, options: RebuildOptions = {}): RebuiltHistory {
304
+ return rebuildHistory(manager.buildContextEntries(), options);
305
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Normalizes client-provided images before they enter a pi AgentSession.
3
+ *
4
+ * AgentSession accepts already-prepared ImageContent blocks; pi's CLI performs
5
+ * conversion and resizing one layer earlier. This host is another input layer,
6
+ * so it applies the same public image utilities and honours pi's auto-resize
7
+ * setting rather than relying on every AHP client to know provider limits.
8
+ */
9
+
10
+ import type { ImageContent } from "@earendil-works/pi-ai";
11
+ import { convertToPng, resizeImage } from "@earendil-works/pi-coding-agent";
12
+ import { normalizeImageMimeType } from "./image-mime.ts";
13
+
14
+ const SUPPORTED_INLINE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
15
+
16
+ async function prepareImage(image: ImageContent, autoResize: boolean, index: number): Promise<ImageContent> {
17
+ let data = image.data;
18
+ let mimeType = normalizeImageMimeType(image.mimeType);
19
+ if (!mimeType) {
20
+ throw new Error(`Image ${index + 1} has an invalid MIME type: ${image.mimeType}`);
21
+ }
22
+
23
+ if (!SUPPORTED_INLINE_TYPES.has(mimeType)) {
24
+ const converted = await convertToPng(data, mimeType);
25
+ if (!converted) {
26
+ throw new Error(`Image ${index + 1} could not be converted from ${mimeType}`);
27
+ }
28
+ data = converted.data;
29
+ mimeType = converted.mimeType;
30
+ }
31
+
32
+ if (!autoResize) {
33
+ return { type: "image", data, mimeType };
34
+ }
35
+
36
+ const resized = await resizeImage(Buffer.from(data, "base64"), mimeType);
37
+ if (!resized) {
38
+ throw new Error(`Image ${index + 1} could not be prepared for inline model input`);
39
+ }
40
+ return { type: "image", data: resized.data, mimeType: resized.mimeType };
41
+ }
42
+
43
+ /** Processes sequentially so several large images do not start several WASM workers at once. */
44
+ export async function prepareImagesForPi(
45
+ images: readonly ImageContent[] | undefined,
46
+ autoResize: boolean,
47
+ ): Promise<ImageContent[] | undefined> {
48
+ if (!images || images.length === 0) {
49
+ return undefined;
50
+ }
51
+ const prepared: ImageContent[] = [];
52
+ for (const [index, image] of images.entries()) {
53
+ prepared.push(await prepareImage(image, autoResize, index));
54
+ }
55
+ return prepared;
56
+ }
@@ -0,0 +1,8 @@
1
+ /** Normalizes MIME values at the AHP/pi image boundary. */
2
+ export function normalizeImageMimeType(mimeType: string): string | undefined {
3
+ const base = mimeType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
4
+ if (!base.startsWith("image/")) {
5
+ return undefined;
6
+ }
7
+ return base === "image/jpg" ? "image/jpeg" : base;
8
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The in-process pi backend.
3
+ *
4
+ * Embeds `AgentSession` directly rather than spawning `pi --mode rpc`, reusing
5
+ * pi's SDK services and configuration. A subprocess would add process lifecycle
6
+ * and stdio backpressure without isolating provider credentials or user
7
+ * resources. The narrow {@link PiBackend} seam keeps those concerns out of the
8
+ * mapper and chat driver.
9
+ *
10
+ * @see ../pi/chat-driver.ts for how a backend is driven
11
+ */
12
+
13
+ import type { ImageContent } from "@earendil-works/pi-ai";
14
+ import {
15
+ type AgentSession,
16
+ type AgentSessionEvent,
17
+ createAgentSessionFromServices,
18
+ createAgentSessionServices,
19
+ type SessionManager,
20
+ SettingsManager,
21
+ } from "@earendil-works/pi-coding-agent";
22
+ import type { ModelSelection } from "@microsoft/agent-host-protocol";
23
+ import type { PiBackend } from "./chat-driver.ts";
24
+ import { prepareImagesForPi } from "./image-input.ts";
25
+ import { findModelBySelectionId, modelSelectionId, THINKING_CONFIG_KEY } from "./models.ts";
26
+ import { type ProjectTrustPolicy, resolveProjectTrust, type TrustDecision } from "./project-trust.ts";
27
+
28
+ export interface InProcessBackendOptions {
29
+ readonly cwd: string;
30
+ /** Reuses the session manager the host already allocated, so ids line up. */
31
+ readonly sessionManager: SessionManager;
32
+ /** How to treat project-local pi resources. Defaults to `trust`. */
33
+ readonly projectTrustPolicy?: ProjectTrustPolicy;
34
+ }
35
+
36
+ /**
37
+ * Wraps an `AgentSession` in the narrow surface the driver needs.
38
+ *
39
+ * `AgentSession.prompt()` spans the full run. The driver tracks that promise
40
+ * without blocking action routing, while session events stream protocol state
41
+ * and `abort()` remains available for cancellation.
42
+ */
43
+ export class InProcessPiBackend implements PiBackend {
44
+ readonly #session: AgentSession;
45
+ readonly #trust: TrustDecision;
46
+
47
+ private constructor(session: AgentSession, trust: TrustDecision) {
48
+ this.#session = session;
49
+ this.#trust = trust;
50
+ }
51
+
52
+ /** How project trust was resolved, so the host can report it to clients. */
53
+ get projectTrust(): TrustDecision {
54
+ return this.#trust;
55
+ }
56
+
57
+ static async create(options: InProcessBackendOptions): Promise<InProcessPiBackend> {
58
+ // SDK session construction defaults projectTrusted to true and does not run
59
+ // the CLI's trust prompt, so pass this host's decision explicitly.
60
+ const trust = resolveProjectTrust(options.cwd, options.projectTrustPolicy);
61
+ const services = await createAgentSessionServices({
62
+ cwd: options.cwd,
63
+ settingsManager: SettingsManager.create(options.cwd, undefined, { projectTrusted: trust.trusted }),
64
+ });
65
+ const { session } = await createAgentSessionFromServices({ services, sessionManager: options.sessionManager });
66
+ return new InProcessPiBackend(session, trust);
67
+ }
68
+
69
+ get session(): AgentSession {
70
+ return this.#session;
71
+ }
72
+
73
+ subscribe(listener: (event: AgentSessionEvent) => void): () => void {
74
+ return this.#session.subscribe(listener);
75
+ }
76
+
77
+ async prompt(text: string, images?: ImageContent[], signal?: AbortSignal): Promise<void> {
78
+ if (signal?.aborted) {
79
+ return;
80
+ }
81
+ const prepared = await prepareImagesForPi(images, this.#session.settingsManager.getImageAutoResize());
82
+ if (signal?.aborted) {
83
+ return;
84
+ }
85
+ await this.#session.prompt(text, prepared ? { images: prepared } : undefined);
86
+ }
87
+
88
+ async steer(text: string, images?: ImageContent[], signal?: AbortSignal): Promise<void> {
89
+ if (signal?.aborted) {
90
+ return;
91
+ }
92
+ const prepared = await prepareImagesForPi(images, this.#session.settingsManager.getImageAutoResize());
93
+ if (signal?.aborted) {
94
+ return;
95
+ }
96
+ await this.#session.steer(text, prepared);
97
+ }
98
+
99
+ async abort(): Promise<void> {
100
+ await this.#session.abort();
101
+ }
102
+
103
+ /**
104
+ * Applies a per-message model selection.
105
+ *
106
+ * The reasoning effort rides in `config` under the key the model's own
107
+ * `configSchema` advertises, so a client that rendered that schema sends
108
+ * back exactly what it was offered.
109
+ */
110
+ async selectModel(selection: ModelSelection): Promise<void> {
111
+ const available = await this.#session.modelRuntime.getAvailable();
112
+ const current = this.#session.model;
113
+ const model = findModelBySelectionId(available, selection.id, current);
114
+ if (!model) {
115
+ // A stale pick from a client whose model list predates a credential
116
+ // change. Running on the current model beats failing the turn.
117
+ return;
118
+ }
119
+ if (!current || current.id !== model.id || current.provider !== model.provider) {
120
+ await this.#session.setModel(model as never);
121
+ }
122
+
123
+ const level = selection.config?.[THINKING_CONFIG_KEY];
124
+ if (typeof level === "string" && this.#session.getAvailableThinkingLevels().includes(level as never)) {
125
+ this.#session.setThinkingLevel(level as never);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Moves the leaf back to `entryId`, dropping everything after it.
131
+ *
132
+ * pi's sessions are append-only trees: nothing is deleted, the leaf simply
133
+ * moves and later appends form a new branch. `buildSessionContext` follows
134
+ * leaf→root, so the abandoned path stops being context — which is exactly
135
+ * what the protocol means by truncation. `navigateTree` also resyncs the
136
+ * agent's in-memory messages, so no reload is needed.
137
+ */
138
+ async truncate(entryId: string): Promise<boolean> {
139
+ const result = await this.#session.navigateTree(entryId);
140
+ return !result.cancelled;
141
+ }
142
+
143
+ /** What a new message would run on right now. */
144
+ currentSelection(): ModelSelection | undefined {
145
+ const model = this.#session.model;
146
+ if (!model) {
147
+ return undefined;
148
+ }
149
+ return { id: modelSelectionId(model), config: { [THINKING_CONFIG_KEY]: this.#session.thinkingLevel } };
150
+ }
151
+
152
+ dispose(): void {
153
+ this.#session.dispose();
154
+ }
155
+ }