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,129 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
import { addPlugin } from "./add-plugin.ts";
|
|
5
|
-
import { init } from "./init.ts";
|
|
6
|
-
import { tempDir } from "./testing/fixtures.ts";
|
|
7
|
-
|
|
8
|
-
const ROOT = resolve(import.meta.dir, "../..");
|
|
9
|
-
const TSC = join(ROOT, "node_modules/.bin/tsc");
|
|
10
|
-
const BIOME = join(ROOT, "node_modules/.bin/biome");
|
|
11
|
-
|
|
12
|
-
function run(cwd: string, cmd: string[]): { ok: boolean; output: string } {
|
|
13
|
-
const result = Bun.spawnSync(cmd, {
|
|
14
|
-
cwd,
|
|
15
|
-
stdout: "pipe",
|
|
16
|
-
stderr: "pipe",
|
|
17
|
-
env: { ...process.env, CI: "" },
|
|
18
|
-
});
|
|
19
|
-
return {
|
|
20
|
-
ok: result.exitCode === 0,
|
|
21
|
-
output: `${result.stdout.toString()}${result.stderr.toString()}`,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The generated project with `pi-roundtable`, `typebox`, and the types linked from this
|
|
27
|
-
* checkout, so it typechecks and tests against the source under test without the network.
|
|
28
|
-
*/
|
|
29
|
-
function link(project: string): void {
|
|
30
|
-
const modules = join(project, "node_modules");
|
|
31
|
-
mkdirSync(join(modules, "pi-roundtable"), { recursive: true });
|
|
32
|
-
mkdirSync(join(modules, "@types"), { recursive: true });
|
|
33
|
-
writeFileSync(
|
|
34
|
-
join(modules, "pi-roundtable/package.json"),
|
|
35
|
-
JSON.stringify({
|
|
36
|
-
name: "pi-roundtable",
|
|
37
|
-
type: "module",
|
|
38
|
-
exports: { ".": "./src/index.ts", "./testing": "./src/testing.ts" },
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
symlinkSync(join(ROOT, "src"), join(modules, "pi-roundtable/src"));
|
|
42
|
-
symlinkSync(join(ROOT, "node_modules/typebox"), join(modules, "typebox"));
|
|
43
|
-
symlinkSync(
|
|
44
|
-
join(ROOT, "node_modules/@types/bun"),
|
|
45
|
-
join(modules, "@types/bun"),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const dir = tempDir("roundtable-template-");
|
|
50
|
-
const bun = { version: Bun.version, required: ">=1.3.0" };
|
|
51
|
-
const check = (label: string) => {
|
|
52
|
-
const typecheck = run(dir.path, [TSC, "--noEmit"]);
|
|
53
|
-
const test = run(dir.path, ["bun", "test"]);
|
|
54
|
-
const lint = run(dir.path, [BIOME, "check", "."]);
|
|
55
|
-
return { label, typecheck, test, lint };
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
let fresh: ReturnType<typeof check>;
|
|
59
|
-
let grown: ReturnType<typeof check>;
|
|
60
|
-
|
|
61
|
-
beforeAll(() => {
|
|
62
|
-
const report = init({ cwd: dir.path, bun, version: "0.1.0" });
|
|
63
|
-
if (!report.ok) throw new Error(report.problems.join("\n"));
|
|
64
|
-
link(dir.path);
|
|
65
|
-
fresh = check("fresh");
|
|
66
|
-
const added = addPlugin({ cwd: dir.path, name: "second-plugin" });
|
|
67
|
-
if (!added.ok) throw new Error(added.problems.join("\n"));
|
|
68
|
-
grown = check("after add plugin");
|
|
69
|
-
}, 120_000);
|
|
70
|
-
afterAll(() => dir.done());
|
|
71
|
-
|
|
72
|
-
describe("every template rendered into one project", () => {
|
|
73
|
-
test("typechecks with no edits", () => {
|
|
74
|
-
expect(fresh.typecheck.output).toBe("");
|
|
75
|
-
expect(fresh.typecheck.ok).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
test("passes its own test with no edits", () => {
|
|
78
|
-
expect(fresh.test.output).toContain("1 pass");
|
|
79
|
-
expect(fresh.test.ok).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
test("passes the linter it ships with", () => {
|
|
82
|
-
expect(fresh.lint.output).toContain("No fixes applied");
|
|
83
|
-
expect(fresh.lint.ok).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
test("still typechecks, tests, and lints after `add plugin`", () => {
|
|
86
|
-
expect(grown.typecheck.output).toBe("");
|
|
87
|
-
expect(grown.test.output).toContain("2 pass");
|
|
88
|
-
expect(grown.lint.output).toContain("No fixes applied");
|
|
89
|
-
expect(grown.typecheck.ok && grown.test.ok && grown.lint.ok).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("the generated project's pinned tools match this package's own", () => {
|
|
94
|
-
const project = JSON.parse(
|
|
95
|
-
readFileSync(join(dir.path, "package.json"), "utf8"),
|
|
96
|
-
) as {
|
|
97
|
-
dependencies: Record<string, string>;
|
|
98
|
-
devDependencies: Record<string, string>;
|
|
99
|
-
};
|
|
100
|
-
const own = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")) as {
|
|
101
|
-
dependencies: Record<string, string>;
|
|
102
|
-
devDependencies: Record<string, string>;
|
|
103
|
-
};
|
|
104
|
-
expect(project.dependencies.typebox).toBe(own.dependencies.typebox);
|
|
105
|
-
for (const name of ["typescript", "@biomejs/biome", "@types/bun"])
|
|
106
|
-
expect(project.devDependencies[name]).toBe(own.devDependencies[name]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("no template file is hidden from git by an ignore rule, which would leave a clone unable to init", () => {
|
|
110
|
-
const inRepository = Bun.spawnSync(["git", "rev-parse", "--git-dir"], {
|
|
111
|
-
cwd: ROOT,
|
|
112
|
-
stdout: "pipe",
|
|
113
|
-
stderr: "pipe",
|
|
114
|
-
});
|
|
115
|
-
// The exported public tree is not a checkout; its own ignore file is packaging/.gitignore.
|
|
116
|
-
if (inRepository.exitCode !== 0) return;
|
|
117
|
-
const ignored = Bun.spawnSync(
|
|
118
|
-
[
|
|
119
|
-
"git",
|
|
120
|
-
"ls-files",
|
|
121
|
-
"--others",
|
|
122
|
-
"--ignored",
|
|
123
|
-
"--exclude-standard",
|
|
124
|
-
"templates",
|
|
125
|
-
],
|
|
126
|
-
{ cwd: ROOT, stdout: "pipe", stderr: "pipe" },
|
|
127
|
-
);
|
|
128
|
-
expect(ignored.stdout.toString().trim()).toBe("");
|
|
129
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import type { RoundtableConfig } from "../../core/config/config.ts";
|
|
5
|
-
import type { DefinedRoundtable } from "../../core/define-roundtable.ts";
|
|
6
|
-
import type { Http, HttpResponse } from "../http.ts";
|
|
7
|
-
import type { Ports } from "../project.ts";
|
|
8
|
-
|
|
9
|
-
/** A configuration the schema accepts. */
|
|
10
|
-
export const validConfig: RoundtableConfig = {
|
|
11
|
-
owner: { id: "100000000000000001", name: "Ada" },
|
|
12
|
-
discord: {
|
|
13
|
-
token: "bot-token",
|
|
14
|
-
guild: "900000000000000001",
|
|
15
|
-
entryChannel: "900000000000000002",
|
|
16
|
-
},
|
|
17
|
-
database: { url: "postgres://user:secret@db.example.test:5432/bot" },
|
|
18
|
-
dataDir: "/data",
|
|
19
|
-
model: "anthropic/claude-sonnet-5-5",
|
|
20
|
-
http: { publicUrl: "https://bot.example.test" },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** A directory that is removed when the test ends; call `done` in `afterEach`. */
|
|
24
|
-
export function tempDir(prefix = "roundtable-cli-"): {
|
|
25
|
-
path: string;
|
|
26
|
-
write(file: string, content: string): void;
|
|
27
|
-
done(): void;
|
|
28
|
-
} {
|
|
29
|
-
const path = mkdtempSync(join(tmpdir(), prefix));
|
|
30
|
-
return {
|
|
31
|
-
path,
|
|
32
|
-
write(file, content) {
|
|
33
|
-
mkdirSync(dirname(join(path, file)), { recursive: true });
|
|
34
|
-
writeFileSync(join(path, file), content);
|
|
35
|
-
},
|
|
36
|
-
done: () => rmSync(path, { recursive: true, force: true }),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Ports that hand out `config`, assemble nothing real, and report a login from `source`. */
|
|
41
|
-
export function fakePorts(
|
|
42
|
-
config: unknown = validConfig,
|
|
43
|
-
overrides: Partial<Ports> = {},
|
|
44
|
-
): Ports {
|
|
45
|
-
const defined: DefinedRoundtable = {
|
|
46
|
-
options: { logger: {} as never },
|
|
47
|
-
plugins: [{ name: "database", setup: () => ({}) }],
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
loadConfig: async () => config,
|
|
51
|
-
define: async () => defined,
|
|
52
|
-
login: async () => "ANTHROPIC_API_KEY",
|
|
53
|
-
...overrides,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** An HTTP client that answers from `routes`, keyed by the path after the host, and fails on any other. */
|
|
58
|
-
export function fakeHttp(
|
|
59
|
-
routes: Record<string, HttpResponse | Error>,
|
|
60
|
-
): Http & { requests: string[] } {
|
|
61
|
-
const requests: string[] = [];
|
|
62
|
-
return {
|
|
63
|
-
requests,
|
|
64
|
-
async get(url) {
|
|
65
|
-
const key = url.replace(/^https?:\/\/[^/]+(\/api\/v10)?/, "") || "/";
|
|
66
|
-
requests.push(key);
|
|
67
|
-
const route = routes[key];
|
|
68
|
-
if (route === undefined) throw new Error(`no route for ${key}`);
|
|
69
|
-
if (route instanceof Error) throw route;
|
|
70
|
-
return route;
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const BOT_ID = "800000000000000001";
|
|
76
|
-
|
|
77
|
-
/** Discord as it answers when everything is right: the token is good, the bot is in the guild, with every permission and the intent. */
|
|
78
|
-
export function healthyDiscord(
|
|
79
|
-
permissions: bigint,
|
|
80
|
-
guild = validConfig.discord.guild,
|
|
81
|
-
channel = validConfig.discord.entryChannel,
|
|
82
|
-
): Record<string, HttpResponse> {
|
|
83
|
-
return {
|
|
84
|
-
"/users/@me": { status: 200, body: { id: BOT_ID, username: "Bot" } },
|
|
85
|
-
"/applications/@me": { status: 200, body: { flags: 1 << 18 } },
|
|
86
|
-
[`/guilds/${guild}`]: {
|
|
87
|
-
status: 200,
|
|
88
|
-
body: {
|
|
89
|
-
name: "Test Guild",
|
|
90
|
-
owner_id: "100000000000000001",
|
|
91
|
-
roles: [
|
|
92
|
-
{ id: guild, permissions: "0" },
|
|
93
|
-
{ id: "700000000000000001", permissions: String(permissions) },
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
[`/guilds/${guild}/members/${BOT_ID}`]: {
|
|
98
|
-
status: 200,
|
|
99
|
-
body: { roles: ["700000000000000001"] },
|
|
100
|
-
},
|
|
101
|
-
[`/channels/${channel}`]: {
|
|
102
|
-
status: 200,
|
|
103
|
-
body: { id: channel, guild_id: guild, permission_overwrites: [] },
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import type { InboundMessage } from "../contract/channels.ts";
|
|
3
|
-
import { silentLogger } from "../log.ts";
|
|
4
|
-
import { type Speaker, speakerPolicy } from "../speakers.ts";
|
|
5
|
-
import { TEST_OWNER } from "../testing/owner.ts";
|
|
6
|
-
import { agentClaim } from "./agent-claim.ts";
|
|
7
|
-
|
|
8
|
-
const OWNER = "100000000000000001";
|
|
9
|
-
const ADMIN = "100000000000000002";
|
|
10
|
-
const STRANGER = "100000000000000009";
|
|
11
|
-
const ROLE = "200000000000000001";
|
|
12
|
-
|
|
13
|
-
function setup() {
|
|
14
|
-
const answered: { kind: string; speaker: Speaker; text: string }[] = [];
|
|
15
|
-
const team = {
|
|
16
|
-
guildId: "1",
|
|
17
|
-
owns: (channel: string) =>
|
|
18
|
-
channel === "discord:10"
|
|
19
|
-
? ("agent" as const)
|
|
20
|
-
: channel === "discord:20"
|
|
21
|
-
? ("group" as const)
|
|
22
|
-
: undefined,
|
|
23
|
-
answerOwner: async (_c: string, speaker: Speaker, text: string) => {
|
|
24
|
-
answered.push({ kind: "agent", speaker, text });
|
|
25
|
-
return { ok: true as const, text: "" };
|
|
26
|
-
},
|
|
27
|
-
answerGroup: async (_c: string, speaker: Speaker, text: string) => {
|
|
28
|
-
answered.push({ kind: "group", speaker, text });
|
|
29
|
-
},
|
|
30
|
-
answerBackground: async (_c: string, speaker: Speaker, text: string) => {
|
|
31
|
-
answered.push({ kind: "background", speaker, text });
|
|
32
|
-
return { ok: true as const, text: "" };
|
|
33
|
-
},
|
|
34
|
-
startFresh: async () => {},
|
|
35
|
-
};
|
|
36
|
-
const claim = agentClaim({
|
|
37
|
-
owner: TEST_OWNER,
|
|
38
|
-
speakers: speakerPolicy({
|
|
39
|
-
owners: [OWNER],
|
|
40
|
-
admins: { users: [ADMIN] },
|
|
41
|
-
members: { roles: [ROLE] },
|
|
42
|
-
}),
|
|
43
|
-
// biome-ignore lint/suspicious/noExplicitAny: a recording stand-in for the team
|
|
44
|
-
team: team as any,
|
|
45
|
-
runtime: { steer: async () => false },
|
|
46
|
-
surface: { react: async () => {}, unreact: async () => {} },
|
|
47
|
-
attachmentDir: () => "/tmp",
|
|
48
|
-
logger: silentLogger(),
|
|
49
|
-
});
|
|
50
|
-
return { claim, answered };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const message = (
|
|
54
|
-
authorId: string,
|
|
55
|
-
channel: `${string}:${string}`,
|
|
56
|
-
extra: Partial<InboundMessage> = {},
|
|
57
|
-
): InboundMessage => ({
|
|
58
|
-
channel,
|
|
59
|
-
messageId: "m1",
|
|
60
|
-
authorId,
|
|
61
|
-
authorName: `name-${authorId.slice(-1)}`,
|
|
62
|
-
authorIsBot: false,
|
|
63
|
-
isDirect: false,
|
|
64
|
-
guildId: "1",
|
|
65
|
-
mentionsBot: false,
|
|
66
|
-
repliesToBot: false,
|
|
67
|
-
text: "hello",
|
|
68
|
-
attachments: [],
|
|
69
|
-
...extra,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("the owner, an admin, and a member by role are each answered as themselves", async () => {
|
|
73
|
-
const { claim, answered } = setup();
|
|
74
|
-
for (const [id, roles] of [
|
|
75
|
-
[OWNER, []],
|
|
76
|
-
[ADMIN, []],
|
|
77
|
-
[STRANGER, [ROLE]],
|
|
78
|
-
] as const) {
|
|
79
|
-
const admission = claim.admit(
|
|
80
|
-
message(id, "discord:10", { authorRoleIds: roles }),
|
|
81
|
-
);
|
|
82
|
-
if (admission?.kind !== "turn") throw new Error("expected a turn");
|
|
83
|
-
await admission.run();
|
|
84
|
-
}
|
|
85
|
-
expect(answered.map((a) => a.speaker.tier)).toEqual([
|
|
86
|
-
"owner",
|
|
87
|
-
"admin",
|
|
88
|
-
"member",
|
|
89
|
-
]);
|
|
90
|
-
expect(answered[1]?.speaker).toEqual({
|
|
91
|
-
id: ADMIN,
|
|
92
|
-
name: "name-2",
|
|
93
|
-
tier: "admin",
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("a turn in an agent's channel names a speaker who is not the owner, and the owner's stays plain", async () => {
|
|
98
|
-
const { claim, answered } = setup();
|
|
99
|
-
for (const id of [OWNER, ADMIN]) {
|
|
100
|
-
const admission = claim.admit(message(id, "discord:10"));
|
|
101
|
-
if (admission?.kind !== "turn") throw new Error("expected a turn");
|
|
102
|
-
await admission.run();
|
|
103
|
-
}
|
|
104
|
-
expect(answered[0]?.text).toBe("hello");
|
|
105
|
-
expect(answered[1]?.text).toBe(
|
|
106
|
-
"(Message from name-2, at the admin tier.)\n\nhello",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("an author the policy does not name gets no turn, in an agent or a group channel", () => {
|
|
111
|
-
const { claim } = setup();
|
|
112
|
-
expect(claim.admit(message(STRANGER, "discord:10"))).toBeUndefined();
|
|
113
|
-
expect(claim.admit(message(STRANGER, "discord:20"))).toBeUndefined();
|
|
114
|
-
expect(
|
|
115
|
-
claim.admit(message(OWNER, "discord:10", { authorIsBot: true })),
|
|
116
|
-
).toBeUndefined();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("a group message carries its speaker", async () => {
|
|
120
|
-
const { claim, answered } = setup();
|
|
121
|
-
const admission = claim.admit(message(ADMIN, "discord:20"));
|
|
122
|
-
if (admission?.kind !== "turn") throw new Error("expected a turn");
|
|
123
|
-
await admission.run();
|
|
124
|
-
expect(answered[0]).toMatchObject({
|
|
125
|
-
kind: "group",
|
|
126
|
-
speaker: { tier: "admin" },
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("a background turn runs at its creator's tier, the owner's when a person set none", async () => {
|
|
131
|
-
const { claim, answered } = setup();
|
|
132
|
-
const turn = {
|
|
133
|
-
channel: "discord:10" as const,
|
|
134
|
-
mode: "owner" as const,
|
|
135
|
-
author: { id: ADMIN, name: "n" },
|
|
136
|
-
turnId: "t",
|
|
137
|
-
text: "go",
|
|
138
|
-
};
|
|
139
|
-
await claim.background?.({ ...turn, tier: "admin" });
|
|
140
|
-
await claim.background?.(turn);
|
|
141
|
-
expect(answered.map((a) => a.speaker.tier)).toEqual(["admin", "owner"]);
|
|
142
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { silentLogger } from "../log.ts";
|
|
3
|
-
import { AgentDashboard, dashboardSections } from "./agent-dashboard.ts";
|
|
4
|
-
import type { TeamStatus } from "./agent-team.ts";
|
|
5
|
-
|
|
6
|
-
const NOW = new Date("2026-09-27T06:30:00Z");
|
|
7
|
-
const TRACES = "https://example.com/traces/";
|
|
8
|
-
const APP = "https://example.com/app/";
|
|
9
|
-
/** The plugins' lines: the web app's, then the trace viewer's. */
|
|
10
|
-
const LINES = [
|
|
11
|
-
`🧭 [Web app](${APP}) full dashboard, notes, and cleanup`,
|
|
12
|
-
`🔭 [Traces](${TRACES}) each message's model, tools, and tokens`,
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const status: TeamStatus = {
|
|
16
|
-
agents: [
|
|
17
|
-
{
|
|
18
|
-
name: "infra",
|
|
19
|
-
displayName: "Infra",
|
|
20
|
-
channelId: "11",
|
|
21
|
-
model: "openai-codex/gpt-6-sol",
|
|
22
|
-
thinking: "high",
|
|
23
|
-
workingIn: "12",
|
|
24
|
-
waiting: 2,
|
|
25
|
-
context: { tokens: 42_000, contextWindow: 200_000 },
|
|
26
|
-
lastActive: new Date("2026-09-27T06:03:00Z"),
|
|
27
|
-
schedules: 1,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: "doctor",
|
|
31
|
-
displayName: "Bot Doctor",
|
|
32
|
-
channelId: "13",
|
|
33
|
-
model: "claude-bridge/claude-opus-5-5",
|
|
34
|
-
thinking: "medium",
|
|
35
|
-
waiting: 0,
|
|
36
|
-
schedules: 0,
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
groups: [
|
|
40
|
-
{
|
|
41
|
-
name: "ops",
|
|
42
|
-
displayName: "Ops group",
|
|
43
|
-
channelId: "12",
|
|
44
|
-
members: ["Coordinator", "Infra"],
|
|
45
|
-
host: "Coordinator",
|
|
46
|
-
busy: 1,
|
|
47
|
-
lastActive: new Date("2026-09-26T06:03:00Z"),
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
describe("dashboardSections", () => {
|
|
53
|
-
test("shows the plugins' links, then each agent's and group's state", () => {
|
|
54
|
-
const [header, agents, groups] = dashboardSections(status, LINES, NOW);
|
|
55
|
-
// The header the links were written into before plugins contributed them, byte for byte.
|
|
56
|
-
expect(header).toBe(
|
|
57
|
-
"## Agent server\n🧭 [Web app](https://example.com/app/) full dashboard, notes, and cleanup\n🔭 [Traces](https://example.com/traces/) each message's model, tools, and tokens\n-# Updated 06:30; refreshed when a turn starts or ends, at most every 10 seconds",
|
|
58
|
-
);
|
|
59
|
-
expect(agents).toContain("**Infra** <#11>");
|
|
60
|
-
expect(agents).toContain("`openai-codex/gpt-6-sol` · thinking `high`");
|
|
61
|
-
expect(agents).toContain(
|
|
62
|
-
"🟠 Working (<#12>), 2 queued · context 42k / 200k (21%)",
|
|
63
|
-
);
|
|
64
|
-
expect(agents).toContain("-# Schedules 1 · Last active 06:03");
|
|
65
|
-
expect(agents).toContain("🟢 Idle · context —");
|
|
66
|
-
expect(agents).toContain("No activity since startup");
|
|
67
|
-
expect(groups).toContain("**Ops group** <#12>");
|
|
68
|
-
expect(groups).toContain("Members: Coordinator, Infra | Host: Coordinator");
|
|
69
|
-
expect(groups).toContain("🟠 1 in progress");
|
|
70
|
-
expect(groups).toContain("-# Last active 09-26 06:03");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("stays within Discord's text limit, saying how many were left out", () => {
|
|
74
|
-
const many: TeamStatus = {
|
|
75
|
-
agents: Array.from({ length: 60 }, (_, i) => ({
|
|
76
|
-
...status.agents[1],
|
|
77
|
-
name: `a${i}`,
|
|
78
|
-
displayName: `Agent ${i}`,
|
|
79
|
-
})) as TeamStatus["agents"],
|
|
80
|
-
groups: [],
|
|
81
|
-
};
|
|
82
|
-
const sections = dashboardSections(many, LINES, NOW);
|
|
83
|
-
expect(sections.join("").length).toBeLessThan(4000);
|
|
84
|
-
expect(sections[1]).toMatch(/\d+ more not listed/);
|
|
85
|
-
expect(sections[2]).toContain("None.");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("AgentDashboard", () => {
|
|
90
|
-
test("updates at once, then at most once per interval, and keeps going after a failure", async () => {
|
|
91
|
-
const shown: string[][] = [];
|
|
92
|
-
let fail = true;
|
|
93
|
-
const dashboard = new AgentDashboard({
|
|
94
|
-
status: async () => status,
|
|
95
|
-
board: {
|
|
96
|
-
show: async (sections) => {
|
|
97
|
-
if (fail) {
|
|
98
|
-
fail = false;
|
|
99
|
-
throw new Error("discord down");
|
|
100
|
-
}
|
|
101
|
-
shown.push(sections);
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
lines: () => LINES,
|
|
105
|
-
logger: silentLogger(),
|
|
106
|
-
minIntervalMs: 40,
|
|
107
|
-
refreshMs: 60_000,
|
|
108
|
-
});
|
|
109
|
-
dashboard.start();
|
|
110
|
-
await Bun.sleep(10);
|
|
111
|
-
expect(shown).toHaveLength(0);
|
|
112
|
-
dashboard.changed();
|
|
113
|
-
dashboard.changed();
|
|
114
|
-
await Bun.sleep(15);
|
|
115
|
-
expect(shown).toHaveLength(0);
|
|
116
|
-
await Bun.sleep(50);
|
|
117
|
-
expect(shown).toHaveLength(1);
|
|
118
|
-
dashboard.stop();
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
-
import { SQL } from "bun";
|
|
3
|
-
import { migrate } from "../db/migrations.ts";
|
|
4
|
-
import { SkillStore } from "../modules/skills/skill-store.ts";
|
|
5
|
-
import {
|
|
6
|
-
describeDb,
|
|
7
|
-
migratedPool,
|
|
8
|
-
TEST_GUILD,
|
|
9
|
-
testDatabaseUrl,
|
|
10
|
-
} from "../testing/database.ts";
|
|
11
|
-
import { AgentStore } from "./agent-store.ts";
|
|
12
|
-
|
|
13
|
-
const OTHER_GUILD = "900000000000000002";
|
|
14
|
-
const TABLES = [
|
|
15
|
-
"agents",
|
|
16
|
-
"agent_groups",
|
|
17
|
-
"agent_group_messages",
|
|
18
|
-
"agent_group_cursors",
|
|
19
|
-
"agent_skills",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
// Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
|
|
23
|
-
describeDb("guild scoping", () => {
|
|
24
|
-
let sql: SQL;
|
|
25
|
-
|
|
26
|
-
beforeEach(async () => {
|
|
27
|
-
const admin = new SQL(testDatabaseUrl);
|
|
28
|
-
for (const table of TABLES)
|
|
29
|
-
await admin.unsafe(`DROP TABLE IF EXISTS ${table}`);
|
|
30
|
-
await admin.close();
|
|
31
|
-
// The tables as they were before rows carried a guild, with rows in every one.
|
|
32
|
-
sql = await migratedPool(AgentStore.migration, SkillStore.migration);
|
|
33
|
-
await sql`INSERT INTO agents (name, display_name, prompt, avatar_prompt, channel_id)
|
|
34
|
-
VALUES ('infra', 'Infra', 'p', 'a', '10'), ('doctor', 'Doctor', 'p', 'a', '11')`;
|
|
35
|
-
await sql`INSERT INTO agent_groups (name, display_name, channel_id, members, host)
|
|
36
|
-
VALUES ('ops', 'Ops', '20', 'infra,doctor', 'infra')`;
|
|
37
|
-
await sql`INSERT INTO agent_group_messages (group_name, author, author_name, text)
|
|
38
|
-
VALUES ('ops', 'owner', 'Riley', 'hi')`;
|
|
39
|
-
await sql`INSERT INTO agent_group_cursors (group_name, agent_name, last_id)
|
|
40
|
-
VALUES ('ops', 'infra', 1)`;
|
|
41
|
-
await sql`INSERT INTO agent_skills (agent, skill) VALUES ('infra', 'ops-skill')`;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(async () => {
|
|
45
|
-
await sql.close();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const upgrade = (guild: string) =>
|
|
49
|
-
migrate(sql, [
|
|
50
|
-
...AgentStore.migrations(guild),
|
|
51
|
-
...SkillStore.migrations(guild),
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
test("existing rows of every table go to the configured guild", async () => {
|
|
55
|
-
await upgrade(TEST_GUILD);
|
|
56
|
-
for (const table of TABLES) {
|
|
57
|
-
const rows: { guild_id: string }[] = await sql.unsafe(
|
|
58
|
-
`SELECT guild_id FROM ${table}`,
|
|
59
|
-
);
|
|
60
|
-
expect(rows.length).toBeGreaterThan(0);
|
|
61
|
-
expect(new Set(rows.map((r) => r.guild_id))).toEqual(
|
|
62
|
-
new Set([TEST_GUILD]),
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const store = await AgentStore.attach(sql, TEST_GUILD);
|
|
66
|
-
expect(store.agents().map((a) => a.name)).toEqual(["infra", "doctor"]);
|
|
67
|
-
expect(store.group("ops")?.members).toEqual(["infra", "doctor"]);
|
|
68
|
-
expect(
|
|
69
|
-
(await store.recentGroupMessages("ops", 5)).map((m) => m.text),
|
|
70
|
-
).toEqual(["hi"]);
|
|
71
|
-
expect((await store.backlog("ops", "doctor", 40)).messages).toHaveLength(1);
|
|
72
|
-
const skills = await SkillStore.attach(sql, TEST_GUILD);
|
|
73
|
-
expect(skills.carriedBy("infra")).toEqual(["ops-skill"]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("running the migration again changes nothing", async () => {
|
|
77
|
-
await upgrade(TEST_GUILD);
|
|
78
|
-
const before = await sql`SELECT * FROM agents ORDER BY name`;
|
|
79
|
-
await upgrade(TEST_GUILD);
|
|
80
|
-
expect(await sql`SELECT * FROM agents ORDER BY name`).toEqual(before);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("a guild sees only its own rows and may reuse a name of another", async () => {
|
|
84
|
-
await upgrade(TEST_GUILD);
|
|
85
|
-
const other = await AgentStore.attach(sql, OTHER_GUILD);
|
|
86
|
-
expect(other.agents()).toEqual([]);
|
|
87
|
-
expect(other.groups()).toEqual([]);
|
|
88
|
-
for (const name of ["infra", "doctor"])
|
|
89
|
-
await other.createAgent({
|
|
90
|
-
name,
|
|
91
|
-
displayName: `Other ${name}`,
|
|
92
|
-
prompt: "p",
|
|
93
|
-
avatarPrompt: "a",
|
|
94
|
-
channelId: `3${name.length}`,
|
|
95
|
-
});
|
|
96
|
-
await other.createGroup({
|
|
97
|
-
name: "ops",
|
|
98
|
-
displayName: "Other Ops",
|
|
99
|
-
channelId: "40",
|
|
100
|
-
members: ["infra", "doctor"],
|
|
101
|
-
host: "infra",
|
|
102
|
-
});
|
|
103
|
-
const mine = await AgentStore.attach(sql, TEST_GUILD);
|
|
104
|
-
expect(mine.agent("infra")?.displayName).toBe("Infra");
|
|
105
|
-
const reopened = await AgentStore.attach(sql, OTHER_GUILD);
|
|
106
|
-
expect(reopened.agent("infra")?.displayName).toBe("Other infra");
|
|
107
|
-
expect(reopened.group("ops")?.displayName).toBe("Other Ops");
|
|
108
|
-
// Messages and cursors of one guild's group are not the other's.
|
|
109
|
-
await other.appendGroupMessage("ops", {
|
|
110
|
-
author: "owner",
|
|
111
|
-
authorName: "X",
|
|
112
|
-
text: "elsewhere",
|
|
113
|
-
});
|
|
114
|
-
expect(
|
|
115
|
-
(await mine.recentGroupMessages("ops", 5)).map((m) => m.text),
|
|
116
|
-
).toEqual(["hi"]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("a name is still unique inside its guild", async () => {
|
|
120
|
-
await upgrade(TEST_GUILD);
|
|
121
|
-
const store = await AgentStore.attach(sql, TEST_GUILD);
|
|
122
|
-
expect(() => store.checkNewName("infra")).toThrow(/taken/);
|
|
123
|
-
});
|
|
124
|
-
});
|