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,124 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { AgentError } from "../domain/errors.ts";
|
|
2
|
+
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
3
|
+
import { messages } from "../i18n/index.ts";
|
|
4
|
+
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
5
|
+
import { addressee } from "../speakers.ts";
|
|
6
|
+
import {
|
|
7
|
+
agentFailureText,
|
|
8
|
+
agentMessageText,
|
|
9
|
+
agentReplyText,
|
|
10
|
+
chainLimitReason,
|
|
11
|
+
deliveredMessagePost,
|
|
12
|
+
returnedAnswerPost,
|
|
13
|
+
} from "./agent-prompt.ts";
|
|
14
|
+
import type { Agent } from "./agent-store.ts";
|
|
15
|
+
import { channelKey, homeScope } from "./team-keys.ts";
|
|
16
|
+
import {
|
|
17
|
+
type Chain,
|
|
18
|
+
type Exchange,
|
|
19
|
+
MAX_CHAIN_MESSAGES,
|
|
20
|
+
type TurnHost,
|
|
21
|
+
} from "./team-turn-types.ts";
|
|
22
|
+
|
|
23
|
+
/** Messages between agents: each runs the target's turn, then wakes the sender with the answer. */
|
|
24
|
+
export class AgentMessages {
|
|
25
|
+
readonly #host: TurnHost;
|
|
26
|
+
|
|
27
|
+
constructor(host: TurnHost) {
|
|
28
|
+
this.#host = host;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
message(caller: AgentTurnScope, to: string, text: string): string {
|
|
32
|
+
const { store, queue, logger } = this.#host.options;
|
|
33
|
+
if (to === caller.name)
|
|
34
|
+
throw new AgentError("message_agent is for other agents, not yourself.");
|
|
35
|
+
const target = store.activeAgent(to);
|
|
36
|
+
const sender = store.activeAgent(caller.name);
|
|
37
|
+
if (!target.channelId) throw new AgentError(`${to} has no channel yet.`);
|
|
38
|
+
// Every agent turn runs through #turn, which registers its chain; a message from anywhere
|
|
39
|
+
// else would start an uncounted chain, so it is refused.
|
|
40
|
+
const chain = this.#host.chains.get(caller.session);
|
|
41
|
+
if (!chain)
|
|
42
|
+
throw new AgentError(
|
|
43
|
+
"message_agent can only be used during an agent turn.",
|
|
44
|
+
);
|
|
45
|
+
if (chain.messages >= MAX_CHAIN_MESSAGES)
|
|
46
|
+
throw new AgentError(
|
|
47
|
+
chainLimitReason(addressee(chain.speaker, this.#host.options.owner)),
|
|
48
|
+
);
|
|
49
|
+
chain.messages += 1;
|
|
50
|
+
const targetChannel = channelKey(target.channelId);
|
|
51
|
+
const exchange = this.#openExchange(caller, sender, target, text);
|
|
52
|
+
void queue
|
|
53
|
+
.run(targetChannel, async () =>
|
|
54
|
+
this.#deliver(sender, target, text, chain, await exchange),
|
|
55
|
+
)
|
|
56
|
+
.catch((error: unknown) =>
|
|
57
|
+
logger.error(
|
|
58
|
+
{ from: sender.name, to: target.name, err: error },
|
|
59
|
+
"agent message not delivered",
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
logger.info(
|
|
63
|
+
{ from: sender.name, to: target.name, chain: chain.messages },
|
|
64
|
+
"agent message queued",
|
|
65
|
+
);
|
|
66
|
+
return `Sent to "${target.displayName}". It works in its own channel; its answer comes back to you as a new turn.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Opens the message's thread in the sender's turn channel and posts the request there under
|
|
71
|
+
* the sender's name; undefined where no thread opens. Never rejects.
|
|
72
|
+
*/
|
|
73
|
+
async #openExchange(
|
|
74
|
+
caller: AgentTurnScope,
|
|
75
|
+
sender: Agent,
|
|
76
|
+
target: Agent,
|
|
77
|
+
text: string,
|
|
78
|
+
): Promise<Exchange | undefined> {
|
|
79
|
+
const parent = this.#host.turnChannel(caller);
|
|
80
|
+
const thread = await this.#host.options.threads?.open(
|
|
81
|
+
parent,
|
|
82
|
+
`→ ${target.displayName}`,
|
|
83
|
+
);
|
|
84
|
+
if (!thread) return undefined;
|
|
85
|
+
await this.#host
|
|
86
|
+
.post(parent, this.#host.current(sender), {
|
|
87
|
+
chunks: splitReply(
|
|
88
|
+
deliveredMessagePost(this.#host.current(target), text),
|
|
89
|
+
),
|
|
90
|
+
threadId: thread.id,
|
|
91
|
+
})
|
|
92
|
+
.catch((error: unknown) =>
|
|
93
|
+
this.#host.options.logger.warn(
|
|
94
|
+
{ err: error },
|
|
95
|
+
"agent request not threaded",
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
return { parent, thread };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Runs the target's turn, then posts its answer and wakes the sender; closes the thread either way. */
|
|
102
|
+
async #deliver(
|
|
103
|
+
sender: Agent,
|
|
104
|
+
target: Agent,
|
|
105
|
+
text: string,
|
|
106
|
+
chain: Chain,
|
|
107
|
+
exchange: Exchange | undefined,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
await this.#exchange(sender, target, text, chain, exchange);
|
|
111
|
+
} finally {
|
|
112
|
+
await exchange?.thread.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #exchange(
|
|
117
|
+
sender: Agent,
|
|
118
|
+
target: Agent,
|
|
119
|
+
text: string,
|
|
120
|
+
chain: Chain,
|
|
121
|
+
exchange: Exchange | undefined,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const { queue, logger } = this.#host.options;
|
|
124
|
+
const targetChannel = channelKey(target.channelId ?? "");
|
|
125
|
+
await this.#host.post(targetChannel, this.#host.current(sender), {
|
|
126
|
+
chunks: splitReply(
|
|
127
|
+
deliveredMessagePost(this.#host.current(target), text),
|
|
128
|
+
),
|
|
129
|
+
});
|
|
130
|
+
const result = await this.#host.turn(
|
|
131
|
+
target,
|
|
132
|
+
homeScope(target),
|
|
133
|
+
targetChannel,
|
|
134
|
+
agentMessageText(
|
|
135
|
+
this.#host.current(sender),
|
|
136
|
+
text,
|
|
137
|
+
addressee(chain.speaker, this.#host.options.owner),
|
|
138
|
+
),
|
|
139
|
+
chain,
|
|
140
|
+
);
|
|
141
|
+
const thread = exchange?.thread;
|
|
142
|
+
if (thread && !result.ok)
|
|
143
|
+
await thread.close(messages().agentNoReply(result.error.message));
|
|
144
|
+
const current = this.#host.options.store.agent(sender.name);
|
|
145
|
+
if (current?.status !== "active" || !current.channelId) return;
|
|
146
|
+
const senderChannel = channelKey(current.channelId);
|
|
147
|
+
// With a thread the answer goes there, leaving the sender's channel its own reply.
|
|
148
|
+
if (result.ok)
|
|
149
|
+
await this.#host
|
|
150
|
+
.post(exchange?.parent ?? senderChannel, this.#host.current(target), {
|
|
151
|
+
chunks: splitReply(returnedAnswerPost(result.text)),
|
|
152
|
+
...(thread ? { threadId: thread.id } : {}),
|
|
153
|
+
})
|
|
154
|
+
.catch((error: unknown) =>
|
|
155
|
+
logger.error({ err: error }, "agent answer not posted back"),
|
|
156
|
+
);
|
|
157
|
+
await thread?.close();
|
|
158
|
+
const followUp = result.ok
|
|
159
|
+
? agentReplyText(
|
|
160
|
+
addressee(chain.speaker, this.#host.options.owner),
|
|
161
|
+
this.#host.current(target),
|
|
162
|
+
result.text,
|
|
163
|
+
thread?.mention,
|
|
164
|
+
)
|
|
165
|
+
: agentFailureText(
|
|
166
|
+
addressee(chain.speaker, this.#host.options.owner),
|
|
167
|
+
this.#host.current(target),
|
|
168
|
+
result.error.message,
|
|
169
|
+
thread?.mention,
|
|
170
|
+
);
|
|
171
|
+
// The answer is a report back to the sender, so its turn may ask the owner on cards.
|
|
172
|
+
void queue
|
|
173
|
+
.run(senderChannel, () =>
|
|
174
|
+
this.#host.turn(
|
|
175
|
+
current,
|
|
176
|
+
homeScope(current),
|
|
177
|
+
senderChannel,
|
|
178
|
+
followUp,
|
|
179
|
+
chain,
|
|
180
|
+
{
|
|
181
|
+
interactive: true,
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
.catch((error: unknown) =>
|
|
186
|
+
logger.error({ err: error }, "agent follow-up failed"),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelKey,
|
|
3
|
+
PendingConfirmation,
|
|
4
|
+
TurnResult,
|
|
5
|
+
} from "../domain/conversation.ts";
|
|
6
|
+
import type { TurnRequest } from "../domain/ports.ts";
|
|
7
|
+
import type { ThinkingSetting } from "../models.ts";
|
|
8
|
+
|
|
9
|
+
/** The part of the Pi runtime the agent team drives. */
|
|
10
|
+
export interface AgentTurnRunner {
|
|
11
|
+
runTurn(request: TurnRequest): Promise<TurnResult>;
|
|
12
|
+
heldActions(session: ChannelKey): Promise<PendingConfirmation | undefined>;
|
|
13
|
+
startFresh(session: ChannelKey): Promise<void>;
|
|
14
|
+
/** Undefined until the conversation has run a turn since startup. */
|
|
15
|
+
contextUsage(session: ChannelKey): ContextUse | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One agent's message in a channel, posted under its name and avatar. */
|
|
19
|
+
export interface AgentPost {
|
|
20
|
+
name: string;
|
|
21
|
+
avatarUrl: string;
|
|
22
|
+
/** The turn's thinking, posted first as a quiet line. */
|
|
23
|
+
thinking?: string;
|
|
24
|
+
chunks: string[];
|
|
25
|
+
files?: { name: string; data: Uint8Array }[];
|
|
26
|
+
/** A thread of the channel to post in instead, such as a dispatch's. */
|
|
27
|
+
threadId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A category's name. Agents live under `Agents` or `Agents-<suffix>`, groups under `Groups` or
|
|
32
|
+
* `Groups-<suffix>`, and archived ones, kept with their history, under `Archive`.
|
|
33
|
+
*/
|
|
34
|
+
export type AgentCategory = string;
|
|
35
|
+
|
|
36
|
+
/** A category of the agent server and its channels, in server order. */
|
|
37
|
+
export interface CategoryLayout {
|
|
38
|
+
/** Absent for a category still to be made. */
|
|
39
|
+
id?: string;
|
|
40
|
+
name: string;
|
|
41
|
+
channelIds: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A message of the agent server as `channel_read` returns it. */
|
|
45
|
+
export interface ChannelMessage {
|
|
46
|
+
id: string;
|
|
47
|
+
author: string;
|
|
48
|
+
at: Date;
|
|
49
|
+
text: string;
|
|
50
|
+
attachments: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The agent server's channels as the agent team needs them. */
|
|
54
|
+
export interface AgentChannels {
|
|
55
|
+
post(channelId: string, post: AgentPost): Promise<void>;
|
|
56
|
+
/** Creates a text channel under the category, made when missing; returns its id. */
|
|
57
|
+
createChannel(
|
|
58
|
+
name: string,
|
|
59
|
+
topic: string,
|
|
60
|
+
category: AgentCategory,
|
|
61
|
+
): Promise<string>;
|
|
62
|
+
/** Moves a channel into the category when it sits elsewhere; true when it moved. */
|
|
63
|
+
placeIn(channelId: string, category: AgentCategory): Promise<boolean>;
|
|
64
|
+
/** Every category of the server in order, each with its channels in order. */
|
|
65
|
+
layout(): Promise<CategoryLayout[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Puts the categories, and the channels in each, in this order, making categories without an
|
|
68
|
+
* id; then deletes the categories of `remove` that hold no channel.
|
|
69
|
+
*/
|
|
70
|
+
arrange(layout: CategoryLayout[], remove: string[]): Promise<void>;
|
|
71
|
+
setTopic(channelId: string, topic: string): Promise<void>;
|
|
72
|
+
/** Deletes the assistant's webhook in the channel, when it has one; its past messages stay. */
|
|
73
|
+
removeWebhook(channelId: string): Promise<void>;
|
|
74
|
+
exists(channelId: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* Up to `limit` messages of a text channel or thread of the agent server, oldest first: the
|
|
77
|
+
* latest, or those around `around`. Other servers' channels are refused.
|
|
78
|
+
*/
|
|
79
|
+
read(
|
|
80
|
+
channelId: string,
|
|
81
|
+
options: { limit: number; around?: string },
|
|
82
|
+
): Promise<ChannelMessage[]>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A conversation's context use after its latest turn, as Pi estimates it. */
|
|
86
|
+
export interface ContextUse {
|
|
87
|
+
/** Null right after a compaction, until the next model response. */
|
|
88
|
+
tokens: number | null;
|
|
89
|
+
contextWindow: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The dashboard channel's one pinned message, made when missing and edited in place. */
|
|
93
|
+
export interface DashboardBoard {
|
|
94
|
+
show(sections: string[]): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The models an agent can run on, and the assistant's own, which unset agents follow. */
|
|
98
|
+
export interface AgentModels {
|
|
99
|
+
/** The assistant's thinking is `auto`: the judge picks each turn's level. */
|
|
100
|
+
defaults: { model: string; thinking: ThinkingSetting };
|
|
101
|
+
/** Every model the host can run, as `<provider>/<id>`. */
|
|
102
|
+
usable(): Promise<string[]>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { messages } from "../i18n/index.ts";
|
|
2
|
+
import { type OwnerIdentity, ownerWords } from "../identity.ts";
|
|
3
|
+
import { addressee, type Speaker } from "../speakers.ts";
|
|
4
|
+
import type { Agent, AgentGroup, Backlog } from "./agent-store.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The base every agent turn starts with, followed by the agent's own prompt. `shared` is the
|
|
8
|
+
* prompt every persona shares, the assistant's included (persona/shared.md).
|
|
9
|
+
*/
|
|
10
|
+
export function agentSystemPrompt(input: {
|
|
11
|
+
agent: Agent;
|
|
12
|
+
/** The agent in the entry channel. */
|
|
13
|
+
coordinator?: Agent;
|
|
14
|
+
shared: string;
|
|
15
|
+
/** The shared prompt for a speaker who is not the owner, where `shared` speaks to the owner. */
|
|
16
|
+
guestShared?: string;
|
|
17
|
+
workDir: string;
|
|
18
|
+
owner: OwnerIdentity;
|
|
19
|
+
/** The host account the shell and file tools run as. */
|
|
20
|
+
shellUser: string;
|
|
21
|
+
group?: { group: AgentGroup; members: readonly Agent[] };
|
|
22
|
+
/** Who the turn is for; without one, or the owner, the prompt speaks to the owner. */
|
|
23
|
+
speaker?: Speaker;
|
|
24
|
+
}): string {
|
|
25
|
+
const { agent, coordinator, shared, workDir, owner, shellUser, group } =
|
|
26
|
+
input;
|
|
27
|
+
const who = addressee(input.speaker, owner);
|
|
28
|
+
const o = ownerWords(owner);
|
|
29
|
+
const w = ownerWords(who);
|
|
30
|
+
const guest = input.speaker && input.speaker.tier !== "owner";
|
|
31
|
+
const parts = [
|
|
32
|
+
(guest && input.guestShared) || shared,
|
|
33
|
+
`You are "${agent.displayName}" (agent name \`${agent.name}\`), one of ${o.name}'s agents in ${o.his} Discord agent server. Each agent owns one channel and one conversation; you all share ${o.his} tools and ${o.his} memory.`,
|
|
34
|
+
roleText(agent, coordinator, who),
|
|
35
|
+
`The team: agent_list shows every agent and group; agent_get and agent_update read and improve any agent's prompt, display name, model, and thinking level, yours included; agent_create adds an agent with its own channel; agent_avatar redraws an avatar; schedule_list with agent reads another agent's schedules. A message marked as coming from another agent is that agent speaking, not ${w.name}; only ${w.name} approves held actions.`,
|
|
36
|
+
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}. Commands that are destructive or change the system, and writes outside the workspace, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm.`,
|
|
37
|
+
];
|
|
38
|
+
if (group) {
|
|
39
|
+
const others = group.members
|
|
40
|
+
.filter((m) => m.name !== agent.name)
|
|
41
|
+
.map((m) => `"${m.displayName}"`)
|
|
42
|
+
.join(", ");
|
|
43
|
+
parts.push(
|
|
44
|
+
`You are now speaking in the group chat "${group.group.displayName}" with ${w.name} and ${others}. Each of your turns shows the group messages since your last turn. Speak only for yourself and add what your role brings; do not repeat what others said. To hand the next word to a member, mention them as @display name.`,
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
parts.push(
|
|
48
|
+
"message_agent hands a self-contained request to another agent; it answers later, and its answer comes back to you as a new turn.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
parts.push(`## Your role\n${agent.prompt}`);
|
|
52
|
+
if (input.speaker && input.speaker.tier !== "owner")
|
|
53
|
+
parts.push(speakerText(input.speaker, owner));
|
|
54
|
+
return parts.join("\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Says whom the turn is for when it is not the owner, after every operator text that names the
|
|
59
|
+
* owner, so those texts read as the owner's and this one decides who is addressed.
|
|
60
|
+
*/
|
|
61
|
+
function speakerText(speaker: Speaker, owner: OwnerIdentity): string {
|
|
62
|
+
const o = ownerWords(owner);
|
|
63
|
+
const w = ownerWords(addressee(speaker, owner));
|
|
64
|
+
return `## The current speaker\nYou are talking with ${speaker.name} (Discord user ${speaker.id}), who speaks at the ${speaker.tier} tier of this server, not with ${o.name}. The text above describes the server's owner, ${o.name}. Address ${w.name}, take ${w.his} answers and approvals, and read a tool description that names ${o.name} as ${w.name}. Your memory is ${w.his} own, not ${o.name}'s, and other people's memory is not yours to share. Tools above the ${speaker.tier} tier are not available to you this turn.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Who coordinates the team, so a specialist never takes that role (spec behavior 55). */
|
|
68
|
+
function roleText(
|
|
69
|
+
agent: Agent,
|
|
70
|
+
coordinator: Agent | undefined,
|
|
71
|
+
addressed: OwnerIdentity,
|
|
72
|
+
): string {
|
|
73
|
+
const o = ownerWords(addressed);
|
|
74
|
+
if (coordinator?.name === agent.name)
|
|
75
|
+
return `You are the team's coordinator: ${o.name}'s general requests come to you, and you hand specialist work to the agent that owns it.`;
|
|
76
|
+
const who = coordinator
|
|
77
|
+
? `The team's coordinator is "${coordinator.displayName}" (\`${coordinator.name}\`) in <#${coordinator.channelId}>.`
|
|
78
|
+
: "The team's coordinator is the agent in the server's entry channel.";
|
|
79
|
+
return `${who} You are a specialist: speak of yourself only as "${agent.displayName}", never as the coordinator. Team work ${o.name} asks of you directly, such as creating agents or groups, archiving, or arranging channels, you do in your own name. When your role below is empty or only a placeholder, ask ${o.him} what your job is before taking on other work.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a target agent receives from message_agent. */
|
|
83
|
+
export function agentMessageText(
|
|
84
|
+
from: Agent,
|
|
85
|
+
text: string,
|
|
86
|
+
who: OwnerIdentity,
|
|
87
|
+
): string {
|
|
88
|
+
return `(Message from the agent "${from.displayName}" (\`${from.name}\`), not from ${who.name}. Answer it; your answer is posted in your channel and sent back to "${from.displayName}".)\n\n${text}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** What the sender receives when the target has answered. */
|
|
92
|
+
export function agentReplyText(
|
|
93
|
+
who: OwnerIdentity,
|
|
94
|
+
from: Agent,
|
|
95
|
+
text: string,
|
|
96
|
+
thread?: string,
|
|
97
|
+
): string {
|
|
98
|
+
return `(The agent "${from.displayName}" (\`${from.name}\`) answered your message. Continue with it and tell ${who.name} what matters; your reply is posted in your channel.${threadNote(thread)})\n\n${text}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Where the exchange was posted, when it had a thread. */
|
|
102
|
+
function threadNote(thread: string | undefined): string {
|
|
103
|
+
return thread
|
|
104
|
+
? ` The exchange is in the thread ${thread}, now archived; link it when useful.`
|
|
105
|
+
: "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the sender receives when the target could not answer. */
|
|
109
|
+
export function agentFailureText(
|
|
110
|
+
who: OwnerIdentity,
|
|
111
|
+
from: Agent,
|
|
112
|
+
reason: string,
|
|
113
|
+
thread?: string,
|
|
114
|
+
): string {
|
|
115
|
+
return `(The agent "${from.displayName}" (\`${from.name}\`) could not answer your message: ${reason}. Tell ${who.name} briefly, or try again later.${threadNote(thread)})`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A new agent's first turn. */
|
|
119
|
+
export function openingTaskText(
|
|
120
|
+
creator: Agent | undefined,
|
|
121
|
+
task: string,
|
|
122
|
+
who: OwnerIdentity,
|
|
123
|
+
): string {
|
|
124
|
+
const by = creator ? `the agent "${creator.displayName}"` : who.name;
|
|
125
|
+
return `(You were just created by ${by}, and this is your channel. Introduce yourself in one or two sentences, then start on this task.)\n\n${task}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A group member's turn: the messages since its last turn, oldest first. The closing line
|
|
130
|
+
* names the speaker, so a message that addresses it is not read as its own.
|
|
131
|
+
*/
|
|
132
|
+
export function groupTurnText(
|
|
133
|
+
group: AgentGroup,
|
|
134
|
+
self: Agent,
|
|
135
|
+
backlog: Backlog,
|
|
136
|
+
): string {
|
|
137
|
+
const skipped =
|
|
138
|
+
backlog.skipped > 0
|
|
139
|
+
? `; ${backlog.skipped} earlier messages are not shown`
|
|
140
|
+
: "";
|
|
141
|
+
const lines = backlog.messages.map((m) => `[${m.authorName}] ${m.text}`);
|
|
142
|
+
const body =
|
|
143
|
+
lines.length > 0
|
|
144
|
+
? lines.join("\n\n")
|
|
145
|
+
: "(No new messages since your last turn.)";
|
|
146
|
+
return `(Group chat "${group.displayName}": messages since your last turn, oldest first${skipped}.)\n\n${body}\n\n(You are ${self.displayName}, and it is your turn to speak in the group. Each bracketed name above is another speaker; a message mentioning @${self.displayName} is addressed to you.)`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** How a message_agent request appears in the target's channel. */
|
|
150
|
+
export function deliveredMessagePost(to: Agent, text: string): string {
|
|
151
|
+
return messages().messageDelivered(to.displayName, text);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** How the target's answer appears in the sender's channel. */
|
|
155
|
+
export function returnedAnswerPost(text: string): string {
|
|
156
|
+
return messages().answerReturned(text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Why a chain of agent messages stops without the owner in between. */
|
|
160
|
+
export function chainLimitReason(who: OwnerIdentity): string {
|
|
161
|
+
const o = ownerWords(who);
|
|
162
|
+
return `This chain of agent messages has reached 8 messages without ${o.name} in between, so it is stopped. Tell ${o.him} what was done and ask whether to continue.`;
|
|
163
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AgentError } from "../domain/errors.ts";
|
|
2
|
+
import { THINKING_LEVELS, type ThinkingLevel } from "../models.ts";
|
|
3
|
+
|
|
4
|
+
export const MAX_PROMPT_CHARS = 4_000;
|
|
5
|
+
export const MAX_AVATAR_PROMPT_CHARS = 1_000;
|
|
6
|
+
export const MIN_GROUP_MEMBERS = 2;
|
|
7
|
+
export const MAX_GROUP_MEMBERS = 6;
|
|
8
|
+
|
|
9
|
+
export type AgentStatus = "active" | "archived";
|
|
10
|
+
|
|
11
|
+
/** One agent of the agent server. `name` never changes; `displayName` is what Discord shows. */
|
|
12
|
+
export interface Agent {
|
|
13
|
+
name: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
avatarPrompt: string;
|
|
17
|
+
/** The drawn picture's content hash, when one exists. */
|
|
18
|
+
avatarHash?: string;
|
|
19
|
+
/** Its channel in the agent server; a seed agent has none until one is created. */
|
|
20
|
+
channelId?: string;
|
|
21
|
+
/** `<provider>/<id>`; unset follows the assistant's model. */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Unset follows the assistant's thinking level. */
|
|
24
|
+
thinking?: ThinkingLevel;
|
|
25
|
+
status: AgentStatus;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AgentGroup {
|
|
29
|
+
name: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
channelId: string;
|
|
32
|
+
/** Agent names in the group's order. */
|
|
33
|
+
members: string[];
|
|
34
|
+
host: string;
|
|
35
|
+
status: AgentStatus;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentSeed {
|
|
39
|
+
name: string;
|
|
40
|
+
displayName: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
avatarPrompt: string;
|
|
43
|
+
channelId?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GroupMessage {
|
|
47
|
+
id: number;
|
|
48
|
+
/** `owner` or an agent name. */
|
|
49
|
+
author: string;
|
|
50
|
+
authorName: string;
|
|
51
|
+
text: string;
|
|
52
|
+
at: Date;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Backlog {
|
|
56
|
+
messages: GroupMessage[];
|
|
57
|
+
/** Waiting messages older than the ones carried. */
|
|
58
|
+
skipped: number;
|
|
59
|
+
/** The newest waiting id, which the cursor moves to once they are delivered. */
|
|
60
|
+
lastId: number | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const RESERVED = /discord|clyde/i;
|
|
64
|
+
|
|
65
|
+
export function checkAgentName(name: string): void {
|
|
66
|
+
if (!/^[a-z0-9-]{1,32}$/.test(name) || RESERVED.test(name))
|
|
67
|
+
throw new AgentError(
|
|
68
|
+
`"${name}" is not a usable name: use 1 to 32 lowercase letters, digits, and dashes, without "discord" or "clyde".`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function checkDisplayName(displayName: string): void {
|
|
73
|
+
const length = [...displayName.trim()].length;
|
|
74
|
+
if (length < 1 || length > 32 || RESERVED.test(displayName))
|
|
75
|
+
throw new AgentError(
|
|
76
|
+
`"${displayName}" is not a usable display name: use 1 to 32 characters, without "discord" or "clyde".`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function checkPrompt(prompt: string): void {
|
|
81
|
+
if (!prompt.trim() || prompt.length > MAX_PROMPT_CHARS)
|
|
82
|
+
throw new AgentError(
|
|
83
|
+
`A prompt must have 1 to ${MAX_PROMPT_CHARS} characters; this one has ${prompt.length}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function checkModelRef(model: string): void {
|
|
88
|
+
if (!/^[\w.-]+\/[\w.:/-]+$/.test(model))
|
|
89
|
+
throw new AgentError(
|
|
90
|
+
`"${model}" is not a model: write it as <provider>/<model>, such as openai-codex/gpt-6-sol.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function checkThinking(level: string): asserts level is ThinkingLevel {
|
|
95
|
+
if (!THINKING_LEVELS.includes(level as ThinkingLevel))
|
|
96
|
+
throw new AgentError(
|
|
97
|
+
`"${level}" is not a thinking level; use one of ${THINKING_LEVELS.join(", ")}.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function checkAvatarPrompt(prompt: string): void {
|
|
102
|
+
if (!prompt.trim() || prompt.length > MAX_AVATAR_PROMPT_CHARS)
|
|
103
|
+
throw new AgentError(
|
|
104
|
+
`An avatar prompt must have 1 to ${MAX_AVATAR_PROMPT_CHARS} characters; this one has ${prompt.length}.`,
|
|
105
|
+
);
|
|
106
|
+
}
|