@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,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle: create, ready, dispose, and the catalogue notifications
|
|
3
|
+
* that keep every client's session list current.
|
|
4
|
+
*
|
|
5
|
+
* @see https://microsoft.github.io/agent-host-protocol/specification/session-channel
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { after, before, describe, it } from "node:test";
|
|
14
|
+
import {
|
|
15
|
+
type ActionEnvelope,
|
|
16
|
+
ActionType,
|
|
17
|
+
type ChatState,
|
|
18
|
+
JsonRpcErrorCodes,
|
|
19
|
+
MessageAttachmentKind,
|
|
20
|
+
MessageKind,
|
|
21
|
+
SessionLifecycle,
|
|
22
|
+
type SessionState,
|
|
23
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
24
|
+
} from "@microsoft/agent-host-protocol";
|
|
25
|
+
import { type AhpClient, RpcError, type Subscription } from "@microsoft/agent-host-protocol/client";
|
|
26
|
+
import { chatUri, ROOT_CHANNEL, sessionUri } from "../src/core/channels.ts";
|
|
27
|
+
import { pathToFileUri } from "../src/core/uri.ts";
|
|
28
|
+
import { PI_PROVIDER } from "../src/pi/provider.ts";
|
|
29
|
+
import { type Harness, nextClientId, startHarness } from "./harness.ts";
|
|
30
|
+
import { assertValid } from "./support/schema.ts";
|
|
31
|
+
|
|
32
|
+
async function nextEvent(
|
|
33
|
+
subscription: Subscription,
|
|
34
|
+
predicate: (event: { type: string }) => boolean,
|
|
35
|
+
timeoutMs = 2_000,
|
|
36
|
+
): Promise<{ type: string; params: unknown }> {
|
|
37
|
+
const timer = new Promise<never>((_, reject) => {
|
|
38
|
+
const handle = setTimeout(() => reject(new Error("timed out waiting for an event")), timeoutMs);
|
|
39
|
+
handle.unref?.();
|
|
40
|
+
});
|
|
41
|
+
const next = (async () => {
|
|
42
|
+
for await (const event of subscription) {
|
|
43
|
+
if (predicate(event)) {
|
|
44
|
+
return event as { type: string; params: unknown };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("subscription ended early");
|
|
48
|
+
})();
|
|
49
|
+
return Promise.race([next, timer]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("session lifecycle", () => {
|
|
53
|
+
let harness: Harness;
|
|
54
|
+
let workspace: string;
|
|
55
|
+
|
|
56
|
+
before(async () => {
|
|
57
|
+
workspace = mkdtempSync(join(tmpdir(), "pi-ahp-session-"));
|
|
58
|
+
harness = await startHarness({ sessions: true, workingDirectory: workspace });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
after(async () => {
|
|
62
|
+
await harness.dispose();
|
|
63
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
async function initialized(subscriptions: string[] = [ROOT_CHANNEL]): Promise<AhpClient> {
|
|
67
|
+
const client = await harness.connect();
|
|
68
|
+
await client.initialize({
|
|
69
|
+
clientId: nextClientId(),
|
|
70
|
+
protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
71
|
+
initialSubscriptions: subscriptions,
|
|
72
|
+
});
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
it("creates a session at the client-chosen URI and reports it ready", async () => {
|
|
77
|
+
const client = await initialized();
|
|
78
|
+
const uri = sessionUri(randomUUID());
|
|
79
|
+
|
|
80
|
+
await client.request("createSession", { channel: uri, provider: PI_PROVIDER });
|
|
81
|
+
|
|
82
|
+
const { result } = await client.subscribe(uri);
|
|
83
|
+
const state = result.snapshot?.state as SessionState;
|
|
84
|
+
assert.equal(state.provider, PI_PROVIDER);
|
|
85
|
+
// Creation is asynchronous in the protocol, but this storage-only fixture
|
|
86
|
+
// has no backend to start, so readiness is immediate.
|
|
87
|
+
assert.equal(state.lifecycle, SessionLifecycle.Ready);
|
|
88
|
+
assert.deepEqual(state.workingDirectories, [`file://${workspace}`]);
|
|
89
|
+
// Every session gets exactly one chat, and it is the default. The agent
|
|
90
|
+
// declares no `multipleChats` capability, which is what tells a client not
|
|
91
|
+
// to call `createChat`.
|
|
92
|
+
assert.equal(state.chats.length, 1);
|
|
93
|
+
assert.equal(state.defaultChat, state.chats[0]?.resource);
|
|
94
|
+
assertValid("state", "SessionState", state);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses the first user message as an unnamed session's display title", async () => {
|
|
98
|
+
const client = await initialized();
|
|
99
|
+
const id = randomUUID();
|
|
100
|
+
const uri = sessionUri(id);
|
|
101
|
+
const chat = chatUri(id);
|
|
102
|
+
await client.request("createSession", { channel: uri });
|
|
103
|
+
await client.subscribe(uri);
|
|
104
|
+
await client.subscribe(chat);
|
|
105
|
+
const events = client.attachSubscription(uri);
|
|
106
|
+
|
|
107
|
+
client.dispatch(chat, {
|
|
108
|
+
type: ActionType.ChatTurnStarted,
|
|
109
|
+
turnId: "title-turn",
|
|
110
|
+
startedAt: new Date().toISOString(),
|
|
111
|
+
message: {
|
|
112
|
+
text: " Review\n auth handling ",
|
|
113
|
+
origin: { kind: MessageKind.User },
|
|
114
|
+
attachments: [
|
|
115
|
+
{
|
|
116
|
+
type: MessageAttachmentKind.EmbeddedResource,
|
|
117
|
+
label: "context.txt",
|
|
118
|
+
contentType: "text/plain",
|
|
119
|
+
data: Buffer.from("attachment text is not a session title").toString("base64"),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const event = await nextEvent(events, (candidate) => {
|
|
125
|
+
if (candidate.type !== "action") return false;
|
|
126
|
+
return (candidate as { params?: ActionEnvelope }).params?.action.type === ActionType.SessionTitleChanged;
|
|
127
|
+
});
|
|
128
|
+
await client.ping();
|
|
129
|
+
|
|
130
|
+
const envelope = event.params as ActionEnvelope;
|
|
131
|
+
assert.deepEqual(envelope.action, { type: ActionType.SessionTitleChanged, title: "Review auth handling" });
|
|
132
|
+
assert.equal(envelope.origin, undefined);
|
|
133
|
+
const session = harness.host.store.get(uri) as SessionState;
|
|
134
|
+
assert.equal(session.title, "Review auth handling");
|
|
135
|
+
assert.equal(session.chats[0]?.title, "Review auth handling");
|
|
136
|
+
assert.equal((harness.host.store.get(chat) as ChatState).title, "Review auth handling");
|
|
137
|
+
assert.equal(harness.sessions?.get(uri)?.sessionManager.getSessionName(), undefined);
|
|
138
|
+
const listed = await client.request("listSessions", { channel: ROOT_CHANNEL });
|
|
139
|
+
assert.equal(listed.items.find((item) => item.resource === uri)?.title, "Review auth handling");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses pi's session id as the URI's uuid", async () => {
|
|
143
|
+
const client = await initialized();
|
|
144
|
+
const id = randomUUID();
|
|
145
|
+
await client.request("createSession", { channel: sessionUri(id) });
|
|
146
|
+
|
|
147
|
+
const live = harness.sessions?.get(sessionUri(id));
|
|
148
|
+
assert.ok(live);
|
|
149
|
+
// One identity space, so no persistent uuid → session-file mapping.
|
|
150
|
+
assert.equal(live.sessionId, id);
|
|
151
|
+
assert.equal(live.sessionManager.getSessionId(), id);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("announces the new session on the root channel", async () => {
|
|
155
|
+
const client = await initialized();
|
|
156
|
+
const rootSubscription = client.attachSubscription(ROOT_CHANNEL);
|
|
157
|
+
const uri = sessionUri(randomUUID());
|
|
158
|
+
|
|
159
|
+
await client.request("createSession", { channel: uri });
|
|
160
|
+
|
|
161
|
+
const event = await nextEvent(rootSubscription, (candidate) => candidate.type === "sessionAdded");
|
|
162
|
+
const params = event.params as { summary: { resource: string } };
|
|
163
|
+
assert.equal(params.summary.resource, uri);
|
|
164
|
+
assertValid("state", "SessionSummary", params.summary);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("rejects a duplicate session URI with SessionAlreadyExists", async () => {
|
|
168
|
+
const client = await initialized();
|
|
169
|
+
const uri = sessionUri(randomUUID());
|
|
170
|
+
await client.request("createSession", { channel: uri });
|
|
171
|
+
|
|
172
|
+
const error = await client.request("createSession", { channel: uri }).then(
|
|
173
|
+
() => undefined,
|
|
174
|
+
(reason: unknown) => reason,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
assert.ok(error instanceof RpcError);
|
|
178
|
+
assert.equal(error.code, -32003);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects an unknown provider with ProviderNotFound", async () => {
|
|
182
|
+
const client = await initialized();
|
|
183
|
+
const error = await client.request("createSession", { channel: sessionUri(randomUUID()), provider: "claude" }).then(
|
|
184
|
+
() => undefined,
|
|
185
|
+
(reason: unknown) => reason,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
assert.ok(error instanceof RpcError);
|
|
189
|
+
assert.equal(error.code, -32002);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("honours a client-supplied working directory", async () => {
|
|
193
|
+
const client = await initialized();
|
|
194
|
+
const other = mkdtempSync(join(tmpdir(), "pi ahp cwd-"));
|
|
195
|
+
try {
|
|
196
|
+
const uri = sessionUri(randomUUID());
|
|
197
|
+
const workingDirectory = pathToFileUri(other);
|
|
198
|
+
await client.request("createSession", { channel: uri, workingDirectories: [workingDirectory] });
|
|
199
|
+
|
|
200
|
+
const state = harness.host.store.get(uri) as SessionState;
|
|
201
|
+
assert.deepEqual(state.workingDirectories, [workingDirectory]);
|
|
202
|
+
} finally {
|
|
203
|
+
rmSync(other, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("tracks the active session count in root state", async () => {
|
|
208
|
+
const client = await initialized();
|
|
209
|
+
const before = (harness.host.store.get(ROOT_CHANNEL) as { activeSessions?: number }).activeSessions ?? 0;
|
|
210
|
+
|
|
211
|
+
await client.request("createSession", { channel: sessionUri(randomUUID()) });
|
|
212
|
+
|
|
213
|
+
const after = (harness.host.store.get(ROOT_CHANNEL) as { activeSessions?: number }).activeSessions ?? 0;
|
|
214
|
+
assert.equal(after, before + 1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("disposes a session, drops its channel, and announces the removal", async () => {
|
|
218
|
+
const client = await initialized();
|
|
219
|
+
const rootSubscription = client.attachSubscription(ROOT_CHANNEL);
|
|
220
|
+
const id = randomUUID();
|
|
221
|
+
const uri = sessionUri(id);
|
|
222
|
+
const chat = chatUri(id);
|
|
223
|
+
await client.request("createSession", { channel: uri });
|
|
224
|
+
await client.subscribe(uri);
|
|
225
|
+
await client.subscribe(chat);
|
|
226
|
+
assert.ok(harness.host.store.has(uri));
|
|
227
|
+
assert.equal(harness.host.subscriberCount(uri), 1);
|
|
228
|
+
assert.equal(harness.host.subscriberCount(chat), 1);
|
|
229
|
+
|
|
230
|
+
await client.request("disposeSession", { channel: uri });
|
|
231
|
+
|
|
232
|
+
const event = await nextEvent(rootSubscription, (candidate) => candidate.type === "sessionRemoved");
|
|
233
|
+
assert.equal((event.params as { session: string }).session, uri);
|
|
234
|
+
assert.equal(harness.host.store.has(uri), false);
|
|
235
|
+
assert.equal(harness.sessions?.get(uri), undefined);
|
|
236
|
+
assert.equal(harness.host.subscriberCount(uri), 0);
|
|
237
|
+
assert.equal(harness.host.subscriberCount(chat), 0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("does not let session disposal target another channel kind", async () => {
|
|
241
|
+
const client = await initialized();
|
|
242
|
+
await assert.rejects(
|
|
243
|
+
client.request("disposeSession", { channel: ROOT_CHANNEL }),
|
|
244
|
+
(error: unknown) => error instanceof RpcError && error.code === JsonRpcErrorCodes.InvalidParams,
|
|
245
|
+
);
|
|
246
|
+
assert.equal(harness.host.store.has(ROOT_CHANNEL), true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("rejects disposing a session that does not exist", async () => {
|
|
250
|
+
const client = await initialized();
|
|
251
|
+
const error = await client.request("disposeSession", { channel: sessionUri("nope") }).then(
|
|
252
|
+
() => undefined,
|
|
253
|
+
(reason: unknown) => reason,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
assert.ok(error instanceof RpcError);
|
|
257
|
+
assert.equal(error.code, -32001);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("accepts session/titleChanged from a client", async () => {
|
|
261
|
+
const client = await initialized();
|
|
262
|
+
const uri = sessionUri(randomUUID());
|
|
263
|
+
await client.request("createSession", { channel: uri });
|
|
264
|
+
await client.subscribe(uri);
|
|
265
|
+
|
|
266
|
+
client.dispatch(uri, { type: ActionType.SessionTitleChanged, title: "Refactor auth" });
|
|
267
|
+
|
|
268
|
+
// Wait for the echo so the reducer has run before asserting.
|
|
269
|
+
const subscription = client.attachSubscription(uri);
|
|
270
|
+
await nextEvent(subscription, (candidate) => candidate.type === "action");
|
|
271
|
+
assert.equal((harness.host.store.get(uri) as SessionState).title, "Refactor auth");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("applies VS Code's chat-addressed rename to the owning session", async () => {
|
|
275
|
+
const client = await harness.connect();
|
|
276
|
+
await client.request("initialize", {
|
|
277
|
+
channel: ROOT_CHANNEL,
|
|
278
|
+
clientId: nextClientId(),
|
|
279
|
+
protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
280
|
+
clientInfo: { name: "vscode-editor-window" },
|
|
281
|
+
});
|
|
282
|
+
const id = randomUUID();
|
|
283
|
+
const clientSession = `pi:/${id}`;
|
|
284
|
+
const clientChat = `ahp-chat://default/${Buffer.from(clientSession).toString("base64url")}`;
|
|
285
|
+
await client.request("createSession", { channel: clientSession });
|
|
286
|
+
await client.subscribe(clientSession);
|
|
287
|
+
const events = client.attachSubscription(clientSession);
|
|
288
|
+
|
|
289
|
+
client.dispatch(clientChat, { type: ActionType.SessionTitleChanged, title: "Renamed from VS Code" });
|
|
290
|
+
const event = await nextEvent(events, (candidate) => candidate.type === "action");
|
|
291
|
+
const envelope = event.params as ActionEnvelope;
|
|
292
|
+
|
|
293
|
+
assert.equal(envelope.channel, clientSession);
|
|
294
|
+
assert.deepEqual(envelope.action, { type: ActionType.SessionTitleChanged, title: "Renamed from VS Code" });
|
|
295
|
+
assert.equal(envelope.rejectionReason, undefined);
|
|
296
|
+
assert.equal((harness.host.store.get(sessionUri(id)) as SessionState).title, "Renamed from VS Code");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("declines VS Code's active client without blocking session creation", async () => {
|
|
300
|
+
const clientId = nextClientId();
|
|
301
|
+
const client = await harness.connect();
|
|
302
|
+
await client.request("initialize", {
|
|
303
|
+
channel: ROOT_CHANNEL,
|
|
304
|
+
clientId,
|
|
305
|
+
protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
306
|
+
clientInfo: { name: "vscode-editor-window" },
|
|
307
|
+
});
|
|
308
|
+
const id = randomUUID();
|
|
309
|
+
const uri = sessionUri(id);
|
|
310
|
+
const clientUri = `pi:/${id}`;
|
|
311
|
+
const activeClient = { clientId, tools: [] };
|
|
312
|
+
|
|
313
|
+
// VS Code supplies this eagerly. It is ignored rather than making an
|
|
314
|
+
// otherwise usable session fail.
|
|
315
|
+
await client.request("createSession", { channel: clientUri, activeClient });
|
|
316
|
+
await client.subscribe(clientUri);
|
|
317
|
+
|
|
318
|
+
for (const action of [
|
|
319
|
+
{ type: ActionType.SessionActiveClientSet, activeClient },
|
|
320
|
+
{ type: ActionType.SessionActiveClientRemoved, clientId },
|
|
321
|
+
] as const) {
|
|
322
|
+
const events = client.attachSubscription(clientUri);
|
|
323
|
+
client.dispatch(clientUri, action);
|
|
324
|
+
const event = await nextEvent(events, (candidate) => candidate.type === "action");
|
|
325
|
+
const envelope = event.params as { action?: { type?: string }; rejectionReason?: string };
|
|
326
|
+
assert.equal(envelope.action?.type, action.type);
|
|
327
|
+
assert.equal(envelope.rejectionReason, "This host does not accept active clients");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
assert.deepEqual((harness.host.store.get(uri) as SessionState).activeClients, []);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("renaming a session", () => {
|
|
335
|
+
let harness: Harness;
|
|
336
|
+
let workspace: string;
|
|
337
|
+
|
|
338
|
+
before(async () => {
|
|
339
|
+
workspace = mkdtempSync(join(tmpdir(), "pi-ahp-rename-"));
|
|
340
|
+
harness = await startHarness({ sessions: true, workingDirectory: workspace });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
after(async () => {
|
|
344
|
+
await harness.dispose();
|
|
345
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
async function renamed(uri: string, title: string): Promise<void> {
|
|
349
|
+
const client = await harness.connect();
|
|
350
|
+
await client.initialize({ clientId: nextClientId(), protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
|
|
351
|
+
const { subscription } = await client.subscribe(uri);
|
|
352
|
+
const dispatched = client.dispatch(uri, { type: ActionType.SessionTitleChanged, title });
|
|
353
|
+
await nextEvent(subscription, (event) => {
|
|
354
|
+
if (event.type !== "action") return false;
|
|
355
|
+
const envelope = (event as { params?: ActionEnvelope }).params;
|
|
356
|
+
return envelope?.origin?.clientSeq === dispatched.clientSeq;
|
|
357
|
+
});
|
|
358
|
+
await client.ping();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
it("records the new name on the session", async () => {
|
|
362
|
+
const client = await harness.connect();
|
|
363
|
+
await client.initialize({ clientId: nextClientId(), protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
|
|
364
|
+
const uri = sessionUri(randomUUID());
|
|
365
|
+
await client.request("createSession", { channel: uri });
|
|
366
|
+
|
|
367
|
+
await renamed(uri, "Refactor auth middleware");
|
|
368
|
+
|
|
369
|
+
// The same call pi's own `/resume` rename makes, so a session renamed
|
|
370
|
+
// here reads the same from pi's CLI.
|
|
371
|
+
assert.equal(harness.sessions?.get(uri)?.sessionManager.getSessionName(), "Refactor auth middleware");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("does not create a file for a session that never got a reply", async () => {
|
|
375
|
+
const client = await harness.connect();
|
|
376
|
+
await client.initialize({ clientId: nextClientId(), protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
|
|
377
|
+
const uri = sessionUri(randomUUID());
|
|
378
|
+
await client.request("createSession", { channel: uri });
|
|
379
|
+
await renamed(uri, "Never answered");
|
|
380
|
+
|
|
381
|
+
// pi withholds the file until a session has an assistant message, so an
|
|
382
|
+
// empty conversation leaves nothing behind. The name is held in memory
|
|
383
|
+
// and flushed with everything else
|
|
384
|
+
// once the first reply arrives — renaming does not change that policy,
|
|
385
|
+
// and forcing a write here would litter the disk with empty sessions.
|
|
386
|
+
const file = harness.sessions?.get(uri)?.sessionManager.getSessionFile();
|
|
387
|
+
assert.ok(file);
|
|
388
|
+
assert.equal(existsSync(file), false);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("mirrors the name onto the chat and the catalogue entry", async () => {
|
|
392
|
+
const client = await harness.connect();
|
|
393
|
+
await client.initialize({ clientId: nextClientId(), protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
|
|
394
|
+
const id = randomUUID();
|
|
395
|
+
const uri = sessionUri(id);
|
|
396
|
+
await client.request("createSession", { channel: uri });
|
|
397
|
+
await renamed(uri, "Ship the release");
|
|
398
|
+
|
|
399
|
+
const session = harness.host.store.get(uri) as SessionState;
|
|
400
|
+
const chat = harness.host.store.get(chatUri(id)) as ChatState;
|
|
401
|
+
assert.equal(session.title, "Ship the release");
|
|
402
|
+
// `ChatState` denormalises its summary fields, so both representations
|
|
403
|
+
// have to move together or a client watching only the session drifts.
|
|
404
|
+
assert.equal(chat.title, "Ship the release");
|
|
405
|
+
assert.equal(session.chats[0]?.title, "Ship the release");
|
|
406
|
+
|
|
407
|
+
client.dispatch(chatUri(id), {
|
|
408
|
+
type: ActionType.ChatTurnStarted,
|
|
409
|
+
turnId: "after-rename",
|
|
410
|
+
startedAt: new Date().toISOString(),
|
|
411
|
+
message: { text: "This must not replace the explicit name", origin: { kind: MessageKind.User } },
|
|
412
|
+
});
|
|
413
|
+
await client.ping();
|
|
414
|
+
assert.equal((harness.host.store.get(uri) as SessionState).title, "Ship the release");
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { it } from "node:test";
|
|
7
|
+
import { fauxAssistantMessage } from "@earendil-works/pi-ai";
|
|
8
|
+
import { createDefaultPiSessionStorage } from "../src/pi/session-storage.ts";
|
|
9
|
+
|
|
10
|
+
it("keeps the default catalogue and Pi session writer on the same isolated profile", async (t) => {
|
|
11
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-ahp-default-storage-agent-"));
|
|
12
|
+
const workspace = mkdtempSync(join(tmpdir(), "pi-ahp-default-storage-cwd-"));
|
|
13
|
+
const previous = process.env.PI_CODING_AGENT_DIR;
|
|
14
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
15
|
+
t.after(() => {
|
|
16
|
+
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
17
|
+
else process.env.PI_CODING_AGENT_DIR = previous;
|
|
18
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
19
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const storage = createDefaultPiSessionStorage();
|
|
23
|
+
const sessionId = randomUUID();
|
|
24
|
+
const manager = storage.createSessionManager(workspace, sessionId);
|
|
25
|
+
manager.appendMessage({ role: "user", content: "hello", timestamp: Date.now() });
|
|
26
|
+
manager.appendMessage(fauxAssistantMessage("hello"));
|
|
27
|
+
|
|
28
|
+
const file = manager.getSessionFile();
|
|
29
|
+
assert.ok(file);
|
|
30
|
+
assert.equal(await storage.catalogue.findSessionFile(sessionId), file);
|
|
31
|
+
assert.equal(relative(join(agentDir, "sessions"), file).startsWith(".."), false);
|
|
32
|
+
});
|