@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,346 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { userInfo } from "node:os";
3
+ import { basename } from "node:path";
4
+ import {
5
+ ActionType,
6
+ AhpErrorCodes,
7
+ type CreateTerminalParams,
8
+ type StateAction,
9
+ TerminalClaimKind,
10
+ TerminalLifecycleStatus,
11
+ type TerminalState,
12
+ type URI,
13
+ } from "@microsoft/agent-host-protocol";
14
+ import { spawn as spawnPty } from "node-pty";
15
+ import { terminalInfo } from "../channels/terminal.ts";
16
+ import { channelKind, ROOT_CHANNEL, sessionIdFromUri } from "../core/channels.ts";
17
+ import type { AhpHost, TerminalHandler } from "../core/host.ts";
18
+ import { fileUriToPath, pathToFileUri } from "../core/uri.ts";
19
+ import { PI_PROVIDER } from "../pi/provider.ts";
20
+ import { ProtocolError } from "../protocol/errors.ts";
21
+
22
+ interface Disposable {
23
+ dispose(): void;
24
+ }
25
+
26
+ interface PtyProcess {
27
+ onData(listener: (data: string) => void): Disposable;
28
+ onExit(listener: (event: { exitCode: number }) => void): Disposable;
29
+ write(data: string): void;
30
+ resize(cols: number, rows: number): void;
31
+ kill(): void;
32
+ }
33
+
34
+ type PtySpawner = (
35
+ file: string,
36
+ args: string[],
37
+ options: { name: string; cols: number; rows: number; cwd: string },
38
+ ) => PtyProcess;
39
+
40
+ export interface TerminalServiceOptions {
41
+ readonly defaultWorkingDirectory: string;
42
+ readonly shell?: string;
43
+ readonly log?: (message: string) => void;
44
+ }
45
+
46
+ interface TerminalEntry {
47
+ readonly pty: PtyProcess;
48
+ dataListener: Disposable | undefined;
49
+ exitListener: Disposable | undefined;
50
+ pendingData: string;
51
+ flushTimer: NodeJS.Timeout | undefined;
52
+ }
53
+
54
+ const DEFAULT_COLS = 80;
55
+ const DEFAULT_ROWS = 24;
56
+ const MAX_DIMENSION = 65_535;
57
+ const OUTPUT_BATCH_DELAY_MS = 8;
58
+ const OUTPUT_BATCH_THRESHOLD_CHARS = 16 * 1024;
59
+
60
+ function validDimension(value: number): boolean {
61
+ return Number.isSafeInteger(value) && value > 0 && value <= MAX_DIMENSION;
62
+ }
63
+
64
+ function dimension(value: number | undefined, fallback: number, name: string): number {
65
+ const result = value ?? fallback;
66
+ if (!validDimension(result)) {
67
+ throw ProtocolError.invalidParams(`${name} must be an integer between 1 and ${MAX_DIMENSION}`);
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function defaultShell(): string {
73
+ try {
74
+ return userInfo().shell || process.env.SHELL || "sh";
75
+ } catch {
76
+ return process.env.SHELL || "sh";
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Bridges terminal actions to independent pseudoterminal processes. `StateStore`
82
+ * owns every protocol-visible `TerminalState`; the runtime map holds only PTY
83
+ * handles and output awaiting publication. Terminals are currently client-owned,
84
+ * so claim transfer is rejected rather than represented in a second state store.
85
+ */
86
+ export class TerminalService implements TerminalHandler {
87
+ readonly #host: AhpHost;
88
+ readonly #options: TerminalServiceOptions;
89
+ readonly #spawn: PtySpawner;
90
+ readonly #entries = new Map<URI, TerminalEntry>();
91
+ readonly #unhookAction: () => void;
92
+ readonly #unhookValidator: () => void;
93
+ #closed = false;
94
+
95
+ constructor(host: AhpHost, options: TerminalServiceOptions, spawn: PtySpawner = spawnPty) {
96
+ this.#host = host;
97
+ this.#options = options;
98
+ this.#spawn = spawn;
99
+ this.#unhookValidator = host.addClientActionValidator((channel, action, clientId) =>
100
+ this.#validateClientAction(channel, action, clientId),
101
+ );
102
+ this.#unhookAction = host.onClientAction((channel, action) => this.#applyClientAction(channel, action));
103
+ }
104
+
105
+ async create(params: CreateTerminalParams, clientId: string): Promise<void> {
106
+ if (this.#closed) {
107
+ throw new Error("Terminal service is closed");
108
+ }
109
+ const channel = params?.channel;
110
+ if (typeof channel !== "string" || !/^[a-zA-Z][a-zA-Z0-9+.-]*:\/[^/?#]+$/.test(channel)) {
111
+ throw ProtocolError.invalidParams("createTerminal requires a URI channel");
112
+ }
113
+ const inferredKind = channelKind(channel);
114
+ if (inferredKind !== undefined && inferredKind !== "terminal") {
115
+ throw ProtocolError.invalidParams(`Channel belongs to ${inferredKind}, not terminal: ${channel}`);
116
+ }
117
+ if (sessionIdFromUri(channel, [PI_PROVIDER])) {
118
+ throw ProtocolError.invalidParams(`Channel uses the ${PI_PROVIDER} session scheme: ${channel}`);
119
+ }
120
+ this.#assertAvailable(channel);
121
+ if (params.claim?.kind !== TerminalClaimKind.Client) {
122
+ throw ProtocolError.invalidParams("Only client-owned terminals are supported");
123
+ }
124
+ if (!clientId || params.claim.clientId !== clientId) {
125
+ throw ProtocolError.invalidParams("Terminal claim must match the initialized client");
126
+ }
127
+ if (params.name !== undefined && typeof params.name !== "string") {
128
+ throw ProtocolError.invalidParams("Terminal name must be a string");
129
+ }
130
+
131
+ const cols = dimension(params.cols, DEFAULT_COLS, "cols");
132
+ const rows = dimension(params.rows, DEFAULT_ROWS, "rows");
133
+ const cwdUri = params.cwd ?? pathToFileUri(this.#options.defaultWorkingDirectory);
134
+ if (typeof cwdUri !== "string" || !cwdUri.startsWith("file://")) {
135
+ throw ProtocolError.invalidParams("Terminal cwd must be a file: URI");
136
+ }
137
+ let cwd: string;
138
+ try {
139
+ cwd = fileUriToPath(cwdUri);
140
+ } catch {
141
+ throw ProtocolError.invalidParams(`Invalid terminal cwd: ${cwdUri}`);
142
+ }
143
+ let cwdStat: Awaited<ReturnType<typeof stat>>;
144
+ try {
145
+ cwdStat = await stat(cwd);
146
+ } catch {
147
+ throw ProtocolError.notFound(cwdUri);
148
+ }
149
+ if (!cwdStat.isDirectory()) {
150
+ throw ProtocolError.invalidParams(`Terminal cwd is not a directory: ${cwdUri}`);
151
+ }
152
+ // `stat` yielded to other requests; close the duplicate-create race before spawning.
153
+ this.#assertAvailable(channel);
154
+
155
+ const shell = this.#options.shell ?? defaultShell();
156
+ const title = params.name ?? basename(shell);
157
+ const pty = this.#spawn(shell, [], {
158
+ name: "xterm-256color",
159
+ cols,
160
+ rows,
161
+ cwd,
162
+ });
163
+ const state: TerminalState = {
164
+ title,
165
+ cwd: pathToFileUri(cwd),
166
+ cols,
167
+ rows,
168
+ content: [],
169
+ lifecycle: { status: TerminalLifecycleStatus.Running },
170
+ claim: { kind: TerminalClaimKind.Client, clientId },
171
+ isPty: true,
172
+ };
173
+ this.#host.store.create(channel, state, "terminal");
174
+
175
+ const entry: TerminalEntry = {
176
+ pty,
177
+ dataListener: undefined,
178
+ exitListener: undefined,
179
+ pendingData: "",
180
+ flushTimer: undefined,
181
+ };
182
+ this.#entries.set(channel, entry);
183
+ entry.dataListener = pty.onData((data) => this.#onData(channel, entry, data));
184
+ entry.exitListener = pty.onExit((event) => this.#onExit(channel, entry, event.exitCode));
185
+ this.#publishCatalogue();
186
+ }
187
+
188
+ dispose(channel: URI): void {
189
+ const entry = this.#entries.get(channel);
190
+ if (entry) this.#remove(channel, entry, true);
191
+ }
192
+
193
+ /** Kills every PTY and unregisters side-effect hooks. Safe to call more than once. */
194
+ shutdown(): void {
195
+ if (this.#closed) return;
196
+ this.#closed = true;
197
+ this.#unhookValidator();
198
+ this.#unhookAction();
199
+ const hadEntries = this.#entries.size > 0;
200
+ for (const [channel, entry] of this.#entries) {
201
+ this.#remove(channel, entry, false);
202
+ }
203
+ if (hadEntries) this.#publishCatalogue();
204
+ }
205
+
206
+ #assertAvailable(channel: URI): void {
207
+ if (this.#entries.has(channel) || this.#host.store.has(channel)) {
208
+ throw new ProtocolError(AhpErrorCodes.AlreadyExists, `Terminal already exists: ${channel}`);
209
+ }
210
+ }
211
+
212
+ #state(channel: URI): TerminalState | undefined {
213
+ return this.#host.store.kindOf(channel) === "terminal"
214
+ ? (this.#host.store.get(channel) as TerminalState)
215
+ : undefined;
216
+ }
217
+
218
+ #validateClientAction(channel: URI, action: StateAction, clientId: string): string | undefined {
219
+ if (this.#host.store.kindOf(channel) !== "terminal") return undefined;
220
+ const state = this.#state(channel);
221
+ if (!state || !this.#entries.has(channel)) return "Terminal process is unavailable";
222
+
223
+ switch (action.type) {
224
+ case ActionType.TerminalClaimed:
225
+ return "This host does not support transferring terminal claims";
226
+ case ActionType.TerminalInput:
227
+ if (typeof action.data !== "string") return "Terminal input must be a string";
228
+ break;
229
+ case ActionType.TerminalResized:
230
+ if (!validDimension(action.cols) || !validDimension(action.rows)) {
231
+ return `Terminal dimensions must be integers between 1 and ${MAX_DIMENSION}`;
232
+ }
233
+ break;
234
+ case ActionType.TerminalTitleChanged:
235
+ if (typeof action.title !== "string") return "Terminal title must be a string";
236
+ break;
237
+ case ActionType.TerminalCleared:
238
+ break;
239
+ default:
240
+ return `This host does not accept ${action.type} on terminal channels`;
241
+ }
242
+
243
+ if (state.claim.kind !== TerminalClaimKind.Client || state.claim.clientId !== clientId) {
244
+ return "This terminal is claimed by another client";
245
+ }
246
+ if (
247
+ (action.type === ActionType.TerminalInput || action.type === ActionType.TerminalResized) &&
248
+ state.lifecycle.status !== TerminalLifecycleStatus.Running
249
+ ) {
250
+ return "Terminal process has exited";
251
+ }
252
+ return undefined;
253
+ }
254
+
255
+ #applyClientAction(channel: URI, action: StateAction): void {
256
+ const entry = this.#entries.get(channel);
257
+ if (!entry) return;
258
+ switch (action.type) {
259
+ case ActionType.TerminalInput:
260
+ entry.pty.write(action.data);
261
+ break;
262
+ case ActionType.TerminalResized:
263
+ entry.pty.resize(action.cols, action.rows);
264
+ break;
265
+ case ActionType.TerminalTitleChanged:
266
+ this.#publishCatalogue();
267
+ break;
268
+ case ActionType.TerminalCleared:
269
+ // The reducer already cleared retained output; discard older data that
270
+ // arrived before the action but was still waiting in the output batch.
271
+ this.#discardPendingData(entry);
272
+ break;
273
+ }
274
+ }
275
+
276
+ #onData(channel: URI, entry: TerminalEntry, data: string): void {
277
+ if (this.#entries.get(channel) !== entry || data.length === 0) return;
278
+ entry.pendingData += data;
279
+ if (entry.pendingData.length >= OUTPUT_BATCH_THRESHOLD_CHARS) {
280
+ this.#flushPendingData(channel, entry);
281
+ return;
282
+ }
283
+ if (!entry.flushTimer) {
284
+ entry.flushTimer = setTimeout(() => this.#flushPendingData(channel, entry), OUTPUT_BATCH_DELAY_MS);
285
+ entry.flushTimer.unref?.();
286
+ }
287
+ }
288
+
289
+ #flushPendingData(channel: URI, entry: TerminalEntry): void {
290
+ if (entry.flushTimer) clearTimeout(entry.flushTimer);
291
+ entry.flushTimer = undefined;
292
+ const data = entry.pendingData;
293
+ entry.pendingData = "";
294
+ if (this.#entries.get(channel) !== entry || data.length === 0) return;
295
+ this.#host.dispatchServerAction(channel, { type: ActionType.TerminalData, data });
296
+ }
297
+
298
+ #discardPendingData(entry: TerminalEntry): void {
299
+ if (entry.flushTimer) clearTimeout(entry.flushTimer);
300
+ entry.flushTimer = undefined;
301
+ entry.pendingData = "";
302
+ }
303
+
304
+ #onExit(channel: URI, entry: TerminalEntry, exitCode: number): void {
305
+ if (this.#entries.get(channel) !== entry) return;
306
+ const state = this.#state(channel);
307
+ if (!state || state.lifecycle.status === TerminalLifecycleStatus.Exited) return;
308
+ this.#flushPendingData(channel, entry);
309
+ entry.dataListener?.dispose();
310
+ entry.exitListener?.dispose();
311
+ entry.dataListener = undefined;
312
+ entry.exitListener = undefined;
313
+ this.#host.dispatchServerAction(channel, { type: ActionType.TerminalExited, exitCode });
314
+ this.#publishCatalogue();
315
+ }
316
+
317
+ #remove(channel: URI, entry: TerminalEntry, notify: boolean): void {
318
+ if (this.#entries.get(channel) !== entry) return;
319
+ if (notify) this.#flushPendingData(channel, entry);
320
+ else this.#discardPendingData(entry);
321
+ const state = this.#state(channel);
322
+ this.#entries.delete(channel);
323
+ entry.dataListener?.dispose();
324
+ entry.exitListener?.dispose();
325
+ if (state?.lifecycle.status === TerminalLifecycleStatus.Running) {
326
+ if (notify) {
327
+ this.#host.dispatchServerAction(channel, { type: ActionType.TerminalExited });
328
+ }
329
+ try {
330
+ entry.pty.kill();
331
+ } catch (error) {
332
+ this.#options.log?.(`cannot kill terminal ${channel}: ${String(error)}`);
333
+ }
334
+ }
335
+ this.#host.store.delete(channel);
336
+ if (notify) this.#publishCatalogue();
337
+ }
338
+
339
+ #publishCatalogue(): void {
340
+ const terminals = [...this.#entries.keys()].sort().flatMap((resource) => {
341
+ const state = this.#state(resource);
342
+ return state ? [terminalInfo(resource, state)] : [];
343
+ });
344
+ this.#host.dispatchServerAction(ROOT_CHANNEL, { type: ActionType.RootTerminalsChanged, terminals });
345
+ }
346
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Human-readable descriptions of what the agent is doing right now.
3
+ *
4
+ * `SessionState.activity` / `ChatState.activity` are the protocol's slot for
5
+ * the line a client shows next to its spinner. Without them a long turn is an
6
+ * opaque wait — pi's event stream knows it is reading `src/main.ts`, but the
7
+ * client only sees "working".
8
+ *
9
+ * These are display strings, not a machine contract: a client renders them
10
+ * verbatim, so they should read like something a person would say.
11
+ */
12
+
13
+ /** Longest a rendered command or path may be before it is shortened. */
14
+ const MAX_LENGTH = 60;
15
+
16
+ /**
17
+ * Shortens the middle rather than the end.
18
+ *
19
+ * The informative parts of both a path and a shell command sit at the two ends
20
+ * — the file name, the flags — so trimming the tail loses the most useful half.
21
+ */
22
+ function shorten(value: string, limit = MAX_LENGTH): string {
23
+ const collapsed = value.replace(/\s+/gu, " ").trim();
24
+ if (collapsed.length <= limit) {
25
+ return collapsed;
26
+ }
27
+ const head = Math.ceil((limit - 1) / 2);
28
+ const tail = Math.floor((limit - 1) / 2);
29
+ return `${collapsed.slice(0, head)}…${collapsed.slice(collapsed.length - tail)}`;
30
+ }
31
+
32
+ /** Renders a path relative to the working directory when it sits underneath it. */
33
+ function displayPath(value: unknown, workingDirectory?: string): string | undefined {
34
+ if (typeof value !== "string" || value.length === 0) {
35
+ return undefined;
36
+ }
37
+ const relative =
38
+ workingDirectory && value.startsWith(`${workingDirectory}/`) ? value.slice(workingDirectory.length + 1) : value;
39
+ return shorten(relative);
40
+ }
41
+
42
+ /**
43
+ * Describes a tool invocation.
44
+ *
45
+ * pi's built-in tools are named individually so the common cases read well;
46
+ * anything else — an extension tool, a custom tool — falls back to its own
47
+ * name, which is still better than nothing.
48
+ */
49
+ export function describeToolCall(toolName: string, args: unknown, workingDirectory?: string): string {
50
+ const input = (args ?? {}) as Record<string, unknown>;
51
+ const path = displayPath(input.path, workingDirectory);
52
+
53
+ switch (toolName) {
54
+ case "read":
55
+ return path ? `Reading ${path}` : "Reading a file";
56
+ case "write":
57
+ return path ? `Writing ${path}` : "Writing a file";
58
+ case "edit":
59
+ return path ? `Editing ${path}` : "Editing a file";
60
+ case "bash": {
61
+ const command = typeof input.command === "string" ? shorten(input.command) : undefined;
62
+ return command ? `Running ${command}` : "Running a command";
63
+ }
64
+ case "ls":
65
+ return path ? `Listing ${path}` : "Listing files";
66
+ case "find": {
67
+ const pattern = typeof input.pattern === "string" ? shorten(input.pattern, 30) : undefined;
68
+ return pattern ? `Finding ${pattern}` : "Finding files";
69
+ }
70
+ case "grep": {
71
+ const pattern = typeof input.pattern === "string" ? shorten(input.pattern, 30) : undefined;
72
+ return pattern ? `Searching for ${pattern}` : "Searching";
73
+ }
74
+ default:
75
+ return `Running ${shorten(toolName, 40)}`;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The one argument that says what a tool call is about.
81
+ *
82
+ * `toolInput` is rendered for the call itself, and the protocol carries no
83
+ * structured parameters next to it, so serialising the whole argument object
84
+ * spends the field on quoting and braces — a shell command comes out as
85
+ * `{"command":"ls -la","timeout":5}` rather than something a reader can run.
86
+ * Anything without an obvious subject keeps the full arguments, which is still
87
+ * the most informative thing available for it.
88
+ */
89
+ export function toolInputFor(toolName: string, args: unknown): string | undefined {
90
+ const input = (args ?? {}) as Record<string, unknown>;
91
+ switch (toolName) {
92
+ case "bash":
93
+ return stringArg(input.command) ?? fullArguments(args);
94
+ case "grep":
95
+ case "find":
96
+ return searchInput(input) ?? fullArguments(args);
97
+ case "read":
98
+ case "write":
99
+ case "edit":
100
+ case "ls":
101
+ return stringArg(input.path) ?? fullArguments(args);
102
+ default:
103
+ return fullArguments(args);
104
+ }
105
+ }
106
+
107
+ function stringArg(value: unknown): string | undefined {
108
+ return typeof value === "string" && value.length > 0 ? value : undefined;
109
+ }
110
+
111
+ function fullArguments(args: unknown): string | undefined {
112
+ return args === undefined ? undefined : JSON.stringify(args, null, 2);
113
+ }
114
+
115
+ /**
116
+ * A search rendered as its pattern plus whatever narrows it.
117
+ *
118
+ * The pattern alone loses the difference between searching one directory and
119
+ * searching the tree. Models fill the defaults in explicitly — a capture has
120
+ * `path: "."`, a glob matching everything, `ignoreCase: false`, `limit: 100`
121
+ * — so appending everything present would bury the pattern in restated
122
+ * defaults. Only arguments that actually narrow the search are shown, in the
123
+ * order `rg` writes them, which is also how they read aloud.
124
+ */
125
+ function searchInput(input: Record<string, unknown>): string | undefined {
126
+ const pattern = stringArg(input.pattern);
127
+ if (!pattern) {
128
+ return undefined;
129
+ }
130
+ const parts = [pattern];
131
+ const glob = stringArg(input.glob);
132
+ if (glob && glob !== "**/*") {
133
+ parts.push(`--glob ${glob}`);
134
+ }
135
+ if (input.ignoreCase === true) {
136
+ parts.push("--ignore-case");
137
+ }
138
+ const path = stringArg(input.path);
139
+ if (path && path !== ".") {
140
+ parts.push(`in ${path}`);
141
+ }
142
+ return parts.join(" ");
143
+ }
144
+
145
+ /**
146
+ * The same description, phrased for a call that has finished.
147
+ *
148
+ * The two sit next to each other in a transcript — one line for a call in
149
+ * flight, one for a call that completed — so reusing the present-tense text
150
+ * leaves a finished call still claiming to be running. Only the leading verb
151
+ * changes, which keeps the subject identical between them.
152
+ */
153
+ const PAST_TENSE_VERBS: Record<string, string> = {
154
+ Reading: "Read",
155
+ Writing: "Wrote",
156
+ Editing: "Edited",
157
+ Running: "Ran",
158
+ Listing: "Listed",
159
+ Finding: "Found",
160
+ Searching: "Searched",
161
+ };
162
+
163
+ export function describeFinishedToolCall(toolName: string, args: unknown, workingDirectory?: string): string {
164
+ const description = describeToolCall(toolName, args, workingDirectory);
165
+ const [verb, ...rest] = description.split(" ");
166
+ const past = verb ? PAST_TENSE_VERBS[verb] : undefined;
167
+ return past ? [past, ...rest].join(" ") : description;
168
+ }
169
+
170
+ export const THINKING_ACTIVITY = "Thinking";
171
+ export const RESPONDING_ACTIVITY = "Responding";