@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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deleting a pi session file.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors pi's own delete affordance (`/resume` → Ctrl+D): try the `trash` CLI
|
|
5
|
+
* first so the file lands in the recycle bin, and fall back to `unlink` when
|
|
6
|
+
* `trash` is unavailable or fails. Disposal is destructive either way, so
|
|
7
|
+
* preferring `trash` matters.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { lstatSync, unlinkSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
export interface DeleteResult {
|
|
14
|
+
readonly ok: boolean;
|
|
15
|
+
readonly method: "trash" | "unlink" | "missing";
|
|
16
|
+
readonly error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isMissing(path: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
lstatSync(path);
|
|
22
|
+
return false;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
25
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function deleteSessionFile(path: string): DeleteResult {
|
|
30
|
+
if (isMissing(path)) {
|
|
31
|
+
return { ok: true, method: "missing" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `--` guards against a path that looks like a flag.
|
|
35
|
+
const args = path.startsWith("-") ? ["--", path] : [path];
|
|
36
|
+
spawnSync("trash", args, { encoding: "utf-8" });
|
|
37
|
+
// Process status alone is not enough for the disposal contract: a wrapper or
|
|
38
|
+
// broken installation can exit successfully without moving the file.
|
|
39
|
+
if (isMissing(path)) {
|
|
40
|
+
return { ok: true, method: "trash" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(path);
|
|
45
|
+
return { ok: true, method: "unlink" };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Another process deleting the same session is still the desired outcome.
|
|
48
|
+
if (isMissing(path)) {
|
|
49
|
+
return { ok: true, method: "missing" };
|
|
50
|
+
}
|
|
51
|
+
return { ok: false, method: "unlink", error: error instanceof Error ? error.message : String(error) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates pi's agent event stream into chat-channel actions.
|
|
3
|
+
*
|
|
4
|
+
* This is the load-bearing piece of the whole host, and it is deliberately a
|
|
5
|
+
* **pure function of the event stream**: feed it a recorded `AgentSessionEvent[]`
|
|
6
|
+
* and it yields the `StateAction[]` a client would have seen. No sockets, no
|
|
7
|
+
* model, no clock. That is what makes the turn semantics testable.
|
|
8
|
+
*
|
|
9
|
+
* Three structural mismatches it has to bridge:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Turn granularity.** pi nests three levels — an agent *run*
|
|
12
|
+
* (`agent_start`…`agent_end`), a pi *turn* (one assistant message), and
|
|
13
|
+
* content blocks. AHP has two — a turn (one user message through to
|
|
14
|
+
* completion) and its `responseParts`. So one AHP turn spans everything from
|
|
15
|
+
* `prompt()` to `agent_settled`, which may contain several agent runs
|
|
16
|
+
* (auto-retry, post-compaction retry) and several assistant messages. Mapping
|
|
17
|
+
* `agent_end` to `chat/turnComplete` would close the turn early, after which
|
|
18
|
+
* every later action is silently dropped by the reducer.
|
|
19
|
+
*
|
|
20
|
+
* 2. **Part identity.** pi's `contentIndex` restarts at 0 for every assistant
|
|
21
|
+
* message and the stream carries no message id, so `contentIndex` alone
|
|
22
|
+
* collides across messages within a turn. Parts are keyed by
|
|
23
|
+
* `<turnId>:<message ordinal>:<contentIndex>` instead.
|
|
24
|
+
*
|
|
25
|
+
* 3. **Part creation.** `chat/delta` *appends* to a part that must already
|
|
26
|
+
* exist; targeting an unknown `partId` is a silent no-op. Parts are therefore
|
|
27
|
+
* created lazily, on the first content that lands in them.
|
|
28
|
+
*
|
|
29
|
+
* @see https://microsoft.github.io/agent-host-protocol/specification/chat-channel
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import {
|
|
34
|
+
ActionType,
|
|
35
|
+
MessageKind,
|
|
36
|
+
ResponsePartKind,
|
|
37
|
+
type StateAction,
|
|
38
|
+
ToolCallConfirmationReason,
|
|
39
|
+
type ToolCallResult,
|
|
40
|
+
type ToolResultContent,
|
|
41
|
+
ToolResultContentType,
|
|
42
|
+
type UsageInfo,
|
|
43
|
+
} from "@microsoft/agent-host-protocol";
|
|
44
|
+
import {
|
|
45
|
+
describeFinishedToolCall,
|
|
46
|
+
describeToolCall,
|
|
47
|
+
RESPONDING_ACTIVITY,
|
|
48
|
+
THINKING_ACTIVITY,
|
|
49
|
+
toolInputFor,
|
|
50
|
+
} from "./activity.ts";
|
|
51
|
+
import { userMessageFromPiContent } from "./user-message.ts";
|
|
52
|
+
|
|
53
|
+
/** How a turn ended, decided from the last assistant message's stop reason. */
|
|
54
|
+
type TurnOutcome = "complete" | "cancelled" | "error";
|
|
55
|
+
|
|
56
|
+
interface OpenPart {
|
|
57
|
+
readonly partId: string;
|
|
58
|
+
readonly kind: ResponsePartKind.Markdown | ResponsePartKind.Reasoning;
|
|
59
|
+
/** Whether `chat/responsePart` has been emitted for it yet. */
|
|
60
|
+
created: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Minimal shape of pi's streaming deltas; pi types these per provider. */
|
|
64
|
+
interface AssistantDelta {
|
|
65
|
+
readonly type: string;
|
|
66
|
+
readonly contentIndex?: number;
|
|
67
|
+
readonly delta?: string;
|
|
68
|
+
/** Final text of a block, carried on `text_end` / `thinking_end`. */
|
|
69
|
+
readonly content?: string;
|
|
70
|
+
readonly toolCall?: { id?: string; name?: string; arguments?: unknown };
|
|
71
|
+
readonly partial?: { content?: unknown[] };
|
|
72
|
+
readonly message?: { usage?: PiUsage; stopReason?: string };
|
|
73
|
+
readonly error?: { errorMessage?: string; stopReason?: string };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface PiUsage {
|
|
77
|
+
readonly input?: number;
|
|
78
|
+
readonly output?: number;
|
|
79
|
+
readonly cacheRead?: number;
|
|
80
|
+
readonly cacheWrite?: number;
|
|
81
|
+
/** Subset of `output`, when the provider reports a reasoning breakdown. */
|
|
82
|
+
readonly reasoning?: number;
|
|
83
|
+
readonly totalTokens?: number;
|
|
84
|
+
readonly cost?: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toUsageInfo(usage: PiUsage | undefined, model: string | undefined): UsageInfo | undefined {
|
|
88
|
+
if (!usage) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// `UsageInfo` only has slots for input/output/cacheRead, but pi reports more
|
|
92
|
+
// (cache writes, a reasoning-token breakdown, and computed cost). Dropping
|
|
93
|
+
// them would lose the numbers a client needs to show what a turn actually
|
|
94
|
+
// cost, so the remainder rides in `_meta` — the protocol's documented place
|
|
95
|
+
// for provider-specific extras.
|
|
96
|
+
const extra: Record<string, unknown> = {};
|
|
97
|
+
if (usage.cacheWrite !== undefined) {
|
|
98
|
+
extra.cacheWriteTokens = usage.cacheWrite;
|
|
99
|
+
}
|
|
100
|
+
if (usage.reasoning !== undefined) {
|
|
101
|
+
extra.reasoningTokens = usage.reasoning;
|
|
102
|
+
}
|
|
103
|
+
if (usage.totalTokens !== undefined) {
|
|
104
|
+
extra.totalTokens = usage.totalTokens;
|
|
105
|
+
}
|
|
106
|
+
if (usage.cost !== undefined) {
|
|
107
|
+
extra.cost = usage.cost;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
...(usage.input !== undefined ? { inputTokens: usage.input } : {}),
|
|
112
|
+
...(usage.output !== undefined ? { outputTokens: usage.output } : {}),
|
|
113
|
+
...(usage.cacheRead !== undefined ? { cacheReadTokens: usage.cacheRead } : {}),
|
|
114
|
+
...(model ? { model } : {}),
|
|
115
|
+
...(Object.keys(extra).length > 0 ? { _meta: extra } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function textContent(text: string): ToolResultContent[] {
|
|
120
|
+
return [{ type: ToolResultContentType.Text, text }];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The unified diff pi computes for an edit, if this result carries one.
|
|
125
|
+
*
|
|
126
|
+
* `edit` is the only built-in tool that reports structured details, and its
|
|
127
|
+
* text says how many blocks were replaced — a count the client cannot render
|
|
128
|
+
* or check. The patch beside it is the part worth showing, in the same
|
|
129
|
+
* monospace block a shell command gets.
|
|
130
|
+
*/
|
|
131
|
+
function editPatch(result: unknown): string | undefined {
|
|
132
|
+
const patch = (result as { details?: { patch?: unknown } } | undefined)?.details?.patch;
|
|
133
|
+
return typeof patch === "string" && patch.length > 0 ? patch : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Flattens pi's tool result content blocks into the protocol's text blocks. */
|
|
137
|
+
function toolResultContent(result: unknown): ToolResultContent[] | undefined {
|
|
138
|
+
const blocks = (result as { content?: unknown[] } | undefined)?.content;
|
|
139
|
+
if (!Array.isArray(blocks)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const texts = blocks
|
|
143
|
+
.filter((block): block is { type: "text"; text: string } => (block as { type?: string })?.type === "text")
|
|
144
|
+
.map((block) => block.text)
|
|
145
|
+
.filter((text) => text.length > 0);
|
|
146
|
+
return texts.length > 0 ? textContent(texts.join("\n")) : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The text a tool reported, joined as a client would render it. */
|
|
150
|
+
function textOf(content: ToolResultContent[] | undefined): string {
|
|
151
|
+
return (content ?? [])
|
|
152
|
+
.map((block) => (block as { type?: string; text?: string }).text ?? "")
|
|
153
|
+
.join("")
|
|
154
|
+
.trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Maps one AHP turn.
|
|
159
|
+
*
|
|
160
|
+
* A mapper instance is created when a turn starts and discarded when it ends;
|
|
161
|
+
* all per-turn identity (message ordinal, open parts, live tool calls) lives
|
|
162
|
+
* here rather than on the host.
|
|
163
|
+
*/
|
|
164
|
+
export interface TurnMapperOptions {
|
|
165
|
+
/** Renders tool paths relative to it, so activity reads as `Reading src/x.ts`. */
|
|
166
|
+
readonly workingDirectory?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class TurnMapper {
|
|
170
|
+
/** The turn currently being mapped. Changes when a message is injected mid-run. */
|
|
171
|
+
#turnId: string;
|
|
172
|
+
/** Id of the turn this mapper was created for, used to derive successors. */
|
|
173
|
+
readonly #rootTurnId: string;
|
|
174
|
+
#turnStartedAt: number;
|
|
175
|
+
#injectedCount = 0;
|
|
176
|
+
|
|
177
|
+
/** Which assistant message we are on, 0-based, across the whole turn. */
|
|
178
|
+
#messageOrdinal = -1;
|
|
179
|
+
/** Open parts of the *current* assistant message, keyed by contentIndex. */
|
|
180
|
+
#parts = new Map<number, OpenPart>();
|
|
181
|
+
/** contentIndex → toolCallId, for correlating streaming deltas. */
|
|
182
|
+
#toolCallsByIndex = new Map<number, string>();
|
|
183
|
+
/** Tool calls that reached `ready` and have not completed. */
|
|
184
|
+
/** Live tool calls, against how each should be described once it ends. */
|
|
185
|
+
readonly #liveToolCalls = new Map<string, string>();
|
|
186
|
+
#outcome: TurnOutcome = "complete";
|
|
187
|
+
#errorMessage: string | undefined;
|
|
188
|
+
#finished = false;
|
|
189
|
+
|
|
190
|
+
readonly #options: TurnMapperOptions;
|
|
191
|
+
/** Last activity emitted, so unchanged descriptions are not re-sent. */
|
|
192
|
+
#activity: string | undefined;
|
|
193
|
+
|
|
194
|
+
constructor(turnId: string, startedAt: number = Date.now(), options: TurnMapperOptions = {}) {
|
|
195
|
+
this.#turnId = turnId;
|
|
196
|
+
this.#rootTurnId = turnId;
|
|
197
|
+
this.#turnStartedAt = startedAt;
|
|
198
|
+
this.#options = options;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get turnId(): string {
|
|
202
|
+
return this.#turnId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get finished(): boolean {
|
|
206
|
+
return this.#finished;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Translates one pi event into zero or more chat actions. */
|
|
210
|
+
handle(event: AgentSessionEvent): StateAction[] {
|
|
211
|
+
if (this.#finished) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
switch (event.type) {
|
|
215
|
+
case "message_start":
|
|
216
|
+
return this.#onMessageStart(event as { message?: { role?: string } });
|
|
217
|
+
case "message_update":
|
|
218
|
+
return this.#onDelta((event as { assistantMessageEvent: AssistantDelta }).assistantMessageEvent);
|
|
219
|
+
case "message_end":
|
|
220
|
+
return this.#onMessageEnd(
|
|
221
|
+
event as {
|
|
222
|
+
message?: {
|
|
223
|
+
role?: string;
|
|
224
|
+
usage?: PiUsage;
|
|
225
|
+
model?: string;
|
|
226
|
+
stopReason?: string;
|
|
227
|
+
errorMessage?: string;
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
case "tool_execution_start":
|
|
232
|
+
// The tool is about to run: say which one, and with what.
|
|
233
|
+
return this.#setActivity(
|
|
234
|
+
describeToolCall(
|
|
235
|
+
(event as { toolName: string }).toolName,
|
|
236
|
+
(event as { args?: unknown }).args,
|
|
237
|
+
this.#options.workingDirectory,
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
case "tool_execution_update":
|
|
241
|
+
return this.#onToolUpdate(event as { toolCallId: string; partialResult?: unknown });
|
|
242
|
+
case "tool_execution_end":
|
|
243
|
+
return [
|
|
244
|
+
...this.#onToolEnd(event as { toolCallId: string; toolName: string; result?: unknown; isError?: boolean }),
|
|
245
|
+
// Back to the model. Kept out of `#onToolEnd`, which bails
|
|
246
|
+
// early for a call it never saw start — activity is display
|
|
247
|
+
// state and should not depend on that bookkeeping.
|
|
248
|
+
...this.#setActivity(THINKING_ACTIVITY),
|
|
249
|
+
];
|
|
250
|
+
case "agent_settled":
|
|
251
|
+
return this.finish();
|
|
252
|
+
case "agent_start":
|
|
253
|
+
// A run beginning, or resuming after a retry.
|
|
254
|
+
return this.#setActivity(THINKING_ACTIVITY);
|
|
255
|
+
default:
|
|
256
|
+
// agent_start / agent_end / turn_start / turn_end carry no
|
|
257
|
+
// protocol-visible state of their own: a retry re-enters
|
|
258
|
+
// agent_start inside the same AHP turn, and pi's turn boundaries
|
|
259
|
+
// are just groupings of response parts.
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Assistant messages ──────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
#onMessageStart(event: { message?: { role?: string; content?: unknown } }): StateAction[] {
|
|
267
|
+
if (event.message?.role === "user") {
|
|
268
|
+
return this.#onInjectedMessage(event.message);
|
|
269
|
+
}
|
|
270
|
+
if (event.message?.role !== "assistant") {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
// A new assistant message restarts contentIndex at 0, so the part map
|
|
274
|
+
// must not leak across messages.
|
|
275
|
+
this.#messageOrdinal += 1;
|
|
276
|
+
this.#parts = new Map();
|
|
277
|
+
this.#toolCallsByIndex = new Map();
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Emits an activity change, skipping no-ops.
|
|
283
|
+
*
|
|
284
|
+
* A turn fires many events that map to the same description — every delta
|
|
285
|
+
* of one response is still "Responding" — and re-sending it would burn a
|
|
286
|
+
* `serverSeq` per token.
|
|
287
|
+
*/
|
|
288
|
+
#setActivity(activity: string | undefined): StateAction[] {
|
|
289
|
+
if (this.#activity === activity) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
this.#activity = activity;
|
|
293
|
+
return [{ type: ActionType.ChatActivityChanged, ...(activity ? { activity } : {}) }];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#onMessageEnd(event: {
|
|
297
|
+
message?: { role?: string; usage?: PiUsage; model?: string; stopReason?: string; errorMessage?: string };
|
|
298
|
+
}): StateAction[] {
|
|
299
|
+
if (event.message?.role !== "assistant") {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// A run cancelled or failed *before* any content streamed produces no
|
|
304
|
+
// `error` delta at all — the outcome shows up only as the finished
|
|
305
|
+
// message's `stopReason`. Observed live: aborting during the first
|
|
306
|
+
// request yields a single assistant message with `stopReason: 'aborted'`
|
|
307
|
+
// and nothing else. Reading it here is what keeps such a turn from being
|
|
308
|
+
// reported as a normal completion.
|
|
309
|
+
if (event.message.stopReason === "aborted") {
|
|
310
|
+
this.#outcome = "cancelled";
|
|
311
|
+
this.#errorMessage ??= event.message.errorMessage;
|
|
312
|
+
} else if (event.message.stopReason === "error") {
|
|
313
|
+
this.#outcome = "error";
|
|
314
|
+
this.#errorMessage ??= event.message.errorMessage;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const usage = toUsageInfo(event.message.usage, event.message.model);
|
|
318
|
+
return usage ? [{ type: ActionType.ChatUsage, turnId: this.turnId, usage }] : [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handles a user message pi injects part-way through a run.
|
|
323
|
+
*
|
|
324
|
+
* pi delivers a steering message as an ordinary user message inside the
|
|
325
|
+
* running turn, and **stores it in the session file as an ordinary user
|
|
326
|
+
* message too** — with nothing to mark it as steering. So when that
|
|
327
|
+
* conversation is later rebuilt from disk it necessarily becomes its own
|
|
328
|
+
* turn, because a user message is what starts one.
|
|
329
|
+
*
|
|
330
|
+
* The live path therefore has to do the same, or the same conversation
|
|
331
|
+
* renders one way while it is running and another way after a reload. The
|
|
332
|
+
* turn in flight is closed and a fresh one opened around the injected
|
|
333
|
+
* message, which also matches how pi's own TUI shows it.
|
|
334
|
+
*
|
|
335
|
+
* The first user message of a run is the prompt itself, which the client
|
|
336
|
+
* already opened the turn with, so only later ones split.
|
|
337
|
+
*/
|
|
338
|
+
#onInjectedMessage(message: { content?: unknown }): StateAction[] {
|
|
339
|
+
if (this.#messageOrdinal < 0) {
|
|
340
|
+
// Nothing has streamed yet: this is the run's own prompt.
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const injected = userMessageFromPiContent(message.content);
|
|
345
|
+
const completed = this.#closeCurrentTurn();
|
|
346
|
+
|
|
347
|
+
this.#injectedCount += 1;
|
|
348
|
+
this.#turnId = `${this.#rootTurnId}#${this.#injectedCount}`;
|
|
349
|
+
this.#turnStartedAt = Date.now();
|
|
350
|
+
this.#messageOrdinal = -1;
|
|
351
|
+
this.#parts = new Map();
|
|
352
|
+
this.#toolCallsByIndex = new Map();
|
|
353
|
+
|
|
354
|
+
return [
|
|
355
|
+
...completed,
|
|
356
|
+
{
|
|
357
|
+
type: ActionType.ChatTurnStarted,
|
|
358
|
+
turnId: this.#turnId,
|
|
359
|
+
startedAt: new Date(this.#turnStartedAt).toISOString(),
|
|
360
|
+
message: injected,
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#closeCurrentTurn(): StateAction[] {
|
|
366
|
+
const duration = Math.max(0, Date.now() - this.#turnStartedAt);
|
|
367
|
+
return [{ type: ActionType.ChatTurnComplete, turnId: this.#turnId, duration }];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Streaming content ───────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
#onDelta(delta: AssistantDelta): StateAction[] {
|
|
373
|
+
switch (delta.type) {
|
|
374
|
+
case "text_delta":
|
|
375
|
+
return this.#appendText(delta, ResponsePartKind.Markdown);
|
|
376
|
+
case "thinking_delta":
|
|
377
|
+
return this.#appendText(delta, ResponsePartKind.Reasoning);
|
|
378
|
+
case "text_end":
|
|
379
|
+
return this.#closeBlock(delta, ResponsePartKind.Markdown);
|
|
380
|
+
case "thinking_end":
|
|
381
|
+
return this.#closeBlock(delta, ResponsePartKind.Reasoning);
|
|
382
|
+
case "toolcall_start":
|
|
383
|
+
return this.#onToolCallStart(delta);
|
|
384
|
+
case "toolcall_delta":
|
|
385
|
+
return this.#onToolCallDelta(delta);
|
|
386
|
+
case "toolcall_end":
|
|
387
|
+
return this.#onToolCallReady(delta);
|
|
388
|
+
case "error":
|
|
389
|
+
// Recorded, not emitted: a mid-stream error may still be retried
|
|
390
|
+
// inside the same AHP turn, so only `agent_settled` decides the
|
|
391
|
+
// turn's outcome.
|
|
392
|
+
this.#outcome = delta.error?.stopReason === "aborted" ? "cancelled" : "error";
|
|
393
|
+
this.#errorMessage = delta.error?.errorMessage;
|
|
394
|
+
return [];
|
|
395
|
+
default:
|
|
396
|
+
// `*_start` / `start` / `done` need no action of their own: parts
|
|
397
|
+
// are created lazily by the first delta.
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Handles the end of a text or reasoning block.
|
|
404
|
+
*
|
|
405
|
+
* Normally a no-op, because the deltas already built the part. But a
|
|
406
|
+
* provider that does not stream a block incrementally emits only
|
|
407
|
+
* `*_end` with the finished `content` — observed with reasoning blocks on
|
|
408
|
+
* some models. Without this the content would be silently dropped, which is
|
|
409
|
+
* the same failure mode as targeting an unknown `partId`.
|
|
410
|
+
*/
|
|
411
|
+
#closeBlock(delta: AssistantDelta, kind: ResponsePartKind.Markdown | ResponsePartKind.Reasoning): StateAction[] {
|
|
412
|
+
const contentIndex = delta.contentIndex ?? 0;
|
|
413
|
+
if (this.#parts.get(contentIndex)?.created) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
return this.#appendText({ ...delta, delta: delta.content ?? "" }, kind);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#partId(contentIndex: number): string {
|
|
420
|
+
return `${this.#turnId}:${this.#messageOrdinal}:${contentIndex}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Appends streamed text, creating the part on first use.
|
|
425
|
+
*
|
|
426
|
+
* The first chunk rides along on `chat/responsePart` rather than being
|
|
427
|
+
* followed by a separate `chat/delta`, which halves the actions for short
|
|
428
|
+
* responses without changing the reduced state.
|
|
429
|
+
*/
|
|
430
|
+
#appendText(delta: AssistantDelta, kind: ResponsePartKind.Markdown | ResponsePartKind.Reasoning): StateAction[] {
|
|
431
|
+
const contentIndex = delta.contentIndex ?? 0;
|
|
432
|
+
const text = delta.delta ?? "";
|
|
433
|
+
if (text.length === 0) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const responding = this.#setActivity(RESPONDING_ACTIVITY);
|
|
438
|
+
|
|
439
|
+
let part = this.#parts.get(contentIndex);
|
|
440
|
+
if (!part) {
|
|
441
|
+
part = { partId: this.#partId(contentIndex), kind, created: false };
|
|
442
|
+
this.#parts.set(contentIndex, part);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!part.created) {
|
|
446
|
+
part.created = true;
|
|
447
|
+
return [
|
|
448
|
+
...responding,
|
|
449
|
+
{
|
|
450
|
+
type: ActionType.ChatResponsePart,
|
|
451
|
+
turnId: this.turnId,
|
|
452
|
+
part:
|
|
453
|
+
kind === ResponsePartKind.Markdown
|
|
454
|
+
? { kind, id: part.partId, content: text }
|
|
455
|
+
: { kind, id: part.partId, content: text },
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return [
|
|
461
|
+
...responding,
|
|
462
|
+
kind === ResponsePartKind.Markdown
|
|
463
|
+
? { type: ActionType.ChatDelta, turnId: this.turnId, partId: part.partId, content: text }
|
|
464
|
+
: { type: ActionType.ChatReasoning, turnId: this.turnId, partId: part.partId, content: text },
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Tool calls ──────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
#onToolCallStart(delta: AssistantDelta): StateAction[] {
|
|
471
|
+
const contentIndex = delta.contentIndex ?? 0;
|
|
472
|
+
const block = delta.partial?.content?.[contentIndex] as { id?: string; name?: string } | undefined;
|
|
473
|
+
const toolCallId = block?.id;
|
|
474
|
+
if (!toolCallId) {
|
|
475
|
+
// Without an id there is nothing to correlate later; wait for the
|
|
476
|
+
// next delta, which carries the resolved block.
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
this.#toolCallsByIndex.set(contentIndex, toolCallId);
|
|
480
|
+
const toolName = block.name ?? "tool";
|
|
481
|
+
return [
|
|
482
|
+
{
|
|
483
|
+
type: ActionType.ChatToolCallStart,
|
|
484
|
+
turnId: this.turnId,
|
|
485
|
+
toolCallId,
|
|
486
|
+
toolName,
|
|
487
|
+
displayName: toolName,
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
#onToolCallDelta(delta: AssistantDelta): StateAction[] {
|
|
493
|
+
const contentIndex = delta.contentIndex ?? 0;
|
|
494
|
+
let toolCallId = this.#toolCallsByIndex.get(contentIndex);
|
|
495
|
+
if (!toolCallId) {
|
|
496
|
+
// The id may only have materialised after `toolcall_start`; recover
|
|
497
|
+
// from the partial message rather than dropping the call.
|
|
498
|
+
const started = this.#onToolCallStart(delta);
|
|
499
|
+
toolCallId = this.#toolCallsByIndex.get(contentIndex);
|
|
500
|
+
if (!toolCallId) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
return [
|
|
504
|
+
...started,
|
|
505
|
+
{
|
|
506
|
+
type: ActionType.ChatToolCallDelta,
|
|
507
|
+
turnId: this.turnId,
|
|
508
|
+
toolCallId,
|
|
509
|
+
content: delta.delta ?? "",
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
return [{ type: ActionType.ChatToolCallDelta, turnId: this.turnId, toolCallId, content: delta.delta ?? "" }];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Parameters are complete.
|
|
518
|
+
*
|
|
519
|
+
* `confirmed: 'setting'` sends the call straight to `running`, skipping
|
|
520
|
+
* `pending-confirmation`. That is a first-class path in the protocol, and it
|
|
521
|
+
* is the honest one here: pi has no permission system, so there is nothing
|
|
522
|
+
* for a client to approve. Because the call never enters
|
|
523
|
+
* `pending-confirmation`, a conforming client renders no approve/deny UI —
|
|
524
|
+
* cancelling the whole turn stays available and maps to `abort()`.
|
|
525
|
+
*/
|
|
526
|
+
#onToolCallReady(delta: AssistantDelta): StateAction[] {
|
|
527
|
+
const contentIndex = delta.contentIndex ?? 0;
|
|
528
|
+
const toolCallId = delta.toolCall?.id ?? this.#toolCallsByIndex.get(contentIndex);
|
|
529
|
+
if (!toolCallId) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
this.#toolCallsByIndex.set(contentIndex, toolCallId);
|
|
533
|
+
const toolName = delta.toolCall?.name ?? "tool";
|
|
534
|
+
const args = delta.toolCall?.arguments;
|
|
535
|
+
const description = describeToolCall(toolName, args, this.#options.workingDirectory);
|
|
536
|
+
const toolInput = toolInputFor(toolName, args);
|
|
537
|
+
this.#liveToolCalls.set(toolCallId, describeFinishedToolCall(toolName, args, this.#options.workingDirectory));
|
|
538
|
+
return [
|
|
539
|
+
{
|
|
540
|
+
type: ActionType.ChatToolCallReady,
|
|
541
|
+
turnId: this.turnId,
|
|
542
|
+
toolCallId,
|
|
543
|
+
// Rendered while the call runs, and the same phrasing the activity
|
|
544
|
+
// indicator uses, so the two do not describe one call differently.
|
|
545
|
+
invocationMessage: description,
|
|
546
|
+
...(toolInput !== undefined ? { toolInput } : {}),
|
|
547
|
+
confirmed: ToolCallConfirmationReason.Setting,
|
|
548
|
+
},
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#onToolUpdate(event: { toolCallId: string; partialResult?: unknown }): StateAction[] {
|
|
553
|
+
if (!this.#liveToolCalls.has(event.toolCallId)) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
const content = toolResultContent(event.partialResult);
|
|
557
|
+
return content
|
|
558
|
+
? [
|
|
559
|
+
{
|
|
560
|
+
type: ActionType.ChatToolCallContentChanged,
|
|
561
|
+
turnId: this.turnId,
|
|
562
|
+
toolCallId: event.toolCallId,
|
|
563
|
+
content,
|
|
564
|
+
},
|
|
565
|
+
]
|
|
566
|
+
: [];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#onToolEnd(event: { toolCallId: string; toolName: string; result?: unknown; isError?: boolean }): StateAction[] {
|
|
570
|
+
const finished = this.#liveToolCalls.get(event.toolCallId);
|
|
571
|
+
if (!this.#liveToolCalls.delete(event.toolCallId)) {
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
const success = event.isError !== true;
|
|
575
|
+
const patch = editPatch(event.result);
|
|
576
|
+
const content = patch ? textContent(patch) : toolResultContent(event.result);
|
|
577
|
+
// A failed tool has already said why — an ENOENT naming the path it could
|
|
578
|
+
// not open, a non-zero exit with its stderr. That text is what belongs in
|
|
579
|
+
// `error.message`, which is where a client looks; restating the tool's name
|
|
580
|
+
// there leaves the reason visible only to clients that also render content.
|
|
581
|
+
const reported = textOf(content);
|
|
582
|
+
const result: ToolCallResult = {
|
|
583
|
+
success,
|
|
584
|
+
// Reuses what the call announced, so the completed line names the same
|
|
585
|
+
// subject the running one did rather than dropping back to the tool.
|
|
586
|
+
pastTenseMessage: success ? (finished ?? `Ran ${event.toolName}`) : `${finished ?? event.toolName} failed`,
|
|
587
|
+
...(content ? { content } : {}),
|
|
588
|
+
// `ToolCallResult.error` is its own shape (message + code), not `ErrorInfo`.
|
|
589
|
+
...(success ? {} : { error: { message: reported || `${event.toolName} failed` } }),
|
|
590
|
+
};
|
|
591
|
+
return [{ type: ActionType.ChatToolCallComplete, turnId: this.turnId, toolCallId: event.toolCallId, result }];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Turn termination ────────────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Closes the turn.
|
|
598
|
+
*
|
|
599
|
+
* Also called directly when a turn ends without `agent_settled` — an
|
|
600
|
+
* extension command that never reaches the model, or a failed `prompt()`.
|
|
601
|
+
* Without it the turn would stay active forever and the session would sit
|
|
602
|
+
* at `InProgress`.
|
|
603
|
+
*/
|
|
604
|
+
finish(outcome?: TurnOutcome, message?: string): StateAction[] {
|
|
605
|
+
if (this.#finished) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
this.#finished = true;
|
|
609
|
+
// Nothing is happening once the turn is over; a stale description would
|
|
610
|
+
// leave the client showing "Reading …" against an idle chat.
|
|
611
|
+
const cleared = this.#setActivity(undefined);
|
|
612
|
+
const resolved = outcome ?? this.#outcome;
|
|
613
|
+
const duration = Math.max(0, Date.now() - this.#turnStartedAt);
|
|
614
|
+
|
|
615
|
+
switch (resolved) {
|
|
616
|
+
case "cancelled":
|
|
617
|
+
return [...cleared, { type: ActionType.ChatTurnCancelled, turnId: this.turnId, duration }];
|
|
618
|
+
case "error":
|
|
619
|
+
return [
|
|
620
|
+
...cleared,
|
|
621
|
+
{
|
|
622
|
+
type: ActionType.ChatError,
|
|
623
|
+
turnId: this.turnId,
|
|
624
|
+
duration,
|
|
625
|
+
part: {
|
|
626
|
+
kind: ResponsePartKind.Error,
|
|
627
|
+
error: {
|
|
628
|
+
errorType: "agentRunFailed",
|
|
629
|
+
message: message ?? this.#errorMessage ?? "The agent run failed",
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
default:
|
|
635
|
+
return [...cleared, { type: ActionType.ChatTurnComplete, turnId: this.turnId, duration }];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Builds the `chat/turnStarted` action for a user message. */
|
|
641
|
+
export function userTurnStarted(turnId: string, text: string, startedAt: string): StateAction {
|
|
642
|
+
return {
|
|
643
|
+
type: ActionType.ChatTurnStarted,
|
|
644
|
+
turnId,
|
|
645
|
+
startedAt,
|
|
646
|
+
message: { text, origin: { kind: MessageKind.User } },
|
|
647
|
+
};
|
|
648
|
+
}
|