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
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { attachmentsOf } from "../attachments/turn-attachments.ts";
|
|
2
|
+
import {
|
|
3
|
+
type ChannelContext,
|
|
4
|
+
withChannelContext,
|
|
5
|
+
} from "../contract/channel-context.ts";
|
|
2
6
|
import type {
|
|
3
7
|
Admission,
|
|
4
8
|
BackgroundTarget,
|
|
@@ -8,12 +12,13 @@ import type {
|
|
|
8
12
|
import type { AgentRuntime } from "../contract/runtime.ts";
|
|
9
13
|
import type { SurfacePort } from "../contract/surface.ts";
|
|
10
14
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
11
|
-
import { messages } from "../i18n/index.ts";
|
|
12
15
|
import type { OwnerIdentity } from "../identity.ts";
|
|
13
16
|
import type { Logger } from "../log.ts";
|
|
17
|
+
import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
|
|
14
18
|
import { withReference } from "../routing/message-text.ts";
|
|
15
19
|
import { outcome } from "../routing/settle-turn.ts";
|
|
16
|
-
import {
|
|
20
|
+
import { systemTurn } from "../routing/system-turns.ts";
|
|
21
|
+
import { addressee, attributed } from "../speakers.ts";
|
|
17
22
|
import type { DiscordAgentTeam } from "./agent-team.ts";
|
|
18
23
|
import { discordIdOf } from "./team-keys.ts";
|
|
19
24
|
|
|
@@ -27,23 +32,15 @@ import { discordIdOf } from "./team-keys.ts";
|
|
|
27
32
|
export const AGENT_SERVER_PRIORITY = 100;
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* The host's own background target
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* because the host applies its catalog at startup.
|
|
35
|
+
* The host's own background target, which the agent server answers.
|
|
36
|
+
* @deprecated Use `PERSONAL_TARGET`, the same target, which the core contributes on every host;
|
|
37
|
+
* this name goes away in 1.0.
|
|
34
38
|
*/
|
|
35
|
-
export const OWNER_TARGET: BackgroundTarget =
|
|
36
|
-
name: "owner",
|
|
37
|
-
label: () => messages().scheduleModeOwner,
|
|
38
|
-
schedules: { perChannel: 20, promptChars: 8_000, aheadDays: 366 },
|
|
39
|
-
delegation: { maxRunning: 3 },
|
|
40
|
-
};
|
|
39
|
+
export const OWNER_TARGET: BackgroundTarget = PERSONAL_TARGET;
|
|
41
40
|
|
|
42
41
|
export interface AgentClaimOptions {
|
|
43
42
|
/** The owner, as forwarded messages name them. */
|
|
44
43
|
owner: OwnerIdentity;
|
|
45
|
-
/** Who may talk to the agents, and at which tier. */
|
|
46
|
-
speakers: SpeakerPolicy;
|
|
47
44
|
team: Pick<
|
|
48
45
|
DiscordAgentTeam,
|
|
49
46
|
| "guildId"
|
|
@@ -60,6 +57,13 @@ export interface AgentClaimOptions {
|
|
|
60
57
|
logger: Logger;
|
|
61
58
|
/** Replaceable in tests. */
|
|
62
59
|
fetchImpl?: typeof fetch;
|
|
60
|
+
/**
|
|
61
|
+
* What was said in an agent's channel since its last post, `DISCORD.channelContext`; without it
|
|
62
|
+
* the turns carry none. A group's round carries what was said since its last turn already.
|
|
63
|
+
*/
|
|
64
|
+
channelContext?: (
|
|
65
|
+
message: InboundMessage,
|
|
66
|
+
) => Promise<ChannelContext | undefined>;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -68,24 +72,39 @@ export interface AgentClaimOptions {
|
|
|
68
72
|
* failure notice, is a labelled report turn for the agent.
|
|
69
73
|
*/
|
|
70
74
|
export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
71
|
-
const {
|
|
72
|
-
options;
|
|
75
|
+
const { team, runtime, surface, attachmentDir, logger, fetchImpl } = options;
|
|
73
76
|
const attachments = (message: InboundMessage) =>
|
|
74
77
|
attachmentsOf(message, attachmentDir(message.channel), logger, fetchImpl);
|
|
78
|
+
/** The channel context of a message in an agent's channel; none, logged, when it cannot be read. */
|
|
79
|
+
const around = async (
|
|
80
|
+
message: InboundMessage,
|
|
81
|
+
): Promise<ChannelContext | undefined> => {
|
|
82
|
+
try {
|
|
83
|
+
return await options.channelContext?.(message);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.warn(
|
|
86
|
+
{ channel: message.channel, err: error },
|
|
87
|
+
"channel context not read; the turn runs without it",
|
|
88
|
+
);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
75
92
|
|
|
76
93
|
const webhookReport = (message: InboundMessage): Admission | undefined => {
|
|
77
94
|
const text = message.text.trim();
|
|
78
95
|
if (!text) return undefined;
|
|
79
96
|
return {
|
|
80
97
|
kind: "background",
|
|
81
|
-
turn
|
|
98
|
+
// The host's own turn, at the owner tier as in 0.8; what the webhook posted is untrusted input.
|
|
99
|
+
turn: systemTurn({
|
|
82
100
|
channel: message.channel,
|
|
83
|
-
target:
|
|
101
|
+
target: PERSONAL_TARGET.name,
|
|
84
102
|
author: { id: message.authorId, name: message.authorName },
|
|
103
|
+
tier: "owner",
|
|
85
104
|
turnId: `webhook-${message.messageId}`,
|
|
86
105
|
text: `Webhook "${message.authorName}" posted in your channel:\n${text}`,
|
|
87
106
|
report: true,
|
|
88
|
-
},
|
|
107
|
+
}),
|
|
89
108
|
unanswered: (result) =>
|
|
90
109
|
logger.warn(
|
|
91
110
|
{
|
|
@@ -112,13 +131,9 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
112
131
|
if (!owned) return undefined;
|
|
113
132
|
if (owned === "agent" && message.integration && !message.integration.own)
|
|
114
133
|
return webhookReport(message);
|
|
115
|
-
|
|
116
|
-
const speaker =
|
|
117
|
-
|
|
118
|
-
name: message.authorName,
|
|
119
|
-
...(message.authorRoleIds ? { roleIds: message.authorRoleIds } : {}),
|
|
120
|
-
});
|
|
121
|
-
if (!speaker) return undefined;
|
|
134
|
+
// Who may talk to the agents, and at which tier, the router resolved through the identity service.
|
|
135
|
+
const { speaker } = message;
|
|
136
|
+
if (message.authorIsBot || !speaker) return undefined;
|
|
122
137
|
const { channel, messageId } = message;
|
|
123
138
|
const forwarder = addressee(speaker, options.owner);
|
|
124
139
|
return {
|
|
@@ -147,7 +162,10 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
147
162
|
await team.answerOwner(
|
|
148
163
|
channel,
|
|
149
164
|
speaker,
|
|
150
|
-
attributed(
|
|
165
|
+
attributed(
|
|
166
|
+
speaker,
|
|
167
|
+
withChannelContext(text, await around(message)),
|
|
168
|
+
),
|
|
151
169
|
message.text,
|
|
152
170
|
saved,
|
|
153
171
|
);
|
|
@@ -166,22 +184,26 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
166
184
|
},
|
|
167
185
|
background: async (turn) => {
|
|
168
186
|
// Fail closed: a turn for any target but the owner's never reaches the owner's tools.
|
|
169
|
-
if (turn.target !==
|
|
187
|
+
if (turn.target !== PERSONAL_TARGET.name)
|
|
170
188
|
return {
|
|
171
189
|
status: "skipped",
|
|
172
|
-
reason: `the agent server answers only "${
|
|
190
|
+
reason: `the agent server answers only "${PERSONAL_TARGET.name}" background turns`,
|
|
173
191
|
};
|
|
174
192
|
const owned = team.owns(turn.channel);
|
|
175
193
|
if (owned === "group")
|
|
176
194
|
return { status: "skipped", reason: "groups have no schedules" };
|
|
195
|
+
// Who the turn runs as, checked by the router: its author's principal, at a tier they hold.
|
|
196
|
+
const { speaker } = turn;
|
|
197
|
+
if (!speaker)
|
|
198
|
+
return {
|
|
199
|
+
status: "skipped",
|
|
200
|
+
reason:
|
|
201
|
+
"a background turn reaches the agent server through the router, which says whom it runs as",
|
|
202
|
+
};
|
|
177
203
|
return outcome(
|
|
178
204
|
await team.answerBackground(
|
|
179
205
|
turn.channel,
|
|
180
|
-
|
|
181
|
-
id: turn.author.id,
|
|
182
|
-
name: turn.author.name,
|
|
183
|
-
tier: turn.tier ?? "owner",
|
|
184
|
-
},
|
|
206
|
+
speaker,
|
|
185
207
|
turn.text,
|
|
186
208
|
turn.report === true,
|
|
187
209
|
),
|
|
@@ -144,12 +144,16 @@ export class TeamTurns {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Whether the speaker may approve the held actions:
|
|
148
|
-
*
|
|
147
|
+
* Whether the speaker may approve the held actions by a message: an owner, as the agents'
|
|
148
|
+
* channels are shared, or the principal whose turn held them (by their id, for actions held
|
|
149
|
+
* before 0.9), when their tier now holds every tool. Nobody else in the channel may.
|
|
149
150
|
*/
|
|
150
151
|
#mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean {
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
const theirs =
|
|
153
|
+
pending.principalId !== undefined
|
|
154
|
+
? speaker.principalId === pending.principalId
|
|
155
|
+
: speaker.id === pending.speakerId;
|
|
156
|
+
if (speaker.tier !== "owner" && !theirs) return false;
|
|
153
157
|
const tiers = this.#options.toolTiers ?? toolTiers();
|
|
154
158
|
return pending.calls.every(
|
|
155
159
|
(call) =>
|
|
@@ -2,7 +2,7 @@ import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SQL } from "bun";
|
|
5
|
-
import { agentClaim
|
|
5
|
+
import { agentClaim } from "../agents/agent-claim.ts";
|
|
6
6
|
import { AgentDashboard } from "../agents/agent-dashboard.ts";
|
|
7
7
|
import { PgAgentStore } from "../agents/agent-store.ts";
|
|
8
8
|
import { DiscordAgentTeam } from "../agents/agent-team.ts";
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
SKILLS,
|
|
31
31
|
} from "../services.ts";
|
|
32
32
|
import type { SessionTool } from "../sessions.ts";
|
|
33
|
-
import type {
|
|
33
|
+
import type { Tier } from "../speakers.ts";
|
|
34
34
|
import { DISCORD } from "./discord.ts";
|
|
35
35
|
import { agentOnly } from "./session-tool.ts";
|
|
36
36
|
|
|
@@ -41,8 +41,6 @@ export interface AgentServerOptions {
|
|
|
41
41
|
owner: OwnerIdentity & { id: string };
|
|
42
42
|
/** The assistant's display name, as the confirmation judge names it. */
|
|
43
43
|
assistant: string;
|
|
44
|
-
/** Who may talk to the agents, and at which tier. */
|
|
45
|
-
speakers: SpeakerPolicy;
|
|
46
44
|
/** Shared with the rest of the host, so logins refresh in one place; lists the models an agent may run. */
|
|
47
45
|
modelRuntime: ModelRuntime;
|
|
48
46
|
dataDir: string;
|
|
@@ -61,6 +59,11 @@ export interface AgentServerOptions {
|
|
|
61
59
|
scratchDir?: string;
|
|
62
60
|
/** The host account the agents' shell and file tools run as. */
|
|
63
61
|
shellUser: string;
|
|
62
|
+
/**
|
|
63
|
+
* Whose turns in the agents' channels load memory: `"everyone"` (the default) or `"owners"`;
|
|
64
|
+
* see `AgentSessions.memory`.
|
|
65
|
+
*/
|
|
66
|
+
memory?: "everyone" | "owners";
|
|
64
67
|
/** The prompt every agent starts with, and the one for speakers other than the owner. */
|
|
65
68
|
prompts: { shared: string; guest?: string };
|
|
66
69
|
/** The listener the avatar pictures are served on. */
|
|
@@ -225,6 +228,7 @@ export function agentServerPlugin(
|
|
|
225
228
|
modelOf: (name) => built.modelOf(name),
|
|
226
229
|
skills: (name) => built.skillsOf(name),
|
|
227
230
|
turnChannel: (scope) => built.turnChannel(scope),
|
|
231
|
+
...(options.memory ? { memory: options.memory } : {}),
|
|
228
232
|
};
|
|
229
233
|
options.agents?.bind(agentSessions);
|
|
230
234
|
context.services.provide(AGENTS, {
|
|
@@ -276,17 +280,15 @@ export function agentServerPlugin(
|
|
|
276
280
|
},
|
|
277
281
|
},
|
|
278
282
|
],
|
|
279
|
-
// The schedules and delegated tasks of the owner's and the agents' conversations are its own.
|
|
280
|
-
backgroundTargets: [OWNER_TARGET],
|
|
281
283
|
channels: [
|
|
282
284
|
agentClaim({
|
|
283
285
|
owner: options.owner,
|
|
284
|
-
speakers: options.speakers,
|
|
285
286
|
team: built,
|
|
286
287
|
runtime: running,
|
|
287
288
|
surface: context.surfaces,
|
|
288
289
|
attachmentDir,
|
|
289
290
|
logger,
|
|
291
|
+
channelContext: (message) => discord.channelContext(message),
|
|
290
292
|
}),
|
|
291
293
|
],
|
|
292
294
|
http: [studio.route(options.avatarListener)],
|
|
@@ -8,7 +8,10 @@ import { DISCORD } from "./discord.ts";
|
|
|
8
8
|
import { fixed } from "./session-tool.ts";
|
|
9
9
|
|
|
10
10
|
export interface DiscordAdminOptions {
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Whose Discord permissions the tools check, and who they serve: the primary owner, by their
|
|
13
|
+
* Discord user id, whichever owner's turn calls them.
|
|
14
|
+
*/
|
|
12
15
|
owner: OwnerIdentity & { id: string };
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelContext,
|
|
4
|
+
ChannelContextOptions,
|
|
5
|
+
} from "../contract/channel-context.ts";
|
|
2
6
|
import { type ServiceKey, serviceKey } from "../contract/services.ts";
|
|
7
|
+
import { ChannelContextReader } from "../discord/channel-context.ts";
|
|
3
8
|
import { CommandCollection } from "../discord/command-collection.ts";
|
|
4
9
|
import type { DiscordConnection } from "../discord/connection.ts";
|
|
10
|
+
import { discordDirectChannel } from "../discord/direct-channel.ts";
|
|
11
|
+
import { DiscordOwners } from "../discord/discord-owners.ts";
|
|
5
12
|
import { DiscordSurface } from "../discord/discord-surface.ts";
|
|
6
13
|
import { DispatchThreads } from "../discord/dispatch-threads.ts";
|
|
7
14
|
import type {
|
|
@@ -9,20 +16,33 @@ import type {
|
|
|
9
16
|
CommandRegistrar,
|
|
10
17
|
} from "../discord/interaction-module.ts";
|
|
11
18
|
import { OwnerCards } from "../discord/owner-cards.ts";
|
|
12
|
-
import {
|
|
19
|
+
import { commandGuard, ownerRootCommand } from "../discord/owner-command.ts";
|
|
13
20
|
import { stopButtonModule } from "../discord/stop-button.ts";
|
|
21
|
+
import type { InboundMessage } from "../domain/conversation.ts";
|
|
14
22
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
15
|
-
import
|
|
23
|
+
import { IDENTITY } from "../services.ts";
|
|
16
24
|
|
|
17
25
|
/** The Discord connection and what stands on it, as plugins use it. Provided by the Discord plugin. */
|
|
18
26
|
export interface DiscordServices {
|
|
19
27
|
connection: DiscordConnection;
|
|
20
28
|
/** Adds a plugin's slash commands and components; during setup only. */
|
|
21
29
|
commands: CommandRegistrar;
|
|
22
|
-
/** Who may use the owner's commands. */
|
|
30
|
+
/** Who may use the owner's commands: every owner, by their Discord identity. */
|
|
23
31
|
guard: CommandGuard;
|
|
24
32
|
/** The threads that carry background reports. */
|
|
25
33
|
threads: DispatchThreads;
|
|
34
|
+
/**
|
|
35
|
+
* What was said in a server channel since the assistant last posted there, before `message`:
|
|
36
|
+
* for a claim that answers it, to append to its turn's text with `withChannelContext`. Call it
|
|
37
|
+
* once per answered message, inside the turn's `run`: the messages it returns, and `message`,
|
|
38
|
+
* are remembered per channel and not returned again. Undefined in a direct message, on another
|
|
39
|
+
* surface, when the host's `discord.channelContext` is `false`, when nothing new was said, and
|
|
40
|
+
* when Discord cannot be read (logged). Never rejects. `options` override the host's for this call.
|
|
41
|
+
*/
|
|
42
|
+
channelContext(
|
|
43
|
+
message: InboundMessage,
|
|
44
|
+
options?: ChannelContextOptions,
|
|
45
|
+
): Promise<ChannelContext | undefined>;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/** The Discord connection, provided by the Discord plugin. */
|
|
@@ -31,16 +51,22 @@ export const DISCORD: ServiceKey<DiscordServices> =
|
|
|
31
51
|
|
|
32
52
|
export interface DiscordOptions {
|
|
33
53
|
token: string;
|
|
54
|
+
/**
|
|
55
|
+
* The primary owner's Discord user id: an owner always, and the one the admin tools act as.
|
|
56
|
+
* Every other owner the identity service knows uses the owner's commands and cards too.
|
|
57
|
+
*/
|
|
34
58
|
ownerId: string;
|
|
35
|
-
/** How the owner is named in audit-log reasons and refusals. */
|
|
59
|
+
/** How the primary owner is named in audit-log reasons and refusals. */
|
|
36
60
|
ownerName: string;
|
|
37
|
-
/** Who may answer a card besides the owner. */
|
|
38
|
-
speakers: SpeakerPolicy;
|
|
39
61
|
dataDir: string;
|
|
40
62
|
/** The name of the root slash command, without the slash. */
|
|
41
63
|
rootCommand: string;
|
|
42
64
|
/** Text appended as it is to the refusal a non-owner gets, such as a pointer to the commands anyone may use. */
|
|
43
65
|
refusalHint?: string;
|
|
66
|
+
/** The host's channel context settings; `false` turns it off. Default: on, with the defaults. */
|
|
67
|
+
channelContext?: ChannelContextOptions | false;
|
|
68
|
+
/** The divider posted in a server channel when its conversation starts over; `false` posts none. Default: a short English divider. */
|
|
69
|
+
freshMarker?: string | false;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
/**
|
|
@@ -62,10 +88,12 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
62
88
|
);
|
|
63
89
|
},
|
|
64
90
|
setup: async ({ conversations, services, logger }) => {
|
|
91
|
+
const identity = services.find(IDENTITY);
|
|
65
92
|
// The surface exists by the time a turn posts a card.
|
|
66
93
|
const cards: OwnerCards = new OwnerCards({
|
|
67
94
|
ownerId: options.ownerId,
|
|
68
|
-
|
|
95
|
+
// Who may answer a card besides the primary owner, by the identity service.
|
|
96
|
+
...(identity ? { identity } : {}),
|
|
69
97
|
channel: (channelId) => connected.cardChannel(channelId),
|
|
70
98
|
logger,
|
|
71
99
|
});
|
|
@@ -73,16 +101,23 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
73
101
|
token: options.token,
|
|
74
102
|
ownerId: options.ownerId,
|
|
75
103
|
ownerName: options.ownerName,
|
|
76
|
-
prompts: (channel,
|
|
104
|
+
prompts: (channel, scope) => cards.prompts(channel, scope),
|
|
77
105
|
logger,
|
|
106
|
+
...(options.freshMarker === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { freshMarker: options.freshMarker }),
|
|
78
109
|
});
|
|
79
110
|
surface = connected;
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
// Every owner may use the owner's commands, by the identity service; the primary owner always.
|
|
112
|
+
const guard = commandGuard({
|
|
113
|
+
ownerId: options.ownerId,
|
|
114
|
+
...(identity ? { identity } : {}),
|
|
115
|
+
root: options.rootCommand,
|
|
82
116
|
logger,
|
|
83
|
-
options.
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
...(options.refusalHint === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { refusalHint: options.refusalHint }),
|
|
120
|
+
});
|
|
86
121
|
// A channel whose claim keeps reports in place, such as an open channel, opens no thread.
|
|
87
122
|
const threads = new DispatchThreads({
|
|
88
123
|
host: {
|
|
@@ -101,15 +136,38 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
101
136
|
collection.registrar.add({
|
|
102
137
|
module: stopButtonModule({ guard, conversations }),
|
|
103
138
|
});
|
|
139
|
+
// Marks the owners in channel context: the primary owner, and every other owner's Discord identities.
|
|
140
|
+
const owners = new DiscordOwners({
|
|
141
|
+
ownerId: options.ownerId,
|
|
142
|
+
...(identity ? { identity } : {}),
|
|
143
|
+
logger,
|
|
144
|
+
});
|
|
145
|
+
const context = new ChannelContextReader({
|
|
146
|
+
read: (channelId, before, limit) =>
|
|
147
|
+
connected.messagesBefore(channelId, before, limit),
|
|
148
|
+
owners: () => owners.ids(),
|
|
149
|
+
settings: options.channelContext ?? {},
|
|
150
|
+
logger,
|
|
151
|
+
});
|
|
104
152
|
services.provide(DISCORD, {
|
|
105
153
|
connection: connected,
|
|
106
154
|
commands: collection.registrar,
|
|
107
155
|
guard,
|
|
108
156
|
threads,
|
|
157
|
+
channelContext: (message, given) => context.of(message, given),
|
|
109
158
|
});
|
|
110
159
|
return {
|
|
111
160
|
// The host starts the surface first; it registers the composed commands as it connects.
|
|
112
161
|
surfaces: [connected],
|
|
162
|
+
// A person's Discord direct messages, through a Discord identity of theirs.
|
|
163
|
+
directChannels: [
|
|
164
|
+
discordDirectChannel({
|
|
165
|
+
directChannel: (userId) => connected.directChannel(userId),
|
|
166
|
+
sendDirect: (userId, text) => connected.sendDirect(userId, text),
|
|
167
|
+
...(identity ? { identity } : {}),
|
|
168
|
+
ownerId: options.ownerId,
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
113
171
|
services: [
|
|
114
172
|
// The sweep reads threads through the connection, so it follows the surface's start.
|
|
115
173
|
{ name: "threads", start: () => void threads.sweep() },
|