@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,171 @@
1
+ /**
2
+ * Activity descriptions.
3
+ *
4
+ * `SessionState.activity` / `ChatState.activity` are what a client shows next
5
+ * to its spinner. Without them a long turn is an opaque wait even though pi's
6
+ * event stream knows exactly which file is being read.
7
+ */
8
+
9
+ import assert from "node:assert/strict";
10
+ import { describe, it } from "node:test";
11
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
12
+ import { ActionType, type ChatState, chatReducer, type StateAction } from "@microsoft/agent-host-protocol";
13
+ import { initialChatState } from "../src/channels/chat.ts";
14
+ import { describeToolCall, RESPONDING_ACTIVITY, THINKING_ACTIVITY, toolInputFor } from "../src/pi/activity.ts";
15
+ import { TurnMapper, userTurnStarted } from "../src/pi/event-mapper.ts";
16
+ import { assertValid } from "./support/schema.ts";
17
+
18
+ const event = (value: object): AgentSessionEvent => value as unknown as AgentSessionEvent;
19
+
20
+ describe("activity descriptions", () => {
21
+ it("names the file a read touches", () => {
22
+ assert.equal(describeToolCall("read", { path: "/ws/src/main.ts" }, "/ws"), "Reading src/main.ts");
23
+ });
24
+
25
+ it("renders paths relative to the workspace", () => {
26
+ // An absolute path is mostly noise the user already knows.
27
+ assert.equal(describeToolCall("edit", { path: "/ws/a/b.ts" }, "/ws"), "Editing a/b.ts");
28
+ assert.equal(describeToolCall("edit", { path: "/elsewhere/b.ts" }, "/ws"), "Editing /elsewhere/b.ts");
29
+ });
30
+
31
+ it("shows the command a bash call runs", () => {
32
+ assert.equal(describeToolCall("bash", { command: "npm test" }), "Running npm test");
33
+ });
34
+
35
+ it("shortens the middle, keeping both informative ends", () => {
36
+ const long = describeToolCall("bash", { command: `git log ${"x".repeat(200)} --oneline` });
37
+ assert.ok(long.length < 80);
38
+ assert.match(long, /^Running git log/);
39
+ assert.match(long, /--oneline$/);
40
+ });
41
+
42
+ it("falls back to the tool's own name", () => {
43
+ // Extension and custom tools still say something useful.
44
+ assert.equal(describeToolCall("my_custom_tool", {}), "Running my_custom_tool");
45
+ });
46
+
47
+ it("copes with missing arguments", () => {
48
+ assert.equal(describeToolCall("read", undefined), "Reading a file");
49
+ assert.equal(describeToolCall("bash", {}), "Running a command");
50
+ });
51
+ });
52
+
53
+ function run(events: AgentSessionEvent[]): { actions: StateAction[]; state: ChatState } {
54
+ const mapper = new TurnMapper("t", 0, { workingDirectory: "/ws" });
55
+ const actions: StateAction[] = [userTurnStarted("t", "go", "1970-01-01T00:00:00.000Z")];
56
+ for (const e of events) {
57
+ actions.push(...mapper.handle(e));
58
+ }
59
+ let state = initialChatState("ahp-chat:/a", "chat");
60
+ for (const action of actions) {
61
+ state = chatReducer(state, action as never);
62
+ }
63
+ return { actions, state };
64
+ }
65
+
66
+ const activities = (actions: StateAction[]): (string | undefined)[] =>
67
+ actions
68
+ .filter((action) => action.type === ActionType.ChatActivityChanged)
69
+ .map((action) => (action as { activity?: string }).activity);
70
+
71
+ describe("activity over a turn", () => {
72
+ it("tracks thinking, tools, and responding in order", () => {
73
+ const { actions } = run([
74
+ event({ type: "agent_start" }),
75
+ event({ type: "message_start", message: { role: "assistant" } }),
76
+ event({ type: "tool_execution_start", toolCallId: "tc1", toolName: "read", args: { path: "/ws/a.ts" } }),
77
+ event({ type: "tool_execution_end", toolCallId: "tc1", toolName: "read", result: {}, isError: false }),
78
+ event({
79
+ type: "message_update",
80
+ message: { role: "assistant" },
81
+ assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "done" },
82
+ }),
83
+ event({ type: "agent_settled" }),
84
+ ]);
85
+
86
+ assert.deepEqual(activities(actions), [
87
+ THINKING_ACTIVITY,
88
+ "Reading a.ts",
89
+ THINKING_ACTIVITY,
90
+ RESPONDING_ACTIVITY,
91
+ undefined,
92
+ ]);
93
+ });
94
+
95
+ it("clears the description when the turn ends", () => {
96
+ // A stale one would leave the client showing "Reading …" against an
97
+ // idle chat.
98
+ const { state } = run([
99
+ event({ type: "agent_start" }),
100
+ event({ type: "tool_execution_start", toolCallId: "tc1", toolName: "bash", args: { command: "ls" } }),
101
+ event({ type: "agent_settled" }),
102
+ ]);
103
+
104
+ assert.equal(state.activity, undefined);
105
+ });
106
+
107
+ it("clears it on a cancelled turn too", () => {
108
+ const mapper = new TurnMapper("t", 0);
109
+ mapper.handle(event({ type: "agent_start" }));
110
+ const closing = mapper.finish("cancelled");
111
+
112
+ assert.deepEqual(activities(closing), [undefined]);
113
+ });
114
+
115
+ it("does not re-send an unchanged description", () => {
116
+ // Every delta of one response is still "Responding"; re-sending would
117
+ // burn a serverSeq per token.
118
+ const { actions } = run([
119
+ event({ type: "agent_start" }),
120
+ event({ type: "message_start", message: { role: "assistant" } }),
121
+ ...["a", "b", "c", "d"].map((delta) =>
122
+ event({
123
+ type: "message_update",
124
+ message: { role: "assistant" },
125
+ assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta },
126
+ }),
127
+ ),
128
+ event({ type: "agent_settled" }),
129
+ ]);
130
+
131
+ assert.deepEqual(activities(actions), [THINKING_ACTIVITY, RESPONDING_ACTIVITY, undefined]);
132
+ });
133
+
134
+ it("emits schema-conforming actions", () => {
135
+ const { actions } = run([
136
+ event({ type: "agent_start" }),
137
+ event({ type: "tool_execution_start", toolCallId: "tc1", toolName: "grep", args: { pattern: "TODO" } }),
138
+ event({ type: "agent_settled" }),
139
+ ]);
140
+
141
+ for (const action of actions.filter((a) => a.type === ActionType.ChatActivityChanged)) {
142
+ assertValid("actions", "StateAction", action);
143
+ }
144
+ });
145
+
146
+ describe("toolInputFor", () => {
147
+ it("shows the one argument a call is about", () => {
148
+ assert.equal(toolInputFor("bash", { command: "ls -la", timeout: 5 }), "ls -la");
149
+ assert.equal(toolInputFor("read", { path: "a.ts", offset: 1, limit: 50 }), "a.ts");
150
+ });
151
+
152
+ it("keeps a search's scope, and drops the defaults models restate", () => {
153
+ // Every one of these is what a capture actually contained: the model
154
+ // fills in the defaults rather than omitting them.
155
+ assert.equal(
156
+ toolInputFor("grep", { pattern: "BEACON", path: ".", glob: "**/*", ignoreCase: false, limit: 100 }),
157
+ "BEACON",
158
+ );
159
+ assert.equal(
160
+ toolInputFor("grep", { pattern: "TODO", path: "src", glob: "*.ts", ignoreCase: true }),
161
+ "TODO --glob *.ts --ignore-case in src",
162
+ );
163
+ });
164
+
165
+ it("falls back to the arguments for a tool it does not know", () => {
166
+ // An extension's tool has no argument this code can single out, and
167
+ // its arguments are still the most informative thing available.
168
+ assert.equal(toolInputFor("some-extension-tool", { a: 1 }), '{\n "a": 1\n}');
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,303 @@
1
+ /** Deterministic concurrency and watcher lifecycle tests for Git changesets. */
2
+
3
+ import assert from "node:assert/strict";
4
+ import { randomUUID } from "node:crypto";
5
+ import { EventEmitter } from "node:events";
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { it } from "node:test";
10
+ import { pathToFileURL } from "node:url";
11
+ import {
12
+ ActionType,
13
+ type ChangesetFile,
14
+ type ChangesetState,
15
+ ChangesetStatus,
16
+ type ChatState,
17
+ MessageKind,
18
+ SessionLifecycle,
19
+ type SessionState,
20
+ SessionStatus,
21
+ SUPPORTED_PROTOCOL_VERSIONS,
22
+ } from "@microsoft/agent-host-protocol";
23
+ import { AhpClient } from "@microsoft/agent-host-protocol/client";
24
+ import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
25
+ import { initialChatState } from "../src/channels/chat.ts";
26
+ import { installRootChannel } from "../src/channels/root.ts";
27
+ import { chatUri, sessionUri } from "../src/core/channels.ts";
28
+ import { AhpHost } from "../src/core/host.ts";
29
+ import { ChangesetService, type ChangesetSession, type ChangesetWatchFactory } from "../src/pi/changeset-service.ts";
30
+ import { piChangesetUri } from "../src/pi/changeset-uri.ts";
31
+ import type { GitBlobRef, GitChangesBackend, GitWorkspace } from "../src/pi/git-changes.ts";
32
+ import { type RunningServer, serveWebSocket } from "../src/transport/websocket.ts";
33
+ import { eventually } from "./support/async.ts";
34
+
35
+ class ControlledWatcher extends EventEmitter {
36
+ async close(): Promise<void> {}
37
+
38
+ change(path: string): void {
39
+ this.emit("change", path);
40
+ }
41
+ }
42
+
43
+ class ControlledWatchFactory {
44
+ readonly watchers: ControlledWatcher[] = [];
45
+ readonly create: ChangesetWatchFactory = () => {
46
+ const watcher = new ControlledWatcher();
47
+ this.watchers.push(watcher);
48
+ queueMicrotask(() => watcher.emit("ready"));
49
+ return watcher as unknown as ReturnType<ChangesetWatchFactory>;
50
+ };
51
+
52
+ get current(): ControlledWatcher {
53
+ const watcher = this.watchers.at(-1);
54
+ if (!watcher) throw new Error("watcher was not created");
55
+ return watcher;
56
+ }
57
+ }
58
+
59
+ class ControlledGit implements GitChangesBackend {
60
+ readonly computeStarted = Promise.withResolvers<void>();
61
+ readonly computeGate = Promise.withResolvers<void>();
62
+ readonly workspace: GitWorkspace;
63
+ computeCalls = 0;
64
+ ignoredCalls = 0;
65
+ failCompute = false;
66
+ failIgnored = false;
67
+ blockFirstCompute = true;
68
+
69
+ constructor(workspace: GitWorkspace) {
70
+ this.workspace = workspace;
71
+ }
72
+
73
+ async inspect(): Promise<GitWorkspace> {
74
+ return this.workspace;
75
+ }
76
+
77
+ async compute(
78
+ _workspace: GitWorkspace,
79
+ _sessionId: string,
80
+ _kind: "latest-commit" | "uncommitted",
81
+ signal?: AbortSignal,
82
+ ): Promise<ChangesetFile[]> {
83
+ this.computeCalls += 1;
84
+ if (this.failCompute) throw new Error("Git diff failed");
85
+ const version = this.computeCalls;
86
+ if (version === 1 && this.blockFirstCompute) {
87
+ this.computeStarted.resolve();
88
+ await new Promise<void>((resolve, reject) => {
89
+ const aborted = (): void => reject(signal?.reason ?? new Error("aborted"));
90
+ signal?.addEventListener("abort", aborted, { once: true });
91
+ void this.computeGate.promise
92
+ .then(resolve, reject)
93
+ .finally(() => signal?.removeEventListener("abort", aborted));
94
+ });
95
+ }
96
+ const uri = pathToFileURL(join(this.workspace.cwd, "result.txt")).toString();
97
+ return [{ id: uri, edit: { after: { uri, content: { uri } }, diff: { added: version, removed: 0 } } }];
98
+ }
99
+
100
+ async ignoredDirectories(): Promise<string[]> {
101
+ this.ignoredCalls += 1;
102
+ if (this.failIgnored) throw new Error("ignored paths unavailable");
103
+ return [];
104
+ }
105
+
106
+ pathForBlob(): string | undefined {
107
+ return undefined;
108
+ }
109
+
110
+ async readBlob(_workspace: GitWorkspace, _blob: GitBlobRef): Promise<Buffer> {
111
+ throw new Error("not used");
112
+ }
113
+ }
114
+
115
+ interface ControlledFixture {
116
+ readonly host: AhpHost;
117
+ readonly client: AhpClient;
118
+ readonly server: RunningServer;
119
+ readonly service: ChangesetService;
120
+ readonly git: ControlledGit;
121
+ readonly watches: ControlledWatchFactory;
122
+ readonly session: ChangesetSession;
123
+ readonly chat: string;
124
+ readonly channel: string;
125
+ readonly workspace: string;
126
+ close(): Promise<void>;
127
+ }
128
+
129
+ async function startControlledFixture(
130
+ options: { failCompute?: boolean; failIgnored?: boolean; blockFirstCompute?: boolean } = {},
131
+ ): Promise<ControlledFixture> {
132
+ const workspace = mkdtempSync(join(tmpdir(), "pi-ahp-changeset-controlled-"));
133
+ const gitDirectory = join(workspace, ".git");
134
+ mkdirSync(join(gitDirectory, "refs", "heads"), { recursive: true });
135
+ mkdirSync(join(gitDirectory, "info"), { recursive: true });
136
+ writeFileSync(join(gitDirectory, "HEAD"), "ref: refs/heads/main\n");
137
+ writeFileSync(join(gitDirectory, "index"), "");
138
+ writeFileSync(join(gitDirectory, "info", "exclude"), "");
139
+
140
+ const id = randomUUID();
141
+ const sessionChannel = sessionUri(id);
142
+ const chatChannel = chatUri(id);
143
+ const session: ChangesetSession = { uri: sessionChannel, sessionId: id, workingDirectory: workspace };
144
+ const host = new AhpHost();
145
+ installRootChannel(host, []);
146
+ const sessionState: SessionState = {
147
+ provider: "pi",
148
+ title: "Controlled",
149
+ status: SessionStatus.Idle,
150
+ lifecycle: SessionLifecycle.Ready,
151
+ workingDirectories: [pathToFileURL(workspace).toString()],
152
+ activeClients: [],
153
+ chats: [],
154
+ };
155
+ host.store.create(sessionChannel, sessionState, "session");
156
+ host.store.create(chatChannel, initialChatState(chatChannel, "Controlled"), "chat");
157
+ const git = new ControlledGit({
158
+ cwd: workspace,
159
+ repositoryRoot: workspace,
160
+ gitDirectory,
161
+ commonGitDirectory: gitDirectory,
162
+ pathspec: ".",
163
+ head: "a".repeat(40),
164
+ parent: "b".repeat(40),
165
+ });
166
+ git.failCompute = options.failCompute ?? false;
167
+ git.failIgnored = options.failIgnored ?? false;
168
+ git.blockFirstCompute = options.blockFirstCompute ?? true;
169
+ const logs: string[] = [];
170
+ const watches = new ControlledWatchFactory();
171
+ const service = new ChangesetService(host, {
172
+ getSession: (sessionId) => (sessionId === id ? session : undefined),
173
+ getSessionByChat: (chat) => (chat === chatChannel ? session : undefined),
174
+ hydrateSession: async (sessionId) => (sessionId === id ? session : undefined),
175
+ git,
176
+ watchFactory: watches.create,
177
+ debounceMs: 5,
178
+ log: (message) => logs.push(message),
179
+ });
180
+ await service.attach(session);
181
+ host.serve({ hydrator: service });
182
+ const server = await serveWebSocket(host, { host: "127.0.0.1", port: 0 });
183
+ const client = new AhpClient(await WebSocketTransport.connect(`ws://127.0.0.1:${server.port}`));
184
+ client.connect();
185
+ await client.initialize({ clientId: `controlled-${id}`, protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
186
+ const channel = piChangesetUri(id, "uncommitted");
187
+ return {
188
+ host,
189
+ client,
190
+ server,
191
+ service,
192
+ git,
193
+ watches,
194
+ session,
195
+ chat: chatChannel,
196
+ channel,
197
+ workspace,
198
+ async close() {
199
+ git.computeGate.resolve();
200
+ await client.shutdown();
201
+ await server.close();
202
+ await service.dispose();
203
+ rmSync(workspace, { recursive: true, force: true });
204
+ },
205
+ };
206
+ }
207
+
208
+ it("observes changes during the initial scan and rebuilds changed ignore rules", async () => {
209
+ const fixture = await startControlledFixture();
210
+ try {
211
+ await fixture.client.subscribe(fixture.channel);
212
+ await fixture.git.computeStarted.promise;
213
+ fixture.watches.current.change(join(fixture.workspace, ".gitignore"));
214
+ fixture.git.computeGate.resolve();
215
+
216
+ await eventually("a follow-up changeset scan", () => fixture.git.computeCalls >= 2, {
217
+ describe: () => ({ computes: fixture.git.computeCalls, ignoredReads: fixture.git.ignoredCalls }),
218
+ });
219
+ await eventually("the rebuilt watcher", () => fixture.git.ignoredCalls >= 2);
220
+ const state = fixture.host.store.get(fixture.channel) as ChangesetState;
221
+ assert.equal(state.status, ChangesetStatus.Ready);
222
+ assert.equal(state.files[0]?.edit.diff?.added, 2);
223
+ } finally {
224
+ await fixture.close();
225
+ }
226
+ });
227
+
228
+ it("refreshes file changes while a turn is still active", async () => {
229
+ const fixture = await startControlledFixture({ blockFirstCompute: false });
230
+ try {
231
+ await fixture.client.subscribe(fixture.channel);
232
+ await eventually(
233
+ "the initial changeset",
234
+ () => (fixture.host.store.get(fixture.channel) as ChangesetState | undefined)?.status === ChangesetStatus.Ready,
235
+ );
236
+ const initialComputes = fixture.git.computeCalls;
237
+ fixture.host.dispatchServerAction(fixture.chat, {
238
+ type: ActionType.ChatTurnStarted,
239
+ turnId: "active-turn",
240
+ startedAt: new Date().toISOString(),
241
+ message: { text: "Keep working", origin: { kind: MessageKind.User } },
242
+ });
243
+ fixture.watches.current.change(join(fixture.workspace, "during-turn.ts"));
244
+
245
+ await eventually("a changeset refresh before turn completion", () => {
246
+ const state = fixture.host.store.get(fixture.channel) as ChangesetState | undefined;
247
+ return (
248
+ fixture.git.computeCalls > initialComputes &&
249
+ state?.status === ChangesetStatus.Ready &&
250
+ (state.files[0]?.edit.diff?.added ?? 0) > initialComputes
251
+ );
252
+ });
253
+ assert.equal((fixture.host.store.get(fixture.chat) as ChatState).activeTurn?.id, "active-turn");
254
+ } finally {
255
+ await fixture.close();
256
+ }
257
+ });
258
+
259
+ it("does not recursively watch when ignored paths cannot be determined", async () => {
260
+ const fixture = await startControlledFixture({ failIgnored: true, blockFirstCompute: false });
261
+ try {
262
+ await fixture.client.subscribe(fixture.channel);
263
+ await eventually(
264
+ "the one-shot changeset scan",
265
+ () => (fixture.host.store.get(fixture.channel) as ChangesetState | undefined)?.status === ChangesetStatus.Ready,
266
+ );
267
+ assert.ok(fixture.git.ignoredCalls > 0);
268
+ assert.equal(fixture.watches.watchers.length, 0);
269
+ assert.equal(fixture.service.activeWatcherCount, 0);
270
+ } finally {
271
+ await fixture.close();
272
+ }
273
+ });
274
+
275
+ it("publishes an explicit error when Git cannot compute the changeset", async () => {
276
+ const fixture = await startControlledFixture({ failCompute: true, blockFirstCompute: false });
277
+ try {
278
+ await fixture.client.subscribe(fixture.channel);
279
+ await eventually("the failed changeset", () => {
280
+ return (fixture.host.store.get(fixture.channel) as ChangesetState | undefined)?.status === ChangesetStatus.Error;
281
+ });
282
+ const state = fixture.host.store.get(fixture.channel) as ChangesetState;
283
+ assert.equal(state.error?.errorType, "gitChangesFailed");
284
+ assert.equal(state.error?.message, "Git diff failed");
285
+ } finally {
286
+ await fixture.close();
287
+ }
288
+ });
289
+
290
+ it("drains an in-flight scan before deleting its channel", async () => {
291
+ const fixture = await startControlledFixture();
292
+ try {
293
+ await fixture.client.subscribe(fixture.channel);
294
+ await fixture.git.computeStarted.promise;
295
+ await fixture.service.removeSession(fixture.session.sessionId);
296
+ assert.equal(fixture.host.store.has(fixture.channel), false);
297
+ fixture.git.computeGate.resolve();
298
+ await Promise.resolve();
299
+ assert.equal(fixture.host.store.has(fixture.channel), false);
300
+ } finally {
301
+ await fixture.close();
302
+ }
303
+ });
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { parsePiChangesetUri, piChangesetCatalogue, piChangesetUri } from "../src/pi/changeset-uri.ts";
4
+
5
+ describe("pi changeset URIs", () => {
6
+ it("builds stable static catalogue channels", () => {
7
+ assert.deepEqual(piChangesetCatalogue("session-1", true), [
8
+ {
9
+ label: "Latest Commit",
10
+ description: "Changes introduced by the current HEAD commit",
11
+ changeKind: "branch",
12
+ uriTemplate: "ahp-changeset:/session-1/latest-commit",
13
+ },
14
+ {
15
+ label: "Uncommitted Changes",
16
+ description: "Current staged, unstaged, and untracked changes",
17
+ changeKind: "uncommitted",
18
+ uriTemplate: "ahp-changeset:/session-1/uncommitted",
19
+ },
20
+ ]);
21
+ assert.deepEqual(parsePiChangesetUri(piChangesetUri("session-1", "uncommitted")), {
22
+ sessionId: "session-1",
23
+ kind: "uncommitted",
24
+ });
25
+ });
26
+
27
+ it("rejects channels the host did not advertise", () => {
28
+ for (const uri of [
29
+ "ahp-changeset:/session-1/turn",
30
+ "ahp-changeset:/session-1/uncommitted/extra",
31
+ "ahp-changeset:/session%2Fother/uncommitted",
32
+ "ahp-changeset:/session-1/uncommitted?ref=HEAD",
33
+ ]) {
34
+ assert.equal(parsePiChangesetUri(uri), undefined);
35
+ }
36
+ });
37
+ });