@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.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
package/CREDITS.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Credits
|
|
2
|
+
|
|
3
|
+
`extensions/ahp` is a Go port of **pi-ahp**, https://github.com/Qusic/pi-ahp, by **Bang Lee**
|
|
4
|
+
(Qusic; MIT, Copyright (c) 2026 Bang Lee).
|
|
5
|
+
|
|
6
|
+
- Original: the `src/` tree of pi-ahp, a standalone host that embeds Pi and serves it over
|
|
7
|
+
Microsoft's Agent Host Protocol; npm package `pi-ahp`.
|
|
8
|
+
- Revision: commit `4065e98309b03c1d2d916300856c3c21a9c10674`.
|
|
9
|
+
- The unmodified `src/`, `test/` and metadata are kept in [`proof/oracle`](proof/oracle) as the
|
|
10
|
+
equivalence oracle, with the upstream license at [`proof/oracle/LICENSE`](proof/oracle/LICENSE).
|
|
11
|
+
The 430 leaf test cases of the original are listed in
|
|
12
|
+
[`upstream-tests.json`](extensions/ahp/testdata/upstream-tests.json); each has a Go twin or a named skipped
|
|
13
|
+
twin ([`proof/PORT.md`](proof/PORT.md)).
|
|
14
|
+
|
|
15
|
+
The protocol is **Agent Host Protocol** (AHP) by **Microsoft Corporation**,
|
|
16
|
+
https://github.com/microsoft/agent-host-protocol (MIT, Copyright (c) Microsoft Corporation),
|
|
17
|
+
protocol 0.9.0, commit `296b25e7b698a4a84a0ee5a28d9573e70048a0bf`. Its Go types and reducers
|
|
18
|
+
(`clients/go`, `ahp` and `ahptypes`) are vendored byte for byte, tests excluded, in
|
|
19
|
+
[`extensions/ahp/third_party/agent-host-protocol-go`](extensions/ahp/third_party/agent-host-protocol-go)
|
|
20
|
+
with the Microsoft license. The five protocol JSON schemas used by the tests are copied from the
|
|
21
|
+
same commit into `extensions/ahp/internal/testkit/schema`, and the recorded Pi event streams in
|
|
22
|
+
`extensions/ahp/internal/mapper/testdata/fixtures` come from pi-ahp's test fixtures.
|
|
23
|
+
|
|
24
|
+
The Go code, the scenarios and the tests were written for this Package by Michael Kinsy. The
|
|
25
|
+
behavior, wording and structure of the host follow the original. Modified paths: none of the
|
|
26
|
+
originals is modified; the port is a separate implementation, and its deviations are listed in
|
|
27
|
+
[`proof/PORT.md`](proof/PORT.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bang Lee
|
|
4
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
5
|
+
Copyright (c) 2026 Michael Kinsy (the Go port)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ahp (Go port of pi-ahp)
|
|
2
|
+
|
|
3
|
+
A Go extension for PiG that serves the **running session** to remote clients over Microsoft's
|
|
4
|
+
[Agent Host Protocol](https://github.com/microsoft/agent-host-protocol) (AHP, protocol 0.9.0,
|
|
5
|
+
pinned): a WebSocket host with multi-client state, reconnect with replay or fresh snapshots,
|
|
6
|
+
token authentication, an origin check, model and session-config discovery, `@`-mentions, the
|
|
7
|
+
session catalogue with history paging, and live mirroring of the agent's turns, including prompts
|
|
8
|
+
typed locally.
|
|
9
|
+
|
|
10
|
+
It is a port of **pi-ahp** by Bang Lee (see [CREDITS.md](CREDITS.md)). pi-ahp is a standalone
|
|
11
|
+
host that embeds Pi and runs many sessions; a PiG extension lives inside one session, so this port
|
|
12
|
+
serves that session (and reads other sessions from their files). See [proof/PORT.md](proof/PORT.md)
|
|
13
|
+
for every deviation and gap.
|
|
14
|
+
|
|
15
|
+
## Safe by default
|
|
16
|
+
|
|
17
|
+
- **No listener** exists unless you start one: `pig --ahp`, or `/ahp start`. Installing the
|
|
18
|
+
extension opens nothing and creates no file.
|
|
19
|
+
- Starting creates `<pig home>/ahp/settings.json` (mode 0600) with a free loopback port and a random
|
|
20
|
+
token, if it is missing. Clients connect to `ws://127.0.0.1:<port>/?token=<token>`; the listener also
|
|
21
|
+
takes the token as `?tkn=`, the spelling Visual Studio Code uses (see below).
|
|
22
|
+
A non-loopback `host` without a token is refused; a browser `Origin` is refused unless listed in
|
|
23
|
+
`allowedOrigins`.
|
|
24
|
+
- **Remote filesystem, watches, terminals and session deletion are separate opt-ins** in the same
|
|
25
|
+
file, all off unless set:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"port": 41234, "token": "…", "host": "127.0.0.1",
|
|
30
|
+
"allowedOrigins": [],
|
|
31
|
+
"filesystem": { "enabled": true, "roots": [], "unrestricted": false },
|
|
32
|
+
"terminals": { "enabled": false },
|
|
33
|
+
"allowSessionDeletion": false
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With `filesystem.enabled` and no `roots`, access is confined to the session's working directory;
|
|
38
|
+
`unrestricted: true` must be said explicitly. A terminal is a shell as you, claimed by the client
|
|
39
|
+
that started it. The dev tunnel of pi-ahp is not bundled.
|
|
40
|
+
|
|
41
|
+
**What a connected client can always do.** The opt-ins above cover the host's own filesystem,
|
|
42
|
+
watch, terminal and deletion services. They do not limit the agent. Anyone who holds the token
|
|
43
|
+
can do all of the following, so treat the token like a shell login:
|
|
44
|
+
|
|
45
|
+
- Send prompts to the running session. The agent answers with the session's tools (for example
|
|
46
|
+
`bash`, `read`, `write`, `edit`), as you, in its working directory. Slash commands typed by a
|
|
47
|
+
client are sent to the model as text and are not run.
|
|
48
|
+
- Switch the session's model and thinking level.
|
|
49
|
+
- List and read the stored history of **every** PiG session under the session directory, from all
|
|
50
|
+
projects, not only the running one.
|
|
51
|
+
- Get `@` completions, which list file names under the working directory.
|
|
52
|
+
|
|
53
|
+
The token is shown in the `/ahp` status and in the "listening" notice.
|
|
54
|
+
|
|
55
|
+
## Commands and flags
|
|
56
|
+
|
|
57
|
+
| | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `--ahp` | start the listener when the session starts |
|
|
60
|
+
| `--ahp-settings <file>` | settings file (default `<pig home>/ahp/settings.json`) |
|
|
61
|
+
| `/ahp [status]` `/ahp start` `/ahp stop` | inspect, start or stop the listener |
|
|
62
|
+
|
|
63
|
+
If the PiG session is replaced (new, resume, fork, reload), the listener closes with it and connected
|
|
64
|
+
clients are dropped. With `--ahp` it opens again for the new session on the same port. After
|
|
65
|
+
`/ahp start` it stays closed, a warning says so, and `/ahp start` serves the new session.
|
|
66
|
+
|
|
67
|
+
## Visual Studio Code
|
|
68
|
+
|
|
69
|
+
**Not verified with a live VS Code.** The listener is tested with the messages VS Code's client sends, as read from VS Code's
|
|
70
|
+
source for 1.140.0 and main (the handshake, the `tkn` token, text frames, a dash-free provider id;
|
|
71
|
+
`internal/ws/vscode_test.go`), not against VS Code itself. Until the owner has tried it, treat the steps below as the intended path.
|
|
72
|
+
|
|
73
|
+
VS Code's Agents window (**Chat: Open Agents Window**) can connect to any WebSocket AHP host:
|
|
74
|
+
|
|
75
|
+
1. Start the listener in the session you want to serve: `pig --ahp`, or `/ahp start`. The notice (and `/ahp status`) starts with
|
|
76
|
+
the address in the form VS Code takes, `ws://127.0.0.1:<port>?tkn=<token>`, then gives the `/?token=` form for other clients.
|
|
77
|
+
2. In the Agents window's Command Palette run **Agents: Add Remote Agent Host...** (`sessions.remoteAgentHost.add`), paste that
|
|
78
|
+
address, and name the host. VS Code moves the token out of the address into the entry's `connectionToken`. Or add it to your
|
|
79
|
+
user `settings.json` by hand:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
"chat.remoteAgentHosts": [
|
|
83
|
+
{ "name": "pig", "address": "ws://127.0.0.1:41234", "connectionToken": "<token>" }
|
|
84
|
+
]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. `chat.remoteAgentHostsEnabled` must be on (it is by default). If you are not signed in to GitHub in VS Code, also set
|
|
88
|
+
`"chat.agentHost.allowSignedOutWhenUsable": true`.
|
|
89
|
+
|
|
90
|
+
Reaching a host on another machine:
|
|
91
|
+
|
|
92
|
+
- **SSH port forward** (simplest): `ssh -L 41234:127.0.0.1:41234 <host>`, start the listener there, and use the forwarded
|
|
93
|
+
`ws://127.0.0.1:41234?tkn=<token>` locally. The host keeps its loopback listener, so no token ever crosses the network unprotected
|
|
94
|
+
beyond the tunnel.
|
|
95
|
+
- **VS Code's SSH remote agent host**: `chat.sshRemoteAgentHostCommand` (a development setting) replaces the command VS Code runs
|
|
96
|
+
on the remote. VS Code reads the first `ws://127.0.0.1:<port>?tkn=<token>` in the command's output, which is how the notice
|
|
97
|
+
starts. The command has to keep pig running without a terminal: `pig --mode rpc --ahp` prints the notice on standard output at
|
|
98
|
+
once and runs while its input stays open (checked with a plain shell, not yet with VS Code).
|
|
99
|
+
- **A dev tunnel** is not bundled (the pi-ahp tunnel feature is not ported), and VS Code's own tunnel support looks for VS Code agent
|
|
100
|
+
hosts, not this one. A tunnel you start yourself can forward the port and give a `wss://` address that **Add Remote Agent Host...**
|
|
101
|
+
accepts, but VS Code sends no tunnel credentials on that connection, so the tunnel has to admit it anonymously and the token is
|
|
102
|
+
the only protection. Not tried.
|
|
103
|
+
|
|
104
|
+
Debugging: set `"chat.agentHost.ahpJsonlLoggingEnabled": true` to have VS Code log every AHP message as JSON lines under the
|
|
105
|
+
window's log directory (it is off by default in stable builds).
|
|
106
|
+
|
|
107
|
+
What to expect, and the limits that apply to this port:
|
|
108
|
+
|
|
109
|
+
- VS Code 1.140 offers protocol 0.9.0 first, then 0.7.0 down to 0.5.1; later builds offer 0.10.0 first and still 0.9.0. This host
|
|
110
|
+
speaks only 0.9.0 and negotiates it; a VS Code that stops offering 0.9.0 will report the host as incompatible. A wrong
|
|
111
|
+
or missing token is refused at the upgrade with HTTP 403, as VS Code's own agent host refuses one (pi-ahp answers 401; see
|
|
112
|
+
[proof/PORT.md](proof/PORT.md), deviation 15). A browser `Origin` that is not allowed also gets 403.
|
|
113
|
+
- The host serves **the one running session** as one agent (`pi`). Other sessions are readable from their files; they are not
|
|
114
|
+
running agents. If the session is replaced (new, resume, fork, reload) clients are dropped (see Commands and flags).
|
|
115
|
+
- No git changeset channels: VS Code's change views will have nothing to show (the changeset service of pi-ahp is not ported).
|
|
116
|
+
- The remote filesystem, watches, terminals and session deletion are off until you opt in (see Safe by default).
|
|
117
|
+
- No dev tunnel is bundled, and project trust cannot be gated from an extension (see [proof/PORT.md](proof/PORT.md)).
|
|
118
|
+
|
|
119
|
+
## Other AHP clients
|
|
120
|
+
|
|
121
|
+
Any client that speaks AHP 0.9.0 over WebSocket text frames can connect with `ws://127.0.0.1:<port>/?token=<token>` (or `?tkn=`).
|
|
122
|
+
Two others are named for trying; **neither has been tried against this host**, and what they support has not been checked here:
|
|
123
|
+
|
|
124
|
+
- **agent-host-protocol-ui**
|
|
125
|
+
- **copilot-remote-host**
|
|
126
|
+
|
|
127
|
+
Two things decide whether a client works: it must offer protocol 0.9.0 (this host does not down-convert from another version), and,
|
|
128
|
+
if it runs in a browser, its `Origin` must be listed in `allowedOrigins` in the settings file, because the host refuses a browser
|
|
129
|
+
`Origin` it does not know.
|
|
130
|
+
|
|
131
|
+
## Install
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
pig install ./components/ahp
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It builds from source on first use (Go toolchain required), or fuses into a Piglet Binary
|
|
138
|
+
(`packages: {ahp: local:../../components/ahp}` in a Piglet). It needs no Node runtime.
|
|
139
|
+
|
|
140
|
+
## Layout
|
|
141
|
+
|
|
142
|
+
`extensions/ahp/internal`: `wire` (JSON-RPC, channel URIs, version negotiation), `host` (channel
|
|
143
|
+
state, sequencing, replay, connections), `ws` (RFC 6455 server), `mapper` (Pi events to AHP
|
|
144
|
+
actions), `pi` (chat driver, session registry, catalogue, hydration, history, models, completions),
|
|
145
|
+
`svc` (resources, watches, terminals, PTY), `live` (the running PiG session behind the SDK),
|
|
146
|
+
`compose` (wiring), `settings`. The Microsoft AHP Go types and reducers are vendored byte for byte
|
|
147
|
+
under `third_party/`. Tests are twins of pi-ahp's 430 test cases (`internal/twin`); `twins_test.go`
|
|
148
|
+
fails if one has neither a Go test nor a named skip.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package ahp
|
|
2
|
+
|
|
3
|
+
// Review (rev-pigpen-ahp): the capability wiring of the runtime was only exercised by the
|
|
4
|
+
// real-pig scenarios, which are skipped unless PIGPEN_AHP_REAL=1, and those did not notice a
|
|
5
|
+
// default root wider than the working directory. These unit tests pin it without a pig.
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"context"
|
|
9
|
+
"errors"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"testing"
|
|
13
|
+
|
|
14
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
15
|
+
|
|
16
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/compose"
|
|
17
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/pi"
|
|
18
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/settings"
|
|
19
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
func TestNoCapabilityUnlessTheSettingsGrantIt(t *testing.T) {
|
|
23
|
+
fs, terminals := accessOptions(settings.Access{}, "/work")
|
|
24
|
+
if fs != nil || terminals != nil {
|
|
25
|
+
t.Fatalf("an empty Access granted filesystem=%+v terminals=%+v", fs, terminals)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestFilesystemDefaultsToTheWorkingDirectoryOnly(t *testing.T) {
|
|
30
|
+
var a settings.Access
|
|
31
|
+
a.Filesystem.Enabled = true
|
|
32
|
+
fs, terminals := accessOptions(a, "/work")
|
|
33
|
+
if terminals != nil {
|
|
34
|
+
t.Fatal("enabling the filesystem must not enable terminals")
|
|
35
|
+
}
|
|
36
|
+
if fs == nil || fs.Unrestricted || len(fs.Roots) != 1 || fs.Roots[0] != "/work" {
|
|
37
|
+
t.Fatalf("filesystem without roots must be confined to the working directory, got %+v", fs)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
a.Filesystem.Roots = []string{"/a", "/b"}
|
|
41
|
+
if fs, _ := accessOptions(a, "/work"); fs == nil || fs.Unrestricted || len(fs.Roots) != 2 || fs.Roots[0] != "/a" || fs.Roots[1] != "/b" {
|
|
42
|
+
t.Fatalf("configured roots must be used as given, got %+v", fs)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
a.Filesystem.Roots, a.Filesystem.Unrestricted = nil, true
|
|
46
|
+
if fs, _ := accessOptions(a, "/work"); fs == nil || !fs.Unrestricted {
|
|
47
|
+
t.Fatalf("unrestricted must be passed on only when stated, got %+v", fs)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func TestTerminalsOnlyWhenEnabled(t *testing.T) {
|
|
52
|
+
var a settings.Access
|
|
53
|
+
a.Terminals.Enabled = true
|
|
54
|
+
fs, terminals := accessOptions(a, "/work")
|
|
55
|
+
if fs != nil || terminals == nil {
|
|
56
|
+
t.Fatalf("terminals enabled alone: filesystem=%+v terminals=%+v", fs, terminals)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The default root, composed for real: a sibling of the working directory is refused.
|
|
61
|
+
func TestDefaultRootRefusesASiblingOfTheWorkingDirectory(t *testing.T) {
|
|
62
|
+
root := t.TempDir()
|
|
63
|
+
work, sibling := filepath.Join(root, "work"), filepath.Join(root, "secret.txt")
|
|
64
|
+
if err := os.MkdirAll(work, 0o755); err != nil {
|
|
65
|
+
t.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
for path, body := range map[string]string{filepath.Join(work, "in.txt"): "inside", sibling: "outside"} {
|
|
68
|
+
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
69
|
+
t.Fatal(err)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var a settings.Access
|
|
73
|
+
a.Filesystem.Enabled = true
|
|
74
|
+
fs, terminals := accessOptions(a, work)
|
|
75
|
+
if fs == nil {
|
|
76
|
+
t.Fatal("filesystem enabled but not composed")
|
|
77
|
+
}
|
|
78
|
+
built, err := compose.Build(compose.Options{
|
|
79
|
+
WorkingDirectory: work, Filesystem: fs, Terminals: terminals,
|
|
80
|
+
CreateBackend: func(*pi.LiveSession) (pi.Backend, error) { return nil, os.ErrInvalid },
|
|
81
|
+
})
|
|
82
|
+
if err != nil {
|
|
83
|
+
t.Fatal(err)
|
|
84
|
+
}
|
|
85
|
+
defer built.Close()
|
|
86
|
+
read := func(path string) error {
|
|
87
|
+
_, err := built.Resources.Read(context.Background(), ahptypes.ResourceReadParams{Uri: wire.PathToFileURI(path)})
|
|
88
|
+
return err
|
|
89
|
+
}
|
|
90
|
+
if err := read(filepath.Join(work, "in.txt")); err != nil {
|
|
91
|
+
t.Fatalf("a file in the working directory must be readable: %v", err)
|
|
92
|
+
}
|
|
93
|
+
err = read(sibling)
|
|
94
|
+
var e *wire.Error
|
|
95
|
+
if !errors.As(err, &e) || e.Code != wire.CodePermissionDenied {
|
|
96
|
+
t.Fatalf("a sibling of the working directory must be refused with PermissionDenied, got %v", err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func TestReplacementNotice(t *testing.T) {
|
|
101
|
+
// the listener is closed with the replaced session; say so unless --ahp brings it back
|
|
102
|
+
for _, reason := range []string{"new", "resume", "fork", "reload"} {
|
|
103
|
+
if n := replacementNotice(reason, true, false); n == "" {
|
|
104
|
+
t.Errorf("reason %q: a listener started with /ahp start closes silently", reason)
|
|
105
|
+
}
|
|
106
|
+
if n := replacementNotice(reason, true, true); n != "" {
|
|
107
|
+
t.Errorf("reason %q: --ahp restarts the listener, but got notice %q", reason, n)
|
|
108
|
+
}
|
|
109
|
+
if n := replacementNotice(reason, false, false); n != "" {
|
|
110
|
+
t.Errorf("reason %q: nothing was running, but got notice %q", reason, n)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if n := replacementNotice("quit", true, false); n != "" {
|
|
114
|
+
t.Errorf("quitting needs no notice, got %q", n)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Package ahp is a Go port of pi-ahp (Bang Lee / Qusic, MIT): a host for Microsoft's Agent
|
|
2
|
+
// Host Protocol that serves the running PiG session to remote AHP clients over WebSocket.
|
|
3
|
+
//
|
|
4
|
+
// The extension opens no listener unless the user asks: with the --ahp flag, or with /ahp start.
|
|
5
|
+
// Remote filesystem and terminal access are further opt-ins in the settings file.
|
|
6
|
+
package ahp
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"fmt"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const (
|
|
16
|
+
flagStart = "ahp"
|
|
17
|
+
flagSettings = "ahp-settings"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// Extension registers the extension. It starts no listener unless the user configured one.
|
|
21
|
+
func Extension() *sdk.Extension {
|
|
22
|
+
e := sdk.New("ahp")
|
|
23
|
+
rt := newRuntime()
|
|
24
|
+
|
|
25
|
+
e.Flag(flagStart, sdk.FlagOptions{Type: sdk.FlagBoolean, Description: "Start the AHP listener for this session (settings file: see /ahp)"})
|
|
26
|
+
e.Flag(flagSettings, sdk.FlagOptions{Type: sdk.FlagString, Description: "AHP settings file (default: <pig home>/ahp/settings.json)"})
|
|
27
|
+
|
|
28
|
+
rt.live.Register(e, rt.onSessionStart, rt.onSessionShutdown)
|
|
29
|
+
rt.live.OnModelChanged(rt.onModelChanged)
|
|
30
|
+
|
|
31
|
+
e.Command("ahp", "AHP host: /ahp [status|start|stop]", func(ctx sdk.Context, args string) error {
|
|
32
|
+
switch strings.TrimSpace(args) {
|
|
33
|
+
case "", "status":
|
|
34
|
+
ctx.Notify(rt.status(), "info")
|
|
35
|
+
case "start":
|
|
36
|
+
addr, err := rt.start(ctx)
|
|
37
|
+
if err != nil {
|
|
38
|
+
ctx.Notify("AHP: "+err.Error(), "error")
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
ctx.Notify(addr, "info")
|
|
42
|
+
case "stop":
|
|
43
|
+
if rt.stop() {
|
|
44
|
+
ctx.Notify("AHP listener stopped", "info")
|
|
45
|
+
} else {
|
|
46
|
+
ctx.Notify("AHP listener is not running", "info")
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return fmt.Errorf("usage: /ahp [status|start|stop]")
|
|
50
|
+
}
|
|
51
|
+
return nil
|
|
52
|
+
})
|
|
53
|
+
return e
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module github.com/MichaelKinsy/pigpen/ahp
|
|
2
|
+
|
|
3
|
+
go 1.26
|
|
4
|
+
|
|
5
|
+
// PiG resolves this requirement to the version-matched staged SDK at build time.
|
|
6
|
+
require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
|
|
7
|
+
|
|
8
|
+
// The Microsoft Agent Host Protocol Go types and reducers (MIT), vendored byte for byte.
|
|
9
|
+
// See third_party/agent-host-protocol-go/NOTICE-PIGPEN.md.
|
|
10
|
+
require github.com/microsoft/agent-host-protocol/clients/go v0.9.0
|
|
11
|
+
|
|
12
|
+
replace github.com/microsoft/agent-host-protocol/clients/go => ./third_party/agent-host-protocol-go
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
go 1.26
|
|
2
|
+
|
|
3
|
+
// Lists the vendored Microsoft AHP module so a fused Piglet Binary build (which does not honour
|
|
4
|
+
// replace directives of the extension's go.mod) resolves it locally. See third_party/.../NOTICE-PIGPEN.md.
|
|
5
|
+
use (
|
|
6
|
+
.
|
|
7
|
+
./third_party/agent-host-protocol-go
|
|
8
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
package channels
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"time"
|
|
6
|
+
|
|
7
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
8
|
+
|
|
9
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// InitialChatState is a fresh, idle chat (port of initialChatState in src/channels/chat.ts).
|
|
14
|
+
// Every session this host serves has exactly one chat, which is why the agent declares no
|
|
15
|
+
// `multipleChats` capability: its absence tells a client not to call `createChat`.
|
|
16
|
+
//
|
|
17
|
+
// A selection seeds the draft's model before the agent exists. Starting one takes seconds, and a
|
|
18
|
+
// client that subscribes in the meantime would otherwise find an empty model picker and be unable
|
|
19
|
+
// to send. The backend refines it once it is up.
|
|
20
|
+
func InitialChatState(uri, title string, selection *ahptypes.ModelSelection) *ahptypes.ChatState {
|
|
21
|
+
state := &ahptypes.ChatState{
|
|
22
|
+
Resource: uri,
|
|
23
|
+
Title: title,
|
|
24
|
+
Status: ahptypes.SessionStatusIdle,
|
|
25
|
+
ModifiedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
|
26
|
+
// A session's default chat exists because the user made the session.
|
|
27
|
+
Origin: &ahptypes.ChatOrigin{Value: &ahptypes.ChatUserOrigin{Kind: ahptypes.ChatOriginKindUser}},
|
|
28
|
+
Turns: []ahptypes.Turn{},
|
|
29
|
+
}
|
|
30
|
+
if selection != nil {
|
|
31
|
+
state.Draft = &ahptypes.Message{Text: "", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}, Model: selection}
|
|
32
|
+
}
|
|
33
|
+
return state
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ChatSummaryOf is the denormalised summary of a chat that its session carries.
|
|
37
|
+
func ChatSummaryOf(state *ahptypes.ChatState) ahptypes.ChatSummary {
|
|
38
|
+
return ahptypes.ChatSummary{
|
|
39
|
+
Resource: state.Resource, Title: state.Title, Status: state.Status, ModifiedAt: state.ModifiedAt,
|
|
40
|
+
Activity: state.Activity, Origin: state.Origin, Interactivity: state.Interactivity,
|
|
41
|
+
WorkingDirectories: state.WorkingDirectories,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// InstallDefaultChat creates a session's default chat and registers it in the session catalogue.
|
|
46
|
+
// The chat id is derived from the session id so the pairing is reconstructible without a lookup
|
|
47
|
+
// table.
|
|
48
|
+
func InstallDefaultChat(h *host.Host, sessionChannel, sessionID, title string, selection *ahptypes.ModelSelection) string {
|
|
49
|
+
uri := wire.ChatURI(sessionID)
|
|
50
|
+
h.Store().Create(uri, InitialChatState(uri, title, selection))
|
|
51
|
+
summary := ChatSummaryOf(h.Store().Chat(uri))
|
|
52
|
+
h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatAddedAction{Type: ahptypes.ActionTypeSessionChatAdded, Summary: summary}})
|
|
53
|
+
h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionDefaultChatChangedAction{Type: ahptypes.ActionTypeSessionDefaultChatChanged, DefaultChat: &uri}})
|
|
54
|
+
return uri
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func jsonEqual(a, b any) bool {
|
|
58
|
+
ja, _ := json.Marshal(a)
|
|
59
|
+
jb, _ := json.Marshal(b)
|
|
60
|
+
return string(ja) == string(jb)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func equalSummary(a, b ahptypes.ChatSummary) bool {
|
|
64
|
+
return a.Resource == b.Resource && a.Title == b.Title && a.Status == b.Status && a.ModifiedAt == b.ModifiedAt &&
|
|
65
|
+
jsonEqual(a.Activity, b.Activity) && jsonEqual(a.Origin, b.Origin) && jsonEqual(a.Interactivity, b.Interactivity) &&
|
|
66
|
+
jsonEqual(a.WorkingDirectories, b.WorkingDirectories)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// SyncChatSummary mirrors a chat's denormalised summary fields into its owning session.
|
|
70
|
+
//
|
|
71
|
+
// Partial updates cannot express removal of an optional JSON field: a nil value disappears on
|
|
72
|
+
// the wire and means "unchanged". The action's full-summary upsert form is used when one of those
|
|
73
|
+
// fields is cleared. It reports whether anything was dispatched.
|
|
74
|
+
func SyncChatSummary(h *host.Host, sessionChannel, chatChannel string) bool {
|
|
75
|
+
chat := h.Store().Chat(chatChannel)
|
|
76
|
+
session := h.Store().Session(sessionChannel)
|
|
77
|
+
if chat == nil || session == nil {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
var previous *ahptypes.ChatSummary
|
|
81
|
+
for i := range session.Chats {
|
|
82
|
+
if session.Chats[i].Resource == chatChannel {
|
|
83
|
+
previous = &session.Chats[i]
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if previous == nil {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
current := ChatSummaryOf(chat)
|
|
91
|
+
if equalSummary(*previous, current) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
clears := (previous.Activity != nil && current.Activity == nil) ||
|
|
95
|
+
(previous.Origin != nil && current.Origin == nil) ||
|
|
96
|
+
(previous.Interactivity != nil && current.Interactivity == nil) ||
|
|
97
|
+
(previous.WorkingDirectories != nil && current.WorkingDirectories == nil)
|
|
98
|
+
if clears {
|
|
99
|
+
h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatAddedAction{Type: ahptypes.ActionTypeSessionChatAdded, Summary: current}})
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
changes := ahptypes.PartialChatSummary{
|
|
103
|
+
Title: ¤t.Title, Status: ¤t.Status, Activity: current.Activity, ModifiedAt: ¤t.ModifiedAt,
|
|
104
|
+
Origin: current.Origin, WorkingDirectories: current.WorkingDirectories,
|
|
105
|
+
}
|
|
106
|
+
h.DispatchServerAction(sessionChannel, ahptypes.StateAction{Value: &ahptypes.SessionChatUpdatedAction{
|
|
107
|
+
Type: ahptypes.ActionTypeSessionChatUpdated, Chat: chatChannel, Changes: changes,
|
|
108
|
+
}})
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package channels
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
5
|
+
|
|
6
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
7
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// Catalogue notifications are ephemeral and never replayed on reconnect; after reconnecting a
|
|
11
|
+
// client re-fetches the catalogue with listSessions.
|
|
12
|
+
|
|
13
|
+
// NotifySessionAdded announces a session on the root channel.
|
|
14
|
+
func NotifySessionAdded(h *host.Host, summary ahptypes.SessionSummary) {
|
|
15
|
+
h.Notify(wire.RootChannel, "root/sessionAdded", map[string]any{"summary": summary})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// NotifySessionRemoved announces a removed session.
|
|
19
|
+
func NotifySessionRemoved(h *host.Host, session string) {
|
|
20
|
+
h.Notify(wire.RootChannel, "root/sessionRemoved", map[string]any{"session": session})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// NotifySessionSummaryChanged announces changed catalogue fields.
|
|
24
|
+
func NotifySessionSummaryChanged(h *host.Host, session string, changes ahptypes.PartialSessionSummary) {
|
|
25
|
+
h.Notify(wire.RootChannel, "root/sessionSummaryChanged", map[string]any{"session": session, "changes": changes})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// TerminalInfoOf is the root catalogue projection of a terminal; process handles never enter
|
|
29
|
+
// protocol state.
|
|
30
|
+
func TerminalInfoOf(resource string, state *ahptypes.TerminalState) ahptypes.TerminalInfo {
|
|
31
|
+
return ahptypes.TerminalInfo{Resource: resource, Title: state.Title, Claim: state.Claim, Lifecycle: state.Lifecycle}
|
|
32
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Package channels builds the protocol-shaped state of the session and chat channels. It
|
|
2
|
+
// ports pi-ahp's src/channels (root.ts, session.ts, chat.ts) and knows nothing about Pi or PiG.
|
|
3
|
+
package channels
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
10
|
+
|
|
11
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// InitialSessionState is a fresh session in lifecycle "creating".
|
|
15
|
+
func InitialSessionState(provider, title, workingDirectory string) *ahptypes.SessionState {
|
|
16
|
+
return &ahptypes.SessionState{
|
|
17
|
+
Provider: provider,
|
|
18
|
+
Title: title,
|
|
19
|
+
Status: ahptypes.SessionStatusIdle,
|
|
20
|
+
Lifecycle: ahptypes.SessionLifecycleCreating,
|
|
21
|
+
WorkingDirectories: []ahptypes.URI{wire.PathToFileURI(workingDirectory)},
|
|
22
|
+
ActiveClients: []ahptypes.SessionActiveClient{},
|
|
23
|
+
Chats: []ahptypes.ChatSummary{},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// statusActivityMask: the low five bits encode activity; higher bits carry independent metadata
|
|
28
|
+
// flags.
|
|
29
|
+
const statusActivityMask = ahptypes.SessionStatus(1<<5 - 1)
|
|
30
|
+
|
|
31
|
+
// SessionChatAggregate is what a session derives from its chat catalogue.
|
|
32
|
+
type SessionChatAggregate struct {
|
|
33
|
+
Status ahptypes.SessionStatus
|
|
34
|
+
Activity *string
|
|
35
|
+
ModifiedAt string // "" when the session has no chats
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// instant parses an ISO 8601 timestamp; an unparsable one sorts before every valid one.
|
|
39
|
+
func instant(summary ahptypes.ChatSummary) (t time.Time, ok bool) {
|
|
40
|
+
for _, layout := range []string{time.RFC3339Nano, "2006-01-02T15:04:05.999999999Z0700", "2006-01-02T15:04:05Z0700"} {
|
|
41
|
+
if parsed, err := time.Parse(layout, summary.ModifiedAt); err == nil {
|
|
42
|
+
return parsed, true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return time.Time{}, false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func newer(a, b ahptypes.ChatSummary) bool { // a is strictly more recent than b
|
|
49
|
+
ta, oka := instant(a)
|
|
50
|
+
tb, okb := instant(b)
|
|
51
|
+
switch {
|
|
52
|
+
case !oka:
|
|
53
|
+
return false
|
|
54
|
+
case !okb:
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
return ta.After(tb)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func mostRecent(chats []ahptypes.ChatSummary, matches func(ahptypes.ChatSummary) bool) (ahptypes.ChatSummary, bool) {
|
|
61
|
+
var latest ahptypes.ChatSummary
|
|
62
|
+
found := false
|
|
63
|
+
for _, chat := range chats {
|
|
64
|
+
if matches(chat) && (!found || newer(chat, latest)) {
|
|
65
|
+
latest, found = chat, true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return latest, found
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// AggregateSessionChats projects the session-level summary fields AHP derives from its chat
|
|
72
|
+
// catalogue. The current host exposes one chat, so this is a pass-through today; keeping the
|
|
73
|
+
// complete 0.9 rule makes the projection deterministic. It never mutates the state.
|
|
74
|
+
func AggregateSessionChats(state *ahptypes.SessionState) SessionChatAggregate {
|
|
75
|
+
if len(state.Chats) == 0 {
|
|
76
|
+
return SessionChatAggregate{Status: state.Status, Activity: state.Activity}
|
|
77
|
+
}
|
|
78
|
+
latest, ok := mostRecent(state.Chats, func(ahptypes.ChatSummary) bool { return true })
|
|
79
|
+
if !ok {
|
|
80
|
+
return SessionChatAggregate{Status: state.Status}
|
|
81
|
+
}
|
|
82
|
+
driver := latest
|
|
83
|
+
if state.DefaultChat != nil {
|
|
84
|
+
for _, chat := range state.Chats {
|
|
85
|
+
if chat.Resource == *state.DefaultChat {
|
|
86
|
+
driver = chat
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if c, ok := mostRecent(state.Chats, func(c ahptypes.ChatSummary) bool {
|
|
92
|
+
return c.Status&ahptypes.SessionStatusInputNeeded == ahptypes.SessionStatusInputNeeded
|
|
93
|
+
}); ok {
|
|
94
|
+
driver = c
|
|
95
|
+
} else if c, ok := mostRecent(state.Chats, func(c ahptypes.ChatSummary) bool {
|
|
96
|
+
return c.Status&ahptypes.SessionStatusError == ahptypes.SessionStatusError
|
|
97
|
+
}); ok {
|
|
98
|
+
driver = c
|
|
99
|
+
}
|
|
100
|
+
metadata := state.Status &^ statusActivityMask
|
|
101
|
+
return SessionChatAggregate{
|
|
102
|
+
Status: metadata | (driver.Status & statusActivityMask),
|
|
103
|
+
Activity: driver.Activity,
|
|
104
|
+
ModifiedAt: latest.ModifiedAt,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SessionSummaryOf is the root-catalogue projection of a session.
|
|
109
|
+
func SessionSummaryOf(resource, createdAt string, state *ahptypes.SessionState, meta map[string]json.RawMessage) ahptypes.SessionSummary {
|
|
110
|
+
aggregate := AggregateSessionChats(state)
|
|
111
|
+
modifiedAt := aggregate.ModifiedAt
|
|
112
|
+
if modifiedAt == "" {
|
|
113
|
+
modifiedAt = createdAt
|
|
114
|
+
}
|
|
115
|
+
summary := ahptypes.SessionSummary{
|
|
116
|
+
Resource: resource, Provider: state.Provider, Title: state.Title, Status: aggregate.Status,
|
|
117
|
+
// Root summary activity is deliberately omitted for AHP 0.9: its partial update shape
|
|
118
|
+
// cannot distinguish "clear" from "unchanged" on the JSON wire.
|
|
119
|
+
CreatedAt: createdAt, ModifiedAt: modifiedAt,
|
|
120
|
+
}
|
|
121
|
+
if state.WorkingDirectories != nil {
|
|
122
|
+
summary.WorkingDirectories = state.WorkingDirectories
|
|
123
|
+
}
|
|
124
|
+
if meta != nil {
|
|
125
|
+
summary.Meta = meta
|
|
126
|
+
}
|
|
127
|
+
return summary
|
|
128
|
+
}
|