@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts supported AHP user-message attachments into pi's prompt shape.
|
|
3
|
+
* Resource references become paths or URIs, simple attachments contribute their
|
|
4
|
+
* model representation, embedded text is decoded as UTF-8, and embedded images
|
|
5
|
+
* become pi image-content blocks. Other attachment kinds remain unsupported.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
10
|
+
import {
|
|
11
|
+
type Message,
|
|
12
|
+
MessageAttachmentKind,
|
|
13
|
+
type MessageEmbeddedResourceAttachment,
|
|
14
|
+
type MessageResourceAttachment,
|
|
15
|
+
type TextRange,
|
|
16
|
+
} from "@microsoft/agent-host-protocol";
|
|
17
|
+
import { normalizeImageMimeType } from "./image-mime.ts";
|
|
18
|
+
|
|
19
|
+
export interface PiMessageInput {
|
|
20
|
+
readonly text: string;
|
|
21
|
+
readonly images?: ImageContent[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type RejectedMessage = { readonly rejectionReason: string };
|
|
25
|
+
type PreparedMessage = PiMessageInput | RejectedMessage;
|
|
26
|
+
type PreparedText = { readonly text: string } | RejectedMessage;
|
|
27
|
+
type PreparedEmbedded = { readonly text: string } | { readonly image: ImageContent } | RejectedMessage;
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
34
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isTextRange(value: unknown): value is TextRange {
|
|
38
|
+
if (!isRecord(value) || !isRecord(value.start) || !isRecord(value.end)) return false;
|
|
39
|
+
return [value.start.line, value.start.character, value.end.line, value.end.character].every(isNonNegativeInteger);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function decodeBase64(data: unknown): Buffer | undefined {
|
|
43
|
+
if (typeof data !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/u.test(data) || data.length % 4 === 1) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const padding = data.indexOf("=");
|
|
47
|
+
if (padding !== -1 && (padding < data.length - 2 || data.length % 4 !== 0)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const decoded = Buffer.from(data, "base64");
|
|
51
|
+
const canonical = decoded.toString("base64").replace(/=+$/u, "");
|
|
52
|
+
return canonical === data.replace(/=+$/u, "") ? decoded : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rangeText(range: TextRange): string {
|
|
56
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function baseContentType(contentType: string): string {
|
|
60
|
+
return contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function embeddedContent(attachment: MessageEmbeddedResourceAttachment): PreparedEmbedded {
|
|
64
|
+
if (typeof attachment.contentType !== "string") {
|
|
65
|
+
return { rejectionReason: `Embedded resource ${attachment.label} requires a content type` };
|
|
66
|
+
}
|
|
67
|
+
const contentType = baseContentType(attachment.contentType);
|
|
68
|
+
if (!contentType) {
|
|
69
|
+
return { rejectionReason: `Embedded resource ${attachment.label} requires a content type` };
|
|
70
|
+
}
|
|
71
|
+
const bytes = decodeBase64(attachment.data);
|
|
72
|
+
if (!bytes) {
|
|
73
|
+
return { rejectionReason: `Embedded resource ${attachment.label} is not valid base64` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const imageMimeType = normalizeImageMimeType(contentType);
|
|
77
|
+
if (imageMimeType) {
|
|
78
|
+
if (bytes.length === 0) {
|
|
79
|
+
return { rejectionReason: `Embedded image ${attachment.label} is empty` };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
image: {
|
|
83
|
+
type: "image",
|
|
84
|
+
data: bytes.toString("base64"),
|
|
85
|
+
mimeType: imageMimeType,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MIME is advisory for non-images: successful UTF-8 decoding decides whether
|
|
91
|
+
// an embedded resource can safely join pi's text prompt.
|
|
92
|
+
let decoded: string;
|
|
93
|
+
try {
|
|
94
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
95
|
+
} catch {
|
|
96
|
+
return { rejectionReason: `Embedded resource ${attachment.label} is not valid UTF-8` };
|
|
97
|
+
}
|
|
98
|
+
const selected = attachment.selection?.range;
|
|
99
|
+
if (!selected) {
|
|
100
|
+
return { text: decoded };
|
|
101
|
+
}
|
|
102
|
+
return { text: `[selection ${rangeText(selected)}]\n${decoded}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resourceText(attachment: MessageResourceAttachment): PreparedText {
|
|
106
|
+
if (typeof attachment.uri !== "string") {
|
|
107
|
+
return { rejectionReason: "A resource attachment requires a URI" };
|
|
108
|
+
}
|
|
109
|
+
const selected = attachment.selection?.range;
|
|
110
|
+
let reference = attachment.uri;
|
|
111
|
+
if (/^file:/iu.test(reference)) {
|
|
112
|
+
try {
|
|
113
|
+
reference = fileURLToPath(reference);
|
|
114
|
+
} catch {
|
|
115
|
+
// Preserve a remote or malformed file URI rather than guessing a path.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { text: `${reference}${selected ? `:${rangeText(selected)}` : ""}` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function prepareMessage(value: unknown): PreparedMessage {
|
|
122
|
+
if (
|
|
123
|
+
!isRecord(value) ||
|
|
124
|
+
typeof value.text !== "string" ||
|
|
125
|
+
!isRecord(value.origin) ||
|
|
126
|
+
typeof value.origin.kind !== "string"
|
|
127
|
+
) {
|
|
128
|
+
return { rejectionReason: "A message requires text and an origin" };
|
|
129
|
+
}
|
|
130
|
+
if (value.attachments !== undefined && !Array.isArray(value.attachments)) {
|
|
131
|
+
return { rejectionReason: "Message attachments must be an array" };
|
|
132
|
+
}
|
|
133
|
+
if (value.model !== undefined && (!isRecord(value.model) || typeof value.model.id !== "string")) {
|
|
134
|
+
return { rejectionReason: "A message model requires an id" };
|
|
135
|
+
}
|
|
136
|
+
if (value.agent !== undefined) {
|
|
137
|
+
return { rejectionReason: "This host does not support custom agents" };
|
|
138
|
+
}
|
|
139
|
+
const message = value as unknown as Message;
|
|
140
|
+
|
|
141
|
+
const representations: string[] = [];
|
|
142
|
+
const images: ImageContent[] = [];
|
|
143
|
+
for (const attachment of message.attachments ?? []) {
|
|
144
|
+
if (!isRecord(attachment) || typeof attachment.type !== "string" || typeof attachment.label !== "string") {
|
|
145
|
+
return { rejectionReason: "Every message attachment requires a type and label" };
|
|
146
|
+
}
|
|
147
|
+
if (attachment.range !== undefined && !isTextRange(attachment.range)) {
|
|
148
|
+
return { rejectionReason: `Attachment ${attachment.label} has an invalid text range` };
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
"selection" in attachment &&
|
|
152
|
+
attachment.selection !== undefined &&
|
|
153
|
+
(!isRecord(attachment.selection) || !isTextRange(attachment.selection.range))
|
|
154
|
+
) {
|
|
155
|
+
return { rejectionReason: `Attachment ${attachment.label} has an invalid text selection` };
|
|
156
|
+
}
|
|
157
|
+
switch (attachment.type) {
|
|
158
|
+
case MessageAttachmentKind.Resource: {
|
|
159
|
+
const resource = resourceText(attachment);
|
|
160
|
+
if ("rejectionReason" in resource) {
|
|
161
|
+
return resource;
|
|
162
|
+
}
|
|
163
|
+
representations.push(resource.text);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case MessageAttachmentKind.Simple:
|
|
167
|
+
if (typeof attachment.modelRepresentation !== "string") {
|
|
168
|
+
return { rejectionReason: "A simple attachment requires modelRepresentation" };
|
|
169
|
+
}
|
|
170
|
+
if (attachment.modelRepresentation) {
|
|
171
|
+
representations.push(attachment.modelRepresentation);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case MessageAttachmentKind.EmbeddedResource: {
|
|
175
|
+
const embedded = embeddedContent(attachment);
|
|
176
|
+
if ("rejectionReason" in embedded) {
|
|
177
|
+
return embedded;
|
|
178
|
+
}
|
|
179
|
+
if ("image" in embedded) {
|
|
180
|
+
images.push(embedded.image);
|
|
181
|
+
} else {
|
|
182
|
+
representations.push(embedded.text);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
return { rejectionReason: `This host does not support ${attachment.type} attachments` };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
text: [message.text, ...representations].filter(Boolean).join("\n\n"),
|
|
193
|
+
...(images.length > 0 ? { images } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function messageRejectionReason(message: unknown): string | undefined {
|
|
198
|
+
const prepared = prepareMessage(message);
|
|
199
|
+
return "rejectionReason" in prepared ? prepared.rejectionReason : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function messageInputForPi(message: Message): PiMessageInput {
|
|
203
|
+
const prepared = prepareMessage(message);
|
|
204
|
+
if ("rejectionReason" in prepared) {
|
|
205
|
+
throw new Error(prepared.rejectionReason);
|
|
206
|
+
}
|
|
207
|
+
return prepared;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function messageTextForPi(message: Message): string {
|
|
211
|
+
return messageInputForPi(message).text;
|
|
212
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps pi's model catalogue onto the protocol's agent/model description.
|
|
3
|
+
*
|
|
4
|
+
* The host advertises exactly one agent — pi — with the models its current
|
|
5
|
+
* runtime reports as available. Model-specific options such as pi's thinking
|
|
6
|
+
* level travel as a `configSchema`, the protocol's escape hatch for this:
|
|
7
|
+
* clients render it as a form and pass the values back in `ModelSelection`.
|
|
8
|
+
*
|
|
9
|
+
* @see https://microsoft.github.io/agent-host-protocol/specification/root-channel
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getSupportedThinkingLevels, type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
13
|
+
import type { AgentInfo, ConfigSchema, SessionModelInfo } from "@microsoft/agent-host-protocol";
|
|
14
|
+
import { PI_PROVIDER } from "./provider.ts";
|
|
15
|
+
|
|
16
|
+
export type ThinkingLevel = ModelThinkingLevel;
|
|
17
|
+
|
|
18
|
+
interface PiModelIdentity {
|
|
19
|
+
readonly provider: string;
|
|
20
|
+
readonly id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** AHP needs one opaque id, so use pi's canonical `provider/modelId` reference. */
|
|
24
|
+
export function modelSelectionId(model: PiModelIdentity): string {
|
|
25
|
+
return `${model.provider}/${model.id}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a wire id, with a narrow fallback for drafts written by older hosts.
|
|
30
|
+
* An ambiguous legacy id is accepted only when it already names the current
|
|
31
|
+
* model; otherwise guessing a provider would run the turn somewhere unintended.
|
|
32
|
+
*/
|
|
33
|
+
export function findModelBySelectionId<T extends PiModelIdentity>(
|
|
34
|
+
models: readonly T[],
|
|
35
|
+
selectionId: string,
|
|
36
|
+
current?: PiModelIdentity,
|
|
37
|
+
): T | undefined {
|
|
38
|
+
const qualified = models.find((model) => modelSelectionId(model) === selectionId);
|
|
39
|
+
if (qualified) {
|
|
40
|
+
return qualified;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const legacyMatches = models.filter((model) => model.id === selectionId);
|
|
44
|
+
if (legacyMatches.length === 1) {
|
|
45
|
+
return legacyMatches[0];
|
|
46
|
+
}
|
|
47
|
+
if (current?.id === selectionId) {
|
|
48
|
+
return legacyMatches.find((model) => model.provider === current.provider);
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The config key carrying the thinking level in `ModelSelection.config`. */
|
|
54
|
+
export const THINKING_CONFIG_KEY = "thinkingLevel";
|
|
55
|
+
|
|
56
|
+
/** Uses pi's own capability rules so the advertised picker cannot drift. */
|
|
57
|
+
export function supportedThinkingLevels(model: Model<never>): ThinkingLevel[] {
|
|
58
|
+
return getSupportedThinkingLevels(model);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function thinkingConfigSchema(model: Model<never>): ConfigSchema | undefined {
|
|
62
|
+
const levels = supportedThinkingLevels(model);
|
|
63
|
+
// A model with only `off` has nothing to configure; omitting the schema
|
|
64
|
+
// keeps the client from rendering a pointless single-option picker.
|
|
65
|
+
if (levels.length <= 1) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
[THINKING_CONFIG_KEY]: {
|
|
72
|
+
type: "string",
|
|
73
|
+
title: "Thinking",
|
|
74
|
+
description: "How much reasoning effort the model spends before answering.",
|
|
75
|
+
default: levels.includes("medium") ? "medium" : levels[0],
|
|
76
|
+
enum: [...levels],
|
|
77
|
+
enumLabels: levels.map((level) => level.charAt(0).toUpperCase() + level.slice(1)),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toSessionModelInfo(model: Model<never>): SessionModelInfo {
|
|
84
|
+
const configSchema = thinkingConfigSchema(model);
|
|
85
|
+
return {
|
|
86
|
+
id: modelSelectionId(model),
|
|
87
|
+
provider: PI_PROVIDER,
|
|
88
|
+
name: model.name,
|
|
89
|
+
maxContextWindow: model.contextWindow,
|
|
90
|
+
maxOutputTokens: model.maxTokens,
|
|
91
|
+
supportsVision: model.input.includes("image"),
|
|
92
|
+
...(configSchema ? { configSchema } : {}),
|
|
93
|
+
// `_meta` is the protocol's documented place for provider-specific
|
|
94
|
+
// extras; clients may surface pricing but must not depend on it.
|
|
95
|
+
_meta: {
|
|
96
|
+
piProvider: model.provider,
|
|
97
|
+
api: model.api,
|
|
98
|
+
pricing: model.cost,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds the single `AgentInfo` this host advertises.
|
|
105
|
+
*
|
|
106
|
+
* Empty `protectedResources`: pi resolves model-provider credentials itself,
|
|
107
|
+
* so there is nothing for the client to authenticate against at the protocol
|
|
108
|
+
* level. Keeping the empty list explicit also distinguishes that settled state
|
|
109
|
+
* for clients that conservatively treat an omitted field as unresolved.
|
|
110
|
+
*
|
|
111
|
+
* No `capabilities`: this host serves one chat per session and one working
|
|
112
|
+
* directory, so neither `multipleChats` nor `multipleWorkingDirectories` is
|
|
113
|
+
* declared. Their absence is what tells a client not to attempt those calls.
|
|
114
|
+
* Vision is model-specific instead and is published on each model above.
|
|
115
|
+
*/
|
|
116
|
+
export function buildAgentInfo(models: readonly Model<never>[]): AgentInfo {
|
|
117
|
+
return {
|
|
118
|
+
provider: PI_PROVIDER,
|
|
119
|
+
displayName: "pi",
|
|
120
|
+
description: "pi coding agent",
|
|
121
|
+
models: models.map(toSessionModelInfo),
|
|
122
|
+
protectedResources: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project trust.
|
|
3
|
+
*
|
|
4
|
+
* A working directory can carry trust-gated pi resources: `.pi` settings,
|
|
5
|
+
* extensions, skills, prompts, themes and system instructions, plus inherited
|
|
6
|
+
* `.agents/skills`. Loading some of them executes project-controlled code or
|
|
7
|
+
* changes the agent's instructions.
|
|
8
|
+
*
|
|
9
|
+
* The interactive CLI resolves trust before loading those resources. Raw SDK
|
|
10
|
+
* construction instead defaults `projectTrusted` to true, so an embedding host
|
|
11
|
+
* must supply its decision explicitly. That matters here because
|
|
12
|
+
* `createSession` accepts the working directory from a remote client.
|
|
13
|
+
*
|
|
14
|
+
* The default `trust` policy preserves the SDK default. The stricter `inherit`
|
|
15
|
+
* policy reuses pi's trust store, so decisions made by either program apply to
|
|
16
|
+
* both.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What to do about a working directory pi has no recorded decision for.
|
|
23
|
+
*
|
|
24
|
+
* - `trust` (default) — load project resources unconditionally, matching raw
|
|
25
|
+
* SDK construction.
|
|
26
|
+
* - `inherit` — honour pi's trust store; decline for a directory it has no
|
|
27
|
+
* decision for.
|
|
28
|
+
* - `never` — never load project resources.
|
|
29
|
+
*/
|
|
30
|
+
export type ProjectTrustPolicy = "trust" | "inherit" | "never";
|
|
31
|
+
|
|
32
|
+
export interface TrustDecision {
|
|
33
|
+
readonly trusted: boolean;
|
|
34
|
+
/** Why, so a client can explain it rather than silently degrade. */
|
|
35
|
+
readonly reason: "no-project-resources" | "user-trusted" | "user-untrusted" | "policy" | "unknown-project";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveProjectTrust(
|
|
39
|
+
cwd: string,
|
|
40
|
+
policy: ProjectTrustPolicy = "trust",
|
|
41
|
+
agentDir: string = getAgentDir(),
|
|
42
|
+
): TrustDecision {
|
|
43
|
+
if (policy === "never") {
|
|
44
|
+
return { trusted: false, reason: "policy" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Nothing in the directory needs gating — trusting it grants nothing.
|
|
48
|
+
if (!hasTrustRequiringProjectResources(cwd)) {
|
|
49
|
+
return { trusted: true, reason: "no-project-resources" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (policy === "trust") {
|
|
53
|
+
return { trusted: true, reason: "policy" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The very same store pi's CLI writes, so decisions carry both ways.
|
|
57
|
+
const stored = new ProjectTrustStore(agentDir).get(cwd);
|
|
58
|
+
if (stored === true) {
|
|
59
|
+
return { trusted: true, reason: "user-trusted" };
|
|
60
|
+
}
|
|
61
|
+
if (stored === false) {
|
|
62
|
+
return { trusted: false, reason: "user-untrusted" };
|
|
63
|
+
}
|
|
64
|
+
// No recorded decision, and a host has no user to ask.
|
|
65
|
+
return { trusted: false, reason: "unknown-project" };
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Shared file-URI parsing and root policy for resource commands and watches. */
|
|
2
|
+
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { lstat, readlink, realpath } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { AhpErrorCodes } from "@microsoft/agent-host-protocol";
|
|
8
|
+
import { ProtocolError } from "../protocol/errors.ts";
|
|
9
|
+
|
|
10
|
+
function errorCodeOf(error: unknown): string | undefined {
|
|
11
|
+
return (error as NodeJS.ErrnoException | undefined)?.code;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolves symlinks in the existing prefix while preserving a missing suffix. */
|
|
15
|
+
async function canonicalPath(path: string): Promise<string> {
|
|
16
|
+
const absolute = resolve(path);
|
|
17
|
+
try {
|
|
18
|
+
return await realpath(absolute);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (errorCodeOf(error) !== "ENOENT") throw error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if ((await lstat(absolute)).isSymbolicLink()) {
|
|
25
|
+
const target = await readlink(absolute);
|
|
26
|
+
return canonicalPath(resolve(dirname(absolute), target));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (errorCodeOf(error) !== "ENOENT") throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parent = dirname(absolute);
|
|
33
|
+
return parent === absolute ? absolute : join(await canonicalPath(parent), basename(absolute));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts resource URIs to local paths and applies the optional root allowlist.
|
|
38
|
+
*
|
|
39
|
+
* A single policy instance can be shared by request/response resource methods
|
|
40
|
+
* and live resource watches, keeping both surfaces on the same access rules.
|
|
41
|
+
* Empty roots intentionally mean unrestricted access: reaching this endpoint
|
|
42
|
+
* already permits starting pi with shell tools, so this is not a sandbox.
|
|
43
|
+
*/
|
|
44
|
+
export class ResourcePathPolicy {
|
|
45
|
+
readonly #roots: readonly string[];
|
|
46
|
+
|
|
47
|
+
constructor(roots: readonly string[] = []) {
|
|
48
|
+
this.#roots = roots.map((root) => realpathSync(resolve(root)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pathFor(uri: unknown): Promise<string> {
|
|
52
|
+
if (typeof uri !== "string" || !uri.startsWith("file://")) {
|
|
53
|
+
throw ProtocolError.invalidParams(`Only file: URIs are supported, got: ${uri}`);
|
|
54
|
+
}
|
|
55
|
+
let path: string;
|
|
56
|
+
try {
|
|
57
|
+
path = fileURLToPath(uri);
|
|
58
|
+
} catch {
|
|
59
|
+
throw ProtocolError.invalidParams(`Malformed file URI: ${uri}`);
|
|
60
|
+
}
|
|
61
|
+
if (this.#roots.length === 0) {
|
|
62
|
+
return path;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let candidate: string;
|
|
66
|
+
try {
|
|
67
|
+
candidate = await canonicalPath(path);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof ProtocolError) throw error;
|
|
70
|
+
throw new ProtocolError(AhpErrorCodes.PermissionDenied, error instanceof Error ? error.message : String(error));
|
|
71
|
+
}
|
|
72
|
+
const permitted = this.#roots.some(
|
|
73
|
+
(root) => candidate === root || candidate.startsWith(root.endsWith(sep) ? root : root + sep),
|
|
74
|
+
);
|
|
75
|
+
if (!permitted) {
|
|
76
|
+
throw new ProtocolError(AhpErrorCodes.PermissionDenied, `Outside the permitted roots: ${uri}`);
|
|
77
|
+
}
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
}
|