pi-roundtable 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/README.md +6 -4
- package/docs/plugins.md +1448 -66
- package/examples/echo-runtime.test.ts +122 -0
- package/examples/echo-runtime.ts +107 -0
- package/examples/events.test.ts +1 -0
- package/examples/events.ts +2 -2
- package/examples/fake-surface.test.ts +180 -0
- package/examples/fake-surface.ts +109 -0
- package/examples/interactions.test.ts +12 -3
- package/examples/interactions.ts +19 -19
- package/examples/shared-services.test.ts +74 -0
- package/examples/shared-services.ts +60 -0
- package/examples/study-room.test.ts +69 -0
- package/examples/study-room.ts +52 -0
- package/examples/support-desk.test.ts +34 -0
- package/examples/support-desk.ts +40 -0
- package/package.json +9 -2
- package/src/cli/checks/database.ts +11 -30
- package/src/cli/checks/images.ts +19 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/main.ts +2 -1
- package/src/core/agents/agent-claim.ts +42 -16
- package/src/core/agents/agent-messages.ts +4 -4
- package/src/core/agents/agent-ports.ts +8 -21
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-store.ts +6 -4
- package/src/core/agents/agent-team-fixture.ts +389 -0
- package/src/core/agents/agent-team.ts +26 -25
- package/src/core/agents/agent-tools.ts +34 -15
- package/src/core/agents/avatar-studio.ts +40 -6
- package/src/core/agents/fallback-avatar.ts +85 -0
- package/src/core/agents/team-editing.ts +49 -10
- package/src/core/agents/team-keys.ts +22 -9
- package/src/core/agents/team-layout.ts +4 -3
- package/src/core/agents/team-lifecycle.ts +28 -2
- package/src/core/agents/team-options.ts +9 -5
- package/src/core/agents/team-status.ts +4 -4
- package/src/core/agents/team-text.ts +8 -6
- package/src/core/agents/team-turn-types.ts +4 -4
- package/src/core/agents/team-turns.ts +6 -5
- package/src/core/builtin/agent-server.ts +187 -121
- package/src/core/builtin/discord-admin.ts +41 -0
- package/src/core/builtin/discord.ts +65 -40
- package/src/core/builtin/modules.ts +67 -58
- package/src/core/builtin/session-tool.ts +24 -0
- package/src/core/builtin/skills.ts +72 -0
- package/src/core/builtin/stores.ts +55 -31
- package/src/core/config/config.ts +45 -4
- package/src/core/config/schema.ts +6 -0
- package/src/core/contract/channels.ts +54 -14
- package/src/core/contract/providers.ts +17 -0
- package/src/core/contract/runtime.ts +138 -0
- package/src/core/contract/services.ts +45 -0
- package/src/core/contract/surface.ts +88 -0
- package/src/core/db/migrations.ts +118 -3
- package/src/core/define-roundtable.ts +70 -46
- package/src/core/define.ts +2 -1
- package/src/core/discord/agent-commands.ts +39 -14
- package/src/core/discord/agent-panel.ts +66 -0
- package/src/core/discord/channel-executor.ts +6 -3
- package/src/core/discord/channel-operations.ts +14 -11
- package/src/core/discord/command-collection.ts +46 -0
- package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
- package/src/core/discord/connection.ts +35 -0
- package/src/core/discord/discord-surface.ts +32 -94
- package/src/core/discord/dispatch-threads.ts +2 -2
- package/src/core/discord/inbound-message.ts +99 -0
- package/src/core/discord/interaction-module.ts +57 -1
- package/src/core/discord/owner-cards.ts +4 -4
- package/src/core/discord/owner-command.ts +44 -11
- package/src/core/discord/owner-discord.ts +2 -2
- package/src/core/discord/schedule-commands.ts +21 -18
- package/src/core/discord/stop-button.ts +44 -1
- package/src/core/domain/attachment.ts +12 -8
- package/src/core/domain/conversation.ts +3 -4
- package/src/core/domain/owner-prompts.ts +5 -5
- package/src/core/domain/ports.ts +9 -53
- package/src/core/freeze.ts +9 -0
- package/src/core/host.ts +255 -82
- package/src/core/http/listeners.ts +55 -20
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/index.ts +12 -2
- package/src/core/i18n/owner.ts +7 -1
- package/src/core/i18n/schedules.ts +12 -6
- package/src/core/identity.ts +1 -1
- package/src/core/judging/effort-judge.ts +25 -3
- package/src/core/log.ts +21 -3
- package/src/core/models.ts +2 -2
- package/src/core/modules/background/background-turns.ts +6 -4
- package/src/core/modules/delegation/delegate.ts +3 -2
- package/src/core/modules/delegation/delegator.ts +15 -13
- package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
- package/src/core/modules/discord-admin/discord-admin.ts +6 -6
- package/src/core/modules/host-shell/shell-policy.ts +8 -3
- package/src/core/modules/memory/owner-memory-store.ts +27 -25
- package/src/core/modules/memory/owner-memory.ts +9 -13
- package/src/core/modules/schedules/recurrence.ts +5 -3
- package/src/core/modules/schedules/schedule-store.ts +11 -11
- package/src/core/modules/schedules/schedule-tools.ts +22 -20
- package/src/core/modules/schedules/scheduler.ts +7 -2
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +2 -15
- package/src/core/modules/skills/skill-tools.ts +10 -4
- package/src/core/ops/error-reporter.ts +1 -1
- package/src/core/plugin.ts +232 -28
- package/src/core/registry/contributions.ts +173 -14
- package/src/core/registry/providers.ts +35 -5
- package/src/core/registry/services.ts +229 -0
- package/src/core/routing/channel-queue.ts +5 -0
- package/src/core/routing/channel-router.ts +21 -7
- package/src/core/routing/conversation-turns.ts +139 -0
- package/src/core/routing/message-text.ts +9 -2
- package/src/core/routing/surface-port.ts +39 -0
- package/src/core/runtime/conversation-sessions.ts +4 -3
- package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
- package/src/core/runtime/mcp.ts +1 -1
- package/src/core/runtime/pending-confirmation-store.ts +10 -12
- package/src/core/runtime/pi-agent-runtime.ts +9 -6
- package/src/core/runtime/prompt-slot.ts +8 -3
- package/src/core/runtime/runtime-types.ts +9 -35
- package/src/core/runtime/session-factory.ts +21 -5
- package/src/core/runtime/text-tools.ts +1 -3
- package/src/core/services.ts +238 -90
- package/src/core/sessions.ts +3 -2
- package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
- package/src/core/shared/delegate-tool.ts +4 -3
- package/src/core/shared/schedule-tools.ts +22 -13
- package/src/core/shared/session-messages.ts +1 -1
- package/src/core/speakers.ts +6 -38
- package/src/core/testing/database.ts +1 -7
- package/src/core/testing/eager-catalog.ts +30 -0
- package/src/core/testing/locale.ts +23 -5
- package/src/core/testing/modules.ts +105 -41
- package/src/core/testing/test-host.ts +279 -0
- package/src/core/testing/tool-set.ts +64 -0
- package/src/core/time.ts +6 -1
- package/src/core/tool-set.snapshot.json +288 -0
- package/src/core/tool-tiers.ts +17 -57
- package/src/discord/index.ts +64 -0
- package/src/index.ts +181 -10
- package/src/kit/channels.ts +14 -0
- package/src/kit/domain.ts +18 -0
- package/src/kit/holds.ts +4 -0
- package/src/kit/index.ts +129 -0
- package/src/kit/judging.ts +10 -0
- package/src/kit/memory.ts +3 -0
- package/src/kit/mirror.ts +19 -0
- package/src/kit/presentation.ts +7 -0
- package/src/kit/shell.ts +7 -0
- package/src/kit/skills.ts +8 -0
- package/src/kit/support.ts +21 -0
- package/src/kit/threads.ts +7 -0
- package/src/kit/tools.ts +14 -0
- package/src/kit/worker.ts +16 -0
- package/src/testing.ts +394 -34
- package/examples/guide.test.ts +0 -52
- package/src/cli/add-plugin.test.ts +0 -107
- package/src/cli/checks/basic.test.ts +0 -276
- package/src/cli/checks/database.test.ts +0 -117
- package/src/cli/checks/discord.test.ts +0 -200
- package/src/cli/cli.test.ts +0 -122
- package/src/cli/config-edit.test.ts +0 -130
- package/src/cli/doctor.test.ts +0 -184
- package/src/cli/init.test.ts +0 -127
- package/src/cli/size.test.ts +0 -9
- package/src/cli/templates.test.ts +0 -129
- package/src/cli/testing/fixtures.ts +0 -106
- package/src/core/agents/agent-claim.test.ts +0 -142
- package/src/core/agents/agent-dashboard.test.ts +0 -120
- package/src/core/agents/agent-guild.test.ts +0 -124
- package/src/core/agents/agent-store.test.ts +0 -205
- package/src/core/agents/avatar-studio.test.ts +0 -88
- package/src/core/agents/group-round.test.ts +0 -143
- package/src/core/agents/owner-identity.test.ts +0 -172
- package/src/core/agents/team-turns.test.ts +0 -278
- package/src/core/attachments/attachments.test.ts +0 -85
- package/src/core/boundary.test.ts +0 -45
- package/src/core/builtin/modules.test.ts +0 -127
- package/src/core/config/config.test.ts +0 -135
- package/src/core/contract/discord.ts +0 -33
- package/src/core/db/migrations.test.ts +0 -237
- package/src/core/define-roundtable.test.ts +0 -134
- package/src/core/define.test.ts +0 -144
- package/src/core/discord/agent-commands.test.ts +0 -58
- package/src/core/discord/agent-discord.test.ts +0 -70
- package/src/core/discord/dispatch-thread-host.test.ts +0 -114
- package/src/core/discord/dispatch-threads.test.ts +0 -94
- package/src/core/discord/owner-cards.test.ts +0 -435
- package/src/core/discord/owner-discord.test.ts +0 -325
- package/src/core/domain/expression.ts +0 -21
- package/src/core/domain/profile.ts +0 -32
- package/src/core/drain.test.ts +0 -46
- package/src/core/events.test.ts +0 -81
- package/src/core/holds.test.ts +0 -54
- package/src/core/host.test.ts +0 -536
- package/src/core/http/listeners.test.ts +0 -164
- package/src/core/i18n/i18n.test.ts +0 -139
- package/src/core/identity.test.ts +0 -18
- package/src/core/judging/effort-judge.test.ts +0 -112
- package/src/core/judging/model-judge.test.ts +0 -126
- package/src/core/modules/delegation/delegator.test.ts +0 -170
- package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
- package/src/core/modules/schedules/schedule.test.ts +0 -358
- package/src/core/modules/skills/skill-kind.test.ts +0 -55
- package/src/core/modules/skills/skill-registry.test.ts +0 -376
- package/src/core/ops/error-reporter.test.ts +0 -283
- package/src/core/presentation/card-cadence.ts +0 -41
- package/src/core/presentation/presentation.test.ts +0 -166
- package/src/core/public-entry.test.ts +0 -59
- package/src/core/registry/contributions.test.ts +0 -215
- package/src/core/registry/interactions.test.ts +0 -87
- package/src/core/registry/providers.test.ts +0 -103
- package/src/core/routing/channel-queue.test.ts +0 -31
- package/src/core/routing/channel-router.test.ts +0 -326
- package/src/core/routing/conversation-kind.ts +0 -14
- package/src/core/routing/settle-turn.test.ts +0 -21
- package/src/core/runtime/compaction-tiers.test.ts +0 -227
- package/src/core/runtime/extensions/ask-user.test.ts +0 -114
- package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
- package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
- package/src/core/runtime/session-archive.test.ts +0 -20
- package/src/core/runtime/steerable-run.test.ts +0 -264
- package/src/core/runtime/text-tools.test.ts +0 -110
- package/src/core/runtime/turn-answer.test.ts +0 -72
- package/src/core/runtime/worker-task.test.ts +0 -82
- package/src/core/services.test.ts +0 -28
- package/src/core/sessions.test.ts +0 -85
- package/src/core/shared/unix-server.ts +0 -18
- package/src/core/size.test.ts +0 -9
- package/src/core/speakers.test.ts +0 -78
- package/src/core/testing/file-size.ts +0 -34
- package/src/core/time.test.ts +0 -166
- package/src/core/tool-tiers.test.ts +0 -117
- package/src/entries.test.ts +0 -112
- package/src/testing.test.ts +0 -156
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Locale } from "../i18n/index.ts";
|
|
1
2
|
import type { ChannelKey } from "../sessions.ts";
|
|
2
3
|
import type { Tier } from "../speakers.ts";
|
|
3
4
|
|
|
@@ -19,15 +20,20 @@ export interface InboundMessage {
|
|
|
19
20
|
/** How the author appears in the channel, for example a server nickname. */
|
|
20
21
|
authorName: string;
|
|
21
22
|
authorIsBot: boolean;
|
|
22
|
-
/** The roles the author holds in the message's
|
|
23
|
+
/** The roles the author holds in the message's space; absent in direct messages. */
|
|
23
24
|
authorRoleIds?: readonly string[];
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The server or workspace the channel belongs to, in the surface's own ids; absent in a
|
|
27
|
+
* direct conversation.
|
|
28
|
+
*/
|
|
29
|
+
space?: string;
|
|
28
30
|
isDirect: boolean;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Set when an integration posted the message, such as a Discord webhook; its name is
|
|
33
|
+
* `authorName`. `own` says the integration is one the assistant posts through itself, such as an
|
|
34
|
+
* agent's voice.
|
|
35
|
+
*/
|
|
36
|
+
integration?: { id: string; own: boolean };
|
|
31
37
|
mentionsBot: boolean;
|
|
32
38
|
/** The message replies to one of the bot's messages. */
|
|
33
39
|
repliesToBot: boolean;
|
|
@@ -44,8 +50,9 @@ export interface InboundMessage {
|
|
|
44
50
|
/** The message the author forwarded with this one; its attachments are in `attachments`. */
|
|
45
51
|
forwarded?: {
|
|
46
52
|
text: string;
|
|
47
|
-
/** The
|
|
48
|
-
|
|
53
|
+
/** The channel the message was forwarded from. */
|
|
54
|
+
source: ChannelKey;
|
|
55
|
+
/** A link to the original message. */
|
|
49
56
|
url: string;
|
|
50
57
|
};
|
|
51
58
|
}
|
|
@@ -60,11 +67,30 @@ export type ScheduledOutcome =
|
|
|
60
67
|
| { status: "failed"; error: string }
|
|
61
68
|
| { status: "skipped"; reason: string };
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Whose turn a schedule or a delegated task asks for, contributed by the plugin whose claim
|
|
72
|
+
* answers it. The target also sets the limits of what may be scheduled or delegated for it, so a
|
|
73
|
+
* channel open to many people can be held tighter than the owner's own.
|
|
74
|
+
*/
|
|
75
|
+
export interface BackgroundTarget {
|
|
76
|
+
/** Stored on schedules and delegated jobs; unique across plugins, as a persona's kind is. */
|
|
77
|
+
name: string;
|
|
78
|
+
/** How lists such as `/<root> schedule` name it, in the host's locale. */
|
|
79
|
+
label(locale: Locale): string;
|
|
80
|
+
/** Limits of the schedules made for it; absent, nothing may be scheduled for it. */
|
|
81
|
+
schedules?: { perChannel: number; promptChars: number; aheadDays: number };
|
|
82
|
+
/** Limits of the delegated tasks reporting to it; absent, nothing may be delegated to it. */
|
|
83
|
+
delegation?: { maxRunning: number };
|
|
84
|
+
}
|
|
85
|
+
|
|
63
86
|
/** A turn nobody wrote in the channel: a schedule's, or a report of work done elsewhere. */
|
|
64
87
|
export interface BackgroundTurn {
|
|
65
88
|
channel: ChannelKey;
|
|
66
|
-
/**
|
|
67
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The name of the `BackgroundTarget` the turn is for. The router skips a turn whose target no
|
|
91
|
+
* plugin contributes, and a claim skips one it does not serve.
|
|
92
|
+
*/
|
|
93
|
+
target: string;
|
|
68
94
|
author: { id: string; name: string };
|
|
69
95
|
/**
|
|
70
96
|
* The tier the turn runs at, which is its creator's when a person set it up; absent for
|
|
@@ -73,10 +99,17 @@ export interface BackgroundTurn {
|
|
|
73
99
|
tier?: Tier;
|
|
74
100
|
turnId: string;
|
|
75
101
|
text: string;
|
|
76
|
-
/** It delivers a report the owner is waiting for, so it may ask
|
|
102
|
+
/** It delivers a report the owner is waiting for, so it may ask them on cards. */
|
|
77
103
|
report?: boolean;
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Whose conversation a channel holds, as the claim that owns it names it when `startFresh` says
|
|
108
|
+
* whose it was: any string, such as "owner" or "study". The host reads none of them, so a plugin
|
|
109
|
+
* narrows the kinds of its own claims itself.
|
|
110
|
+
*/
|
|
111
|
+
export type ConversationKind = string;
|
|
112
|
+
|
|
80
113
|
/** What a claim does with a message in a channel it owns. */
|
|
81
114
|
export type Admission =
|
|
82
115
|
| {
|
|
@@ -111,14 +144,19 @@ export type Admission =
|
|
|
111
144
|
export interface ChannelClaim {
|
|
112
145
|
name: string;
|
|
113
146
|
priority: number;
|
|
114
|
-
/** Whether the claim owns the channel; `
|
|
115
|
-
owns(channel: ChannelKey,
|
|
147
|
+
/** Whether the claim owns the channel; `space` is the message's space, when routing one. */
|
|
148
|
+
owns(channel: ChannelKey, space?: string): boolean;
|
|
116
149
|
/** What to do with a message in an owned channel; undefined drops it. */
|
|
117
150
|
admit(message: InboundMessage): Admission | undefined;
|
|
118
151
|
/** A background turn in an owned channel; a claim without one skips them. */
|
|
119
152
|
background?(turn: BackgroundTurn): Promise<ScheduledOutcome>;
|
|
120
153
|
/** Starts the channel's conversation over, saying whose it was. */
|
|
121
154
|
startFresh(channel: ChannelKey): Promise<string>;
|
|
155
|
+
/**
|
|
156
|
+
* Stops the channel's running turn; true when one was running. The router asks only the claim
|
|
157
|
+
* that owns the channel, and a claim without `stop` has nothing to stop there.
|
|
158
|
+
*/
|
|
159
|
+
stop?(channel: ChannelKey): boolean;
|
|
122
160
|
/** Removes the channel's conversation for good; a claim without one refuses. */
|
|
123
161
|
deleteConversation?(channel: ChannelKey): Promise<void>;
|
|
124
162
|
/** Reports of work started in an owned channel stay in it instead of opening a thread. */
|
|
@@ -130,6 +168,8 @@ export interface ConversationPort {
|
|
|
130
168
|
/** Never rejects; resolves when the message's turn is done or dropped. */
|
|
131
169
|
handle(message: InboundMessage): Promise<void>;
|
|
132
170
|
background(turn: BackgroundTurn): Promise<ScheduledOutcome>;
|
|
171
|
+
/** A contributed background target, read when used; undefined when no plugin contributes it. */
|
|
172
|
+
target(name: string): BackgroundTarget | undefined;
|
|
133
173
|
/** Waits for the channel's running turn, then starts its conversation over; says whose it was. */
|
|
134
174
|
startFresh(channel: ChannelKey): Promise<string>;
|
|
135
175
|
/** `busy` while a turn runs or waits in the channel. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RuntimeFactory } from "./runtime.ts";
|
|
2
|
+
|
|
1
3
|
/** A choice among named options; the answer is one option's name. */
|
|
2
4
|
export interface ChoiceQuestion {
|
|
3
5
|
type: "choice";
|
|
@@ -64,4 +66,19 @@ export type ImageDrawer = (
|
|
|
64
66
|
export interface Providers {
|
|
65
67
|
judge: Judge;
|
|
66
68
|
images: ImageDrawer;
|
|
69
|
+
/**
|
|
70
|
+
* Builds the runtime that runs the agent server's conversations and every turn run through
|
|
71
|
+
* `context.turns`, in place of the Pi runtime. The default refuses: the agent server builds the
|
|
72
|
+
* Pi runtime itself when no plugin fills this slot, so check `filled` before calling it.
|
|
73
|
+
*/
|
|
74
|
+
runtime: RuntimeFactory;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Each slot as the host resolved it, and which slots a plugin fills. A slot no plugin fills holds
|
|
79
|
+
* the core's default, and `filled` is how a caller tells that default from a provider without
|
|
80
|
+
* calling it.
|
|
81
|
+
*/
|
|
82
|
+
export interface ResolvedProviders extends Providers {
|
|
83
|
+
readonly filled: ReadonlySet<keyof Providers>;
|
|
67
84
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { TurnAttachments } from "../domain/attachment.ts";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelKey,
|
|
4
|
+
PendingConfirmation,
|
|
5
|
+
TranscriptEntry,
|
|
6
|
+
TurnResult,
|
|
7
|
+
} from "../domain/conversation.ts";
|
|
8
|
+
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
9
|
+
import type { TurnRequest } from "../domain/ports.ts";
|
|
10
|
+
import type { Logger } from "../log.ts";
|
|
11
|
+
import type { ThinkingSetting } from "../models.ts";
|
|
12
|
+
import type { HostEnv, LinkedSessions } from "../plugin.ts";
|
|
13
|
+
import type { AgentTurnScope } from "../sessions.ts";
|
|
14
|
+
import type { Speaker } from "../speakers.ts";
|
|
15
|
+
import type { ToolTiers } from "../tool-tiers.ts";
|
|
16
|
+
import type { Judge } from "./providers.ts";
|
|
17
|
+
|
|
18
|
+
/** A conversation's context use after its latest turn. */
|
|
19
|
+
export interface ContextUse {
|
|
20
|
+
/** Null right after a compaction, until the next model response. */
|
|
21
|
+
tokens: number | null;
|
|
22
|
+
contextWindow: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs the conversations of the agent server, and of every claim that runs turns through
|
|
27
|
+
* `context.turns`: one persistent conversation per channel key (an agent's conversation has the
|
|
28
|
+
* key of its `AgentTurnScope.session`), its turns, its held actions, and its history. A plugin
|
|
29
|
+
* replaces the whole runtime by filling the `runtime` provider slot; the default runs Pi.
|
|
30
|
+
*/
|
|
31
|
+
export interface AgentRuntime {
|
|
32
|
+
/**
|
|
33
|
+
* Runs one turn and returns how it ended. A turn that cannot run is `{ ok: false }`; a throw is
|
|
34
|
+
* settled into a failed result by the caller.
|
|
35
|
+
*/
|
|
36
|
+
runTurn(request: TurnRequest): Promise<TurnResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Adds the message to the conversation's running turn when that turn is steerable and holds no
|
|
39
|
+
* actions; false when the message must wait for its own turn.
|
|
40
|
+
*/
|
|
41
|
+
steer(
|
|
42
|
+
conversation: ChannelKey,
|
|
43
|
+
text: string,
|
|
44
|
+
attachments: TurnAttachments,
|
|
45
|
+
/** The message's author; a turn another speaker started takes no steering from them. */
|
|
46
|
+
speakerId?: string,
|
|
47
|
+
): Promise<boolean>;
|
|
48
|
+
/** Aborts the conversation's running turn and drops what was steered into it; false when none runs. */
|
|
49
|
+
stop(conversation: ChannelKey): boolean;
|
|
50
|
+
/** Archives a conversation and drops its held actions; called between turns. */
|
|
51
|
+
startFresh(conversation: ChannelKey): Promise<void>;
|
|
52
|
+
/** Like `startFresh`, but removes the conversation and every archive of it for good; called between turns. */
|
|
53
|
+
deleteConversation(conversation: ChannelKey): Promise<void>;
|
|
54
|
+
/** The conversation's held actions known in memory since startup. */
|
|
55
|
+
pendingConfirmation(
|
|
56
|
+
conversation: ChannelKey,
|
|
57
|
+
): PendingConfirmation | undefined;
|
|
58
|
+
/** The conversation's held actions, restored from the store after a restart. */
|
|
59
|
+
heldActions(
|
|
60
|
+
conversation: ChannelKey,
|
|
61
|
+
): Promise<PendingConfirmation | undefined>;
|
|
62
|
+
recentTranscript(
|
|
63
|
+
conversation: ChannelKey,
|
|
64
|
+
limit: number,
|
|
65
|
+
): Promise<TranscriptEntry[]>;
|
|
66
|
+
/** Undefined when unknown; the team status shows no context bar for the conversation then. */
|
|
67
|
+
contextUsage?(conversation: ChannelKey): ContextUse | undefined;
|
|
68
|
+
/** Runs in the host's preflight, before anything starts; a throw stops the boot. */
|
|
69
|
+
preflight?(): Promise<void>;
|
|
70
|
+
/** Runs when the host stops the agent server's runtime service. */
|
|
71
|
+
dispose?(): Promise<void> | void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A skill file a session loads; only its name and description enter the prompt. */
|
|
75
|
+
export interface LoadedSkill {
|
|
76
|
+
name: string;
|
|
77
|
+
description: string;
|
|
78
|
+
file: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The agent server's per-agent settings, which a runtime reads when it runs an agent's turn. */
|
|
82
|
+
export interface AgentSessions {
|
|
83
|
+
/** The shell's working directory, shared by every agent; writes outside it are held. */
|
|
84
|
+
workDir: string;
|
|
85
|
+
/**
|
|
86
|
+
* The skills the agent carries, read at the start of every run; a change rebuilds its
|
|
87
|
+
* sessions, keeping their history.
|
|
88
|
+
*/
|
|
89
|
+
skills(name: string): readonly LoadedSkill[];
|
|
90
|
+
/** The agent's model (`<provider>/<id>`) and thinking setting, read at the start of every run. */
|
|
91
|
+
modelOf(name: string): { model: string; thinking: ThinkingSetting };
|
|
92
|
+
/** The channel the scope's turns run in: the group's for a seat in one, else the agent's own. */
|
|
93
|
+
turnChannel(scope: AgentTurnScope): ChannelKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Where a runtime keeps each conversation's held actions, so they survive a restart. */
|
|
97
|
+
export interface HeldActionStore {
|
|
98
|
+
/** The held actions stored for the conversation, if any. */
|
|
99
|
+
load(conversation: ChannelKey): Promise<PendingConfirmation | undefined>;
|
|
100
|
+
/** Stores the conversation's held actions; `undefined` clears them. */
|
|
101
|
+
save(
|
|
102
|
+
conversation: ChannelKey,
|
|
103
|
+
held: PendingConfirmation | undefined,
|
|
104
|
+
): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** What the agent server hands a runtime provider when it builds the runtime. */
|
|
108
|
+
export interface RuntimeDeps {
|
|
109
|
+
logger: Logger;
|
|
110
|
+
/** The host's locale and time zone. */
|
|
111
|
+
env: HostEnv;
|
|
112
|
+
/** Who the conversations serve. */
|
|
113
|
+
owner: { id: string; name: string };
|
|
114
|
+
/**
|
|
115
|
+
* The linked session parts: hold rules, packages, session tools, and personas. Read once the
|
|
116
|
+
* host has linked them, from the preflight on; calling it during the factory throws.
|
|
117
|
+
*/
|
|
118
|
+
sessions(): LinkedSessions;
|
|
119
|
+
/** What each tool needs; plugin tools are added when the host links, so ask at use time. */
|
|
120
|
+
toolTiers: ToolTiers;
|
|
121
|
+
/**
|
|
122
|
+
* The owner's approval and question prompts in a conversation, from its chat surface; undefined
|
|
123
|
+
* when the surface has none, and the action then waits for the owner's next message.
|
|
124
|
+
*/
|
|
125
|
+
prompts(
|
|
126
|
+
conversation: ChannelKey,
|
|
127
|
+
speaker?: Speaker,
|
|
128
|
+
): OwnerPrompts | undefined;
|
|
129
|
+
/** The agent server's per-agent settings, for agent turns. */
|
|
130
|
+
agents: AgentSessions;
|
|
131
|
+
/** Where held actions persist across restarts. */
|
|
132
|
+
confirmations: HeldActionStore;
|
|
133
|
+
/** The host's judge, resolved from the `judge` slot. */
|
|
134
|
+
judge: Judge;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Builds the runtime, once, when the agent server sets up. */
|
|
138
|
+
export type RuntimeFactory = (deps: RuntimeDeps) => AgentRuntime;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed name for a service one plugin provides and other plugins read. Two keys with one `id`
|
|
3
|
+
* are the same service, so a key survives being imported through two copies of a package.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServiceKey<T> {
|
|
6
|
+
/** Unique across plugins, such as `"roundtable.schedules"` or `"my-notes.index"`. */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
/** Type only; never set. Ties the key to the service it names, so keys of different services stay apart. */
|
|
9
|
+
readonly __service?: () => T;
|
|
10
|
+
/**
|
|
11
|
+
* What to tell a plugin that reads the service while no registered plugin provides it, such as
|
|
12
|
+
* how to switch the addon that provides it on. Without one the host gives its generic advice.
|
|
13
|
+
*/
|
|
14
|
+
readonly absent?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Makes a key. The id is what the host matches on; give it a prefix of your own, such as your plugin's name. */
|
|
18
|
+
export function serviceKey<T>(
|
|
19
|
+
id: string,
|
|
20
|
+
options: { absent?: string } = {},
|
|
21
|
+
): ServiceKey<T> {
|
|
22
|
+
if (typeof id !== "string" || id.trim() === "")
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`a service key needs an id, a non-empty string such as "my-notes.index"; got ${JSON.stringify(id)}.`,
|
|
25
|
+
);
|
|
26
|
+
return Object.freeze(
|
|
27
|
+
options.absent === undefined ? { id } : { id, absent: options.absent },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a plugin provides and reads through `PluginContext.services`. */
|
|
32
|
+
export interface Services {
|
|
33
|
+
/** The service, or throws a PluginError naming the key and the plugin to register first when it is not provided yet. */
|
|
34
|
+
get<T>(key: ServiceKey<T>): T;
|
|
35
|
+
/**
|
|
36
|
+
* The service, or undefined when no registered plugin declares it, such as an addon switched off.
|
|
37
|
+
* Throws like `get` when a plugin declares it but has not been set up yet: that is order, not absence.
|
|
38
|
+
*/
|
|
39
|
+
find<T>(key: ServiceKey<T>): T | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Provides a service the plugin declares in `provides`; only while the plugin's setup runs, and
|
|
42
|
+
* once per key.
|
|
43
|
+
*/
|
|
44
|
+
provide<T>(key: ServiceKey<T>, value: T): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OutboundReply } from "../domain/conversation.ts";
|
|
2
|
+
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
3
|
+
import { PluginError } from "../errors.ts";
|
|
4
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
5
|
+
import type { Speaker } from "../speakers.ts";
|
|
6
|
+
import type { InboundMessage } from "./channels.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Splits a channel key, `<surface>:<id>`, at its first colon: the surface names the chat network
|
|
10
|
+
* that owns the channel, such as `discord`, and the id is that network's own, and may contain colons.
|
|
11
|
+
* Throws PluginError for a key without a surface.
|
|
12
|
+
*/
|
|
13
|
+
export function parseChannelKey(key: ChannelKey): {
|
|
14
|
+
surface: string;
|
|
15
|
+
id: string;
|
|
16
|
+
} {
|
|
17
|
+
const at = typeof key === "string" ? key.indexOf(":") : -1;
|
|
18
|
+
if (at <= 0)
|
|
19
|
+
throw new PluginError(
|
|
20
|
+
`"${String(key)}" is not a channel key. A key is <surface>:<id>, such as discord:123.`,
|
|
21
|
+
);
|
|
22
|
+
return { surface: key.slice(0, at), id: key.slice(at + 1) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The key of a channel on a surface; the inverse of `parseChannelKey`. */
|
|
26
|
+
export function channelKey(surface: string, id: string): ChannelKey {
|
|
27
|
+
if (surface === "" || surface.includes(":"))
|
|
28
|
+
throw new PluginError(
|
|
29
|
+
`"${surface}" is not a surface name. A surface is a non-empty word without a colon, such as discord.`,
|
|
30
|
+
);
|
|
31
|
+
return `${surface}:${id}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One chat network the host talks through. The host ships Discord's; a plugin may contribute
|
|
36
|
+
* another with `surfaces`. A surface serves the channels whose key starts with its `surface`
|
|
37
|
+
* prefix, and only those reach its methods.
|
|
38
|
+
*/
|
|
39
|
+
export interface ChatSurface {
|
|
40
|
+
/** The key prefix of this surface's channels, such as "discord"; unique per host. */
|
|
41
|
+
readonly surface: string;
|
|
42
|
+
/**
|
|
43
|
+
* Connects and delivers every incoming message; the host passes its conversation router. A
|
|
44
|
+
* message whose channel key has another prefix is logged and dropped.
|
|
45
|
+
*/
|
|
46
|
+
start(deliver: (message: InboundMessage) => void): Promise<void>;
|
|
47
|
+
/** Runs when the host stops, after the services started later have stopped. */
|
|
48
|
+
stop?(): Promise<void>;
|
|
49
|
+
sendReply(channel: ChannelKey, reply: OutboundReply): Promise<void>;
|
|
50
|
+
/** Shows a typing indicator until the returned function is called; absent = none shown. */
|
|
51
|
+
startTyping?(channel: ChannelKey): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Shows the owner a stop control until the returned function is called; it calls
|
|
54
|
+
* `conversations.stop(channel)` when used. Absent = none shown.
|
|
55
|
+
*/
|
|
56
|
+
showStop?(channel: ChannelKey): () => void;
|
|
57
|
+
/** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
|
|
58
|
+
react?(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
59
|
+
unreact?(
|
|
60
|
+
channel: ChannelKey,
|
|
61
|
+
messageId: string,
|
|
62
|
+
emoji: string,
|
|
63
|
+
): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* How the owner approves held actions or answers ask_user in a running turn; undefined, or
|
|
66
|
+
* absent, = the action is held until the owner's next message.
|
|
67
|
+
*/
|
|
68
|
+
prompts?(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every contributed surface, chosen by the prefix of a channel's key. Calls during setup throw
|
|
73
|
+
* NotLinkedError, because the surfaces are collected once every plugin is set up.
|
|
74
|
+
*/
|
|
75
|
+
export interface SurfacePort {
|
|
76
|
+
/** The surface that serves the channel's prefix; undefined when none does. */
|
|
77
|
+
of(channel: ChannelKey): ChatSurface | undefined;
|
|
78
|
+
/** Throws PluginError naming the prefix when no surface serves it. */
|
|
79
|
+
sendReply(channel: ChannelKey, reply: OutboundReply): Promise<void>;
|
|
80
|
+
/** A no-op when no surface serves the channel or its surface shows none. */
|
|
81
|
+
startTyping(channel: ChannelKey): () => void;
|
|
82
|
+
/** A no-op when no surface serves the channel or its surface shows none. */
|
|
83
|
+
showStop(channel: ChannelKey): () => void;
|
|
84
|
+
react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
85
|
+
unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
86
|
+
/** The owner's prompts in the channel; undefined when its surface has none. */
|
|
87
|
+
prompts(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
|
|
88
|
+
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
1
|
+
import { SQL, type TransactionSQL } from "bun";
|
|
2
2
|
import { MigrationError, PluginError } from "../errors.ts";
|
|
3
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
+
/** A plugin's tables, as DDL its `up` runs. */
|
|
5
6
|
export interface Migration {
|
|
6
7
|
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* "once" (the default): the ledger records it, so it runs one time over a database and never
|
|
10
|
+
* again. "every-boot": runs at every boot and is never recorded; it must be idempotent, which
|
|
11
|
+
* suits a migration that converges data an older build may have written since.
|
|
12
|
+
*/
|
|
13
|
+
runs?: "once" | "every-boot";
|
|
7
14
|
up(sql: SQL): Promise<void>;
|
|
8
15
|
}
|
|
9
16
|
|
|
17
|
+
/** What one boot's migrations did, each entry a ledger id (`<plugin>/<migration>`). */
|
|
18
|
+
export interface MigrationReport {
|
|
19
|
+
/** The "once" migrations that ran now and were recorded. */
|
|
20
|
+
applied: string[];
|
|
21
|
+
/** The "once" migrations the ledger already held. */
|
|
22
|
+
skipped: string[];
|
|
23
|
+
/** The "every-boot" migrations that ran. */
|
|
24
|
+
everyBoot: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
28
|
* The host's one connection pool. PostgreSQL's 100 connections are shared with the other services on the same server,
|
|
12
29
|
* so the pool stays small and closes idle connections; Bun's defaults (10, never closed)
|
|
@@ -16,7 +33,23 @@ export function openPool(databaseUrl: string): SQL {
|
|
|
16
33
|
return new SQL(databaseUrl, { max: 10, idleTimeout: 60 });
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The transaction as a migration sees the pool: a migration that opens its own transaction
|
|
38
|
+
* with `begin` gets a savepoint of the running one, which PostgreSQL nests.
|
|
39
|
+
*/
|
|
40
|
+
export function nested(transaction: TransactionSQL): SQL {
|
|
41
|
+
return new Proxy(transaction, {
|
|
42
|
+
get(target, property) {
|
|
43
|
+
if (property === "begin")
|
|
44
|
+
return (...args: Parameters<TransactionSQL["savepoint"]>) =>
|
|
45
|
+
target.savepoint(...args);
|
|
46
|
+
const value: unknown = Reflect.get(target, property);
|
|
47
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Runs each migration in order, none of them recorded; a repeated name is a plugin bug, a failing one stops. Test setup uses it for tables a test drops and rebuilds. */
|
|
20
53
|
export async function migrate(
|
|
21
54
|
sql: SQL,
|
|
22
55
|
migrations: readonly Migration[],
|
|
@@ -35,3 +68,85 @@ export async function migrate(
|
|
|
35
68
|
}
|
|
36
69
|
}
|
|
37
70
|
}
|
|
71
|
+
|
|
72
|
+
const LEDGER = "roundtable_migrations";
|
|
73
|
+
|
|
74
|
+
/** The lock key for one name; two hosts booting together queue on it instead of racing. */
|
|
75
|
+
const lockOf = (name: string) => `pi-roundtable:migration:${name}`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Runs every plugin's migrations in plugin order, then declaration order, the way a boot does. A
|
|
79
|
+
* "once" migration runs inside its own transaction under an advisory lock, and the ledger row is
|
|
80
|
+
* written in that transaction, so a migration that failed leaves no record and two hosts booting at
|
|
81
|
+
* once apply it one time in total. An "every-boot" migration runs each time and is not recorded.
|
|
82
|
+
*/
|
|
83
|
+
export async function runMigrations(
|
|
84
|
+
sql: SQL,
|
|
85
|
+
plugins: readonly Pick<RoundtablePlugin, "name" | "migrations">[],
|
|
86
|
+
): Promise<MigrationReport> {
|
|
87
|
+
const steps = plugins.flatMap((plugin) =>
|
|
88
|
+
(plugin.migrations ?? []).map((migration) => ({
|
|
89
|
+
id: `${plugin.name}/${migration.name}`,
|
|
90
|
+
plugin: plugin.name,
|
|
91
|
+
migration,
|
|
92
|
+
})),
|
|
93
|
+
);
|
|
94
|
+
const ids = new Set<string>();
|
|
95
|
+
for (const { id } of steps) {
|
|
96
|
+
if (ids.has(id)) throw new PluginError(`migration ${id} is declared twice`);
|
|
97
|
+
ids.add(id);
|
|
98
|
+
}
|
|
99
|
+
const report: MigrationReport = { applied: [], skipped: [], everyBoot: [] };
|
|
100
|
+
if (steps.length === 0) return report;
|
|
101
|
+
|
|
102
|
+
await sql.begin(async (tx) => {
|
|
103
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended(${lockOf("ledger")}, 0))`;
|
|
104
|
+
await tx.unsafe(`
|
|
105
|
+
CREATE TABLE IF NOT EXISTS ${LEDGER} (
|
|
106
|
+
id text PRIMARY KEY,
|
|
107
|
+
plugin text NOT NULL,
|
|
108
|
+
name text NOT NULL,
|
|
109
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
110
|
+
)`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
for (const { id, plugin, migration } of steps) {
|
|
114
|
+
try {
|
|
115
|
+
if (migration.runs === "every-boot") {
|
|
116
|
+
await migration.up(sql);
|
|
117
|
+
report.everyBoot.push(id);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const ran = await sql.begin(async (tx) => {
|
|
121
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended(${lockOf(id)}, 0))`;
|
|
122
|
+
const [recorded] =
|
|
123
|
+
await tx`SELECT 1 FROM roundtable_migrations WHERE id = ${id}`;
|
|
124
|
+
if (recorded) return false;
|
|
125
|
+
await migration.up(nested(tx));
|
|
126
|
+
await tx`INSERT INTO roundtable_migrations (id, plugin, name) VALUES (${id}, ${plugin}, ${migration.name})`;
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
(ran ? report.applied : report.skipped).push(id);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new MigrationError(id, error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return report;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Runs these plugins' migrations over the database at `url` with the ledger and the lock a boot
|
|
139
|
+
* uses, then closes its connection. Pass the plugins, with the names, the host runs: the ledger ids
|
|
140
|
+
* come from them, so another name would record the same tables a second time under it.
|
|
141
|
+
*/
|
|
142
|
+
export async function migrateDatabase(
|
|
143
|
+
url: string,
|
|
144
|
+
plugins: readonly Pick<RoundtablePlugin, "name" | "migrations">[],
|
|
145
|
+
): Promise<MigrationReport> {
|
|
146
|
+
const pool = new SQL(url, { max: 2 });
|
|
147
|
+
try {
|
|
148
|
+
return await runMigrations(pool, plugins);
|
|
149
|
+
} finally {
|
|
150
|
+
await pool.close();
|
|
151
|
+
}
|
|
152
|
+
}
|