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,80 @@
|
|
|
1
|
+
import type { ThinkingSetting } from "../models.ts";
|
|
2
|
+
import type { ContextUse } from "./agent-ports.ts";
|
|
3
|
+
import { channelIdOf, channelKey } from "./team-keys.ts";
|
|
4
|
+
import type { TeamContext } from "./team-options.ts";
|
|
5
|
+
|
|
6
|
+
/** An active agent as the dashboard shows it. */
|
|
7
|
+
export interface AgentStatus {
|
|
8
|
+
name: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
channelId?: string;
|
|
11
|
+
model: string;
|
|
12
|
+
thinking: ThinkingSetting;
|
|
13
|
+
/** The channel of its running turn: its own, a group's, or where it answers a message. */
|
|
14
|
+
workingIn?: string;
|
|
15
|
+
/** Turns waiting in its own channel. */
|
|
16
|
+
waiting: number;
|
|
17
|
+
context?: ContextUse;
|
|
18
|
+
lastActive?: Date;
|
|
19
|
+
schedules: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An active group as the dashboard shows it. */
|
|
23
|
+
export interface GroupStatus {
|
|
24
|
+
name: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
channelId: string;
|
|
27
|
+
members: string[];
|
|
28
|
+
host: string;
|
|
29
|
+
/** Messages of its channel running or waiting, a round counting as one. */
|
|
30
|
+
busy: number;
|
|
31
|
+
lastActive?: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TeamStatus {
|
|
35
|
+
agents: AgentStatus[];
|
|
36
|
+
groups: GroupStatus[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every active agent and group, with what each is doing now. */
|
|
40
|
+
export async function teamStatus(ctx: TeamContext): Promise<TeamStatus> {
|
|
41
|
+
const { options, turns } = ctx;
|
|
42
|
+
const { store, queue, schedules } = options;
|
|
43
|
+
const runtime = options.runtime();
|
|
44
|
+
const agents: AgentStatus[] = [];
|
|
45
|
+
for (const agent of store.agents()) {
|
|
46
|
+
if (agent.status !== "active") continue;
|
|
47
|
+
const home = agent.channelId ? channelKey(agent.channelId) : undefined;
|
|
48
|
+
const workingIn = turns.workingIn(agent.name);
|
|
49
|
+
const queued = home ? queue.size(home) : 0;
|
|
50
|
+
const context = home ? runtime.contextUsage(home) : undefined;
|
|
51
|
+
const lastActive = turns.lastActive(agent.name);
|
|
52
|
+
agents.push({
|
|
53
|
+
name: agent.name,
|
|
54
|
+
displayName: agent.displayName,
|
|
55
|
+
...(agent.channelId ? { channelId: agent.channelId } : {}),
|
|
56
|
+
...ctx.modelOf(agent.name),
|
|
57
|
+
...(workingIn ? { workingIn: channelIdOf(workingIn) } : {}),
|
|
58
|
+
waiting: Math.max(0, queued - (workingIn === home ? 1 : 0)),
|
|
59
|
+
...(context ? { context } : {}),
|
|
60
|
+
...(lastActive ? { lastActive } : {}),
|
|
61
|
+
schedules: home ? (await schedules.forChannel(home)).length : 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const groups: GroupStatus[] = store
|
|
65
|
+
.groups()
|
|
66
|
+
.filter((group) => group.status === "active")
|
|
67
|
+
.map((group) => {
|
|
68
|
+
const lastActive = turns.lastGroupActive(group.name);
|
|
69
|
+
return {
|
|
70
|
+
name: group.name,
|
|
71
|
+
displayName: group.displayName,
|
|
72
|
+
channelId: group.channelId,
|
|
73
|
+
members: group.members.map((m) => store.agent(m)?.displayName ?? m),
|
|
74
|
+
host: store.agent(group.host)?.displayName ?? group.host,
|
|
75
|
+
busy: queue.size(channelKey(group.channelId)),
|
|
76
|
+
...(lastActive ? { lastActive } : {}),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return { agents, groups };
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AgentError } from "../domain/errors.ts";
|
|
2
|
+
import { assistantName } from "../i18n/index.ts";
|
|
3
|
+
import type { ThinkingSetting } from "../models.ts";
|
|
4
|
+
import { zonedStamp } from "../time.ts";
|
|
5
|
+
import type { ChannelMessage } from "./agent-ports.ts";
|
|
6
|
+
import { describeThinking } from "./agent-settings.ts";
|
|
7
|
+
import type { Agent, AgentStore } from "./agent-store.ts";
|
|
8
|
+
|
|
9
|
+
/** One message as channel_read shows it: id, local time, author, text, attachments. */
|
|
10
|
+
export function messageLine(message: ChannelMessage): string {
|
|
11
|
+
const files =
|
|
12
|
+
message.attachments.length > 0
|
|
13
|
+
? ` [attachments: ${message.attachments.join(", ")}]`
|
|
14
|
+
: "";
|
|
15
|
+
return `[${message.id}] ${zonedStamp(message.at)} ${message.author}: ${message.text}${files}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A channel given as `<#id>` or its id, as its id. */
|
|
19
|
+
export function channelIdFromRef(channel: string): string {
|
|
20
|
+
const id = /^<#(\d+)>$/.exec(channel.trim())?.[1] ?? channel.trim();
|
|
21
|
+
if (!/^\d+$/.test(id))
|
|
22
|
+
throw new AgentError(
|
|
23
|
+
"Give the channel as <#id> or its id, as in a forwarded message.",
|
|
24
|
+
);
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Every agent and group as agent_list shows them, before the category layout. */
|
|
29
|
+
export function teamListText(store: Pick<AgentStore, "agents" | "groups">) {
|
|
30
|
+
const agents = store.agents().map((a) => {
|
|
31
|
+
const channel = a.channelId ? `<#${a.channelId}>` : "no channel";
|
|
32
|
+
return `- ${a.name} "${a.displayName}" — ${channel}, ${a.status}`;
|
|
33
|
+
});
|
|
34
|
+
const groups = store
|
|
35
|
+
.groups()
|
|
36
|
+
.map(
|
|
37
|
+
(g) =>
|
|
38
|
+
`- ${g.name} "${g.displayName}" — <#${g.channelId}>, ${g.status}; members ${g.members.join(", ")}; host ${g.host}`,
|
|
39
|
+
);
|
|
40
|
+
return `Agents:\n${agents.join("\n") || "none"}\n\nGroups:\n${groups.join("\n") || "none"}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One agent as agent_get shows it; unset settings name the assistant's. */
|
|
44
|
+
export function agentDetails(
|
|
45
|
+
agent: Agent,
|
|
46
|
+
defaults: { model: string; thinking: ThinkingSetting },
|
|
47
|
+
skills: string,
|
|
48
|
+
): string {
|
|
49
|
+
return [
|
|
50
|
+
`Name: ${agent.name}`,
|
|
51
|
+
`Display name: ${agent.displayName}`,
|
|
52
|
+
`Status: ${agent.status}`,
|
|
53
|
+
`Channel: ${agent.channelId ? `<#${agent.channelId}>` : "none"}`,
|
|
54
|
+
`Model: ${agent.model ?? `${assistantName()}'s (${defaults.model})`}`,
|
|
55
|
+
`Thinking: ${agent.thinking ?? `${assistantName()}'s (${describeThinking(defaults.thinking)})`}`,
|
|
56
|
+
`Skills: ${skills}`,
|
|
57
|
+
`Avatar prompt: ${agent.avatarPrompt}`,
|
|
58
|
+
`Prompt:\n${agent.prompt}`,
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { QueuePort } from "../contract/channels.ts";
|
|
2
|
+
import type {
|
|
3
|
+
DispatchThread,
|
|
4
|
+
DispatchThreads,
|
|
5
|
+
} from "../discord/dispatch-threads.ts";
|
|
6
|
+
import type { TurnAttachments } from "../domain/attachment.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ChannelKey,
|
|
9
|
+
PendingConfirmation,
|
|
10
|
+
TurnResult,
|
|
11
|
+
} from "../domain/conversation.ts";
|
|
12
|
+
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
13
|
+
import type { OwnerIdentity } from "../identity.ts";
|
|
14
|
+
import type { Logger } from "../log.ts";
|
|
15
|
+
import type { EventSink } from "../plugin.ts";
|
|
16
|
+
import type { TurnSelection } from "../sessions.ts";
|
|
17
|
+
import type { Speaker } from "../speakers.ts";
|
|
18
|
+
import type { ToolTiers } from "../tool-tiers.ts";
|
|
19
|
+
import type { AgentChannels, AgentTurnRunner } from "./agent-ports.ts";
|
|
20
|
+
import type { Agent, AgentStore } from "./agent-store.ts";
|
|
21
|
+
import type { AvatarStudio } from "./avatar-studio.ts";
|
|
22
|
+
import type { RelevanceScorer } from "./group-round.ts";
|
|
23
|
+
|
|
24
|
+
/** Agent messages without an owner message in between (spec behavior 13). */
|
|
25
|
+
export const MAX_CHAIN_MESSAGES = 8;
|
|
26
|
+
export const BACKLOG_LIMIT = 40;
|
|
27
|
+
|
|
28
|
+
/** A run of agent messages started by one owner message, schedule, or report. */
|
|
29
|
+
export interface Chain {
|
|
30
|
+
messages: number;
|
|
31
|
+
/** Whoever started the run; the agents it wakes work for the same person. */
|
|
32
|
+
speaker: Speaker;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A message's thread in the sender's turn channel, which holds the request and the answer. */
|
|
36
|
+
export interface Exchange {
|
|
37
|
+
parent: ChannelKey;
|
|
38
|
+
thread: DispatchThread;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TeamTurnsOptions {
|
|
42
|
+
/** The agent server's entry channel, where the coordinator lives. */
|
|
43
|
+
entryChannelId: string;
|
|
44
|
+
/** Who the agents work for, as their prompts and group history name them. */
|
|
45
|
+
owner: OwnerIdentity;
|
|
46
|
+
store: AgentStore;
|
|
47
|
+
channels: Pick<AgentChannels, "post">;
|
|
48
|
+
studio: Pick<AvatarStudio, "url">;
|
|
49
|
+
/** Set once the runtime exists, which itself needs the team's tools. */
|
|
50
|
+
runtime: () => AgentTurnRunner;
|
|
51
|
+
/** The tools every agent turn runs with. */
|
|
52
|
+
selection: (scope: AgentTurnScope) => TurnSelection;
|
|
53
|
+
/** The lowest tier that may use each tool, which also decides who may approve a held one. */
|
|
54
|
+
toolTiers?: ToolTiers;
|
|
55
|
+
/** Judges whether an owner message approves held actions. */
|
|
56
|
+
confirmations: {
|
|
57
|
+
approves(pending: PendingConfirmation, reply: string): Promise<boolean>;
|
|
58
|
+
};
|
|
59
|
+
scorer: Pick<RelevanceScorer, "score">;
|
|
60
|
+
/** Shared with the conversation service, so each channel runs one turn at a time. */
|
|
61
|
+
queue: QueuePort;
|
|
62
|
+
startTyping(channel: ChannelKey): () => void;
|
|
63
|
+
/** The owner's stop button for a turn in an agent's own channel, until the returned function is called. */
|
|
64
|
+
showStop(channel: ChannelKey): () => void;
|
|
65
|
+
logger: Logger;
|
|
66
|
+
/** Called when a turn starts or ends. */
|
|
67
|
+
changed(): void;
|
|
68
|
+
/** Where turns and team changes are reported for the plugins' handlers. */
|
|
69
|
+
events?: EventSink;
|
|
70
|
+
/** A thread for each message between agents; without it both go in the sender's channel. */
|
|
71
|
+
threads?: Pick<DispatchThreads, "open">;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How a turn runs, beyond its text. */
|
|
75
|
+
export interface TurnExtra {
|
|
76
|
+
attachments?: TurnAttachments;
|
|
77
|
+
confirmed?: boolean;
|
|
78
|
+
steerable?: boolean;
|
|
79
|
+
/** The owner started it by writing, or it delivers a report, so it may ask him on cards. */
|
|
80
|
+
interactive?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** What the team's message and group collaborators need of the turns that own their state. */
|
|
84
|
+
export interface TurnHost {
|
|
85
|
+
readonly options: TeamTurnsOptions;
|
|
86
|
+
/** The chain of each running turn, by session key. */
|
|
87
|
+
readonly chains: ReadonlyMap<ChannelKey, Chain>;
|
|
88
|
+
/** Runs one agent turn and posts its reply under the agent's name; never rejects. */
|
|
89
|
+
turn(
|
|
90
|
+
agent: Agent,
|
|
91
|
+
scope: AgentTurnScope,
|
|
92
|
+
postTo: ChannelKey,
|
|
93
|
+
text: string,
|
|
94
|
+
chain: Chain,
|
|
95
|
+
extra?: TurnExtra,
|
|
96
|
+
): Promise<TurnResult>;
|
|
97
|
+
post(
|
|
98
|
+
channel: ChannelKey,
|
|
99
|
+
as: Agent,
|
|
100
|
+
body: { thinking?: string; chunks: string[]; threadId?: string },
|
|
101
|
+
): Promise<void>;
|
|
102
|
+
/** The agent's latest row. */
|
|
103
|
+
current(agent: Agent): Agent;
|
|
104
|
+
/** Whether the speaker's tier holds every tool of the held actions. */
|
|
105
|
+
mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean;
|
|
106
|
+
turnChannel(scope: AgentTurnScope): ChannelKey;
|
|
107
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NO_ATTACHMENTS } from "../domain/attachment.ts";
|
|
3
|
+
import type {
|
|
4
|
+
PendingConfirmation,
|
|
5
|
+
TurnResult,
|
|
6
|
+
} from "../domain/conversation.ts";
|
|
7
|
+
import { AgentRunError } from "../domain/errors.ts";
|
|
8
|
+
import type { AgentTurnScope, TurnRequest } from "../domain/ports.ts";
|
|
9
|
+
import { messages } from "../i18n/index.ts";
|
|
10
|
+
import { silentLogger } from "../log.ts";
|
|
11
|
+
import { ChannelQueue } from "../routing/channel-queue.ts";
|
|
12
|
+
import { TEST_OWNER as OWNER, OWNER_SPEAKER } from "../testing/owner.ts";
|
|
13
|
+
import { FakeThreadHost, fakeThreads } from "../testing/thread-host.ts";
|
|
14
|
+
import type { AgentPost } from "./agent-ports.ts";
|
|
15
|
+
import type { Agent, AgentGroup, AgentStore } from "./agent-store.ts";
|
|
16
|
+
import { channelKey } from "./team-keys.ts";
|
|
17
|
+
import { TeamTurns } from "./team-turns.ts";
|
|
18
|
+
|
|
19
|
+
const agent = (name: string, channelId: string): Agent => ({
|
|
20
|
+
name,
|
|
21
|
+
displayName: name === "infra" ? "Infrastructure" : "Coordinator",
|
|
22
|
+
prompt: "",
|
|
23
|
+
avatarPrompt: "",
|
|
24
|
+
channelId,
|
|
25
|
+
status: "active",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const AGENTS = [agent("coordinator", "1000"), agent("infra", "2000")];
|
|
29
|
+
const GROUP: AgentGroup = {
|
|
30
|
+
name: "ops",
|
|
31
|
+
displayName: "Ops",
|
|
32
|
+
channelId: "3000",
|
|
33
|
+
members: ["coordinator", "infra"],
|
|
34
|
+
host: "coordinator",
|
|
35
|
+
status: "active",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The store methods messages between agents read, over two agents and one group. */
|
|
39
|
+
const store = {
|
|
40
|
+
agent: (name: string) => AGENTS.find((a) => a.name === name),
|
|
41
|
+
activeAgent: (name: string) => {
|
|
42
|
+
const found = AGENTS.find((a) => a.name === name);
|
|
43
|
+
if (!found) throw new Error(`no agent ${name}`);
|
|
44
|
+
return found;
|
|
45
|
+
},
|
|
46
|
+
agentByChannel: (id: string) => AGENTS.find((a) => a.channelId === id),
|
|
47
|
+
group: (name: string) => (name === GROUP.name ? GROUP : undefined),
|
|
48
|
+
groupByChannel: (id: string) => (id === GROUP.channelId ? GROUP : undefined),
|
|
49
|
+
} as unknown as AgentStore;
|
|
50
|
+
|
|
51
|
+
const coordinator: AgentTurnScope = {
|
|
52
|
+
name: "coordinator",
|
|
53
|
+
session: channelKey("1000"),
|
|
54
|
+
home: channelKey("1000"),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function setup(
|
|
58
|
+
answer: (request: TurnRequest) => TurnResult,
|
|
59
|
+
host = new FakeThreadHost(),
|
|
60
|
+
withThreads = true,
|
|
61
|
+
held?: PendingConfirmation,
|
|
62
|
+
) {
|
|
63
|
+
const posts: { channelId: string; post: AgentPost }[] = [];
|
|
64
|
+
const turns: TurnRequest[] = [];
|
|
65
|
+
/** The replies the judge was asked to read as approvals. */
|
|
66
|
+
const judged: string[] = [];
|
|
67
|
+
const { threads } = fakeThreads(host);
|
|
68
|
+
const queue = new ChannelQueue();
|
|
69
|
+
const team = new TeamTurns({
|
|
70
|
+
owner: OWNER,
|
|
71
|
+
entryChannelId: "1000",
|
|
72
|
+
store,
|
|
73
|
+
channels: {
|
|
74
|
+
post: async (channelId, post) => {
|
|
75
|
+
posts.push({ channelId, post });
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
studio: { url: () => "https://x/a.png" },
|
|
79
|
+
runtime: () => ({
|
|
80
|
+
runTurn: async (request) => {
|
|
81
|
+
turns.push(request);
|
|
82
|
+
return answer(request);
|
|
83
|
+
},
|
|
84
|
+
heldActions: async () => held,
|
|
85
|
+
startFresh: async () => undefined,
|
|
86
|
+
contextUsage: () => undefined,
|
|
87
|
+
}),
|
|
88
|
+
selection: () => ({ id: "agent", tools: [], groups: [] }),
|
|
89
|
+
confirmations: {
|
|
90
|
+
approves: async (_pending, reply) => {
|
|
91
|
+
judged.push(reply);
|
|
92
|
+
return true;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
scorer: { score: async () => undefined },
|
|
96
|
+
queue,
|
|
97
|
+
startTyping: () => () => undefined,
|
|
98
|
+
showStop: () => () => undefined,
|
|
99
|
+
logger: silentLogger(),
|
|
100
|
+
changed: () => undefined,
|
|
101
|
+
...(withThreads ? { threads } : {}),
|
|
102
|
+
});
|
|
103
|
+
/** Posts in a channel, or in one of its threads, as `name: text`. */
|
|
104
|
+
const texts = (channelId: string, threadId?: string) =>
|
|
105
|
+
posts
|
|
106
|
+
.filter((p) => p.channelId === channelId && p.post.threadId === threadId)
|
|
107
|
+
.map((p) => `${p.post.name}: ${p.post.chunks.join("")}`);
|
|
108
|
+
return { team, host, posts, turns, texts, judged };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Runs a coordinator turn, whose model sends the message, and waits for the chain to settle. */
|
|
112
|
+
async function run(team: TeamTurns): Promise<void> {
|
|
113
|
+
await team.answerBackground(channelKey("1000"), OWNER_SPEAKER, "go");
|
|
114
|
+
for (let i = 0; i < 30; i++) await Bun.sleep(2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe("message_agent threads", () => {
|
|
118
|
+
test("the exchange goes in a thread of the sender's channel, archived before the sender's follow-up", async () => {
|
|
119
|
+
let archivedAtFollowUp: string[] | undefined;
|
|
120
|
+
const host = new FakeThreadHost();
|
|
121
|
+
const { team, texts, turns } = setup((request) => {
|
|
122
|
+
if (request.agent?.name === "coordinator" && turns.length === 1)
|
|
123
|
+
team.message(coordinator, "infra", "check the disk");
|
|
124
|
+
if (request.agent?.name === "coordinator" && turns.length > 1)
|
|
125
|
+
archivedAtFollowUp = [...host.closed];
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
text: request.agent?.name === "infra" ? "disk 80%" : "ok",
|
|
129
|
+
};
|
|
130
|
+
}, host);
|
|
131
|
+
await run(team);
|
|
132
|
+
expect(host.opened).toEqual([
|
|
133
|
+
{ parentId: "1000", name: "→ Infrastructure", id: "900" },
|
|
134
|
+
]);
|
|
135
|
+
expect(host.lines.get("1000")).toBe(
|
|
136
|
+
messages().threadStarted("→ Infrastructure", "<#900>"),
|
|
137
|
+
);
|
|
138
|
+
expect(texts("1000", "900")).toEqual([
|
|
139
|
+
`Coordinator: ${messages().messageDelivered("Infrastructure", "check the disk")}`,
|
|
140
|
+
`Infrastructure: ${messages().answerReturned("disk 80%")}`,
|
|
141
|
+
]);
|
|
142
|
+
// The target's own channel is unchanged; the sender's channel holds only its own replies.
|
|
143
|
+
expect(texts("2000")).toEqual([
|
|
144
|
+
`Coordinator: ${messages().messageDelivered("Infrastructure", "check the disk")}`,
|
|
145
|
+
"Infrastructure: disk 80%",
|
|
146
|
+
]);
|
|
147
|
+
expect(texts("1000")).toEqual(["Coordinator: ok", "Coordinator: ok"]);
|
|
148
|
+
expect(archivedAtFollowUp).toEqual(["900"]);
|
|
149
|
+
const followUp = turns.at(-1);
|
|
150
|
+
expect(followUp?.channel).toBe(channelKey("1000"));
|
|
151
|
+
expect(followUp?.text).toContain("in the thread <#900>");
|
|
152
|
+
expect(followUp?.text).toContain("disk 80%"); // Only the answer's follow-up is a report turn, which may ask the owner on cards.
|
|
153
|
+
expect(turns.map((t) => [t.agent?.name, t.interactive])).toEqual([
|
|
154
|
+
["coordinator", undefined],
|
|
155
|
+
["infra", undefined],
|
|
156
|
+
["coordinator", true],
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a report turn in an agent's channel may ask on cards; a schedule's may not", async () => {
|
|
161
|
+
const { team, turns } = setup(() => ({ ok: true, text: "ok" }));
|
|
162
|
+
await team.answerBackground(channelKey("1000"), OWNER_SPEAKER, "scheduled");
|
|
163
|
+
await team.answerBackground(
|
|
164
|
+
channelKey("1000"),
|
|
165
|
+
OWNER_SPEAKER,
|
|
166
|
+
"report",
|
|
167
|
+
true,
|
|
168
|
+
);
|
|
169
|
+
expect(turns.map((t) => t.interactive)).toEqual([undefined, true]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a failed or stopped answer is reported in the thread, which is archived", async () => {
|
|
173
|
+
const { team, host, turns } = setup((request) => {
|
|
174
|
+
if (request.agent?.name === "coordinator" && turns.length === 1)
|
|
175
|
+
team.message(coordinator, "infra", "x");
|
|
176
|
+
return request.agent?.name === "infra"
|
|
177
|
+
? {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: new AgentRunError("stopped by the owner"),
|
|
180
|
+
stopped: true,
|
|
181
|
+
}
|
|
182
|
+
: { ok: true, text: "ok" };
|
|
183
|
+
});
|
|
184
|
+
await run(team);
|
|
185
|
+
expect(host.textsIn("900")).toEqual([
|
|
186
|
+
messages().agentNoReply("stopped by the owner"),
|
|
187
|
+
]);
|
|
188
|
+
expect(host.closed).toEqual(["900"]);
|
|
189
|
+
expect(turns.at(-1)?.text).toContain("could not answer");
|
|
190
|
+
expect(turns.at(-1)?.text).toContain("<#900>");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("without a thread the answer comes back in the sender's channel as before", async () => {
|
|
194
|
+
const host = new FakeThreadHost();
|
|
195
|
+
host.failOpen = true;
|
|
196
|
+
const { team, texts, turns } = setup((request) => {
|
|
197
|
+
if (request.agent?.name === "coordinator" && turns.length === 1)
|
|
198
|
+
team.message(coordinator, "infra", "x");
|
|
199
|
+
return { ok: true, text: `${request.agent?.name} ok` };
|
|
200
|
+
}, host);
|
|
201
|
+
await run(team);
|
|
202
|
+
expect(texts("1000")).toContain(
|
|
203
|
+
`Infrastructure: ${messages().answerReturned("infra ok")}`,
|
|
204
|
+
);
|
|
205
|
+
expect(turns.at(-1)?.text).not.toContain("thread");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a group round's dispatches open their threads in the group channel", () => {
|
|
209
|
+
const { team } = setup(() => ({ ok: true, text: "" }), undefined, false);
|
|
210
|
+
expect(team.turnChannel({ ...coordinator, group: "ops" })).toBe(
|
|
211
|
+
channelKey("3000"),
|
|
212
|
+
);
|
|
213
|
+
expect(team.turnChannel(coordinator)).toBe(channelKey("1000"));
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("speakers", () => {
|
|
218
|
+
test("a message chain keeps the speaker who started it through every hop", async () => {
|
|
219
|
+
const admin = { id: "2", name: "Ada", tier: "admin" } as const;
|
|
220
|
+
const { team, turns } = setup((request) => {
|
|
221
|
+
if (request.agent?.name === "coordinator" && turns.length === 1)
|
|
222
|
+
team.message(coordinator, "infra", "check the disk");
|
|
223
|
+
return { ok: true, text: "ok" };
|
|
224
|
+
});
|
|
225
|
+
await team.answerBackground(channelKey("1000"), admin, "go");
|
|
226
|
+
for (let i = 0; i < 30; i++) await Bun.sleep(2);
|
|
227
|
+
expect(turns.map((t) => [t.agent?.name, t.speaker])).toEqual([
|
|
228
|
+
["coordinator", admin],
|
|
229
|
+
["infra", admin],
|
|
230
|
+
["coordinator", admin],
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("approvals", () => {
|
|
236
|
+
const shell: PendingConfirmation = {
|
|
237
|
+
profile: "general",
|
|
238
|
+
heldAt: new Date(),
|
|
239
|
+
calls: [{ tool: "bash", input: "{}", action: "run a command" }],
|
|
240
|
+
};
|
|
241
|
+
const answer = () => ({ ok: true, text: "ok" }) as const;
|
|
242
|
+
const reply = (
|
|
243
|
+
team: TeamTurns,
|
|
244
|
+
tier: "owner" | "admin" | "member",
|
|
245
|
+
): Promise<unknown> =>
|
|
246
|
+
team.answerOwner(
|
|
247
|
+
channelKey("1000"),
|
|
248
|
+
{ id: "2", name: "Ada", tier },
|
|
249
|
+
"yes, run it",
|
|
250
|
+
"yes, run it",
|
|
251
|
+
NO_ATTACHMENTS,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
test("a speaker whose tier holds the held tool approves it", async () => {
|
|
255
|
+
const { team, turns, judged } = setup(answer, undefined, true, shell);
|
|
256
|
+
await reply(team, "owner");
|
|
257
|
+
expect(judged).toEqual(["yes, run it"]);
|
|
258
|
+
expect(turns[0]?.confirmed).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("a lower tier is not asked to approve, so the held action stays held", async () => {
|
|
262
|
+
const { team, turns, judged } = setup(answer, undefined, true, shell);
|
|
263
|
+
await reply(team, "admin");
|
|
264
|
+
await reply(team, "member");
|
|
265
|
+
expect(judged).toEqual([]);
|
|
266
|
+
expect(turns.map((t) => t.confirmed)).toEqual([undefined, undefined]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("an admin approves what an admin may use", async () => {
|
|
270
|
+
const held: PendingConfirmation = {
|
|
271
|
+
...shell,
|
|
272
|
+
calls: [{ tool: "agent_create", input: "{}", action: "create an agent" }],
|
|
273
|
+
};
|
|
274
|
+
const { team, turns } = setup(answer, undefined, true, held);
|
|
275
|
+
await reply(team, "admin");
|
|
276
|
+
expect(turns[0]?.confirmed).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
});
|