@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,196 @@
1
+ package mapper_test
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "regexp"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
10
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
12
+ )
13
+
14
+ const onePixelJPEG = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AVN//2Q=="
15
+
16
+ func message(attachments []any, text ...string) obj {
17
+ t := "question"
18
+ if len(text) > 0 {
19
+ t = text[0]
20
+ }
21
+ return obj{"text": t, "origin": obj{"kind": "user"}, "attachments": attachments}
22
+ }
23
+
24
+ func embedded(data any, extra obj) obj {
25
+ a := obj{"type": "embeddedResource", "label": "notes.txt", "contentType": "text/plain", "data": data}
26
+ for k, v := range extra {
27
+ if v == nil {
28
+ delete(a, k)
29
+ } else {
30
+ a[k] = v
31
+ }
32
+ }
33
+ return a
34
+ }
35
+
36
+ func embeddedImage(data string, contentType string) obj {
37
+ return embedded(data, obj{"label": "screenshot.png", "contentType": contentType, "displayKind": "image"})
38
+ }
39
+
40
+ func b64(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
41
+
42
+ func rng(sl, sc, el, ec int) obj {
43
+ return obj{"range": obj{"start": obj{"line": sl, "character": sc}, "end": obj{"line": el, "character": ec}}}
44
+ }
45
+
46
+ func mustText(t *testing.T, m obj) string {
47
+ t.Helper()
48
+ got, err := mapper.MessageTextForPi(m)
49
+ if err != nil {
50
+ t.Fatal(err)
51
+ }
52
+ return got
53
+ }
54
+
55
+ // Twins of upstream test/message-input.test.ts.
56
+ func TestMessageInput(t *testing.T) {
57
+
58
+ twin.Run(t, "message-input", "appends a file resource as a path with its selection", func(t *testing.T) {
59
+ in := message([]any{obj{"type": "resource", "label": "outside.ts", "uri": "file:///outside.ts", "selection": rng(4, 2, 8, 0)}})
60
+ if r := mapper.MessageRejectionReason(in); r != "" {
61
+ t.Fatal(r)
62
+ }
63
+ path, _ := wire.FileURIToPath("file:///outside.ts")
64
+ if got := mustText(t, in); got != "question\n\n"+path+":5:3-9:1" {
65
+ t.Fatalf("%q", got)
66
+ }
67
+ })
68
+
69
+ twin.Run(t, "message-input", "appends attachment representations in order", func(t *testing.T) {
70
+ in := message([]any{
71
+ obj{"type": "simple", "label": "first", "modelRepresentation": "first context"},
72
+ obj{"type": "resource", "label": "path", "uri": "file:///path.ts"},
73
+ obj{"type": "simple", "label": "second", "modelRepresentation": "second context"},
74
+ })
75
+ path, _ := wire.FileURIToPath("file:///path.ts")
76
+ if got := mustText(t, in); got != "question\n\nfirst context\n\n"+path+"\n\nsecond context" {
77
+ t.Fatalf("%q", got)
78
+ }
79
+ })
80
+
81
+ twin.Run(t, "message-input", "preserves resource URIs whose schemes may carry their own routing", func(t *testing.T) {
82
+ for _, uri := range []string{"https://example.com/context.txt", "virtual://client/context.txt?revision=1"} {
83
+ in := message([]any{obj{"type": "resource", "label": "remote", "uri": uri}})
84
+ if got := mustText(t, in); got != "question\n\n"+uri {
85
+ t.Fatalf("%q", got)
86
+ }
87
+ }
88
+ })
89
+
90
+ twin.Run(t, "message-input", "preserves a VS Code-wrapped resource without guessing which host owns it", func(t *testing.T) {
91
+ wrapped := "vscode-agent-host://another-host/Users/user/project/some%20file.ts?_ah%3DeyJzY2hlbWUiOiJmaWxlIn0"
92
+ in := message([]any{obj{"type": "resource", "label": "some file.ts", "uri": wrapped}})
93
+ if got := mustText(t, in); got != "question\n\n"+wrapped {
94
+ t.Fatalf("%q", got)
95
+ }
96
+ })
97
+
98
+ twin.Run(t, "message-input", "requires client-created simple attachments to carry a model representation", func(t *testing.T) {
99
+ // JSON null and an absent key are both "not a string".
100
+ for _, a := range []obj{
101
+ {"type": "simple", "label": "missing", "modelRepresentation": nil},
102
+ {"type": "simple", "label": "missing"},
103
+ } {
104
+ in := message([]any{a})
105
+ if got := mapper.MessageRejectionReason(in); got != "A simple attachment requires modelRepresentation" {
106
+ t.Fatalf("reason = %q", got)
107
+ }
108
+ if _, err := mapper.MessageTextForPi(in); err == nil || !strings.Contains(err.Error(), "requires modelRepresentation") {
109
+ t.Fatalf("err = %v", err)
110
+ }
111
+ }
112
+ })
113
+
114
+ twin.Run(t, "message-input", "separates embedded images from the text prompt", func(t *testing.T) {
115
+ in := message([]any{
116
+ obj{"type": "simple", "label": "context", "modelRepresentation": "selected context"},
117
+ embeddedImage(onePixelJPEG, "IMAGE/JPG; name=screenshot.jpg"),
118
+ })
119
+ got, err := mapper.MessageInputForPi(in)
120
+ if err != nil {
121
+ t.Fatal(err)
122
+ }
123
+ equal(t, got, mapper.MessageInput{
124
+ Text: "question\n\nselected context",
125
+ Images: []mapper.Image{{Type: "image", Data: onePixelJPEG, MimeType: "image/jpeg"}},
126
+ }, "input")
127
+ })
128
+
129
+ twin.Run(t, "message-input", "rejects an empty embedded image", func(t *testing.T) {
130
+ if got := mapper.MessageRejectionReason(message([]any{embeddedImage("", "image/png")})); got != "Embedded image screenshot.png is empty" {
131
+ t.Fatal(got)
132
+ }
133
+ })
134
+
135
+ twin.Run(t, "message-input", "appends UTF-8 embedded text without interpreting its MIME type", func(t *testing.T) {
136
+ in := message([]any{embedded(b64("const answer = 42;"), obj{"contentType": "application/x-uncommon-text"})})
137
+ if got := mustText(t, in); got != "question\n\nconst answer = 42;" {
138
+ t.Fatalf("%q", got)
139
+ }
140
+ })
141
+
142
+ twin.Run(t, "message-input", "keeps embedded payload under its original 1-based selection marker", func(t *testing.T) {
143
+ in := message([]any{embedded(b64("selected text"), obj{"selection": rng(20, 3, 20, 16)})})
144
+ if got := mustText(t, in); got != "question\n\n[selection 21:4-21:17]\nselected text" {
145
+ t.Fatalf("%q", got)
146
+ }
147
+ })
148
+
149
+ twin.Run(t, "message-input", "rejects malformed or non-text embedded payloads", func(t *testing.T) {
150
+ cases := []struct {
151
+ a obj
152
+ reason string
153
+ }{
154
+ {embedded("%%%", nil), "valid base64"},
155
+ {embedded(nil, nil), "valid base64"},
156
+ {embedded(base64.StdEncoding.EncodeToString([]byte{0xc3, 0x28}), nil), "valid UTF-8"},
157
+ }
158
+ for _, c := range cases {
159
+ if got := mapper.MessageRejectionReason(message([]any{c.a})); !strings.Contains(got, c.reason) {
160
+ t.Fatalf("reason %q lacks %q", got, c.reason)
161
+ }
162
+ }
163
+ })
164
+
165
+ twin.Run(t, "message-input", "rejects malformed message and selection shapes without throwing", func(t *testing.T) {
166
+ cases := []struct {
167
+ in any
168
+ reason string
169
+ }{
170
+ {nil, `requires text and an origin`},
171
+ {obj{"text": 42, "origin": obj{"kind": "user"}}, `requires text and an origin`},
172
+ {obj{"text": "x", "origin": obj{"kind": "user"}, "attachments": obj{}}, `must be an array`},
173
+ {message([]any{nil}), `requires a type and label`},
174
+ {message([]any{obj{"type": "resource", "label": "bad range", "uri": "file:///bad",
175
+ "selection": obj{"range": obj{"start": obj{"line": -1, "character": 0}, "end": obj{"line": 0, "character": 0}}}}}), `invalid text selection`},
176
+ {message([]any{embedded("", obj{"contentType": nil})}), `requires a content type`},
177
+ }
178
+ for i, c := range cases {
179
+ got := mapper.MessageRejectionReason(c.in)
180
+ if !regexp.MustCompile(c.reason).MatchString(got) {
181
+ t.Fatalf("case %d: reason %q does not match %q", i, got, c.reason)
182
+ }
183
+ }
184
+ })
185
+
186
+ twin.Run(t, "message-input", "rejects annotations and chat attachments", func(t *testing.T) {
187
+ for _, a := range []obj{
188
+ {"type": "annotations", "label": "diagnostics", "resource": "ahp-annotations:/fixture"},
189
+ {"type": "chat", "label": "other chat", "resource": "ahp-chat:/fixture"},
190
+ } {
191
+ if got := mapper.MessageRejectionReason(message([]any{a})); !strings.Contains(got, "does not support") {
192
+ t.Fatalf("reason = %q", got)
193
+ }
194
+ }
195
+ })
196
+ }
@@ -0,0 +1,272 @@
1
+ package mapper
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "errors"
6
+ "fmt"
7
+ "math"
8
+ "regexp"
9
+ "strings"
10
+ "unicode/utf8"
11
+
12
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
13
+ )
14
+
15
+ // Image is a pi image-content block.
16
+ type Image struct {
17
+ Type string `json:"type"`
18
+ Data string `json:"data"`
19
+ MimeType string `json:"mimeType"`
20
+ }
21
+
22
+ // MessageInput is pi's prompt shape: text plus optional images.
23
+ type MessageInput struct {
24
+ Text string `json:"text"`
25
+ Images []Image `json:"images,omitempty"`
26
+ }
27
+
28
+ // The port of src/pi/message-input.ts. A message arrives as decoded JSON straight from a client,
29
+ // so every shape is checked: a malformed value is rejected with a reason, never a panic. Resource
30
+ // references become paths or URIs, simple attachments contribute their model representation,
31
+ // embedded text is decoded as UTF-8, embedded images become pi image blocks; other attachment
32
+ // kinds are unsupported.
33
+
34
+ func isRecord(v any) (map[string]any, bool) {
35
+ m, ok := v.(map[string]any)
36
+ return m, ok
37
+ }
38
+
39
+ func isNonNegativeInteger(v any) bool {
40
+ f, ok := num(v)
41
+ return ok && f >= 0 && f == math.Trunc(f) && f <= 1<<53-1
42
+ }
43
+
44
+ func isTextRange(v any) bool {
45
+ rng, ok := isRecord(v)
46
+ if !ok {
47
+ return false
48
+ }
49
+ start, ok1 := isRecord(rng["start"])
50
+ end, ok2 := isRecord(rng["end"])
51
+ if !ok1 || !ok2 {
52
+ return false
53
+ }
54
+ return isNonNegativeInteger(start["line"]) && isNonNegativeInteger(start["character"]) &&
55
+ isNonNegativeInteger(end["line"]) && isNonNegativeInteger(end["character"])
56
+ }
57
+
58
+ var base64Shape = regexp.MustCompile(`^[A-Za-z0-9+/]*={0,2}$`)
59
+
60
+ func decodeBase64(v any) ([]byte, bool) {
61
+ data, ok := v.(string)
62
+ if !ok || !base64Shape.MatchString(data) || len(data)%4 == 1 {
63
+ return nil, false
64
+ }
65
+ if padding := strings.IndexByte(data, '='); padding != -1 && (padding < len(data)-2 || len(data)%4 != 0) {
66
+ return nil, false
67
+ }
68
+ trimmed := strings.TrimRight(data, "=")
69
+ decoded, err := base64.RawStdEncoding.DecodeString(trimmed)
70
+ if err != nil {
71
+ return nil, false
72
+ }
73
+ // Canonical form only: the bytes must re-encode to the same text.
74
+ if base64.RawStdEncoding.EncodeToString(decoded) != trimmed {
75
+ return nil, false
76
+ }
77
+ return decoded, true
78
+ }
79
+
80
+ func rangeText(rng map[string]any) string {
81
+ start, _ := isRecord(rng["start"])
82
+ end, _ := isRecord(rng["end"])
83
+ return fmt.Sprintf("%d:%d-%d:%d", intOr(start["line"], 0)+1, intOr(start["character"], 0)+1, intOr(end["line"], 0)+1, intOr(end["character"], 0)+1)
84
+ }
85
+
86
+ func selectionRange(attachment map[string]any) (map[string]any, bool) {
87
+ sel, ok := isRecord(attachment["selection"])
88
+ if !ok {
89
+ return nil, false
90
+ }
91
+ r, ok := isRecord(sel["range"])
92
+ return r, ok
93
+ }
94
+
95
+ func baseContentType(contentType string) string {
96
+ base, _, _ := strings.Cut(contentType, ";")
97
+ return strings.ToLower(trimJS(base))
98
+ }
99
+
100
+ type rejection string
101
+
102
+ // embeddedContent returns text or an image, or a rejection reason.
103
+ func embeddedContent(att map[string]any) (text string, image *Image, reject rejection) {
104
+ label, _ := att["label"].(string)
105
+ ct, ok := att["contentType"].(string)
106
+ if !ok {
107
+ return "", nil, rejection(fmt.Sprintf("Embedded resource %s requires a content type", label))
108
+ }
109
+ contentType := baseContentType(ct)
110
+ if contentType == "" {
111
+ return "", nil, rejection(fmt.Sprintf("Embedded resource %s requires a content type", label))
112
+ }
113
+ bytesData, ok := decodeBase64(att["data"])
114
+ if !ok {
115
+ return "", nil, rejection(fmt.Sprintf("Embedded resource %s is not valid base64", label))
116
+ }
117
+ if mime := NormalizeImageMimeType(contentType); mime != "" {
118
+ if len(bytesData) == 0 {
119
+ return "", nil, rejection(fmt.Sprintf("Embedded image %s is empty", label))
120
+ }
121
+ return "", &Image{Type: "image", Data: base64.StdEncoding.EncodeToString(bytesData), MimeType: mime}, ""
122
+ }
123
+ // MIME is advisory for non-images: successful UTF-8 decoding decides whether an embedded
124
+ // resource can safely join pi's text prompt.
125
+ if !utf8.Valid(bytesData) {
126
+ return "", nil, rejection(fmt.Sprintf("Embedded resource %s is not valid UTF-8", label))
127
+ }
128
+ decoded := string(bytesData)
129
+ if r, ok := selectionRange(att); ok {
130
+ return fmt.Sprintf("[selection %s]\n%s", rangeText(r), decoded), nil, ""
131
+ }
132
+ return decoded, nil, ""
133
+ }
134
+
135
+ func resourceText(att map[string]any) (string, rejection) {
136
+ uri, ok := att["uri"].(string)
137
+ if !ok {
138
+ return "", "A resource attachment requires a URI"
139
+ }
140
+ reference := uri
141
+ if len(reference) >= 5 && strings.EqualFold(reference[:5], "file:") {
142
+ // A remote or malformed file URI stays as it was rather than being guessed into a path.
143
+ if p, err := wire.FileURIToPath(reference); err == nil {
144
+ reference = p
145
+ }
146
+ }
147
+ if r, ok := selectionRange(att); ok {
148
+ reference += ":" + rangeText(r)
149
+ }
150
+ return reference, ""
151
+ }
152
+
153
+ type prepared struct {
154
+ input MessageInput
155
+ reject rejection
156
+ }
157
+
158
+ func prepareMessage(value any) prepared {
159
+ rej := func(s string) prepared { return prepared{reject: rejection(s)} }
160
+ m, ok := isRecord(value)
161
+ if !ok {
162
+ return rej("A message requires text and an origin")
163
+ }
164
+ text, okText := m["text"].(string)
165
+ origin, okOrigin := isRecord(m["origin"])
166
+ if !okText || !okOrigin {
167
+ return rej("A message requires text and an origin")
168
+ }
169
+ if _, ok := origin["kind"].(string); !ok {
170
+ return rej("A message requires text and an origin")
171
+ }
172
+ var attachments []any
173
+ if raw, present := m["attachments"]; present && raw != nil {
174
+ arr, ok := raw.([]any)
175
+ if !ok {
176
+ return rej("Message attachments must be an array")
177
+ }
178
+ attachments = arr
179
+ }
180
+ if model, present := m["model"]; present && model != nil {
181
+ mm, ok := isRecord(model)
182
+ if !ok {
183
+ return rej("A message model requires an id")
184
+ }
185
+ if _, ok := mm["id"].(string); !ok {
186
+ return rej("A message model requires an id")
187
+ }
188
+ }
189
+ if agent, present := m["agent"]; present && agent != nil {
190
+ return rej("This host does not support custom agents")
191
+ }
192
+
193
+ representations := []string{}
194
+ var images []Image
195
+ for _, raw := range attachments {
196
+ att, ok := isRecord(raw)
197
+ if !ok {
198
+ return rej("Every message attachment requires a type and label")
199
+ }
200
+ typ, okType := att["type"].(string)
201
+ label, okLabel := att["label"].(string)
202
+ if !okType || !okLabel {
203
+ return rej("Every message attachment requires a type and label")
204
+ }
205
+ if r, present := att["range"]; present && r != nil && !isTextRange(r) {
206
+ return rej(fmt.Sprintf("Attachment %s has an invalid text range", label))
207
+ }
208
+ if sel, present := att["selection"]; present && sel != nil {
209
+ sm, ok := isRecord(sel)
210
+ if !ok || !isTextRange(sm["range"]) {
211
+ return rej(fmt.Sprintf("Attachment %s has an invalid text selection", label))
212
+ }
213
+ }
214
+ switch typ {
215
+ case "resource":
216
+ s, r := resourceText(att)
217
+ if r != "" {
218
+ return prepared{reject: r}
219
+ }
220
+ representations = append(representations, s)
221
+ case "simple":
222
+ rep, ok := att["modelRepresentation"].(string)
223
+ if !ok {
224
+ return rej("A simple attachment requires modelRepresentation")
225
+ }
226
+ if rep != "" {
227
+ representations = append(representations, rep)
228
+ }
229
+ case "embeddedResource":
230
+ s, img, r := embeddedContent(att)
231
+ if r != "" {
232
+ return prepared{reject: r}
233
+ }
234
+ if img != nil {
235
+ images = append(images, *img)
236
+ } else {
237
+ representations = append(representations, s)
238
+ }
239
+ default:
240
+ return rej(fmt.Sprintf("This host does not support %s attachments", typ))
241
+ }
242
+ }
243
+
244
+ parts := []string{}
245
+ for _, p := range append([]string{text}, representations...) {
246
+ if p != "" {
247
+ parts = append(parts, p)
248
+ }
249
+ }
250
+ return prepared{input: MessageInput{Text: strings.Join(parts, "\n\n"), Images: images}}
251
+ }
252
+
253
+ // MessageRejectionReason says why a client message cannot become a pi prompt ("" when it can).
254
+ func MessageRejectionReason(message any) string {
255
+ return string(prepareMessage(message).reject)
256
+ }
257
+
258
+ // MessageInputForPi converts an AHP message to pi's prompt shape; the error carries the
259
+ // rejection reason.
260
+ func MessageInputForPi(message any) (MessageInput, error) {
261
+ p := prepareMessage(message)
262
+ if p.reject != "" {
263
+ return MessageInput{}, errors.New(string(p.reject))
264
+ }
265
+ return p.input, nil
266
+ }
267
+
268
+ // MessageTextForPi is the text of MessageInputForPi.
269
+ func MessageTextForPi(message any) (string, error) {
270
+ in, err := MessageInputForPi(message)
271
+ return in.Text, err
272
+ }
@@ -0,0 +1,5 @@
1
+ package mapper_test
2
+
3
+ import "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
4
+
5
+ func checkAction(a any) error { return testkit.CheckSchema("actions", "StateAction", a) }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bang Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.