pi-roundtable 0.8.0 → 0.9.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 +102 -0
- package/README.md +35 -11
- package/README.zh-TW.md +27 -10
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +606 -78
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.ts +5 -1
- package/examples/prechecks.test.ts +1 -0
- package/examples/principals.test.ts +60 -0
- package/examples/principals.ts +48 -0
- package/examples/prompt.test.ts +1 -1
- package/examples/study-room.test.ts +29 -1
- package/examples/study-room.ts +16 -19
- package/examples/support-desk.test.ts +3 -1
- package/examples/tools.test.ts +6 -1
- package/package.json +5 -2
- package/src/cli/checks/database.ts +11 -0
- package/src/cli/checks/identity.ts +225 -0
- package/src/cli/checks/model.ts +21 -3
- package/src/cli/cli.ts +115 -1
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +7 -0
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +5 -0
- package/src/cli/report.ts +12 -3
- package/src/cli/upgrade-rewrite.ts +102 -0
- package/src/cli/upgrade-source.ts +430 -0
- package/src/cli/upgrade.ts +176 -0
- package/src/core/agents/agent-claim.ts +56 -34
- package/src/core/agents/team-turns.ts +8 -4
- package/src/core/builtin/agent-server.ts +9 -7
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +140 -85
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +64 -1
- package/src/core/config/config.ts +78 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +77 -9
- package/src/core/contract/runtime.ts +23 -7
- package/src/core/contract/surface.ts +22 -6
- package/src/core/conversations/conversation-registry.ts +14 -0
- package/src/core/conversations/conversation-store.ts +13 -0
- package/src/core/define-roundtable.ts +41 -11
- package/src/core/discord/agent-panel.ts +2 -2
- package/src/core/discord/card-audience.ts +148 -0
- package/src/core/discord/channel-context.ts +348 -0
- package/src/core/discord/connection.ts +15 -3
- package/src/core/discord/direct-channel.ts +54 -0
- package/src/core/discord/discord-owners.ts +133 -0
- package/src/core/discord/discord-surface.ts +69 -14
- package/src/core/discord/fresh-marker.ts +24 -0
- package/src/core/discord/inbound-message.ts +21 -7
- package/src/core/discord/interaction-module.ts +11 -1
- package/src/core/discord/owner-cards-fixture.ts +80 -0
- package/src/core/discord/owner-cards.ts +58 -97
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +7 -2
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +27 -2
- package/src/core/host.ts +27 -8
- package/src/core/i18n/cards.ts +8 -4
- package/src/core/identity/access-policy.ts +227 -0
- package/src/core/identity/actor-facts.ts +65 -0
- package/src/core/identity/caching-principal-store.ts +111 -0
- package/src/core/identity/config-owner.ts +145 -0
- package/src/core/identity/contact.ts +27 -0
- package/src/core/identity/identity-plugin.ts +56 -0
- package/src/core/identity/identity-schema.ts +282 -0
- package/src/core/identity/identity-service.ts +489 -0
- package/src/core/identity/identity-view.ts +77 -0
- package/src/core/identity/plugin-identities.ts +109 -0
- package/src/core/identity/principal-reads.ts +38 -0
- package/src/core/identity/principal-store.ts +478 -0
- package/src/core/identity/principal-tables.ts +53 -0
- package/src/core/identity/seen-throttle.ts +57 -0
- package/src/core/identity/ulid.ts +36 -0
- package/src/core/identity.ts +34 -5
- package/src/core/interactions/prompts.ts +127 -0
- package/src/core/modules/background/background-turns.ts +56 -17
- package/src/core/modules/background/personal-target.ts +50 -0
- package/src/core/modules/delegation/delegate.ts +28 -14
- package/src/core/modules/delegation/delegator.ts +23 -7
- package/src/core/modules/memory/owner-memory.ts +123 -50
- package/src/core/modules/notify/notify.ts +53 -13
- package/src/core/modules/notify/session-notify.ts +110 -0
- package/src/core/modules/schedules/prechecks.ts +8 -2
- package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
- package/src/core/modules/schedules/schedule-store.ts +51 -20
- package/src/core/modules/schedules/schedule-tools.ts +122 -12
- package/src/core/modules/schedules/scheduler.ts +43 -2
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/plugin.ts +48 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/routing/channel-router.ts +218 -17
- package/src/core/routing/conversation-turns.ts +38 -8
- package/src/core/routing/surface-port.ts +32 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/bridge-guard.ts +225 -0
- package/src/core/runtime/compaction-tiers.ts +5 -1
- package/src/core/runtime/conversation-sessions.ts +124 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +28 -14
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/pending-confirmation-store.ts +30 -5
- package/src/core/runtime/pi-agent-runtime.ts +135 -134
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +4 -4
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +50 -5
- package/src/core/runtime/runtime-types.ts +40 -12
- package/src/core/runtime/session-conversation.ts +143 -0
- package/src/core/runtime/session-factory.ts +115 -15
- package/src/core/runtime/session-scope.ts +78 -0
- package/src/core/runtime/steerable-run.ts +9 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/services.ts +27 -1
- package/src/core/sessions.ts +30 -0
- package/src/core/speakers.ts +21 -19
- package/src/core/testing/background.ts +31 -0
- package/src/core/testing/fixture-database.ts +55 -0
- package/src/core/testing/map-identity.ts +76 -0
- package/src/core/testing/module-sessions.ts +134 -0
- package/src/core/testing/modules.ts +108 -21
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +118 -41
- package/src/core/testing/test-host.ts +79 -16
- package/src/core/testing/tool-set.ts +32 -0
- package/src/core/tool-set.snapshot.json +4 -4
- package/src/core/tool-tiers.ts +57 -3
- package/src/discord/index.ts +8 -0
- package/src/index.ts +47 -8
- package/src/kit/compaction.ts +13 -0
- package/src/kit/index.ts +10 -0
- package/src/kit/support.ts +1 -1
- package/src/testing.ts +55 -16
- package/templates/.env.example +1 -1
- package/templates/adapters/web/README.md +7 -0
- package/templates/adapters/web/roundtable.config.ts +14 -10
- package/templates/roundtable.config.ts +12 -1
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -14,20 +14,21 @@ import {
|
|
|
14
14
|
} from "discord.js";
|
|
15
15
|
import { parseChannelKey } from "../contract/surface.ts";
|
|
16
16
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
17
|
+
import { messages } from "../i18n/index.ts";
|
|
17
18
|
import type {
|
|
18
19
|
Approval,
|
|
19
20
|
OwnerAnswer,
|
|
20
|
-
OwnerPrompts,
|
|
21
21
|
OwnerQuestion,
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
PromptScope,
|
|
23
|
+
Prompts,
|
|
24
|
+
} from "../interactions/prompts.ts";
|
|
24
25
|
import type { Logger } from "../log.ts";
|
|
25
26
|
import {
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} from "
|
|
27
|
+
type Audience,
|
|
28
|
+
type CardAudienceOptions,
|
|
29
|
+
CardAudiences,
|
|
30
|
+
} from "./card-audience.ts";
|
|
31
|
+
import { DiscordOwners, discordUser } from "./discord-owners.ts";
|
|
31
32
|
import type { InteractionModule } from "./interaction-module.ts";
|
|
32
33
|
import { ownerPanel, type PanelContent, plain } from "./owner-panel.ts";
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ export const CARD_TIMEOUT_MS = 30 * 60_000;
|
|
|
37
38
|
const OTHER = "other";
|
|
38
39
|
const TEXT_FIELD = "text";
|
|
39
40
|
|
|
40
|
-
/** A card's message: the panel, led by a mention of
|
|
41
|
+
/** A card's message: the panel, led in a thread by a mention of those it is for. */
|
|
41
42
|
export type CardPayload = Omit<
|
|
42
43
|
ReturnType<typeof ownerPanel>,
|
|
43
44
|
"components" | "allowedMentions"
|
|
@@ -57,14 +58,18 @@ export interface CardMessage {
|
|
|
57
58
|
/** Where a channel's cards are posted. */
|
|
58
59
|
export interface CardChannel {
|
|
59
60
|
send(payload: CardPayload): Promise<CardMessage>;
|
|
60
|
-
/** A thread, where
|
|
61
|
+
/** A thread, where those a card is for are mentioned so they hear of it; a channel mentions no one. */
|
|
61
62
|
thread?: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export interface OwnerCardsOptions {
|
|
66
|
+
/** The primary owner's Discord user id. */
|
|
65
67
|
ownerId: string;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Who holds a tier now, and which owners have a Discord identity, for cards that others than
|
|
70
|
+
* the primary owner may answer; without it only the primary owner answers.
|
|
71
|
+
*/
|
|
72
|
+
identity?: CardAudienceOptions["identity"];
|
|
68
73
|
/** The Discord channel by id; throws when it cannot take messages. */
|
|
69
74
|
channel(channelId: string): Promise<CardChannel>;
|
|
70
75
|
logger: Logger;
|
|
@@ -91,42 +96,41 @@ interface OpenCard {
|
|
|
91
96
|
audience: Audience;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
/** The people a card is for. */
|
|
95
|
-
interface Audience {
|
|
96
|
-
allows(user: { id: string; roleIds: readonly string[] }): boolean;
|
|
97
|
-
/** The user the card mentions in a thread. */
|
|
98
|
-
mentionId: string;
|
|
99
|
-
/** Shown on the card. */
|
|
100
|
-
note: string;
|
|
101
|
-
refusal: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
99
|
/**
|
|
105
|
-
* The
|
|
106
|
-
* answered with buttons, a menu, or a form
|
|
107
|
-
* turn asks, when the card allows (a question always; an
|
|
108
|
-
*
|
|
109
|
-
*
|
|
100
|
+
* The cards in the assistant's channels: approvals of held actions and ask_user questions,
|
|
101
|
+
* answered with buttons, a menu, or a form, by those the turn's prompt scope names (see
|
|
102
|
+
* `CardAudiences`): the speaker whose turn asks, when the card allows (a question always; an
|
|
103
|
+
* approval at its tier), and, in a shared conversation, the owners. An unanswered card expires
|
|
104
|
+
* after 30 minutes and a stopped turn cancels it. Open cards live in memory, so a restart
|
|
105
|
+
* abandons them; pressing one then says it no longer works.
|
|
110
106
|
*/
|
|
111
107
|
export class OwnerCards implements InteractionModule {
|
|
112
108
|
readonly #options: OwnerCardsOptions;
|
|
109
|
+
readonly #audiences: CardAudiences;
|
|
113
110
|
readonly #open = new Map<string, OpenCard>();
|
|
114
111
|
|
|
115
112
|
constructor(options: OwnerCardsOptions) {
|
|
116
113
|
this.#options = options;
|
|
114
|
+
this.#audiences = new CardAudiences(options, new DiscordOwners(options));
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
commands() {
|
|
120
118
|
return [];
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The channel's cards for the scope's turn; without a scope they are the owners'. The chat
|
|
123
|
+
* surface port sends only Discord keys here.
|
|
124
|
+
*/
|
|
125
|
+
prompts(channel: ChannelKey, scope?: PromptScope): Prompts | undefined {
|
|
125
126
|
const channelId = parseChannelKey(channel).id;
|
|
126
127
|
return {
|
|
127
|
-
confirm: (title, message, signal, minTier = "owner") =>
|
|
128
|
-
this.#
|
|
129
|
-
|
|
128
|
+
confirm: async (title, message, signal, minTier = "owner") => {
|
|
129
|
+
const audience = await this.#audiences.approval(scope, minTier);
|
|
130
|
+
// A private conversation's call above its person's tier is no one's to approve.
|
|
131
|
+
if (!audience) return "expired";
|
|
132
|
+
return this.#post<Approval>(channelId, signal, "expired", "cancelled", {
|
|
133
|
+
audience,
|
|
130
134
|
title,
|
|
131
135
|
sections: [message],
|
|
132
136
|
rows: (id, disabled) => [
|
|
@@ -146,22 +150,26 @@ export class OwnerCards implements InteractionModule {
|
|
|
146
150
|
),
|
|
147
151
|
],
|
|
148
152
|
footer: messages().cardApprovalFooter,
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
ask: async (title, question, signal) => {
|
|
156
|
+
const audience = await this.#audiences.question(scope);
|
|
157
|
+
if (!audience) return undefined;
|
|
158
|
+
return this.#post<OwnerAnswer | undefined>(
|
|
152
159
|
channelId,
|
|
153
160
|
signal,
|
|
154
161
|
undefined,
|
|
155
162
|
undefined,
|
|
156
163
|
{
|
|
157
|
-
audience
|
|
164
|
+
audience,
|
|
158
165
|
title,
|
|
159
166
|
sections: [question.question],
|
|
160
167
|
question,
|
|
161
168
|
rows: (id, disabled) => askRows(id, question, disabled),
|
|
162
169
|
footer: messages().cardQuestionFooter,
|
|
163
170
|
},
|
|
164
|
-
)
|
|
171
|
+
);
|
|
172
|
+
},
|
|
165
173
|
};
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -214,7 +222,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
214
222
|
footer,
|
|
215
223
|
},
|
|
216
224
|
open.mention === true,
|
|
217
|
-
card.audience.
|
|
225
|
+
card.audience.mentions,
|
|
218
226
|
);
|
|
219
227
|
const abort = () => settle(cancelled, messages().cardStopped);
|
|
220
228
|
const timer = setTimeout(
|
|
@@ -250,67 +258,22 @@ export class OwnerCards implements InteractionModule {
|
|
|
250
258
|
}
|
|
251
259
|
|
|
252
260
|
/** The card's panel; in a thread it leads with the mention of who may answer. */
|
|
253
|
-
#panel(
|
|
261
|
+
#panel(
|
|
262
|
+
content: PanelContent,
|
|
263
|
+
mention: boolean,
|
|
264
|
+
userIds: readonly string[],
|
|
265
|
+
): CardPayload {
|
|
254
266
|
const panel = ownerPanel(content);
|
|
255
267
|
if (!mention) return panel;
|
|
256
268
|
return {
|
|
257
269
|
...panel,
|
|
258
270
|
components: [
|
|
259
|
-
new TextDisplayBuilder().setContent(
|
|
271
|
+
new TextDisplayBuilder().setContent(
|
|
272
|
+
userIds.map((id) => `<@${id}>`).join(" "),
|
|
273
|
+
),
|
|
260
274
|
...panel.components,
|
|
261
275
|
],
|
|
262
|
-
allowedMentions: { users: [
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* An approval is for the speaker whose turn held the call, when their tier holds it, and for
|
|
268
|
-
* the owner; the owner's alone by default. Nobody else in the channel may approve it.
|
|
269
|
-
*/
|
|
270
|
-
#approvers(minTier: Tier, speaker: Speaker | undefined): Audience {
|
|
271
|
-
const { ownerId, speakers } = this.#options;
|
|
272
|
-
if (
|
|
273
|
-
minTier === "owner" ||
|
|
274
|
-
!speakers ||
|
|
275
|
-
!speaker ||
|
|
276
|
-
speaker.id === ownerId ||
|
|
277
|
-
!tierAtLeast(speaker.tier, minTier)
|
|
278
|
-
)
|
|
279
|
-
return {
|
|
280
|
-
allows: (user) => user.id === ownerId,
|
|
281
|
-
mentionId: ownerId,
|
|
282
|
-
note: "",
|
|
283
|
-
refusal: messages().cardOwnerOnly,
|
|
284
|
-
};
|
|
285
|
-
return {
|
|
286
|
-
allows: (user) => {
|
|
287
|
-
if (user.id === ownerId) return true;
|
|
288
|
-
if (user.id !== speaker.id) return false;
|
|
289
|
-
// Their tier when they press, in case it was lowered during the turn.
|
|
290
|
-
const who = speakers.resolve({ ...user, name: "" });
|
|
291
|
-
return who !== undefined && tierAtLeast(who.tier, minTier);
|
|
292
|
-
},
|
|
293
|
-
mentionId: speaker.id,
|
|
294
|
-
note: messages().cardApproversNote(speaker.id),
|
|
295
|
-
refusal: messages().cardApproversRefusal,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** A question is for the speaker whose turn asks it, and for the owner. */
|
|
300
|
-
#asker(speaker: Speaker | undefined): Audience {
|
|
301
|
-
const { ownerId } = this.#options;
|
|
302
|
-
if (!speaker || speaker.id === ownerId)
|
|
303
|
-
return {
|
|
304
|
-
allows: (user) => user.id === ownerId,
|
|
305
|
-
mentionId: ownerId,
|
|
306
|
-
note: "",
|
|
307
|
-
refusal: messages().cardOwnerOnly,
|
|
308
|
-
};
|
|
309
|
-
return {
|
|
310
|
-
allows: (user) => user.id === speaker.id || user.id === ownerId,
|
|
311
|
-
mentionId: speaker.id,
|
|
312
|
-
note: messages().cardAskerNote(speaker.id),
|
|
313
|
-
refusal: messages().cardAskerRefusal,
|
|
276
|
+
allowedMentions: { users: [...userIds] },
|
|
314
277
|
};
|
|
315
278
|
}
|
|
316
279
|
|
|
@@ -335,10 +298,8 @@ export class OwnerCards implements InteractionModule {
|
|
|
335
298
|
});
|
|
336
299
|
return true;
|
|
337
300
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
member && "cache" in member.roles ? [...member.roles.cache.keys()] : [];
|
|
341
|
-
if (!card.audience.allows({ id: interaction.user.id, roleIds })) {
|
|
301
|
+
const user = discordUser(interaction);
|
|
302
|
+
if (!(await card.audience.allows(user))) {
|
|
342
303
|
await interaction.reply({
|
|
343
304
|
content: card.audience.refusal,
|
|
344
305
|
flags: MessageFlags.Ephemeral,
|
|
@@ -354,7 +315,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
354
315
|
footer: outcome,
|
|
355
316
|
},
|
|
356
317
|
card.mention === true,
|
|
357
|
-
card.audience.
|
|
318
|
+
card.audience.mentions,
|
|
358
319
|
);
|
|
359
320
|
if (interaction.isButton() && (action === "yes" || action === "no")) {
|
|
360
321
|
const approved = action === "yes";
|
|
@@ -7,7 +7,13 @@ import {
|
|
|
7
7
|
SlashCommandSubcommandGroupBuilder,
|
|
8
8
|
} from "discord.js";
|
|
9
9
|
import { messages } from "../i18n/index.ts";
|
|
10
|
+
import type { IdentityService } from "../identity/identity-service.ts";
|
|
10
11
|
import type { Logger } from "../log.ts";
|
|
12
|
+
import {
|
|
13
|
+
type DiscordActor,
|
|
14
|
+
DiscordOwners,
|
|
15
|
+
discordUser,
|
|
16
|
+
} from "./discord-owners.ts";
|
|
11
17
|
import type {
|
|
12
18
|
CommandGuard,
|
|
13
19
|
CommandRoot,
|
|
@@ -33,31 +39,36 @@ export const groupOption = (
|
|
|
33
39
|
): RootOption => build(new SlashCommandSubcommandGroupBuilder()).toJSON();
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
|
-
* Only
|
|
37
|
-
*
|
|
42
|
+
* Only an owner may use the root command: the primary owner by user ID, and, with the identity
|
|
43
|
+
* service, every other owner principal's Discord identity, checked again each time. Every answer
|
|
44
|
+
* is visible only to them, and a failure becomes a panel instead of a hanging interaction.
|
|
38
45
|
*/
|
|
39
|
-
// pi-lens-ignore: large-class —
|
|
46
|
+
// pi-lens-ignore: large-class — three methods: the owner checks and the failure panel
|
|
40
47
|
export class OwnerGuard implements CommandGuard {
|
|
41
|
-
readonly #
|
|
48
|
+
readonly #owners: DiscordOwners;
|
|
42
49
|
readonly #logger: Logger;
|
|
43
50
|
/** The name of the root command the guard serves, without the slash. */
|
|
44
51
|
readonly root: string;
|
|
45
52
|
readonly refusalHint: string | undefined;
|
|
46
53
|
|
|
47
54
|
constructor(
|
|
48
|
-
|
|
55
|
+
owners: DiscordOwners,
|
|
49
56
|
logger: Logger,
|
|
50
57
|
root: string,
|
|
51
58
|
refusalHint?: string,
|
|
52
59
|
) {
|
|
53
|
-
this.#
|
|
60
|
+
this.#owners = owners;
|
|
54
61
|
this.#logger = logger;
|
|
55
62
|
this.root = root;
|
|
56
63
|
this.refusalHint = refusalHint;
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
allows(actor: DiscordActor): Promise<boolean> {
|
|
67
|
+
return this.#owners.isOwner(discordUser(actor));
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
isOwner(actor: { user: { id: string } }): boolean {
|
|
60
|
-
return actor.user.id === this.#
|
|
71
|
+
return actor.user.id === this.#owners.primary;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
/** Runs a handler; failures become a panel instead of a hanging interaction. */
|
|
@@ -82,8 +93,10 @@ export class OwnerGuard implements CommandGuard {
|
|
|
82
93
|
|
|
83
94
|
/** What `commandGuard` needs. */
|
|
84
95
|
export interface CommandGuardOptions {
|
|
85
|
-
/** The
|
|
96
|
+
/** The primary owner's Discord user id, who may always use the owner's commands. */
|
|
86
97
|
ownerId: string;
|
|
98
|
+
/** Which other owners there are and whether they still are; without it only `ownerId` may. */
|
|
99
|
+
identity?: Pick<IdentityService, "resolve" | "owners" | "identities">;
|
|
87
100
|
/** The name of the root command the guard serves, without the slash. */
|
|
88
101
|
root: string;
|
|
89
102
|
/** Where a failing command is logged. */
|
|
@@ -94,9 +107,10 @@ export interface CommandGuardOptions {
|
|
|
94
107
|
|
|
95
108
|
/** A guard for the owner's commands, such as the one a test hands a module; the Discord plugin makes its own. */
|
|
96
109
|
export function commandGuard(options: CommandGuardOptions): CommandGuard {
|
|
110
|
+
const { ownerId, identity, logger } = options;
|
|
97
111
|
return new OwnerGuard(
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
new DiscordOwners({ ownerId, ...(identity ? { identity } : {}), logger }),
|
|
113
|
+
logger,
|
|
100
114
|
options.root,
|
|
101
115
|
options.refusalHint,
|
|
102
116
|
);
|
|
@@ -121,7 +135,7 @@ const refusal = (hint: string | undefined) =>
|
|
|
121
135
|
sections: [`${messages().ownerRefusalBody}${hint ?? ""}`],
|
|
122
136
|
});
|
|
123
137
|
|
|
124
|
-
/** Answers a feature's root subcommands and components, for the
|
|
138
|
+
/** Answers a feature's root subcommands and components, for the owners only. */
|
|
125
139
|
export function ownerCommandModule(
|
|
126
140
|
guard: CommandGuard,
|
|
127
141
|
handlers: OwnerCommandHandlers,
|
|
@@ -139,14 +153,14 @@ export function ownerCommandModule(
|
|
|
139
153
|
async handle(interaction) {
|
|
140
154
|
if (interaction.isAutocomplete()) {
|
|
141
155
|
if (!owned(interaction)) return false;
|
|
142
|
-
if (!guard.
|
|
156
|
+
if (!(await guard.allows(interaction))) await interaction.respond([]);
|
|
143
157
|
else await handlers.autocomplete?.(interaction);
|
|
144
158
|
return true;
|
|
145
159
|
}
|
|
146
160
|
if (await handlers.component?.(interaction)) return true;
|
|
147
161
|
if (!interaction.isChatInputCommand() || !owned(interaction))
|
|
148
162
|
return false;
|
|
149
|
-
if (!guard.
|
|
163
|
+
if (!(await guard.allows(interaction))) {
|
|
150
164
|
await interaction.reply(refusal(guard.refusalHint));
|
|
151
165
|
return true;
|
|
152
166
|
}
|
|
@@ -12,7 +12,7 @@ import type { CommandGuard, InteractionModule } from "./interaction-module.ts";
|
|
|
12
12
|
|
|
13
13
|
export const STOP_BUTTON_ID = "roundtable:stop";
|
|
14
14
|
|
|
15
|
-
/** The message a long turn shows: a stop button only
|
|
15
|
+
/** The message a long turn shows: a stop button only an owner may press. */
|
|
16
16
|
export function stopPanel() {
|
|
17
17
|
return {
|
|
18
18
|
content: messages().stopNote,
|
|
@@ -29,13 +29,13 @@ export function stopPanel() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface StopButtonOptions {
|
|
32
|
-
guard: Pick<CommandGuard, "
|
|
32
|
+
guard: Pick<CommandGuard, "allows">;
|
|
33
33
|
/** Stops the running turn of the channel the button was pressed in, through its claim. */
|
|
34
34
|
conversations: Pick<ConversationPort, "stop">;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Answers a press of the stop button: only
|
|
38
|
+
* Answers a press of the stop button: only an owner stops the channel's turn, and the answer,
|
|
39
39
|
* visible only to whoever pressed, says whether one was running.
|
|
40
40
|
*/
|
|
41
41
|
export function stopButtonModule(
|
|
@@ -46,7 +46,7 @@ export function stopButtonModule(
|
|
|
46
46
|
handle: async (interaction: Interaction) => {
|
|
47
47
|
if (!interaction.isButton() || interaction.customId !== STOP_BUTTON_ID)
|
|
48
48
|
return false;
|
|
49
|
-
const owner = options.guard.
|
|
49
|
+
const owner = await options.guard.allows(interaction);
|
|
50
50
|
const stopped =
|
|
51
51
|
owner &&
|
|
52
52
|
options.conversations.stop(
|
|
@@ -27,10 +27,15 @@ export interface PendingConfirmation {
|
|
|
27
27
|
heldAt: Date;
|
|
28
28
|
calls: HeldCall[];
|
|
29
29
|
/**
|
|
30
|
-
* The speaker whose turn held the calls
|
|
31
|
-
* tier that holds them; without one, the
|
|
30
|
+
* The speaker whose turn held the calls, by their id on the surface. Besides the owners, only
|
|
31
|
+
* they may approve them, at a tier that holds them; without one, the owners alone may.
|
|
32
32
|
*/
|
|
33
33
|
speakerId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The principal of that speaker, who approves them on any of their identities; absent for
|
|
36
|
+
* calls held before 0.9, which are matched by `speakerId`.
|
|
37
|
+
*/
|
|
38
|
+
principalId?: string;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** A file produced for a reply; data is raw bytes, not a path or base64. */
|
|
@@ -29,3 +29,8 @@ export class DelegationError extends Error {
|
|
|
29
29
|
export class AgentError extends Error {
|
|
30
30
|
override name = "AgentError";
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/** A principal, identity link, or role change that cannot be made; its message names what to fix. */
|
|
34
|
+
export class IdentityError extends Error {
|
|
35
|
+
override name = "IdentityError";
|
|
36
|
+
}
|
package/src/core/domain/ports.ts
CHANGED
|
@@ -7,6 +7,11 @@ import type { TurnProgress } from "./progress.ts";
|
|
|
7
7
|
|
|
8
8
|
export type { AgentTurnScope };
|
|
9
9
|
|
|
10
|
+
/** Who a turn's conversation belongs to. */
|
|
11
|
+
export type TurnConversation =
|
|
12
|
+
| { visibility: "private"; principalId: string }
|
|
13
|
+
| { visibility: "shared" };
|
|
14
|
+
|
|
10
15
|
export interface TurnRequest {
|
|
11
16
|
/** Where the turn's message arrived and its attachments are saved. */
|
|
12
17
|
channel: ChannelKey;
|
|
@@ -19,8 +24,18 @@ export interface TurnRequest {
|
|
|
19
24
|
confirmed?: boolean;
|
|
20
25
|
/** Set for a turn of an agent-server agent. */
|
|
21
26
|
agent?: AgentTurnScope;
|
|
22
|
-
/**
|
|
23
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The person the turn is for: who wrote the message, or who set up the work; the turn runs at
|
|
29
|
+
* their tier. Required: a turn without one is refused, never run as the owner's. A plugin that
|
|
30
|
+
* starts a turn on someone's behalf gets theirs from `IDENTITY.speakerFor`.
|
|
31
|
+
*/
|
|
32
|
+
speaker: Speaker;
|
|
33
|
+
/**
|
|
34
|
+
* Who the conversation belongs to, as the host records it: `private` to one principal, or
|
|
35
|
+
* `shared` by whoever its claim admits, which it is taken to be when absent. A private
|
|
36
|
+
* conversation's prompts are its person's alone; a shared one's escalate to the owners.
|
|
37
|
+
*/
|
|
38
|
+
conversation?: TurnConversation;
|
|
24
39
|
/**
|
|
25
40
|
* The conversation's kind, fixed when its session is made: the string a claim's `startFresh`
|
|
26
41
|
* returns, which picks the persona of a non-agent conversation. "owner" when absent.
|
|
@@ -54,6 +69,16 @@ export type {
|
|
|
54
69
|
YesNoQuestion,
|
|
55
70
|
} from "../contract/providers.ts";
|
|
56
71
|
|
|
72
|
+
/** Sends a person a notice through their own channel; resolves false when they have none. */
|
|
73
|
+
export interface Notifier {
|
|
74
|
+
notify(principalId: string, text: string): Promise<boolean>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sends the primary owner a direct message.
|
|
79
|
+
* @deprecated Since 0.9 there may be more than one owner: reach a person with a `Notifier`, such as
|
|
80
|
+
* `context.directChannels`. Goes away in 1.0.
|
|
81
|
+
*/
|
|
57
82
|
export interface OwnerNotifier {
|
|
58
83
|
notifyOwner(text: string): Promise<void>;
|
|
59
84
|
}
|
package/src/core/host.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { MigrationError, NotLinkedError, PluginError } from "./errors.ts";
|
|
|
7
7
|
import { EventBus } from "./events.ts";
|
|
8
8
|
import { HttpListeners, type ListenerConfig } from "./http/listeners.ts";
|
|
9
9
|
import { type Locale, setLocale } from "./i18n/index.ts";
|
|
10
|
+
import { contactsOf } from "./identity/identity-view.ts";
|
|
10
11
|
import type { JudgeModel } from "./judging/model-judge.ts";
|
|
11
12
|
import type { Logger } from "./log.ts";
|
|
12
13
|
import type {
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
conversationTurns,
|
|
33
34
|
} from "./routing/conversation-turns.ts";
|
|
34
35
|
import { surfacePort } from "./routing/surface-port.ts";
|
|
35
|
-
import { CONVERSATIONS, RUNTIME } from "./services.ts";
|
|
36
|
+
import { CONVERSATIONS, IDENTITY, RUNTIME } from "./services.ts";
|
|
36
37
|
import { setTimeZone } from "./time.ts";
|
|
37
38
|
import { type ToolTierTable, toolTiers } from "./tool-tiers.ts";
|
|
38
39
|
|
|
@@ -154,6 +155,7 @@ export class Roundtable {
|
|
|
154
155
|
#listeners: HttpListeners | undefined;
|
|
155
156
|
#pool: SQL | undefined;
|
|
156
157
|
readonly #queue = new ChannelQueue();
|
|
158
|
+
readonly #warned = new Set<string>();
|
|
157
159
|
readonly #tiers: ToolTierTable;
|
|
158
160
|
readonly #events: EventBus;
|
|
159
161
|
#services: ServiceRegistry | undefined;
|
|
@@ -191,6 +193,7 @@ export class Roundtable {
|
|
|
191
193
|
return {
|
|
192
194
|
handle: (message) => router().handle(message),
|
|
193
195
|
background: (turn) => router().background(turn),
|
|
196
|
+
runsAs: (turn) => router().runsAs(turn),
|
|
194
197
|
target: (name) => router().target(name),
|
|
195
198
|
startFresh: (channel) => router().startFresh(channel),
|
|
196
199
|
deleteConversation: (channel) => router().deleteConversation(channel),
|
|
@@ -198,18 +201,29 @@ export class Roundtable {
|
|
|
198
201
|
postsInPlace: (channel) => router().postsInPlace(channel),
|
|
199
202
|
owns: (channel) => router().owns(channel),
|
|
200
203
|
takesBackground: (channel) => router().takesBackground(channel),
|
|
204
|
+
takesSystemReports: (channel) => router().takesSystemReports(channel),
|
|
201
205
|
};
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/** The contributed surfaces by channel prefix; every call before linking throws NotLinkedError. */
|
|
205
209
|
#surfaces(): SurfacePort {
|
|
206
|
-
return surfacePort(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
return surfacePort(
|
|
211
|
+
() => {
|
|
212
|
+
if (!this.#sessions)
|
|
213
|
+
throw new NotLinkedError(
|
|
214
|
+
"chat surfaces are linked once every plugin is set up. Use surfaces from a service's start or from a handler, not during setup.",
|
|
215
|
+
);
|
|
216
|
+
return this.#registry.surfaces;
|
|
217
|
+
},
|
|
218
|
+
{ deprecated: (message) => this.#deprecated(message) },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Logs a deprecation once per host. */
|
|
223
|
+
#deprecated(message: string): void {
|
|
224
|
+
if (this.#warned.has(message)) return;
|
|
225
|
+
this.#warned.add(message);
|
|
226
|
+
this.#options.logger.warn(message);
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
/** Turns over the runtime plugin's runtime and the surfaces; every call before linking is refused with NotLinkedError. */
|
|
@@ -328,11 +342,16 @@ export class Roundtable {
|
|
|
328
342
|
const { routes, channels } = this.#registry;
|
|
329
343
|
this.#sessions = linkSessions(this.#registry);
|
|
330
344
|
this.#events.link(this.#registry.handlers);
|
|
345
|
+
const identity = this.#services.find(IDENTITY);
|
|
331
346
|
this.#router = new ChannelRouter({
|
|
332
347
|
claims: channels,
|
|
348
|
+
...(identity
|
|
349
|
+
? { contacts: contactsOf(identity), principals: identity }
|
|
350
|
+
: {}),
|
|
333
351
|
targets: (name) =>
|
|
334
352
|
this.#registry.backgroundTargets.find((t) => t.name === name),
|
|
335
353
|
queue: this.#queue,
|
|
354
|
+
surfaces: this.#surfaces(),
|
|
336
355
|
logger,
|
|
337
356
|
...(this.#options.conversations?.forwardJoinMs === undefined
|
|
338
357
|
? {}
|
package/src/core/i18n/cards.ts
CHANGED
|
@@ -11,10 +11,12 @@ export function cardsEn(ctx: CatalogContext) {
|
|
|
11
11
|
cardStopped: "⏹️ This round was stopped.",
|
|
12
12
|
cardExpired: "⌛ Timed out with no answer.",
|
|
13
13
|
cardOwnerOnly: `Only ${ctx.assistant}'s owner can answer this card.`,
|
|
14
|
-
cardApproversNote: (
|
|
14
|
+
cardApproversNote: (...userIds: readonly string[]) =>
|
|
15
|
+
`-# Who can approve: ${userIds.map((id) => `<@${id}>`).join(" ")}`,
|
|
15
16
|
cardApproversRefusal:
|
|
16
17
|
"This approval is for the speaker whose turn asked for it.",
|
|
17
|
-
cardAskerNote: (
|
|
18
|
+
cardAskerNote: (...userIds: readonly string[]) =>
|
|
19
|
+
`-# Who can answer: ${userIds.map((id) => `<@${id}>`).join(" ")}`,
|
|
18
20
|
cardAskerRefusal: "This question is for another speaker.",
|
|
19
21
|
cardInactive: `This card is no longer active (it timed out, or ${ctx.assistant} restarted); reply in text if you need to.`,
|
|
20
22
|
cardApproved: "✅ Approved to run.",
|
|
@@ -41,9 +43,11 @@ export function cardsZhTW(ctx: CatalogContext): ReturnType<typeof cardsEn> {
|
|
|
41
43
|
cardStopped: "⏹️ 這一輪已停止。",
|
|
42
44
|
cardExpired: "⌛ 已逾時,沒有回答。",
|
|
43
45
|
cardOwnerOnly: `只有 ${ctx.assistant} 的擁有者能回答這張卡片。`,
|
|
44
|
-
cardApproversNote: (
|
|
46
|
+
cardApproversNote: (...userIds: readonly string[]) =>
|
|
47
|
+
`-# 可以核准的人:${userIds.map((id) => `<@${id}>`).join(" ")}`,
|
|
45
48
|
cardApproversRefusal: "這張核准卡是給提出這一輪的發話者的。",
|
|
46
|
-
cardAskerNote: (
|
|
49
|
+
cardAskerNote: (...userIds: readonly string[]) =>
|
|
50
|
+
`-# 可以回答的人:${userIds.map((id) => `<@${id}>`).join(" ")}`,
|
|
47
51
|
cardAskerRefusal: "這張問題是問另一位發話者的。",
|
|
48
52
|
cardInactive: `這張卡片已經失效(逾時,或 ${ctx.assistant} 重新啟動過);需要的話直接打字回覆。`,
|
|
49
53
|
cardApproved: "✅ 已核准執行。",
|