pi-roundtable 0.1.0 → 0.2.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/CHANGELOG.md +138 -0
- package/README.md +6 -4
- package/docs/plugins.md +1448 -66
- package/examples/echo-runtime.test.ts +122 -0
- package/examples/echo-runtime.ts +107 -0
- package/examples/events.test.ts +1 -0
- package/examples/events.ts +2 -2
- package/examples/fake-surface.test.ts +180 -0
- package/examples/fake-surface.ts +109 -0
- package/examples/interactions.test.ts +12 -3
- package/examples/interactions.ts +19 -19
- package/examples/shared-services.test.ts +74 -0
- package/examples/shared-services.ts +60 -0
- package/examples/study-room.test.ts +69 -0
- package/examples/study-room.ts +52 -0
- package/examples/support-desk.test.ts +34 -0
- package/examples/support-desk.ts +40 -0
- package/package.json +9 -2
- package/src/cli/checks/database.ts +11 -30
- package/src/cli/checks/images.ts +19 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/main.ts +2 -1
- package/src/core/agents/agent-claim.ts +42 -16
- package/src/core/agents/agent-messages.ts +4 -4
- package/src/core/agents/agent-ports.ts +8 -21
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-store.ts +6 -4
- package/src/core/agents/agent-team-fixture.ts +389 -0
- package/src/core/agents/agent-team.ts +26 -25
- package/src/core/agents/agent-tools.ts +34 -15
- package/src/core/agents/avatar-studio.ts +40 -6
- package/src/core/agents/fallback-avatar.ts +85 -0
- package/src/core/agents/team-editing.ts +49 -10
- package/src/core/agents/team-keys.ts +22 -9
- package/src/core/agents/team-layout.ts +4 -3
- package/src/core/agents/team-lifecycle.ts +28 -2
- package/src/core/agents/team-options.ts +9 -5
- package/src/core/agents/team-status.ts +4 -4
- package/src/core/agents/team-text.ts +8 -6
- package/src/core/agents/team-turn-types.ts +4 -4
- package/src/core/agents/team-turns.ts +6 -5
- package/src/core/builtin/agent-server.ts +187 -121
- package/src/core/builtin/discord-admin.ts +41 -0
- package/src/core/builtin/discord.ts +65 -40
- package/src/core/builtin/modules.ts +67 -58
- package/src/core/builtin/session-tool.ts +24 -0
- package/src/core/builtin/skills.ts +72 -0
- package/src/core/builtin/stores.ts +55 -31
- package/src/core/config/config.ts +45 -4
- package/src/core/config/schema.ts +6 -0
- package/src/core/contract/channels.ts +54 -14
- package/src/core/contract/providers.ts +17 -0
- package/src/core/contract/runtime.ts +138 -0
- package/src/core/contract/services.ts +45 -0
- package/src/core/contract/surface.ts +88 -0
- package/src/core/db/migrations.ts +118 -3
- package/src/core/define-roundtable.ts +70 -46
- package/src/core/define.ts +2 -1
- package/src/core/discord/agent-commands.ts +39 -14
- package/src/core/discord/agent-panel.ts +66 -0
- package/src/core/discord/channel-executor.ts +6 -3
- package/src/core/discord/channel-operations.ts +14 -11
- package/src/core/discord/command-collection.ts +46 -0
- package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
- package/src/core/discord/connection.ts +35 -0
- package/src/core/discord/discord-surface.ts +32 -94
- package/src/core/discord/dispatch-threads.ts +2 -2
- package/src/core/discord/inbound-message.ts +99 -0
- package/src/core/discord/interaction-module.ts +57 -1
- package/src/core/discord/owner-cards.ts +4 -4
- package/src/core/discord/owner-command.ts +44 -11
- package/src/core/discord/owner-discord.ts +2 -2
- package/src/core/discord/schedule-commands.ts +21 -18
- package/src/core/discord/stop-button.ts +44 -1
- package/src/core/domain/attachment.ts +12 -8
- package/src/core/domain/conversation.ts +3 -4
- package/src/core/domain/owner-prompts.ts +5 -5
- package/src/core/domain/ports.ts +9 -53
- package/src/core/freeze.ts +9 -0
- package/src/core/host.ts +255 -82
- package/src/core/http/listeners.ts +55 -20
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/index.ts +12 -2
- package/src/core/i18n/owner.ts +7 -1
- package/src/core/i18n/schedules.ts +12 -6
- package/src/core/identity.ts +1 -1
- package/src/core/judging/effort-judge.ts +25 -3
- package/src/core/log.ts +21 -3
- package/src/core/models.ts +2 -2
- package/src/core/modules/background/background-turns.ts +6 -4
- package/src/core/modules/delegation/delegate.ts +3 -2
- package/src/core/modules/delegation/delegator.ts +15 -13
- package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
- package/src/core/modules/discord-admin/discord-admin.ts +6 -6
- package/src/core/modules/host-shell/shell-policy.ts +8 -3
- package/src/core/modules/memory/owner-memory-store.ts +27 -25
- package/src/core/modules/memory/owner-memory.ts +9 -13
- package/src/core/modules/schedules/recurrence.ts +5 -3
- package/src/core/modules/schedules/schedule-store.ts +11 -11
- package/src/core/modules/schedules/schedule-tools.ts +22 -20
- package/src/core/modules/schedules/scheduler.ts +7 -2
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +2 -15
- package/src/core/modules/skills/skill-tools.ts +10 -4
- package/src/core/ops/error-reporter.ts +1 -1
- package/src/core/plugin.ts +232 -28
- package/src/core/registry/contributions.ts +173 -14
- package/src/core/registry/providers.ts +35 -5
- package/src/core/registry/services.ts +229 -0
- package/src/core/routing/channel-queue.ts +5 -0
- package/src/core/routing/channel-router.ts +21 -7
- package/src/core/routing/conversation-turns.ts +139 -0
- package/src/core/routing/message-text.ts +9 -2
- package/src/core/routing/surface-port.ts +39 -0
- package/src/core/runtime/conversation-sessions.ts +4 -3
- package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
- package/src/core/runtime/mcp.ts +1 -1
- package/src/core/runtime/pending-confirmation-store.ts +10 -12
- package/src/core/runtime/pi-agent-runtime.ts +9 -6
- package/src/core/runtime/prompt-slot.ts +8 -3
- package/src/core/runtime/runtime-types.ts +9 -35
- package/src/core/runtime/session-factory.ts +21 -5
- package/src/core/runtime/text-tools.ts +1 -3
- package/src/core/services.ts +238 -90
- package/src/core/sessions.ts +3 -2
- package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
- package/src/core/shared/delegate-tool.ts +4 -3
- package/src/core/shared/schedule-tools.ts +22 -13
- package/src/core/shared/session-messages.ts +1 -1
- package/src/core/speakers.ts +6 -38
- package/src/core/testing/database.ts +1 -7
- package/src/core/testing/eager-catalog.ts +30 -0
- package/src/core/testing/locale.ts +23 -5
- package/src/core/testing/modules.ts +105 -41
- package/src/core/testing/test-host.ts +279 -0
- package/src/core/testing/tool-set.ts +64 -0
- package/src/core/time.ts +6 -1
- package/src/core/tool-set.snapshot.json +288 -0
- package/src/core/tool-tiers.ts +17 -57
- package/src/discord/index.ts +64 -0
- package/src/index.ts +181 -10
- package/src/kit/channels.ts +14 -0
- package/src/kit/domain.ts +18 -0
- package/src/kit/holds.ts +4 -0
- package/src/kit/index.ts +129 -0
- package/src/kit/judging.ts +10 -0
- package/src/kit/memory.ts +3 -0
- package/src/kit/mirror.ts +19 -0
- package/src/kit/presentation.ts +7 -0
- package/src/kit/shell.ts +7 -0
- package/src/kit/skills.ts +8 -0
- package/src/kit/support.ts +21 -0
- package/src/kit/threads.ts +7 -0
- package/src/kit/tools.ts +14 -0
- package/src/kit/worker.ts +16 -0
- package/src/testing.ts +394 -34
- package/examples/guide.test.ts +0 -52
- package/src/cli/add-plugin.test.ts +0 -107
- package/src/cli/checks/basic.test.ts +0 -276
- package/src/cli/checks/database.test.ts +0 -117
- package/src/cli/checks/discord.test.ts +0 -200
- package/src/cli/cli.test.ts +0 -122
- package/src/cli/config-edit.test.ts +0 -130
- package/src/cli/doctor.test.ts +0 -184
- package/src/cli/init.test.ts +0 -127
- package/src/cli/size.test.ts +0 -9
- package/src/cli/templates.test.ts +0 -129
- package/src/cli/testing/fixtures.ts +0 -106
- package/src/core/agents/agent-claim.test.ts +0 -142
- package/src/core/agents/agent-dashboard.test.ts +0 -120
- package/src/core/agents/agent-guild.test.ts +0 -124
- package/src/core/agents/agent-store.test.ts +0 -205
- package/src/core/agents/avatar-studio.test.ts +0 -88
- package/src/core/agents/group-round.test.ts +0 -143
- package/src/core/agents/owner-identity.test.ts +0 -172
- package/src/core/agents/team-turns.test.ts +0 -278
- package/src/core/attachments/attachments.test.ts +0 -85
- package/src/core/boundary.test.ts +0 -45
- package/src/core/builtin/modules.test.ts +0 -127
- package/src/core/config/config.test.ts +0 -135
- package/src/core/contract/discord.ts +0 -33
- package/src/core/db/migrations.test.ts +0 -237
- package/src/core/define-roundtable.test.ts +0 -134
- package/src/core/define.test.ts +0 -144
- package/src/core/discord/agent-commands.test.ts +0 -58
- package/src/core/discord/agent-discord.test.ts +0 -70
- package/src/core/discord/dispatch-thread-host.test.ts +0 -114
- package/src/core/discord/dispatch-threads.test.ts +0 -94
- package/src/core/discord/owner-cards.test.ts +0 -435
- package/src/core/discord/owner-discord.test.ts +0 -325
- package/src/core/domain/expression.ts +0 -21
- package/src/core/domain/profile.ts +0 -32
- package/src/core/drain.test.ts +0 -46
- package/src/core/events.test.ts +0 -81
- package/src/core/holds.test.ts +0 -54
- package/src/core/host.test.ts +0 -536
- package/src/core/http/listeners.test.ts +0 -164
- package/src/core/i18n/i18n.test.ts +0 -139
- package/src/core/identity.test.ts +0 -18
- package/src/core/judging/effort-judge.test.ts +0 -112
- package/src/core/judging/model-judge.test.ts +0 -126
- package/src/core/modules/delegation/delegator.test.ts +0 -170
- package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
- package/src/core/modules/schedules/schedule.test.ts +0 -358
- package/src/core/modules/skills/skill-kind.test.ts +0 -55
- package/src/core/modules/skills/skill-registry.test.ts +0 -376
- package/src/core/ops/error-reporter.test.ts +0 -283
- package/src/core/presentation/card-cadence.ts +0 -41
- package/src/core/presentation/presentation.test.ts +0 -166
- package/src/core/public-entry.test.ts +0 -59
- package/src/core/registry/contributions.test.ts +0 -215
- package/src/core/registry/interactions.test.ts +0 -87
- package/src/core/registry/providers.test.ts +0 -103
- package/src/core/routing/channel-queue.test.ts +0 -31
- package/src/core/routing/channel-router.test.ts +0 -326
- package/src/core/routing/conversation-kind.ts +0 -14
- package/src/core/routing/settle-turn.test.ts +0 -21
- package/src/core/runtime/compaction-tiers.test.ts +0 -227
- package/src/core/runtime/extensions/ask-user.test.ts +0 -114
- package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
- package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
- package/src/core/runtime/session-archive.test.ts +0 -20
- package/src/core/runtime/steerable-run.test.ts +0 -264
- package/src/core/runtime/text-tools.test.ts +0 -110
- package/src/core/runtime/turn-answer.test.ts +0 -72
- package/src/core/runtime/worker-task.test.ts +0 -82
- package/src/core/services.test.ts +0 -28
- package/src/core/sessions.test.ts +0 -85
- package/src/core/shared/unix-server.ts +0 -18
- package/src/core/size.test.ts +0 -9
- package/src/core/speakers.test.ts +0 -78
- package/src/core/testing/file-size.ts +0 -34
- package/src/core/time.test.ts +0 -166
- package/src/core/tool-tiers.test.ts +0 -117
- package/src/entries.test.ts +0 -112
- package/src/testing.test.ts +0 -156
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
COMPACT_TOOL,
|
|
5
|
-
selfCompactGuardExtension,
|
|
6
|
-
} from "./self-compact-guard.ts";
|
|
7
|
-
|
|
8
|
-
type Handler = (event: {
|
|
9
|
-
toolName: string;
|
|
10
|
-
input: unknown;
|
|
11
|
-
}) => { block: true; reason: string } | undefined;
|
|
12
|
-
|
|
13
|
-
function handler(): Handler {
|
|
14
|
-
let registered: Handler | undefined;
|
|
15
|
-
selfCompactGuardExtension()({
|
|
16
|
-
on: (event: string, h: Handler) => {
|
|
17
|
-
if (event === "tool_call") registered = h;
|
|
18
|
-
},
|
|
19
|
-
} as unknown as ExtensionAPI);
|
|
20
|
-
if (!registered) throw new Error("no tool_call handler");
|
|
21
|
-
return registered;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe("selfCompactGuardExtension", () => {
|
|
25
|
-
test("refuses compact_session with resume, with the reason", () => {
|
|
26
|
-
const result = handler()({
|
|
27
|
-
toolName: COMPACT_TOOL,
|
|
28
|
-
input: { resume: "continue the report" },
|
|
29
|
-
});
|
|
30
|
-
expect(result?.block).toBe(true);
|
|
31
|
-
expect(result?.reason).toContain("without resume");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("lets compact_session without resume, or with a blank one, run", () => {
|
|
35
|
-
const h = handler();
|
|
36
|
-
expect(h({ toolName: COMPACT_TOOL, input: {} })).toBeUndefined();
|
|
37
|
-
expect(
|
|
38
|
-
h({ toolName: COMPACT_TOOL, input: { instructions: "keep paths" } }),
|
|
39
|
-
).toBeUndefined();
|
|
40
|
-
expect(
|
|
41
|
-
h({ toolName: COMPACT_TOOL, input: { resume: " " } }),
|
|
42
|
-
).toBeUndefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("ignores other tools", () => {
|
|
46
|
-
expect(
|
|
47
|
-
handler()({ toolName: "bash", input: { resume: "x" } }),
|
|
48
|
-
).toBeUndefined();
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
-
import { SQL } from "bun";
|
|
3
|
-
import type { PendingConfirmation } from "../domain/conversation.ts";
|
|
4
|
-
import {
|
|
5
|
-
describeDb,
|
|
6
|
-
openTestStore,
|
|
7
|
-
type TestStore,
|
|
8
|
-
testDatabaseUrl,
|
|
9
|
-
} from "../testing/database.ts";
|
|
10
|
-
import { PendingConfirmationStore } from "./pending-confirmation-store.ts";
|
|
11
|
-
|
|
12
|
-
const held: PendingConfirmation = {
|
|
13
|
-
profile: "workspace",
|
|
14
|
-
heldAt: new Date("2026-09-27T01:00:00Z"),
|
|
15
|
-
calls: [
|
|
16
|
-
{
|
|
17
|
-
tool: "google-send-gmail-message",
|
|
18
|
-
input: '{"subject":"hi","to":"a@b.c"}',
|
|
19
|
-
action: "send an email to a@b.c",
|
|
20
|
-
},
|
|
21
|
-
],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
|
|
25
|
-
describeDb("PendingConfirmationStore", () => {
|
|
26
|
-
let store: TestStore<PendingConfirmationStore>;
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
const admin = new SQL(testDatabaseUrl);
|
|
30
|
-
await admin`DROP TABLE IF EXISTS pending_confirmations`;
|
|
31
|
-
await admin.close();
|
|
32
|
-
store = await openTestStore(PendingConfirmationStore);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterAll(async () => {
|
|
36
|
-
await store.close();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("held actions survive a restart, are replaced, and are cleared", async () => {
|
|
40
|
-
expect(await store.load("discord:1")).toBeUndefined();
|
|
41
|
-
await store.save("discord:1", held);
|
|
42
|
-
expect(await store.load("discord:1")).toEqual(held);
|
|
43
|
-
|
|
44
|
-
const other = { ...held, calls: [] };
|
|
45
|
-
await store.save("discord:1", other);
|
|
46
|
-
expect(await store.load("discord:1")).toEqual(other);
|
|
47
|
-
expect(await store.load("discord:2")).toBeUndefined();
|
|
48
|
-
|
|
49
|
-
await store.save("discord:1", undefined);
|
|
50
|
-
expect(await store.load("discord:1")).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { archiveSessions } from "./session-archive.ts";
|
|
6
|
-
|
|
7
|
-
describe("archiveSessions", () => {
|
|
8
|
-
test("moves the conversations out of the top level, keeping them", () => {
|
|
9
|
-
const dir = mkdtempSync(join(tmpdir(), "roundtable-archive-"));
|
|
10
|
-
writeFileSync(join(dir, "a.jsonl"), "{}\n");
|
|
11
|
-
writeFileSync(join(dir, "notes.txt"), "stay");
|
|
12
|
-
expect(archiveSessions(dir, new Date("2026-09-27T01:02:03Z"))).toBe(1);
|
|
13
|
-
expect(readdirSync(dir).sort()).toEqual(["archive", "notes.txt"]);
|
|
14
|
-
expect(
|
|
15
|
-
readdirSync(join(dir, "archive", "2026-09-27T01-02-03.000Z")),
|
|
16
|
-
).toEqual(["a.jsonl"]);
|
|
17
|
-
expect(archiveSessions(dir)).toBe(0);
|
|
18
|
-
expect(archiveSessions(join(dir, "missing"))).toBe(0);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
type AssistantMessage,
|
|
7
|
-
createFauxCore,
|
|
8
|
-
type FauxResponseStep,
|
|
9
|
-
fauxAssistantMessage,
|
|
10
|
-
type TranscriptContext,
|
|
11
|
-
} from "@earendil-works/pi-ai";
|
|
12
|
-
import {
|
|
13
|
-
type AgentSession,
|
|
14
|
-
createAgentSession,
|
|
15
|
-
DefaultResourceLoader,
|
|
16
|
-
ModelRuntime,
|
|
17
|
-
SessionManager,
|
|
18
|
-
SettingsManager,
|
|
19
|
-
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { SteerableRun, type SteeringSession } from "./steerable-run.ts";
|
|
21
|
-
|
|
22
|
-
/** A real Pi session on the faux provider, whose scripted answers may wait on the test. */
|
|
23
|
-
async function fauxSession(steps: FauxResponseStep[]) {
|
|
24
|
-
const dir = mkdtempSync(join(tmpdir(), "roundtable-steer-"));
|
|
25
|
-
const core = createFauxCore({ provider: "faux", models: [{ id: "faux-1" }] });
|
|
26
|
-
core.setResponses(steps);
|
|
27
|
-
const modelRuntime = await ModelRuntime.create({
|
|
28
|
-
authPath: join(dir, "auth.json"),
|
|
29
|
-
modelsPath: null,
|
|
30
|
-
allowModelNetwork: false,
|
|
31
|
-
refreshOnCreate: false,
|
|
32
|
-
});
|
|
33
|
-
modelRuntime.registerProvider("faux", {
|
|
34
|
-
api: core.api,
|
|
35
|
-
apiKey: "test",
|
|
36
|
-
baseUrl: "http://faux.invalid",
|
|
37
|
-
streamSimple: core.streamSimple,
|
|
38
|
-
models: [
|
|
39
|
-
{
|
|
40
|
-
id: "faux-1",
|
|
41
|
-
name: "Faux",
|
|
42
|
-
reasoning: false,
|
|
43
|
-
input: ["text", "image"],
|
|
44
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
45
|
-
contextWindow: 100_000,
|
|
46
|
-
maxTokens: 1_000,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
});
|
|
50
|
-
const model = modelRuntime.getModel("faux", "faux-1");
|
|
51
|
-
if (!model) throw new Error("faux model not registered");
|
|
52
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
53
|
-
cwd: dir,
|
|
54
|
-
agentDir: dir,
|
|
55
|
-
noExtensions: true,
|
|
56
|
-
noSkills: true,
|
|
57
|
-
noPromptTemplates: true,
|
|
58
|
-
noThemes: true,
|
|
59
|
-
noContextFiles: true,
|
|
60
|
-
});
|
|
61
|
-
await resourceLoader.reload();
|
|
62
|
-
const { session } = await createAgentSession({
|
|
63
|
-
cwd: dir,
|
|
64
|
-
agentDir: dir,
|
|
65
|
-
modelRuntime,
|
|
66
|
-
model,
|
|
67
|
-
resourceLoader,
|
|
68
|
-
sessionManager: SessionManager.inMemory(),
|
|
69
|
-
settingsManager: SettingsManager.inMemory({}),
|
|
70
|
-
noTools: "all",
|
|
71
|
-
});
|
|
72
|
-
return { session, core };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** A promise the test resolves by hand. */
|
|
76
|
-
function gate() {
|
|
77
|
-
let open = () => {};
|
|
78
|
-
const opened = new Promise<void>((resolve) => {
|
|
79
|
-
open = resolve;
|
|
80
|
-
});
|
|
81
|
-
return { opened, open };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const userTexts = (context: TranscriptContext) =>
|
|
85
|
-
context.messages
|
|
86
|
-
.filter((m) => m.role === "user")
|
|
87
|
-
.map((m) =>
|
|
88
|
-
typeof m.content === "string"
|
|
89
|
-
? m.content
|
|
90
|
-
: m.content.map((c) => (c.type === "text" ? c.text : "")).join(""),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const answers = (session: AgentSession) =>
|
|
94
|
-
session.messages
|
|
95
|
-
.filter((m): m is AssistantMessage => m.role === "assistant")
|
|
96
|
-
.map((m) =>
|
|
97
|
-
m.content.map((c) => (c.type === "text" ? c.text : "")).join(""),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
describe("SteerableRun on a Pi session", () => {
|
|
101
|
-
let seen: string[][];
|
|
102
|
-
beforeEach(() => {
|
|
103
|
-
seen = [];
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("a steer during the run is answered in the same turn", async () => {
|
|
107
|
-
const thinking = gate();
|
|
108
|
-
const started = gate();
|
|
109
|
-
const { session } = await fauxSession([
|
|
110
|
-
async (context) => {
|
|
111
|
-
seen.push(userTexts(context));
|
|
112
|
-
started.open();
|
|
113
|
-
await thinking.opened;
|
|
114
|
-
return fauxAssistantMessage("first answer");
|
|
115
|
-
},
|
|
116
|
-
(context) => {
|
|
117
|
-
seen.push(userTexts(context));
|
|
118
|
-
return fauxAssistantMessage("answer after the addition");
|
|
119
|
-
},
|
|
120
|
-
]);
|
|
121
|
-
const run = new SteerableRun(session, () => true);
|
|
122
|
-
const done = run.run(() => session.prompt("look this up for me"));
|
|
123
|
-
await started.opened;
|
|
124
|
-
expect(await run.steer("also check journald", [])).toBe(true);
|
|
125
|
-
thinking.open();
|
|
126
|
-
await done;
|
|
127
|
-
expect(run.steered).toBe(true);
|
|
128
|
-
expect(seen[1]).toEqual(["look this up for me", "also check journald"]);
|
|
129
|
-
expect(answers(session)).toEqual([
|
|
130
|
-
"first answer",
|
|
131
|
-
"answer after the addition",
|
|
132
|
-
]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("a steer at agent_end is answered in the same turn by Pi itself", async () => {
|
|
136
|
-
const { session } = await fauxSession([
|
|
137
|
-
fauxAssistantMessage("first answer"),
|
|
138
|
-
(context) => {
|
|
139
|
-
seen.push(userTexts(context));
|
|
140
|
-
return fauxAssistantMessage("answer after the addition");
|
|
141
|
-
},
|
|
142
|
-
]);
|
|
143
|
-
const run = new SteerableRun(session, () => true);
|
|
144
|
-
let late: Promise<boolean> | undefined;
|
|
145
|
-
// agent_end fires after Pi's last look at its steering queue.
|
|
146
|
-
session.subscribe((event) => {
|
|
147
|
-
if (event.type === "agent_end" && !late)
|
|
148
|
-
late = run.steer("a late addition", []);
|
|
149
|
-
});
|
|
150
|
-
await run.run(() => session.prompt("look this up for me"));
|
|
151
|
-
expect(await late).toBe(true);
|
|
152
|
-
expect(seen.at(-1)).toContain("a late addition");
|
|
153
|
-
expect(session.getSteeringMessages()).toEqual([]);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("stop aborts the run and drops its steers", async () => {
|
|
157
|
-
const started = gate();
|
|
158
|
-
const { session, core } = await fauxSession([
|
|
159
|
-
(_context, options) => {
|
|
160
|
-
started.open();
|
|
161
|
-
return new Promise<AssistantMessage>((resolve) =>
|
|
162
|
-
options?.signal?.addEventListener("abort", () =>
|
|
163
|
-
resolve(fauxAssistantMessage("", { stopReason: "aborted" })),
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
},
|
|
167
|
-
fauxAssistantMessage("must not appear"),
|
|
168
|
-
]);
|
|
169
|
-
const run = new SteerableRun(session, () => true);
|
|
170
|
-
const done = run.run(() => session.prompt("a long-running job"));
|
|
171
|
-
await started.opened;
|
|
172
|
-
expect(await run.steer("this line is dropped", [])).toBe(true);
|
|
173
|
-
expect(run.stop()).toBe(true);
|
|
174
|
-
await done;
|
|
175
|
-
expect(run.stopped).toBe(true);
|
|
176
|
-
expect(core.state.callCount).toBe(1);
|
|
177
|
-
expect(session.getSteeringMessages()).toEqual([]);
|
|
178
|
-
expect(run.stop()).toBe(false);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("a turn that may not be steered, or has ended, takes nothing", async () => {
|
|
182
|
-
const started = gate();
|
|
183
|
-
const finish = gate();
|
|
184
|
-
const { session } = await fauxSession([
|
|
185
|
-
async () => {
|
|
186
|
-
started.open();
|
|
187
|
-
await finish.opened;
|
|
188
|
-
return fauxAssistantMessage("done");
|
|
189
|
-
},
|
|
190
|
-
]);
|
|
191
|
-
const run = new SteerableRun(session, () => false);
|
|
192
|
-
const done = run.run(() => session.prompt("a scheduled job"));
|
|
193
|
-
await started.opened;
|
|
194
|
-
expect(await run.steer("cannot be inserted", [])).toBe(false);
|
|
195
|
-
finish.open();
|
|
196
|
-
await done;
|
|
197
|
-
expect(await run.steer("too late", [])).toBe(false);
|
|
198
|
-
expect(session.getSteeringMessages()).toEqual([]);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* The window Pi leaves open: its input handling ends after the session settled, so the steer
|
|
204
|
-
* sits in the queue with no run to take it.
|
|
205
|
-
*/
|
|
206
|
-
class SettledSession {
|
|
207
|
-
isStreaming = true;
|
|
208
|
-
queue: string[] = [];
|
|
209
|
-
prompts: string[] = [];
|
|
210
|
-
async steer(text: string) {
|
|
211
|
-
this.queue.push(text);
|
|
212
|
-
}
|
|
213
|
-
getSteeringMessages() {
|
|
214
|
-
return this.queue;
|
|
215
|
-
}
|
|
216
|
-
clearQueue() {
|
|
217
|
-
const steering = this.queue;
|
|
218
|
-
this.queue = [];
|
|
219
|
-
return { steering, followUp: [] };
|
|
220
|
-
}
|
|
221
|
-
async abort() {}
|
|
222
|
-
async waitForIdle() {}
|
|
223
|
-
async prompt(text: string) {
|
|
224
|
-
this.prompts.push(text);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
describe("SteerableRun leftovers", () => {
|
|
229
|
-
test("a steer Pi left queued as the run ended is prompted in the same turn", async () => {
|
|
230
|
-
const session = new SettledSession();
|
|
231
|
-
const run = new SteerableRun(
|
|
232
|
-
session as unknown as SteeringSession,
|
|
233
|
-
() => true,
|
|
234
|
-
);
|
|
235
|
-
await run.run(async () => {
|
|
236
|
-
expect(await run.steer("a late addition", [])).toBe(true);
|
|
237
|
-
session.isStreaming = false;
|
|
238
|
-
});
|
|
239
|
-
expect(session.prompts).toEqual(["a late addition"]);
|
|
240
|
-
expect(session.queue).toEqual([]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("a steer queued after the turn ended is taken back", async () => {
|
|
244
|
-
const session = new SettledSession();
|
|
245
|
-
const run = new SteerableRun(
|
|
246
|
-
session as unknown as SteeringSession,
|
|
247
|
-
() => true,
|
|
248
|
-
);
|
|
249
|
-
const settled = gate();
|
|
250
|
-
let late: Promise<boolean> | undefined;
|
|
251
|
-
session.steer = async (text: string) => {
|
|
252
|
-
// Pi's input handling finishes only after the turn is over.
|
|
253
|
-
await settled.opened;
|
|
254
|
-
session.queue.push(text);
|
|
255
|
-
};
|
|
256
|
-
await run.run(async () => {
|
|
257
|
-
late = run.steer("a late addition", []);
|
|
258
|
-
});
|
|
259
|
-
settled.open();
|
|
260
|
-
expect(await late).toBe(false);
|
|
261
|
-
expect(session.queue).toEqual([]);
|
|
262
|
-
expect(session.prompts).toEqual([]);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Type } from "typebox";
|
|
4
|
-
import { AgentError, ScheduleError } from "../domain/errors.ts";
|
|
5
|
-
import {
|
|
6
|
-
optionalString,
|
|
7
|
-
requiredString,
|
|
8
|
-
stringList,
|
|
9
|
-
type TextToolDef,
|
|
10
|
-
textToolsExtension,
|
|
11
|
-
} from "./text-tools.ts";
|
|
12
|
-
|
|
13
|
-
interface Registered {
|
|
14
|
-
name: string;
|
|
15
|
-
label: string;
|
|
16
|
-
execute: (
|
|
17
|
-
id: string,
|
|
18
|
-
params: unknown,
|
|
19
|
-
signal: AbortSignal | undefined,
|
|
20
|
-
) => Promise<{
|
|
21
|
-
isError?: boolean;
|
|
22
|
-
content: { type: string; text: string }[];
|
|
23
|
-
details?: unknown;
|
|
24
|
-
}>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function registered(defs: TextToolDef[]): Promise<Registered[]> {
|
|
28
|
-
const tools: Registered[] = [];
|
|
29
|
-
await textToolsExtension(
|
|
30
|
-
defs,
|
|
31
|
-
AgentError,
|
|
32
|
-
)({
|
|
33
|
-
registerTool: (tool: Registered) => {
|
|
34
|
-
tools.push(tool);
|
|
35
|
-
},
|
|
36
|
-
} as unknown as ExtensionAPI);
|
|
37
|
-
return tools;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const def = (run: TextToolDef["run"]): TextToolDef => ({
|
|
41
|
-
name: "echo",
|
|
42
|
-
label: "Echo",
|
|
43
|
-
description: "Echo.",
|
|
44
|
-
parameters: Type.Object({}),
|
|
45
|
-
run,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("textToolsExtension", () => {
|
|
49
|
-
test("registers each tool and returns its text", async () => {
|
|
50
|
-
const [tool] = await registered([def((i) => `got ${String(i.word)}`)]);
|
|
51
|
-
expect(tool?.name).toBe("echo");
|
|
52
|
-
expect(tool?.label).toBe("Echo");
|
|
53
|
-
const result = await tool?.execute("1", { word: "hi" }, undefined);
|
|
54
|
-
expect(result).toEqual({
|
|
55
|
-
content: [{ type: "text", text: "got hi" }],
|
|
56
|
-
details: {},
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("passes the call's signal to the tool", async () => {
|
|
61
|
-
const signal = new AbortController().signal;
|
|
62
|
-
let seen: AbortSignal | undefined;
|
|
63
|
-
const [tool] = await registered([
|
|
64
|
-
def((_i, s) => {
|
|
65
|
-
seen = s;
|
|
66
|
-
return "ok";
|
|
67
|
-
}),
|
|
68
|
-
]);
|
|
69
|
-
await tool?.execute("1", {}, signal);
|
|
70
|
-
expect(seen).toBe(signal);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("turns the refusal class into an error result", async () => {
|
|
74
|
-
const [tool] = await registered([
|
|
75
|
-
def(() => {
|
|
76
|
-
throw new AgentError("no such agent");
|
|
77
|
-
}),
|
|
78
|
-
]);
|
|
79
|
-
const result = await tool?.execute("1", {}, undefined);
|
|
80
|
-
expect(result?.isError).toBe(true);
|
|
81
|
-
expect(result?.content[0]?.text).toBe("no such agent");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("lets any other error fail the call", async () => {
|
|
85
|
-
const [tool] = await registered([
|
|
86
|
-
def(() => {
|
|
87
|
-
throw new ScheduleError("wrong domain");
|
|
88
|
-
}),
|
|
89
|
-
]);
|
|
90
|
-
expect(tool?.execute("1", {}, undefined)).rejects.toThrow("wrong domain");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("tool input", () => {
|
|
95
|
-
test("reads strings and string lists", () => {
|
|
96
|
-
const input = { a: "x", n: 3, list: ["p", 4, "q"] };
|
|
97
|
-
expect(optionalString(input, "a")).toBe("x");
|
|
98
|
-
expect(optionalString(input, "n")).toBeUndefined();
|
|
99
|
-
expect(stringList(input, "list")).toEqual(["p", "q"]);
|
|
100
|
-
expect(stringList(input, "a")).toBeUndefined();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("refuses a blank required string with AgentError", () => {
|
|
104
|
-
expect(requiredString({ a: " x " }, "a")).toBe(" x ");
|
|
105
|
-
expect(() => requiredString({ a: " " }, "a")).toThrow(
|
|
106
|
-
new AgentError("a is required."),
|
|
107
|
-
);
|
|
108
|
-
expect(() => requiredString({}, "b")).toThrow(AgentError);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { lastReply, thinkingOf, turnAnswer } from "./turn-answer.ts";
|
|
3
|
-
|
|
4
|
-
const assistant = (
|
|
5
|
-
text: string,
|
|
6
|
-
stopReason = "stop",
|
|
7
|
-
thinking?: { thinking: string; redacted?: boolean },
|
|
8
|
-
) => ({
|
|
9
|
-
role: "assistant",
|
|
10
|
-
stopReason,
|
|
11
|
-
content: [
|
|
12
|
-
...(thinking ? [{ type: "thinking", ...thinking }] : []),
|
|
13
|
-
{ type: "text", text },
|
|
14
|
-
],
|
|
15
|
-
});
|
|
16
|
-
const user = { role: "user", content: "hi" };
|
|
17
|
-
|
|
18
|
-
describe("turnAnswer", () => {
|
|
19
|
-
test("answers with the last message's text and its visible thinking", () => {
|
|
20
|
-
expect(
|
|
21
|
-
turnAnswer(
|
|
22
|
-
[
|
|
23
|
-
user,
|
|
24
|
-
assistant("tool call", "toolUse", { thinking: " plan " }),
|
|
25
|
-
assistant(" done ", "stop", { thinking: "secret", redacted: true }),
|
|
26
|
-
],
|
|
27
|
-
false,
|
|
28
|
-
),
|
|
29
|
-
).toEqual({ ok: true, text: "done", thinking: "plan" });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("keeps every final answer of a steered run", () => {
|
|
33
|
-
expect(
|
|
34
|
-
turnAnswer(
|
|
35
|
-
[
|
|
36
|
-
assistant("first"),
|
|
37
|
-
user,
|
|
38
|
-
assistant("tool", "toolUse"),
|
|
39
|
-
assistant("second"),
|
|
40
|
-
],
|
|
41
|
-
true,
|
|
42
|
-
),
|
|
43
|
-
).toEqual({ ok: true, text: "first\n\nsecond" });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("fails without a usable answer", () => {
|
|
47
|
-
const none = turnAnswer([user], false);
|
|
48
|
-
expect(none.ok || none.error.message).toBe(
|
|
49
|
-
"the run produced no assistant message",
|
|
50
|
-
);
|
|
51
|
-
const blank = turnAnswer([assistant(" ")], false);
|
|
52
|
-
expect(blank.ok || blank.error.message).toBe(
|
|
53
|
-
"the final assistant message has no text",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("session helpers", () => {
|
|
59
|
-
test("lastReply is the last answer's text, or undefined", () => {
|
|
60
|
-
expect(lastReply([assistant("a"), user, assistant(" b ")])).toBe("b");
|
|
61
|
-
expect(lastReply([user])).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("thinkingOf skips redacted thinking", () => {
|
|
65
|
-
expect(
|
|
66
|
-
thinkingOf([
|
|
67
|
-
assistant("x", "stop", { thinking: "one" }),
|
|
68
|
-
assistant("y", "stop", { thinking: "two", redacted: true }),
|
|
69
|
-
]),
|
|
70
|
-
).toBe("one");
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { AgentRunError } from "../domain/errors.ts";
|
|
3
|
-
import { lastAssistant, textOf } from "../shared/session-messages.ts";
|
|
4
|
-
import { MAX_REPORT_CHARS, workerReport } from "./worker-task.ts";
|
|
5
|
-
|
|
6
|
-
const user = { role: "user", content: "task" };
|
|
7
|
-
const answer = (text: string, extra: Record<string, unknown> = {}) => ({
|
|
8
|
-
role: "assistant",
|
|
9
|
-
content: [
|
|
10
|
-
{ type: "thinking", thinking: "hm" },
|
|
11
|
-
{ type: "text", text },
|
|
12
|
-
],
|
|
13
|
-
stopReason: "stop",
|
|
14
|
-
...extra,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("textOf", () => {
|
|
18
|
-
test("joins text parts and passes a string through", () => {
|
|
19
|
-
expect(
|
|
20
|
-
textOf([
|
|
21
|
-
{ type: "text", text: "a" },
|
|
22
|
-
{ type: "image" },
|
|
23
|
-
{ type: "text", text: "b" },
|
|
24
|
-
]),
|
|
25
|
-
).toBe("a\nb");
|
|
26
|
-
expect(textOf("plain")).toBe("plain");
|
|
27
|
-
expect(textOf(undefined)).toBe("");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("lastAssistant", () => {
|
|
32
|
-
test("finds the last assistant message", () => {
|
|
33
|
-
const result = lastAssistant([user, answer("first"), answer("second")]);
|
|
34
|
-
expect(result.ok && textOf(result.message.content)).toBe("second");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("says why there is no usable answer", () => {
|
|
38
|
-
expect(lastAssistant([user])).toEqual({
|
|
39
|
-
ok: false,
|
|
40
|
-
error: "the run produced no assistant message",
|
|
41
|
-
});
|
|
42
|
-
expect(
|
|
43
|
-
lastAssistant([answer("", { stopReason: "error", errorMessage: "503" })]),
|
|
44
|
-
).toEqual({ ok: false, error: "503" });
|
|
45
|
-
expect(lastAssistant([answer("", { stopReason: "error" })])).toEqual({
|
|
46
|
-
ok: false,
|
|
47
|
-
error: "the run stopped with error",
|
|
48
|
-
});
|
|
49
|
-
expect(lastAssistant([answer("", { stopReason: "aborted" })])).toEqual({
|
|
50
|
-
ok: false,
|
|
51
|
-
error: "the run stopped with aborted",
|
|
52
|
-
});
|
|
53
|
-
expect(
|
|
54
|
-
lastAssistant([answer("", { stopReason: "aborted" })], "timed out"),
|
|
55
|
-
).toEqual({ ok: false, error: "timed out" });
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("workerReport", () => {
|
|
60
|
-
test("returns the trimmed answer", () => {
|
|
61
|
-
expect(workerReport([user, answer(" done ")], "stopped")).toBe("done");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("cuts a long report and says so", () => {
|
|
65
|
-
const report = workerReport(
|
|
66
|
-
[answer("x".repeat(MAX_REPORT_CHARS + 5))],
|
|
67
|
-
"stopped",
|
|
68
|
-
);
|
|
69
|
-
expect(report).toBe(
|
|
70
|
-
`${"x".repeat(MAX_REPORT_CHARS)}\n\n[report cut at ${MAX_REPORT_CHARS} characters]`,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("throws AgentRunError without a usable answer", () => {
|
|
75
|
-
expect(() =>
|
|
76
|
-
workerReport([answer("", { stopReason: "aborted" })], "stopped"),
|
|
77
|
-
).toThrow(new AgentRunError("stopped"));
|
|
78
|
-
expect(() => workerReport([answer(" ")], "stopped")).toThrow(
|
|
79
|
-
new AgentRunError("the worker's answer is empty"),
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { PluginError } from "./errors.ts";
|
|
3
|
-
import { CoreRegistry, type CoreStores } from "./services.ts";
|
|
4
|
-
|
|
5
|
-
const stores = {} as CoreStores;
|
|
6
|
-
|
|
7
|
-
describe("CoreRegistry", () => {
|
|
8
|
-
test("gives a plugin what a built-in one provided", () => {
|
|
9
|
-
const core = new CoreRegistry();
|
|
10
|
-
core.provide("stores", stores);
|
|
11
|
-
expect(core.stores).toBe(stores);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("reading a service before it is provided names the service and the fix", () => {
|
|
15
|
-
expect(() => new CoreRegistry().stores).toThrow(PluginError);
|
|
16
|
-
expect(() => new CoreRegistry().stores).toThrow(
|
|
17
|
-
"core service stores is not provided yet. Register the built-in plugin that provides it before the plugin that reads it.",
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("a service provided twice is refused", () => {
|
|
22
|
-
const core = new CoreRegistry();
|
|
23
|
-
core.provide("stores", stores);
|
|
24
|
-
expect(() => core.provide("stores", stores)).toThrow(
|
|
25
|
-
"core service stores is provided twice. Register only one plugin that provides it.",
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
});
|