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,99 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import type { AgentGroup, Backlog, GroupMessage } from "./agent-rules.ts";
|
|
3
|
+
|
|
4
|
+
interface MessageRow {
|
|
5
|
+
id: string | number;
|
|
6
|
+
author: string;
|
|
7
|
+
author_name: string;
|
|
8
|
+
text: string;
|
|
9
|
+
created_at: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const toMessage = (row: MessageRow): GroupMessage => ({
|
|
13
|
+
id: Number(row.id),
|
|
14
|
+
author: row.author,
|
|
15
|
+
authorName: row.author_name,
|
|
16
|
+
text: row.text,
|
|
17
|
+
at: row.created_at,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** A guild's group messages and each member's cursor into them. */
|
|
21
|
+
export class GroupMessages {
|
|
22
|
+
readonly #sql: SQL;
|
|
23
|
+
readonly #guild: string;
|
|
24
|
+
|
|
25
|
+
constructor(sql: SQL, guildId: string) {
|
|
26
|
+
this.#sql = sql;
|
|
27
|
+
this.#guild = guildId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async append(
|
|
31
|
+
group: string,
|
|
32
|
+
message: { author: string; authorName: string; text: string },
|
|
33
|
+
): Promise<number> {
|
|
34
|
+
const rows: { id: string | number }[] = await this.#sql`
|
|
35
|
+
INSERT INTO agent_group_messages (guild_id, group_name, author, author_name, text)
|
|
36
|
+
VALUES (${this.#guild}, ${group}, ${message.author}, ${message.authorName}, ${message.text})
|
|
37
|
+
RETURNING id`;
|
|
38
|
+
return Number(rows[0]?.id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The group's newest messages, oldest first. */
|
|
42
|
+
async recent(group: string, limit: number): Promise<GroupMessage[]> {
|
|
43
|
+
const rows: MessageRow[] = await this.#sql`
|
|
44
|
+
SELECT id, author, author_name, text, created_at FROM agent_group_messages
|
|
45
|
+
WHERE guild_id = ${this.#guild} AND group_name = ${group} ORDER BY id DESC LIMIT ${limit}`;
|
|
46
|
+
return rows.reverse().map(toMessage);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Group messages the member has not received yet, its own excluded; the newest `limit` are carried. */
|
|
50
|
+
async backlog(group: string, agent: string, limit: number): Promise<Backlog> {
|
|
51
|
+
const cursor = await this.#cursor(group, agent);
|
|
52
|
+
const rows: MessageRow[] = await this.#sql`
|
|
53
|
+
SELECT id, author, author_name, text, created_at FROM agent_group_messages
|
|
54
|
+
WHERE guild_id = ${this.#guild} AND group_name = ${group} AND id > ${cursor}
|
|
55
|
+
ORDER BY id`;
|
|
56
|
+
const waiting = rows.map(toMessage);
|
|
57
|
+
const others = waiting.filter((m) => m.author !== agent);
|
|
58
|
+
const carried = others.slice(-limit);
|
|
59
|
+
return {
|
|
60
|
+
messages: carried,
|
|
61
|
+
skipped: others.length - carried.length,
|
|
62
|
+
lastId: waiting.at(-1)?.id,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async advanceCursor(
|
|
67
|
+
group: string,
|
|
68
|
+
agent: string,
|
|
69
|
+
lastId: number,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
await this.#sql`
|
|
72
|
+
INSERT INTO agent_group_cursors (guild_id, group_name, agent_name, last_id)
|
|
73
|
+
VALUES (${this.#guild}, ${group}, ${agent}, ${lastId})
|
|
74
|
+
ON CONFLICT (guild_id, group_name, agent_name)
|
|
75
|
+
DO UPDATE SET last_id = GREATEST(agent_group_cursors.last_id, EXCLUDED.last_id)`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Marks every message so far as received by every member, for a group started over. */
|
|
79
|
+
async catchUp(group: AgentGroup): Promise<void> {
|
|
80
|
+
const rows: { id: string | number | null }[] = await this.#sql`
|
|
81
|
+
SELECT max(id) AS id FROM agent_group_messages
|
|
82
|
+
WHERE guild_id = ${this.#guild} AND group_name = ${group.name}`;
|
|
83
|
+
const last = rows[0]?.id;
|
|
84
|
+
if (last === null || last === undefined) return;
|
|
85
|
+
for (const member of group.members)
|
|
86
|
+
await this.advanceCursor(group.name, member, Number(last));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A member's cursor; one that joined late starts at the messages before it joined, so it
|
|
91
|
+
* catches up on the group's recent history.
|
|
92
|
+
*/
|
|
93
|
+
async #cursor(group: string, agent: string): Promise<number> {
|
|
94
|
+
const rows: { last_id: string | number }[] = await this.#sql`
|
|
95
|
+
SELECT last_id FROM agent_group_cursors
|
|
96
|
+
WHERE guild_id = ${this.#guild} AND group_name = ${group} AND agent_name = ${agent}`;
|
|
97
|
+
return rows[0] ? Number(rows[0].last_id) : 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
import type { YesNoQuestion } from "../domain/ports.ts";
|
|
4
|
+
import { TEST_OWNER as OWNER } from "../testing/owner.ts";
|
|
5
|
+
import type { Agent } from "./agent-store.ts";
|
|
6
|
+
import { mentionedMembers, planRound, RelevanceScorer } from "./group-round.ts";
|
|
7
|
+
|
|
8
|
+
const agent = (name: string, displayName: string): Agent => ({
|
|
9
|
+
name,
|
|
10
|
+
displayName,
|
|
11
|
+
prompt: `You are ${displayName}.`,
|
|
12
|
+
avatarPrompt: "",
|
|
13
|
+
status: "active",
|
|
14
|
+
});
|
|
15
|
+
const members = [
|
|
16
|
+
agent("coordinator", "Координатор"),
|
|
17
|
+
agent("infra", "Infra"),
|
|
18
|
+
agent("doctor", "Bot Доктор"),
|
|
19
|
+
];
|
|
20
|
+
const logger = pino({ level: "silent" });
|
|
21
|
+
|
|
22
|
+
describe("mentionedMembers", () => {
|
|
23
|
+
test("matches display names and names, ignoring case", () => {
|
|
24
|
+
expect(mentionedMembers("@Bot Доктор what do you think", members)).toEqual([
|
|
25
|
+
"doctor",
|
|
26
|
+
]);
|
|
27
|
+
expect(
|
|
28
|
+
mentionedMembers("@infra and @Координатор take a look", members),
|
|
29
|
+
).toEqual(["coordinator", "infra"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("does not match a longer word", () => {
|
|
33
|
+
expect(mentionedMembers("@infrastructure", members)).toEqual([]);
|
|
34
|
+
expect(mentionedMembers("infra hello", members)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("planRound", () => {
|
|
39
|
+
const names = members.map((m) => m.name);
|
|
40
|
+
|
|
41
|
+
test("members above the threshold answer, most relevant last", () => {
|
|
42
|
+
expect(
|
|
43
|
+
planRound({
|
|
44
|
+
members: names,
|
|
45
|
+
host: "coordinator",
|
|
46
|
+
scores: { coordinator: 0.6, infra: 0.95, doctor: 0.2 },
|
|
47
|
+
forced: new Set(),
|
|
48
|
+
}),
|
|
49
|
+
).toEqual(["coordinator", "infra"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a forced member answers whatever its score, in score order", () => {
|
|
53
|
+
expect(
|
|
54
|
+
planRound({
|
|
55
|
+
members: names,
|
|
56
|
+
host: "coordinator",
|
|
57
|
+
scores: { coordinator: 0.1, infra: 0.9, doctor: 0.05 },
|
|
58
|
+
forced: new Set(["doctor"]),
|
|
59
|
+
}),
|
|
60
|
+
).toEqual(["doctor", "infra"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("ties keep group order", () => {
|
|
64
|
+
expect(
|
|
65
|
+
planRound({
|
|
66
|
+
members: names,
|
|
67
|
+
host: "coordinator",
|
|
68
|
+
scores: { coordinator: 0.7, infra: 0.7, doctor: 0.7 },
|
|
69
|
+
forced: new Set(),
|
|
70
|
+
}),
|
|
71
|
+
).toEqual(names);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("with nobody qualifying the host answers alone", () => {
|
|
75
|
+
expect(
|
|
76
|
+
planRound({
|
|
77
|
+
members: names,
|
|
78
|
+
host: "infra",
|
|
79
|
+
scores: { coordinator: 0.1, infra: 0.2, doctor: 0.3 },
|
|
80
|
+
forced: new Set(),
|
|
81
|
+
}),
|
|
82
|
+
).toEqual(["infra"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("without scores only forced members answer, or the host", () => {
|
|
86
|
+
expect(
|
|
87
|
+
planRound({
|
|
88
|
+
members: names,
|
|
89
|
+
host: "coordinator",
|
|
90
|
+
scores: undefined,
|
|
91
|
+
forced: new Set(["doctor"]),
|
|
92
|
+
}),
|
|
93
|
+
).toEqual(["doctor"]);
|
|
94
|
+
expect(
|
|
95
|
+
planRound({
|
|
96
|
+
members: names,
|
|
97
|
+
host: "coordinator",
|
|
98
|
+
scores: undefined,
|
|
99
|
+
forced: new Set(),
|
|
100
|
+
}),
|
|
101
|
+
).toEqual(["coordinator"]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("RelevanceScorer", () => {
|
|
106
|
+
test("asks one question per member in one request and maps the answers back", async () => {
|
|
107
|
+
const asked: Record<string, YesNoQuestion>[] = [];
|
|
108
|
+
const scorer = new RelevanceScorer(
|
|
109
|
+
{
|
|
110
|
+
askYesNo: async (_state, questions) => {
|
|
111
|
+
asked.push(questions);
|
|
112
|
+
return { m_coordinator: 0.3, m_infra: 0.9, m_doctor: 0.1 };
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
logger,
|
|
116
|
+
OWNER,
|
|
117
|
+
);
|
|
118
|
+
expect(await scorer.score(members, [], "the disk is almost full")).toEqual({
|
|
119
|
+
coordinator: 0.3,
|
|
120
|
+
infra: 0.9,
|
|
121
|
+
doctor: 0.1,
|
|
122
|
+
});
|
|
123
|
+
expect(asked).toHaveLength(1);
|
|
124
|
+
expect(Object.keys(asked[0] ?? {})).toEqual([
|
|
125
|
+
"m_coordinator",
|
|
126
|
+
"m_infra",
|
|
127
|
+
"m_doctor",
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a failed request yields no scores", async () => {
|
|
132
|
+
const scorer = new RelevanceScorer(
|
|
133
|
+
{
|
|
134
|
+
askYesNo: async () => {
|
|
135
|
+
throw new Error("down");
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
logger,
|
|
139
|
+
OWNER,
|
|
140
|
+
);
|
|
141
|
+
expect(await scorer.score(members, [], "hi")).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Judge } from "../contract/providers.ts";
|
|
2
|
+
import { type OwnerIdentity, ownerWords } from "../identity.ts";
|
|
3
|
+
import type { Logger } from "../log.ts";
|
|
4
|
+
import type { Agent, GroupMessage } from "./agent-store.ts";
|
|
5
|
+
|
|
6
|
+
/** Members at or above this relevance answer an owner message nobody is named in. */
|
|
7
|
+
export const RELEVANCE_THRESHOLD = 0.5;
|
|
8
|
+
/** Replies one round may hold, handoffs included (spec behavior 29). */
|
|
9
|
+
export const MAX_ROUND_REPLIES = 8;
|
|
10
|
+
/** Group messages the scorer reads before the new one. */
|
|
11
|
+
export const RECENT_FOR_SCORING = 12;
|
|
12
|
+
const SCORING_TEXT_CHARS = 1_500;
|
|
13
|
+
const PROMPT_CHARS_FOR_SCORING = 1_200;
|
|
14
|
+
|
|
15
|
+
const escapeRegExp = (value: string) =>
|
|
16
|
+
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
|
|
18
|
+
/** Members a text names with `@display name` or `@name`, in group order. */
|
|
19
|
+
export function mentionedMembers(
|
|
20
|
+
text: string,
|
|
21
|
+
members: readonly Agent[],
|
|
22
|
+
): string[] {
|
|
23
|
+
return members
|
|
24
|
+
.filter((member) =>
|
|
25
|
+
[member.displayName, member.name].some((label) =>
|
|
26
|
+
new RegExp(`@${escapeRegExp(label)}(?![\\p{L}\\p{N}_-])`, "iu").test(
|
|
27
|
+
text,
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
.map((member) => member.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Who answers, in speaking order: members at or above the threshold and every forced member,
|
|
36
|
+
* lowest relevance first so the most relevant speaks last; ties keep group order. Without
|
|
37
|
+
* scores only the forced members answer; with nobody at all, the host answers alone.
|
|
38
|
+
*/
|
|
39
|
+
export function planRound(input: {
|
|
40
|
+
members: readonly string[];
|
|
41
|
+
host: string;
|
|
42
|
+
scores: Readonly<Record<string, number>> | undefined;
|
|
43
|
+
forced: ReadonlySet<string>;
|
|
44
|
+
threshold?: number;
|
|
45
|
+
}): string[] {
|
|
46
|
+
const { members, host, scores, forced } = input;
|
|
47
|
+
const threshold = input.threshold ?? RELEVANCE_THRESHOLD;
|
|
48
|
+
const chosen = members.filter(
|
|
49
|
+
(name) => forced.has(name) || (scores?.[name] ?? 0) >= threshold,
|
|
50
|
+
);
|
|
51
|
+
if (chosen.length === 0) return [host];
|
|
52
|
+
return chosen
|
|
53
|
+
.map((name, index) => ({ name, index, score: scores?.[name] ?? 0 }))
|
|
54
|
+
.sort((a, b) => a.score - b.score || a.index - b.index)
|
|
55
|
+
.map((entry) => entry.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One judge request with one yes-or-no question per member: should it answer this message? */
|
|
59
|
+
export class RelevanceScorer {
|
|
60
|
+
readonly #judge: Pick<Judge, "askYesNo">;
|
|
61
|
+
readonly #logger: Logger;
|
|
62
|
+
readonly #owner: OwnerIdentity;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
judge: Pick<Judge, "askYesNo">,
|
|
66
|
+
logger: Logger,
|
|
67
|
+
owner: OwnerIdentity,
|
|
68
|
+
) {
|
|
69
|
+
this.#judge = judge;
|
|
70
|
+
this.#logger = logger;
|
|
71
|
+
this.#owner = owner;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Each member's relevance from 0 to 1; undefined when the judge failed. */
|
|
75
|
+
async score(
|
|
76
|
+
members: readonly Agent[],
|
|
77
|
+
recent: readonly GroupMessage[],
|
|
78
|
+
text: string,
|
|
79
|
+
): Promise<Record<string, number> | undefined> {
|
|
80
|
+
const key = (name: string) => `m_${name.replaceAll("-", "_")}`;
|
|
81
|
+
const questions = Object.fromEntries(
|
|
82
|
+
members.map((member) => [
|
|
83
|
+
key(member.name),
|
|
84
|
+
{
|
|
85
|
+
type: "yesno" as const,
|
|
86
|
+
instructions: `Should the agent "${member.displayName}" answer the owner's newest message in this group chat? Its role:\n${member.prompt.slice(0, PROMPT_CHARS_FOR_SCORING)}`,
|
|
87
|
+
criteria: {
|
|
88
|
+
true: "The message is about this agent's role, asks for knowledge or work it owns, or its view would add something the others would not.",
|
|
89
|
+
false:
|
|
90
|
+
"The message is outside this agent's role, or another member clearly owns it and this agent would add nothing.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
const o = ownerWords(this.#owner);
|
|
96
|
+
const state = {
|
|
97
|
+
task: `A group chat between the owner, ${o.name}, and ${o.his} agents. Decide who should answer ${o.his} newest message.`,
|
|
98
|
+
members: members.map((m) => ({ name: m.displayName, id: m.name })),
|
|
99
|
+
recent: recent.slice(-RECENT_FOR_SCORING).map((m) => ({
|
|
100
|
+
from: m.authorName,
|
|
101
|
+
text: m.text.slice(0, SCORING_TEXT_CHARS),
|
|
102
|
+
})),
|
|
103
|
+
newest: text.slice(0, SCORING_TEXT_CHARS),
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const answers = await this.#judge.askYesNo(state, questions);
|
|
107
|
+
const scores = Object.fromEntries(
|
|
108
|
+
members.map((m) => [m.name, answers[key(m.name)] ?? 0]),
|
|
109
|
+
);
|
|
110
|
+
this.#logger.info({ scores }, "group relevance scored");
|
|
111
|
+
return scores;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.#logger.warn({ err: error }, "group relevance scoring failed");
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { TurnAttachments } from "../domain/attachment.ts";
|
|
2
|
+
import type { ChannelKey } from "../domain/conversation.ts";
|
|
3
|
+
import { AgentError } from "../domain/errors.ts";
|
|
4
|
+
import { messages } from "../i18n/index.ts";
|
|
5
|
+
import type { Speaker } from "../speakers.ts";
|
|
6
|
+
import { groupTurnText } from "./agent-prompt.ts";
|
|
7
|
+
import type { Agent } from "./agent-store.ts";
|
|
8
|
+
import {
|
|
9
|
+
MAX_ROUND_REPLIES,
|
|
10
|
+
mentionedMembers,
|
|
11
|
+
planRound,
|
|
12
|
+
RECENT_FOR_SCORING,
|
|
13
|
+
} from "./group-round.ts";
|
|
14
|
+
import { channelIdOf, groupScope, groupSessionKey } from "./team-keys.ts";
|
|
15
|
+
import { BACKLOG_LIMIT, type Chain, type TurnHost } from "./team-turn-types.ts";
|
|
16
|
+
|
|
17
|
+
/** A group channel's rounds: scoring, then the chosen members answer one at a time. */
|
|
18
|
+
export class GroupTurns {
|
|
19
|
+
readonly #host: TurnHost;
|
|
20
|
+
|
|
21
|
+
constructor(host: TurnHost) {
|
|
22
|
+
this.#host = host;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One owner message in a group channel: scored, then answered by the chosen members one at
|
|
27
|
+
* a time, each seeing every group message since its last turn. Call inside the queue.
|
|
28
|
+
*/
|
|
29
|
+
async answerGroup(
|
|
30
|
+
channel: ChannelKey,
|
|
31
|
+
speaker: Speaker,
|
|
32
|
+
text: string,
|
|
33
|
+
replyText: string,
|
|
34
|
+
attachments: TurnAttachments,
|
|
35
|
+
repliedToName: string | undefined,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const { store, scorer, confirmations, logger } = this.#host.options;
|
|
38
|
+
const runtime = this.#host.options.runtime();
|
|
39
|
+
const group = store.groupByChannel(channelIdOf(channel));
|
|
40
|
+
if (!group) throw new AgentError(`no group owns ${channel}`);
|
|
41
|
+
const members = group.members
|
|
42
|
+
.map((name) => store.agent(name))
|
|
43
|
+
.filter((m): m is Agent => m?.status === "active");
|
|
44
|
+
const recent = await store.recentGroupMessages(
|
|
45
|
+
group.name,
|
|
46
|
+
RECENT_FOR_SCORING,
|
|
47
|
+
);
|
|
48
|
+
await store.appendGroupMessage(group.name, {
|
|
49
|
+
author: speaker.tier === "owner" ? "owner" : `human:${speaker.id}`,
|
|
50
|
+
authorName:
|
|
51
|
+
speaker.tier === "owner" ? this.#host.options.owner.name : speaker.name,
|
|
52
|
+
text,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const forced = new Set(mentionedMembers(replyText, members));
|
|
56
|
+
const repliedTo = members.find((m) => m.displayName === repliedToName);
|
|
57
|
+
if (repliedTo) forced.add(repliedTo.name);
|
|
58
|
+
const approvals = new Map<string, boolean>();
|
|
59
|
+
for (const member of members) {
|
|
60
|
+
const pending = await runtime.heldActions(
|
|
61
|
+
groupSessionKey(group, member.name),
|
|
62
|
+
);
|
|
63
|
+
// Only a speaker whose tier holds the held tools may approve or wake a seat for them.
|
|
64
|
+
if (!pending || !this.#host.mayApprove(speaker, pending)) continue;
|
|
65
|
+
forced.add(member.name);
|
|
66
|
+
approvals.set(
|
|
67
|
+
member.name,
|
|
68
|
+
await confirmations.approves(pending, replyText),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const scores =
|
|
72
|
+
forced.size === members.length
|
|
73
|
+
? undefined
|
|
74
|
+
: await scorer.score(members, recent, text);
|
|
75
|
+
const order = planRound({
|
|
76
|
+
members: members.map((m) => m.name),
|
|
77
|
+
host: members.some((m) => m.name === group.host)
|
|
78
|
+
? group.host
|
|
79
|
+
: (members[0]?.name ?? group.host),
|
|
80
|
+
scores,
|
|
81
|
+
forced,
|
|
82
|
+
});
|
|
83
|
+
logger.info({ group: group.name, scores, order }, "group round planned");
|
|
84
|
+
|
|
85
|
+
const chain: Chain = { messages: 0, speaker };
|
|
86
|
+
const queued = [...order];
|
|
87
|
+
const spoken = new Set<string>();
|
|
88
|
+
let last: Agent | undefined;
|
|
89
|
+
let replies = 0;
|
|
90
|
+
while (queued.length > 0 && replies < MAX_ROUND_REPLIES) {
|
|
91
|
+
// A member may archive the group during the round.
|
|
92
|
+
if (store.group(group.name)?.status !== "active") return;
|
|
93
|
+
const name = queued.shift() as string;
|
|
94
|
+
const member = store.agent(name);
|
|
95
|
+
if (member?.status !== "active") continue;
|
|
96
|
+
const first = !spoken.has(name);
|
|
97
|
+
spoken.add(name);
|
|
98
|
+
const backlog = await store.backlog(group.name, name, BACKLOG_LIMIT);
|
|
99
|
+
// Delivered once the session has the text, so a message never comes twice.
|
|
100
|
+
if (backlog.lastId !== undefined)
|
|
101
|
+
await store.advanceCursor(group.name, name, backlog.lastId);
|
|
102
|
+
const result = await this.#host.turn(
|
|
103
|
+
member,
|
|
104
|
+
groupScope(member, group),
|
|
105
|
+
channel,
|
|
106
|
+
groupTurnText(group, member, backlog),
|
|
107
|
+
chain,
|
|
108
|
+
first
|
|
109
|
+
? {
|
|
110
|
+
attachments,
|
|
111
|
+
interactive: true,
|
|
112
|
+
...(approvals.get(name) ? { confirmed: true } : {}),
|
|
113
|
+
}
|
|
114
|
+
: { interactive: true },
|
|
115
|
+
);
|
|
116
|
+
replies += 1;
|
|
117
|
+
last = member;
|
|
118
|
+
if (!result.ok) continue;
|
|
119
|
+
await store.appendGroupMessage(group.name, {
|
|
120
|
+
author: name,
|
|
121
|
+
authorName: this.#host.current(member).displayName,
|
|
122
|
+
text: result.text,
|
|
123
|
+
});
|
|
124
|
+
for (const handoff of mentionedMembers(result.text, members))
|
|
125
|
+
if (handoff !== name && !queued.includes(handoff)) queued.push(handoff);
|
|
126
|
+
}
|
|
127
|
+
if (queued.length > 0 && last)
|
|
128
|
+
await this.#host.post(channel, this.#host.current(last), {
|
|
129
|
+
chunks: [messages().roundLimitNotice],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { InboundMessage } from "../domain/conversation.ts";
|
|
4
|
+
import type { OwnerIdentity } from "../identity.ts";
|
|
5
|
+
import { withReference } from "../routing/message-text.ts";
|
|
6
|
+
import {
|
|
7
|
+
answerText,
|
|
8
|
+
askUserExtension,
|
|
9
|
+
} from "../runtime/extensions/ask-user.ts";
|
|
10
|
+
import {
|
|
11
|
+
ConfirmationGate,
|
|
12
|
+
confirmedTurnText,
|
|
13
|
+
} from "../runtime/extensions/confirmation-gate.ts";
|
|
14
|
+
import { PromptSlot } from "../runtime/prompt-slot.ts";
|
|
15
|
+
import { addressee, type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
16
|
+
import {
|
|
17
|
+
agentFailureText,
|
|
18
|
+
agentMessageText,
|
|
19
|
+
agentReplyText,
|
|
20
|
+
agentSystemPrompt,
|
|
21
|
+
chainLimitReason,
|
|
22
|
+
openingTaskText,
|
|
23
|
+
} from "./agent-prompt.ts";
|
|
24
|
+
import type { Agent } from "./agent-store.ts";
|
|
25
|
+
|
|
26
|
+
const ALICE: OwnerIdentity = {
|
|
27
|
+
name: "Alice",
|
|
28
|
+
pronouns: { subject: "she", object: "her", possessive: "her" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const agent: Agent = {
|
|
32
|
+
name: "scout",
|
|
33
|
+
displayName: "Scout",
|
|
34
|
+
prompt: "Scout things.",
|
|
35
|
+
avatarPrompt: "",
|
|
36
|
+
channelId: "5",
|
|
37
|
+
status: "active",
|
|
38
|
+
};
|
|
39
|
+
const coordinator: Agent = {
|
|
40
|
+
...agent,
|
|
41
|
+
name: "coordinator",
|
|
42
|
+
displayName: "Coordinator",
|
|
43
|
+
channelId: "1",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Every text these modules show a model, built for an owner who is not the test default. */
|
|
47
|
+
function texts(): string[] {
|
|
48
|
+
const gate = new ConfirmationGate(() => "delete the task", ALICE);
|
|
49
|
+
return [
|
|
50
|
+
agentSystemPrompt({
|
|
51
|
+
agent,
|
|
52
|
+
coordinator,
|
|
53
|
+
shared: "",
|
|
54
|
+
workDir: "/w",
|
|
55
|
+
owner: ALICE,
|
|
56
|
+
shellUser: "alice-bot",
|
|
57
|
+
}),
|
|
58
|
+
agentSystemPrompt({
|
|
59
|
+
agent: coordinator,
|
|
60
|
+
coordinator,
|
|
61
|
+
shared: "",
|
|
62
|
+
workDir: "/w",
|
|
63
|
+
owner: ALICE,
|
|
64
|
+
shellUser: "alice-bot",
|
|
65
|
+
}),
|
|
66
|
+
agentMessageText(agent, "x", ALICE),
|
|
67
|
+
agentReplyText(ALICE, agent, "x"),
|
|
68
|
+
agentFailureText(ALICE, agent, "r"),
|
|
69
|
+
openingTaskText(undefined, "t", ALICE),
|
|
70
|
+
chainLimitReason(ALICE),
|
|
71
|
+
answerText(undefined, ALICE),
|
|
72
|
+
answerText({ choices: ["A"], text: "b" }, ALICE),
|
|
73
|
+
gate.hold("any", {}) ?? "",
|
|
74
|
+
confirmedTurnText(
|
|
75
|
+
{ profile: "general", heldAt: new Date(0), calls: [] },
|
|
76
|
+
"ok",
|
|
77
|
+
ALICE,
|
|
78
|
+
),
|
|
79
|
+
withReference(
|
|
80
|
+
{
|
|
81
|
+
text: "look",
|
|
82
|
+
forwarded: { channelMention: "<#2>", url: "https://x", text: "f" },
|
|
83
|
+
} as InboundMessage,
|
|
84
|
+
ALICE,
|
|
85
|
+
),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test("prompts name the configured owner and shell user, never the default's", () => {
|
|
90
|
+
const all = texts().join("\n");
|
|
91
|
+
expect(all).not.toContain("Riley");
|
|
92
|
+
expect(all).not.toContain("mcops");
|
|
93
|
+
expect(all).not.toMatch(/\b(he|him|his|He|His)\b/);
|
|
94
|
+
expect(all).toContain("Alice");
|
|
95
|
+
expect(all).toContain("alice-bot");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const BOB: Speaker = { id: "100000000000000007", name: "Bob", tier: "member" };
|
|
99
|
+
const promptFor = (speaker?: Speaker) =>
|
|
100
|
+
agentSystemPrompt({
|
|
101
|
+
agent,
|
|
102
|
+
coordinator,
|
|
103
|
+
shared: "",
|
|
104
|
+
workDir: "/w",
|
|
105
|
+
owner: ALICE,
|
|
106
|
+
shellUser: "alice-bot",
|
|
107
|
+
...(speaker ? { speaker } : {}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("an owner-tier speaker gets the owner's prompts unchanged", () => {
|
|
111
|
+
const owner: Speaker = { id: "1", name: "Alice", tier: "owner" };
|
|
112
|
+
expect(promptFor(owner)).toBe(promptFor());
|
|
113
|
+
expect(openingTaskText(undefined, "t", addressee(owner, ALICE))).toBe(
|
|
114
|
+
openingTaskText(undefined, "t", ALICE),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a member's turn addresses the member and says whose memory and approvals apply", () => {
|
|
119
|
+
const prompt = promptFor(BOB);
|
|
120
|
+
expect(prompt).toContain("only Bob approves held actions");
|
|
121
|
+
expect(prompt).toContain("held for Bob's confirmation");
|
|
122
|
+
expect(prompt).toContain(
|
|
123
|
+
"You are talking with Bob (Discord user 100000000000000007), who speaks at the member tier",
|
|
124
|
+
);
|
|
125
|
+
expect(prompt).toContain("Your memory is Bob's own, not Alice's");
|
|
126
|
+
expect(prompt).not.toContain("only Alice approves");
|
|
127
|
+
const bob = addressee(BOB, ALICE);
|
|
128
|
+
expect(chainLimitReason(bob)).toContain("without Bob in between");
|
|
129
|
+
expect(agentMessageText(agent, "x", bob)).toContain("not from Bob");
|
|
130
|
+
expect(gateHold(bob)).toContain("Held for Bob's confirmation");
|
|
131
|
+
expect(answerText(undefined, bob)).toContain("Bob did not answer");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function gateHold(who: OwnerIdentity): string {
|
|
135
|
+
const gate = new ConfirmationGate(() => "delete the task", ALICE);
|
|
136
|
+
gate.beginTurn("general", false, who);
|
|
137
|
+
return gate.hold("any", {}) ?? "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test("a tool description in a shared session names the speaker, not the owner", () => {
|
|
141
|
+
let description = "";
|
|
142
|
+
askUserExtension(
|
|
143
|
+
new PromptSlot(),
|
|
144
|
+
THE_SPEAKER,
|
|
145
|
+
)({
|
|
146
|
+
registerTool: (tool: { description: string }) => {
|
|
147
|
+
description = tool.description;
|
|
148
|
+
},
|
|
149
|
+
} as unknown as ExtensionAPI);
|
|
150
|
+
expect(description).toContain("Ask the speaker a question");
|
|
151
|
+
expect(description).toContain("wait for the speaker's answer");
|
|
152
|
+
expect(description).toContain("The speaker has 30 minutes");
|
|
153
|
+
expect(description).not.toContain("Alice");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a speaker who is not the owner gets the guest's shared prompt, the owner the owner's", () => {
|
|
157
|
+
const prompt = (speaker?: Speaker) =>
|
|
158
|
+
agentSystemPrompt({
|
|
159
|
+
agent,
|
|
160
|
+
coordinator,
|
|
161
|
+
shared: "For the owner.",
|
|
162
|
+
guestShared: "For guests.",
|
|
163
|
+
workDir: "/w",
|
|
164
|
+
owner: ALICE,
|
|
165
|
+
shellUser: "alice-bot",
|
|
166
|
+
...(speaker ? { speaker } : {}),
|
|
167
|
+
});
|
|
168
|
+
expect(prompt().startsWith("For the owner.")).toBe(true);
|
|
169
|
+
expect(prompt({ id: "1", name: "Alice", tier: "owner" })).toBe(prompt());
|
|
170
|
+
expect(prompt(BOB).startsWith("For guests.")).toBe(true);
|
|
171
|
+
expect(prompt(BOB)).not.toContain("For the owner.");
|
|
172
|
+
});
|