pi-roundtable 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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import { AgentError } from "../domain/errors.ts";
|
|
4
|
+
import { toolError, toolText } from "../shared/tool-result.ts";
|
|
5
|
+
|
|
6
|
+
export type ToolInput = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/** A tool whose result is text for the model. */
|
|
9
|
+
export interface TextToolDef<Name extends string = string> {
|
|
10
|
+
name: Name;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: TSchema;
|
|
14
|
+
run(input: ToolInput, signal?: AbortSignal): Promise<string> | string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ErrorClass = abstract new (...args: never[]) => Error;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers text tools. A thrown `refusal` becomes an error result the model reads; any other
|
|
21
|
+
* error fails the call.
|
|
22
|
+
*/
|
|
23
|
+
export function textToolsExtension(
|
|
24
|
+
defs: readonly TextToolDef[],
|
|
25
|
+
refusal: ErrorClass,
|
|
26
|
+
): ExtensionFactory {
|
|
27
|
+
return (pi) => {
|
|
28
|
+
for (const def of defs) {
|
|
29
|
+
pi.registerTool({
|
|
30
|
+
name: def.name,
|
|
31
|
+
label: def.label,
|
|
32
|
+
description: def.description,
|
|
33
|
+
parameters: def.parameters,
|
|
34
|
+
execute: async (_toolCallId, params, signal) => {
|
|
35
|
+
try {
|
|
36
|
+
return toolText(await def.run(params as ToolInput, signal));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof refusal)) throw error;
|
|
39
|
+
return toolError(error.message);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A string argument, or undefined when absent. */
|
|
48
|
+
export function optionalString(input: ToolInput, key: string) {
|
|
49
|
+
const value = input[key];
|
|
50
|
+
return typeof value === "string" ? value : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A string argument that is not blank; throws AgentError otherwise. */
|
|
54
|
+
export function requiredString(input: ToolInput, key: string): string {
|
|
55
|
+
const value = optionalString(input, key);
|
|
56
|
+
if (!value?.trim()) throw new AgentError(`${key} is required.`);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The strings of an array argument, or undefined when it is not an array. */
|
|
61
|
+
export function stringList(input: ToolInput, key: string) {
|
|
62
|
+
const value = input[key];
|
|
63
|
+
return Array.isArray(value)
|
|
64
|
+
? value.filter((v): v is string => typeof v === "string")
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { TurnResult } from "../domain/conversation.ts";
|
|
2
|
+
import { AgentRunError } from "../domain/errors.ts";
|
|
3
|
+
import { lastAssistant, textOf } from "../shared/session-messages.ts";
|
|
4
|
+
|
|
5
|
+
interface MessagePart {
|
|
6
|
+
type?: unknown;
|
|
7
|
+
text?: unknown;
|
|
8
|
+
thinking?: unknown;
|
|
9
|
+
redacted?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Message = { role: string };
|
|
13
|
+
type Assistant = Message & {
|
|
14
|
+
role: "assistant";
|
|
15
|
+
content: unknown;
|
|
16
|
+
stopReason?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isAssistant = (message: Message): message is Assistant =>
|
|
20
|
+
message.role === "assistant";
|
|
21
|
+
|
|
22
|
+
/** The session's last answer, which tells the judge what a short follow-up continues. */
|
|
23
|
+
export function lastReply(messages: readonly Message[]): string | undefined {
|
|
24
|
+
const last = messages.findLast(isAssistant);
|
|
25
|
+
return last ? textOf(last.content).trim() || undefined : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The visible thinking of the messages a turn added, in order. */
|
|
29
|
+
export function thinkingOf(messages: readonly Message[]): string {
|
|
30
|
+
return messages
|
|
31
|
+
.filter(isAssistant)
|
|
32
|
+
.flatMap((message) =>
|
|
33
|
+
Array.isArray(message.content)
|
|
34
|
+
? (message.content as (MessagePart | undefined)[])
|
|
35
|
+
: [],
|
|
36
|
+
)
|
|
37
|
+
.filter(
|
|
38
|
+
(
|
|
39
|
+
part,
|
|
40
|
+
): part is { type: "thinking"; thinking: string; redacted?: boolean } =>
|
|
41
|
+
part?.type === "thinking" &&
|
|
42
|
+
typeof part.thinking === "string" &&
|
|
43
|
+
part.redacted !== true,
|
|
44
|
+
)
|
|
45
|
+
.map((part) => part.thinking.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join("\n\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A finished turn's result from the messages it added. A steered run may have answered before
|
|
52
|
+
* the steer arrived, so every final answer of it is kept.
|
|
53
|
+
*/
|
|
54
|
+
export function turnAnswer(
|
|
55
|
+
messages: readonly Message[],
|
|
56
|
+
steered: boolean,
|
|
57
|
+
): TurnResult {
|
|
58
|
+
const last = lastAssistant(messages);
|
|
59
|
+
if (!last.ok) return { ok: false, error: new AgentRunError(last.error) };
|
|
60
|
+
const text = (
|
|
61
|
+
steered
|
|
62
|
+
? messages
|
|
63
|
+
.filter(
|
|
64
|
+
(message): message is Assistant =>
|
|
65
|
+
isAssistant(message) && message.stopReason === "stop",
|
|
66
|
+
)
|
|
67
|
+
.map((message) => textOf(message.content).trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join("\n\n")
|
|
70
|
+
: textOf(last.message.content)
|
|
71
|
+
).trim();
|
|
72
|
+
if (!text)
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: new AgentRunError("the final assistant message has no text"),
|
|
76
|
+
};
|
|
77
|
+
const thinking = thinkingOf(messages);
|
|
78
|
+
return { ok: true, text, ...(thinking ? { thinking } : {}) };
|
|
79
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentSession,
|
|
3
|
+
ModelRuntime,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { AgentRunError, ConfigError } from "../domain/errors.ts";
|
|
6
|
+
import { parseModelRef } from "../models.ts";
|
|
7
|
+
import { lastAssistant, textOf } from "../shared/session-messages.ts";
|
|
8
|
+
|
|
9
|
+
export const MAX_REPORT_CHARS = 20_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A finished worker's report: its last answer, cut at MAX_REPORT_CHARS. Throws AgentRunError
|
|
13
|
+
* when it has none; `aborted` says why an aborted run stopped.
|
|
14
|
+
*/
|
|
15
|
+
export function workerReport(
|
|
16
|
+
messages: readonly { role: string }[],
|
|
17
|
+
aborted: string,
|
|
18
|
+
): string {
|
|
19
|
+
const last = lastAssistant(messages, aborted);
|
|
20
|
+
if (!last.ok) throw new AgentRunError(last.error);
|
|
21
|
+
const report = textOf(last.message.content).trim();
|
|
22
|
+
if (!report) throw new AgentRunError("the worker's answer is empty");
|
|
23
|
+
return report.length > MAX_REPORT_CHARS
|
|
24
|
+
? `${report.slice(0, MAX_REPORT_CHARS)}\n\n[report cut at ${MAX_REPORT_CHARS} characters]`
|
|
25
|
+
: report;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The host's model for `<provider>/<id>`; throws ConfigError when it has none. */
|
|
29
|
+
export function hostModel(runtime: ModelRuntime, model: string) {
|
|
30
|
+
const ref = parseModelRef(model);
|
|
31
|
+
const resolved = ref && runtime.getModel(ref.provider, ref.id);
|
|
32
|
+
if (!resolved)
|
|
33
|
+
throw new ConfigError(`model ${model} is not available on this host`);
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runs one task in a fresh session on the model and returns its report; the signal aborts it.
|
|
39
|
+
* The session is disposed of either way.
|
|
40
|
+
*/
|
|
41
|
+
export async function runWorkerTask(
|
|
42
|
+
session: AgentSession,
|
|
43
|
+
options: {
|
|
44
|
+
modelRuntime: ModelRuntime;
|
|
45
|
+
/** `<provider>/<id>`. */
|
|
46
|
+
model: string;
|
|
47
|
+
task: string;
|
|
48
|
+
signal: AbortSignal;
|
|
49
|
+
/** Why an aborted run stopped, as the report's error. */
|
|
50
|
+
aborted: string;
|
|
51
|
+
},
|
|
52
|
+
): Promise<string> {
|
|
53
|
+
const { modelRuntime, model, task, signal, aborted } = options;
|
|
54
|
+
const onAbort = () => void session.abort();
|
|
55
|
+
signal.addEventListener("abort", onAbort);
|
|
56
|
+
try {
|
|
57
|
+
// Extension providers such as claude-bridge exist only once the session loaded them.
|
|
58
|
+
await session.setModel(hostModel(modelRuntime, model));
|
|
59
|
+
await session.prompt(task);
|
|
60
|
+
return workerReport(session.messages, aborted);
|
|
61
|
+
} finally {
|
|
62
|
+
signal.removeEventListener("abort", onAbort);
|
|
63
|
+
session.dispose();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { AgentStore } from "./agents/agent-store.ts";
|
|
2
|
+
import type { AgentTeam } from "./agents/agent-team.ts";
|
|
3
|
+
import type { AvatarStudio } from "./agents/avatar-studio.ts";
|
|
4
|
+
import type { DiscordSurface } from "./discord/discord-surface.ts";
|
|
5
|
+
import type { DispatchThreads } from "./discord/dispatch-threads.ts";
|
|
6
|
+
import type { OwnerCards } from "./discord/owner-cards.ts";
|
|
7
|
+
import type { OwnerGuard } from "./discord/owner-command.ts";
|
|
8
|
+
import { PluginError } from "./errors.ts";
|
|
9
|
+
import type { ConfirmationJudge } from "./judging/confirmation-judge.ts";
|
|
10
|
+
import type { BackgroundTurns } from "./modules/background/background-turns.ts";
|
|
11
|
+
import type { Delegator } from "./modules/delegation/delegator.ts";
|
|
12
|
+
import type { OwnerMemoryStore } from "./modules/memory/owner-memory-store.ts";
|
|
13
|
+
import type { ScheduleStore } from "./modules/schedules/schedule-store.ts";
|
|
14
|
+
import type { SkillRegistry } from "./modules/skills/skill-registry.ts";
|
|
15
|
+
import type { SkillStore } from "./modules/skills/skill-store.ts";
|
|
16
|
+
import type { PendingConfirmationStore } from "./runtime/pending-confirmation-store.ts";
|
|
17
|
+
import type { PiAgentRuntime } from "./runtime/pi-agent-runtime.ts";
|
|
18
|
+
|
|
19
|
+
/** The stores every process has, attached to the host's one pool. */
|
|
20
|
+
export interface CoreStores {
|
|
21
|
+
/** The owner's memory. */
|
|
22
|
+
memory: OwnerMemoryStore;
|
|
23
|
+
schedules: ScheduleStore;
|
|
24
|
+
/** Held actions, kept across a restart. */
|
|
25
|
+
confirmations: PendingConfirmationStore;
|
|
26
|
+
/** The agent server's agents and groups. */
|
|
27
|
+
agents: AgentStore;
|
|
28
|
+
skills: SkillStore;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The Discord connection and what stands on it. */
|
|
32
|
+
export interface CoreDiscord {
|
|
33
|
+
surface: DiscordSurface;
|
|
34
|
+
/** The owner's cards, which also answer the interactions of held actions. */
|
|
35
|
+
cards: OwnerCards;
|
|
36
|
+
guard: OwnerGuard;
|
|
37
|
+
studio: AvatarStudio;
|
|
38
|
+
/** The threads that carry background reports. */
|
|
39
|
+
threads: DispatchThreads;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the agent server builds. */
|
|
43
|
+
export interface CoreAgents {
|
|
44
|
+
team: AgentTeam;
|
|
45
|
+
runtime: PiAgentRuntime;
|
|
46
|
+
skills: SkillRegistry;
|
|
47
|
+
/** Decides whether the owner's reply approves a held action. */
|
|
48
|
+
confirmations: ConfirmationJudge;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What the built-in plugins build for the plugins registered after them. */
|
|
52
|
+
export interface CoreServices {
|
|
53
|
+
stores: CoreStores;
|
|
54
|
+
discord: CoreDiscord;
|
|
55
|
+
/** Turns nobody wrote: a due schedule's, a delegated task's report, a logged error's. */
|
|
56
|
+
background: BackgroundTurns;
|
|
57
|
+
delegator: Delegator;
|
|
58
|
+
/** The agent server: its team, the runtime that runs every turn, and the skill registry. */
|
|
59
|
+
agents: CoreAgents;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Reads the core's services; reading one before its built-in plugin has set up throws a PluginError. */
|
|
63
|
+
export type CoreAccess = {
|
|
64
|
+
readonly [K in keyof CoreServices]: CoreServices[K];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Holds what the built-in plugins provide, for the plugins set up after them. */
|
|
68
|
+
export class CoreRegistry implements CoreAccess {
|
|
69
|
+
readonly #values = new Map<keyof CoreServices, unknown>();
|
|
70
|
+
|
|
71
|
+
/** Stores a service; a built-in plugin calls this once in its setup. */
|
|
72
|
+
provide<K extends keyof CoreServices>(key: K, value: CoreServices[K]): void {
|
|
73
|
+
if (this.#values.has(key))
|
|
74
|
+
throw new PluginError(
|
|
75
|
+
`core service ${key} is provided twice. Register only one plugin that provides it.`,
|
|
76
|
+
);
|
|
77
|
+
this.#values.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#need<K extends keyof CoreServices>(key: K): CoreServices[K] {
|
|
81
|
+
if (!this.#values.has(key))
|
|
82
|
+
throw new PluginError(
|
|
83
|
+
`core service ${key} is not provided yet. Register the built-in plugin that provides it before the plugin that reads it.`,
|
|
84
|
+
);
|
|
85
|
+
return this.#values.get(key) as CoreServices[K];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get stores(): CoreStores {
|
|
89
|
+
return this.#need("stores");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get discord(): CoreDiscord {
|
|
93
|
+
return this.#need("discord");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get background(): BackgroundTurns {
|
|
97
|
+
return this.#need("background");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get delegator(): Delegator {
|
|
101
|
+
return this.#need("delegator");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get agents(): CoreAgents {
|
|
105
|
+
return this.#need("agents");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { PluginError } from "./errors.ts";
|
|
3
|
+
import { compileSessionPlan, planOrder, type SessionTool } from "./sessions.ts";
|
|
4
|
+
|
|
5
|
+
const tool = (name: string, phase: SessionTool["phase"]): SessionTool => ({
|
|
6
|
+
name,
|
|
7
|
+
phase,
|
|
8
|
+
...(phase === "compaction" ? { engine: `${name}-engine` } : {}),
|
|
9
|
+
snapshot: () => ({ revision: 0, factory: () => null }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const names = (tools: readonly SessionTool[]) => tools.map((t) => t.name);
|
|
13
|
+
|
|
14
|
+
describe("compileSessionPlan", () => {
|
|
15
|
+
test("orders tools, then the compactor, then MCP, keeping registration order in each", () => {
|
|
16
|
+
const plan = compileSessionPlan([
|
|
17
|
+
tool("mcp-a", "mcp"),
|
|
18
|
+
tool("memory", "tools"),
|
|
19
|
+
tool("summarizer", "compaction"),
|
|
20
|
+
tool("notify", "tools"),
|
|
21
|
+
tool("mcp-b", "mcp"),
|
|
22
|
+
]);
|
|
23
|
+
expect(names(planOrder(plan))).toEqual([
|
|
24
|
+
"memory",
|
|
25
|
+
"notify",
|
|
26
|
+
"summarizer",
|
|
27
|
+
"mcp-a",
|
|
28
|
+
"mcp-b",
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a plan without a compactor has none", () => {
|
|
33
|
+
const plan = compileSessionPlan([tool("memory", "tools")]);
|
|
34
|
+
expect(plan.compaction).toBeUndefined();
|
|
35
|
+
expect(names(planOrder(plan))).toEqual(["memory"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("refuses a name registered twice", () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
compileSessionPlan([tool("memory", "tools"), tool("memory", "mcp")]),
|
|
41
|
+
).toThrow(
|
|
42
|
+
new PluginError(
|
|
43
|
+
"session tool memory is registered twice. Rename one of the two.",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("refuses the names of the core's own extensions", () => {
|
|
49
|
+
for (const name of [
|
|
50
|
+
"read-attachment",
|
|
51
|
+
"confirmation-gate",
|
|
52
|
+
"ask-user",
|
|
53
|
+
"self-compact-guard",
|
|
54
|
+
"profile-tools",
|
|
55
|
+
]) {
|
|
56
|
+
expect(() => compileSessionPlan([tool(name, "tools")])).toThrow(
|
|
57
|
+
new PluginError(
|
|
58
|
+
`session tool ${name} takes a core extension name. Rename it.`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("refuses a compactor that names no engine", () => {
|
|
65
|
+
const { engine: _engine, ...unmarked } = tool("summarizer", "compaction");
|
|
66
|
+
expect(() => compileSessionPlan([unmarked])).toThrow(
|
|
67
|
+
new PluginError(
|
|
68
|
+
"compactor summarizer needs an engine: the value its compactions record as details.engine.",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("refuses a second compactor", () => {
|
|
74
|
+
expect(() =>
|
|
75
|
+
compileSessionPlan([
|
|
76
|
+
tool("summarizer", "compaction"),
|
|
77
|
+
tool("pi", "compaction"),
|
|
78
|
+
]),
|
|
79
|
+
).toThrow(
|
|
80
|
+
new PluginError(
|
|
81
|
+
"only one compactor may run; got summarizer, pi. Keep one.",
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|