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,45 @@
|
|
|
1
|
+
import type { Tier } from "../speakers.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The owner answering inside a running turn: a card in the turn's channel that only he may
|
|
4
|
+
* answer. Unanswered cards expire; a stopped turn cancels its open cards.
|
|
5
|
+
*/
|
|
6
|
+
export type Approval = "approved" | "declined" | "expired" | "cancelled";
|
|
7
|
+
|
|
8
|
+
export interface AskOption {
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OwnerQuestion {
|
|
14
|
+
question: string;
|
|
15
|
+
/** Up to 25; none means a free-text answer. */
|
|
16
|
+
options: readonly AskOption[];
|
|
17
|
+
/** More than one option may be chosen. */
|
|
18
|
+
multi: boolean;
|
|
19
|
+
/** He may write an answer of his own besides the options. */
|
|
20
|
+
allowOther: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OwnerAnswer {
|
|
24
|
+
/** The chosen options' labels, in the order they were offered. */
|
|
25
|
+
choices: string[];
|
|
26
|
+
/** What he wrote himself, if anything. */
|
|
27
|
+
text?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OwnerPrompts {
|
|
31
|
+
/** Asks him to approve an action; `signal` cancels the card when the turn stops. */
|
|
32
|
+
confirm(
|
|
33
|
+
title: string,
|
|
34
|
+
message: string,
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
/** The lowest tier that may approve; the owner only when absent. */
|
|
37
|
+
minTier?: Tier,
|
|
38
|
+
): Promise<Approval>;
|
|
39
|
+
/** Asks him a question; undefined when it expired or was cancelled. */
|
|
40
|
+
ask(
|
|
41
|
+
title: string,
|
|
42
|
+
question: OwnerQuestion,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
): Promise<OwnerAnswer | undefined>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AgentTurnScope, TurnSelection } from "../sessions.ts";
|
|
2
|
+
import type { Speaker } from "../speakers.ts";
|
|
3
|
+
import type { TurnAttachments } from "./attachment.ts";
|
|
4
|
+
import type {
|
|
5
|
+
ChannelKey,
|
|
6
|
+
InboundMessage,
|
|
7
|
+
OutboundReply,
|
|
8
|
+
PendingConfirmation,
|
|
9
|
+
TranscriptEntry,
|
|
10
|
+
TurnResult,
|
|
11
|
+
} from "./conversation.ts";
|
|
12
|
+
|
|
13
|
+
export interface ChatSurface {
|
|
14
|
+
start(onMessage: (message: InboundMessage) => void): Promise<void>;
|
|
15
|
+
sendReply(channel: ChannelKey, reply: OutboundReply): Promise<void>;
|
|
16
|
+
/** Shows the typing indicator until the returned function is called. */
|
|
17
|
+
startTyping(channel: ChannelKey): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Shows the owner a stop button once the turn has run a few seconds, until the returned
|
|
20
|
+
* function is called.
|
|
21
|
+
*/
|
|
22
|
+
showStop(channel: ChannelKey): () => void;
|
|
23
|
+
/** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
|
|
24
|
+
react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
25
|
+
unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { AgentTurnScope };
|
|
29
|
+
|
|
30
|
+
export interface TurnRequest {
|
|
31
|
+
/** Where the turn's message arrived and its attachments are saved. */
|
|
32
|
+
channel: ChannelKey;
|
|
33
|
+
/** The turn's tools; the owner's conversations pick a profile's, an agent's are the agent union. */
|
|
34
|
+
selection: TurnSelection;
|
|
35
|
+
text: string;
|
|
36
|
+
/** Files and images the message carries; none when absent. */
|
|
37
|
+
attachments?: TurnAttachments;
|
|
38
|
+
/** The owner confirmed the channel's pending actions, so they may run in this turn. */
|
|
39
|
+
confirmed?: boolean;
|
|
40
|
+
/** Set for a turn of an agent-server agent. */
|
|
41
|
+
agent?: AgentTurnScope;
|
|
42
|
+
/** The person the turn is for: who wrote the message, or who set up the work. */
|
|
43
|
+
speaker?: Speaker;
|
|
44
|
+
/** The owner's own chat turn, which his next messages may steer. */
|
|
45
|
+
steerable?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The owner started the turn by writing in its channel, or it delivers a dispatch's report,
|
|
48
|
+
* so it may ask him there on cards: approvals of held actions and ask_user questions. A
|
|
49
|
+
* schedule's turn is not.
|
|
50
|
+
*/
|
|
51
|
+
interactive?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AgentRuntime {
|
|
55
|
+
runTurn(request: TurnRequest): Promise<TurnResult>;
|
|
56
|
+
/** Held actions of a conversation: a channel, or an agent's session key. */
|
|
57
|
+
pendingConfirmation(channel: ChannelKey): PendingConfirmation | undefined;
|
|
58
|
+
recentTranscript(
|
|
59
|
+
channel: ChannelKey,
|
|
60
|
+
limit: number,
|
|
61
|
+
): Promise<TranscriptEntry[]>;
|
|
62
|
+
/** Archives a conversation (a channel, or an agent's session key) and drops its held actions; call between turns. */
|
|
63
|
+
startFresh(channel: ChannelKey): Promise<void>;
|
|
64
|
+
/** Like `startFresh`, but removes the conversation and every archive of it for good; call between turns. */
|
|
65
|
+
deleteConversation(channel: ChannelKey): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Adds the message to the conversation's running turn when that turn is steerable and holds
|
|
68
|
+
* no actions; false when the message must wait for its own turn.
|
|
69
|
+
*/
|
|
70
|
+
steer(
|
|
71
|
+
channel: ChannelKey,
|
|
72
|
+
text: string,
|
|
73
|
+
attachments: TurnAttachments,
|
|
74
|
+
/** The message's author; a turn another speaker started takes no steering from them. */
|
|
75
|
+
speakerId?: string,
|
|
76
|
+
): Promise<boolean>;
|
|
77
|
+
/** Aborts the conversation's running turn and drops what was steered into it; false when none runs. */
|
|
78
|
+
stop(channel: ChannelKey): boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The judge's question and answer shapes. */
|
|
82
|
+
export type {
|
|
83
|
+
ChoiceAnswer,
|
|
84
|
+
ChoiceQuestion,
|
|
85
|
+
ScoreQuestion,
|
|
86
|
+
YesNoQuestion,
|
|
87
|
+
} from "../contract/providers.ts";
|
|
88
|
+
|
|
89
|
+
export interface OwnerNotifier {
|
|
90
|
+
notifyOwner(text: string): Promise<void>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The built-in coordinator profiles; each owner connector adds one more at runtime. */
|
|
2
|
+
export const STATIC_PROFILE_IDS = [
|
|
3
|
+
"general",
|
|
4
|
+
"research",
|
|
5
|
+
"workspace",
|
|
6
|
+
"discord",
|
|
7
|
+
"coding",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type StaticProfileId = (typeof STATIC_PROFILE_IDS)[number];
|
|
11
|
+
|
|
12
|
+
/** A built-in profile id, or `connector:<name>` for an owner connector. */
|
|
13
|
+
export type ProfileId = string;
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_PROFILE: StaticProfileId = "general";
|
|
16
|
+
|
|
17
|
+
/** A coordinator profile: the same persona with a different set of active tools. */
|
|
18
|
+
export interface Profile {
|
|
19
|
+
id: ProfileId;
|
|
20
|
+
/** What the judge reads to decide whether a message belongs to this profile. */
|
|
21
|
+
criterion: string;
|
|
22
|
+
/** Exact names of the assistant's own and Pi package tools active while this profile handles a turn. */
|
|
23
|
+
tools: readonly string[];
|
|
24
|
+
/** MCP gateway virtual servers whose every tool is also active. */
|
|
25
|
+
mcpServers: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The profiles a turn can be routed to right now. */
|
|
29
|
+
export interface ProfileCatalog {
|
|
30
|
+
list(): Profile[];
|
|
31
|
+
get(id: ProfileId): Profile | undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { waitUntilIdle } from "./drain.ts";
|
|
3
|
+
import { ChannelQueue } from "./routing/channel-queue.ts";
|
|
4
|
+
|
|
5
|
+
describe("waitUntilIdle", () => {
|
|
6
|
+
test("returns at once when nothing runs", async () => {
|
|
7
|
+
expect(await waitUntilIdle({ busy: () => [] })).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("waits for queued work, including work that arrives while waiting", async () => {
|
|
11
|
+
const queue = new ChannelQueue();
|
|
12
|
+
const done: string[] = [];
|
|
13
|
+
let release = () => {};
|
|
14
|
+
void queue.run("discord:1", async () => {
|
|
15
|
+
await new Promise<void>((r) => {
|
|
16
|
+
release = r;
|
|
17
|
+
});
|
|
18
|
+
done.push("first");
|
|
19
|
+
});
|
|
20
|
+
const waiting = waitUntilIdle({ busy: () => queue.busy(), intervalMs: 5 });
|
|
21
|
+
await Bun.sleep(20);
|
|
22
|
+
void queue.run("discord:2", async () => {
|
|
23
|
+
await Bun.sleep(30);
|
|
24
|
+
done.push("second");
|
|
25
|
+
});
|
|
26
|
+
release();
|
|
27
|
+
expect(await waiting).toEqual([]);
|
|
28
|
+
expect(done).toEqual(["first", "second"]);
|
|
29
|
+
expect(queue.busy()).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("gives up at the limit and says what is still busy", async () => {
|
|
33
|
+
let clock = 0;
|
|
34
|
+
const left = await waitUntilIdle({
|
|
35
|
+
busy: () => ["discord:1", "discord:1"],
|
|
36
|
+
limitMs: 3_000,
|
|
37
|
+
intervalMs: 1_000,
|
|
38
|
+
now: () => clock,
|
|
39
|
+
sleep: async (ms) => {
|
|
40
|
+
clock += ms;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(left).toEqual(["discord:1", "discord:1"]);
|
|
44
|
+
expect(clock).toBe(3_000);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** How long a shutdown waits for running work before aborting it (self-maintenance spec behavior 11). */
|
|
2
|
+
export const DRAIN_LIMIT_MS = 60 * 60_000;
|
|
3
|
+
|
|
4
|
+
export interface DrainOptions {
|
|
5
|
+
/** What still runs or waits, one entry per piece of work; empty when idle. */
|
|
6
|
+
busy: () => string[];
|
|
7
|
+
limitMs?: number;
|
|
8
|
+
intervalMs?: number;
|
|
9
|
+
sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Waits until nothing runs or waits, checking every interval, for at most the limit. Work that
|
|
15
|
+
* arrives meanwhile is served as usual and waited for too. Returns what was still busy when the
|
|
16
|
+
* limit ran out, or an empty list once idle.
|
|
17
|
+
*/
|
|
18
|
+
export async function waitUntilIdle(options: DrainOptions): Promise<string[]> {
|
|
19
|
+
const {
|
|
20
|
+
busy,
|
|
21
|
+
limitMs = DRAIN_LIMIT_MS,
|
|
22
|
+
intervalMs = 1_000,
|
|
23
|
+
sleep = (ms) => Bun.sleep(ms),
|
|
24
|
+
now = Date.now,
|
|
25
|
+
} = options;
|
|
26
|
+
const deadline = now() + limitMs;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const left = busy();
|
|
29
|
+
if (left.length === 0) return [];
|
|
30
|
+
if (now() >= deadline) return left;
|
|
31
|
+
await sleep(intervalMs);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A plugin that cannot be registered: it adds nothing, or clashes with another. */
|
|
2
|
+
export class PluginError extends Error {
|
|
3
|
+
override name = "PluginError";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** A core port read during setup, before the host has linked every plugin's contributions. */
|
|
7
|
+
export class NotLinkedError extends Error {
|
|
8
|
+
override name = "NotLinkedError";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A plugin's migration failed, so the host stops before any plugin is set up. */
|
|
12
|
+
export class MigrationError extends Error {
|
|
13
|
+
override name = "MigrationError";
|
|
14
|
+
readonly migration: string;
|
|
15
|
+
constructor(migration: string, cause: unknown) {
|
|
16
|
+
super(`migration ${migration} failed: ${String(cause)}`, { cause });
|
|
17
|
+
this.migration = migration;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A judge could not answer: its model failed or said something other than the asked JSON. */
|
|
22
|
+
export class JudgeError extends Error {
|
|
23
|
+
override name = "JudgeError";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A provider slot nobody filled, read by a part that needs it. */
|
|
27
|
+
export class ProviderError extends Error {
|
|
28
|
+
override name = "ProviderError";
|
|
29
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { EventBus } from "./events.ts";
|
|
3
|
+
import { silentLogger } from "./log.ts";
|
|
4
|
+
import type { EventHandlers } from "./plugin.ts";
|
|
5
|
+
|
|
6
|
+
const turn = {
|
|
7
|
+
agent: "infra",
|
|
8
|
+
channel: "discord:1" as const,
|
|
9
|
+
speaker: undefined,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("EventBus", () => {
|
|
13
|
+
test("delivers to every plugin's handler in registration order", async () => {
|
|
14
|
+
const log: string[] = [];
|
|
15
|
+
const bus = new EventBus(silentLogger());
|
|
16
|
+
const handlers = (name: string): EventHandlers => ({
|
|
17
|
+
turnStarted: (event) => {
|
|
18
|
+
log.push(`${name} ${event.agent}`);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
bus.link([
|
|
22
|
+
{ plugin: "a", events: handlers("a") },
|
|
23
|
+
{ plugin: "b", events: handlers("b") },
|
|
24
|
+
]);
|
|
25
|
+
await bus.deliver("turnStarted", turn);
|
|
26
|
+
expect(log).toEqual(["a infra", "b infra"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a handler that throws is logged with its plugin and never stops the others", async () => {
|
|
30
|
+
const logged: { plugin: string; event: string }[] = [];
|
|
31
|
+
const bus = new EventBus({
|
|
32
|
+
...silentLogger(),
|
|
33
|
+
error: (fields: unknown) => {
|
|
34
|
+
logged.push(fields as { plugin: string; event: string });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const reached: string[] = [];
|
|
38
|
+
bus.link([
|
|
39
|
+
{
|
|
40
|
+
plugin: "bad",
|
|
41
|
+
events: {
|
|
42
|
+
changed: () => {
|
|
43
|
+
throw new Error("boom");
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
plugin: "good",
|
|
49
|
+
events: {
|
|
50
|
+
changed: () => {
|
|
51
|
+
reached.push("good");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
await bus.deliver("changed");
|
|
57
|
+
expect(reached).toEqual(["good"]);
|
|
58
|
+
expect(logged).toMatchObject([{ plugin: "bad", event: "changed" }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("reports to the sink before the handlers are linked are dropped", async () => {
|
|
62
|
+
const log: string[] = [];
|
|
63
|
+
const bus = new EventBus(silentLogger());
|
|
64
|
+
bus.sink.changed();
|
|
65
|
+
bus.link([
|
|
66
|
+
{
|
|
67
|
+
plugin: "a",
|
|
68
|
+
events: {
|
|
69
|
+
changed: () => {
|
|
70
|
+
log.push("changed");
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
await Bun.sleep(1);
|
|
76
|
+
expect(log).toEqual([]);
|
|
77
|
+
bus.sink.changed();
|
|
78
|
+
await Bun.sleep(1);
|
|
79
|
+
expect(log).toEqual(["changed"]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Logger } from "./log.ts";
|
|
2
|
+
import type { EventHandlers, EventSink } from "./plugin.ts";
|
|
3
|
+
|
|
4
|
+
/** One registered plugin's handlers. */
|
|
5
|
+
interface Registered {
|
|
6
|
+
plugin: string;
|
|
7
|
+
events: EventHandlers;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Delivers the core's events to the plugins' handlers, in registration order. A handler runs
|
|
12
|
+
* apart from the code that reported the event, and one that throws is logged with its plugin's
|
|
13
|
+
* name and never stops the others. Nothing is delivered until the host has linked the handlers.
|
|
14
|
+
*/
|
|
15
|
+
export class EventBus {
|
|
16
|
+
readonly #logger: Logger;
|
|
17
|
+
#handlers: readonly Registered[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(logger: Logger) {
|
|
20
|
+
this.#logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Starts delivering to these handlers. */
|
|
24
|
+
link(handlers: readonly Registered[]): void {
|
|
25
|
+
this.#handlers = handlers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Delivers one event and waits for every handler; used where the caller must not go on before. */
|
|
29
|
+
async deliver<Name extends keyof EventHandlers>(
|
|
30
|
+
name: Name,
|
|
31
|
+
...args: Parameters<NonNullable<EventHandlers[Name]>>
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
for (const { plugin, events } of this.#handlers) {
|
|
34
|
+
const handler = events[name] as
|
|
35
|
+
| ((...values: unknown[]) => Promise<void> | void)
|
|
36
|
+
| undefined;
|
|
37
|
+
if (!handler) continue;
|
|
38
|
+
try {
|
|
39
|
+
await handler.apply(events, args);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.#logger.error(
|
|
42
|
+
{ plugin, event: name, err: error },
|
|
43
|
+
"event handler failed",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What the core reports to; each call returns at once. */
|
|
50
|
+
readonly sink: EventSink = {
|
|
51
|
+
turnStarted: (turn) => void this.deliver("turnStarted", turn),
|
|
52
|
+
turnEnded: (turn) => void this.deliver("turnEnded", turn),
|
|
53
|
+
changed: () => void this.deliver("changed"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { PluginError } from "./errors.ts";
|
|
3
|
+
import { type HoldRule, holdChain } from "./holds.ts";
|
|
4
|
+
|
|
5
|
+
/** A rule that holds every call to `tool`, saying `said`. */
|
|
6
|
+
const rule = (name: string, tool: string, said: string): HoldRule => ({
|
|
7
|
+
name,
|
|
8
|
+
describe: (called) => (called === tool ? said : undefined),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("holdChain", () => {
|
|
12
|
+
test("lets a call run when no rule holds it", () => {
|
|
13
|
+
const holds = holdChain([rule("a", "x", "do x")]);
|
|
14
|
+
expect(holds("y", {}, {})).toBeUndefined();
|
|
15
|
+
expect(holdChain([])("x", {}, {})).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("the first rule that describes a call holds it", () => {
|
|
19
|
+
const holds = holdChain([
|
|
20
|
+
rule("first", "x", "first says x"),
|
|
21
|
+
rule("second", "x", "second says x"),
|
|
22
|
+
rule("third", "y", "third says y"),
|
|
23
|
+
]);
|
|
24
|
+
expect(holds("x", {}, {})).toBe("first says x");
|
|
25
|
+
expect(holds("y", {}, {})).toBe("third says y");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("hands each rule the call's input and the session's workspace", () => {
|
|
29
|
+
const seen: unknown[] = [];
|
|
30
|
+
const holds = holdChain([
|
|
31
|
+
{
|
|
32
|
+
name: "spy",
|
|
33
|
+
describe: (tool, input, context) => {
|
|
34
|
+
seen.push({ tool, input, context });
|
|
35
|
+
return undefined;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
holds("bash", { command: "ls" }, { workspace: "/work" });
|
|
40
|
+
expect(seen).toEqual([
|
|
41
|
+
{
|
|
42
|
+
tool: "bash",
|
|
43
|
+
input: { command: "ls" },
|
|
44
|
+
context: { workspace: "/work" },
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("refuses two rules with one name", () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
holdChain([rule("same", "x", "a"), rule("same", "y", "b")]),
|
|
52
|
+
).toThrow(PluginError);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { PluginError } from "./errors.ts";
|
|
2
|
+
|
|
3
|
+
/** What a hold rule knows about the session making the call. */
|
|
4
|
+
export interface HoldContext {
|
|
5
|
+
/** The shared workspace of a session with a shell; calls reaching outside it may be held. */
|
|
6
|
+
workspace?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decides whether a tool call waits for the owner's approval. A rule answers with a short
|
|
11
|
+
* description of what the call would do, or undefined when it has no opinion on the call.
|
|
12
|
+
*/
|
|
13
|
+
export interface HoldRule {
|
|
14
|
+
name: string;
|
|
15
|
+
describe(
|
|
16
|
+
tool: string,
|
|
17
|
+
input: Record<string, unknown>,
|
|
18
|
+
context: HoldContext,
|
|
19
|
+
): string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The description of a call that must be approved first, or undefined to let it run. */
|
|
23
|
+
export type HoldCheck = (
|
|
24
|
+
tool: string,
|
|
25
|
+
input: Record<string, unknown>,
|
|
26
|
+
context: HoldContext,
|
|
27
|
+
) => string | undefined;
|
|
28
|
+
|
|
29
|
+
/** Asks the rules in order; the first description holds the call. */
|
|
30
|
+
export function holdChain(rules: readonly HoldRule[]): HoldCheck {
|
|
31
|
+
const names = new Set<string>();
|
|
32
|
+
for (const rule of rules) {
|
|
33
|
+
if (names.has(rule.name))
|
|
34
|
+
throw new PluginError(
|
|
35
|
+
`hold rule ${rule.name} is registered twice. Rename one of the two.`,
|
|
36
|
+
);
|
|
37
|
+
names.add(rule.name);
|
|
38
|
+
}
|
|
39
|
+
return (tool, input, context) => {
|
|
40
|
+
for (const rule of rules) {
|
|
41
|
+
const description = rule.describe(tool, input, context);
|
|
42
|
+
if (description !== undefined) return description;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|