pi-roundtable 0.8.0 → 0.9.1
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 +115 -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 +618 -86
- 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 +150 -86
- 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 +87 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +82 -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 +42 -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/drain.ts +56 -5
- package/src/core/errors.ts +13 -0
- package/src/core/host-background.ts +56 -0
- package/src/core/host.ts +52 -68
- package/src/core/i18n/cards.ts +8 -4
- package/src/core/i18n/conversation.ts +2 -0
- 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 +56 -4
- 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-queue.ts +18 -1
- package/src/core/routing/channel-router.ts +274 -21
- package/src/core/routing/conversation-turns.ts +45 -10
- package/src/core/routing/surface-port.ts +32 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/abort-guard.ts +58 -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 +139 -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 +174 -190
- 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 +51 -5
- package/src/core/runtime/runtime-types.ts +48 -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 +21 -3
- package/src/core/runtime/thinking-level.ts +47 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/runtime/turn-wrapup.ts +63 -0
- 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 +48 -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,8 +1,8 @@
|
|
|
1
1
|
import type { OutboundReply } from "../domain/conversation.ts";
|
|
2
2
|
import type { InterimPosts } from "../domain/interim.ts";
|
|
3
|
-
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
4
3
|
import type { TurnProgress } from "../domain/progress.ts";
|
|
5
4
|
import { PluginError } from "../errors.ts";
|
|
5
|
+
import type { PromptScope, Prompts } from "../interactions/prompts.ts";
|
|
6
6
|
import type { ChannelKey } from "../sessions.ts";
|
|
7
7
|
import type { Speaker } from "../speakers.ts";
|
|
8
8
|
import type { InboundMessage } from "./channels.ts";
|
|
@@ -58,6 +58,13 @@ export interface ChatSurface {
|
|
|
58
58
|
* `conversations.stop(channel)` when used. Absent = none shown.
|
|
59
59
|
*/
|
|
60
60
|
showStop?(channel: ChannelKey): () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Marks in the channel, publicly, that its conversation just started over, so what a reader of
|
|
63
|
+
* the channel's history would take for the conversation's context ends there; called after the
|
|
64
|
+
* claim's `startFresh` succeeded. A rejection is logged and the restart still counts. Absent =
|
|
65
|
+
* nothing is posted.
|
|
66
|
+
*/
|
|
67
|
+
markFresh?(channel: ChannelKey): Promise<void>;
|
|
61
68
|
/** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
|
|
62
69
|
react?(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
63
70
|
unreact?(
|
|
@@ -66,10 +73,12 @@ export interface ChatSurface {
|
|
|
66
73
|
emoji: string,
|
|
67
74
|
): Promise<void>;
|
|
68
75
|
/**
|
|
69
|
-
* How the
|
|
70
|
-
*
|
|
76
|
+
* How those the scope names approve held actions or answer ask_user in a running turn: the
|
|
77
|
+
* scope's speaker, and in a shared conversation the owners; see `Prompts` and `PromptScope`.
|
|
78
|
+
* Without a scope the prompts are the owners'. Undefined, or absent, = the action is held
|
|
79
|
+
* until a message approves it.
|
|
71
80
|
*/
|
|
72
|
-
prompts?(channel: ChannelKey,
|
|
81
|
+
prompts?(channel: ChannelKey, scope?: PromptScope): Prompts | undefined;
|
|
73
82
|
/**
|
|
74
83
|
* Where a running turn posts the text it writes before its final answer: ordinary messages it
|
|
75
84
|
* may edit in place. Absent, or undefined, = only the final reply is posted.
|
|
@@ -98,8 +107,15 @@ export interface SurfacePort {
|
|
|
98
107
|
showStop(channel: ChannelKey): () => void;
|
|
99
108
|
react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
100
109
|
unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
101
|
-
/**
|
|
102
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The prompts in the channel for the scope's turn; undefined when its surface has none. A
|
|
112
|
+
* `Speaker`, the 0.8 form, is read as theirs in a shared conversation, with a deprecation
|
|
113
|
+
* warning, until 1.0.
|
|
114
|
+
*/
|
|
115
|
+
prompts(
|
|
116
|
+
channel: ChannelKey,
|
|
117
|
+
scope?: PromptScope | Speaker,
|
|
118
|
+
): Prompts | undefined;
|
|
103
119
|
/** The channel's interim posts; undefined when its surface has none. */
|
|
104
120
|
interim(channel: ChannelKey): InterimPosts | undefined;
|
|
105
121
|
/**
|
|
@@ -34,6 +34,20 @@ export interface ConversationRegistry {
|
|
|
34
34
|
get(key: ChannelKey): Promise<ConversationRecord | undefined>;
|
|
35
35
|
/** One principal's conversations, or every one; the most recently active first. */
|
|
36
36
|
list(filter?: { principal?: string }): Promise<ConversationRecord[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Only for migrating a conversation recorded before 0.9, when every turn run without a
|
|
39
|
+
* visibility was recorded `shared` with no principal: makes such a conversation private to
|
|
40
|
+
* `principalId`, as the plugin that hands over what 0.8 left behind decides, such as remote-mcp
|
|
41
|
+
* giving 0.8's sessions to the primary owner. A conversation that is private or names a
|
|
42
|
+
* principal never changes, so adopting is safe to repeat and to race: one principal wins.
|
|
43
|
+
* Returns what is stored, or undefined when the key is unknown. Adopting a conversation 0.9 or
|
|
44
|
+
* later recorded is outside this contract: today it changes one recorded shared like any other,
|
|
45
|
+
* and a later version may refuse it.
|
|
46
|
+
*/
|
|
47
|
+
adopt(
|
|
48
|
+
key: ChannelKey,
|
|
49
|
+
principalId: string,
|
|
50
|
+
): Promise<ConversationRecord | undefined>;
|
|
37
51
|
/** Names a conversation, or clears its name; undefined when the key is unknown. */
|
|
38
52
|
setTitle(
|
|
39
53
|
key: ChannelKey,
|
|
@@ -100,6 +100,19 @@ export class PgConversationRegistry implements ConversationRegistry {
|
|
|
100
100
|
return rows.map(recordOf);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async adopt(
|
|
104
|
+
key: ChannelKey,
|
|
105
|
+
principalId: string,
|
|
106
|
+
): Promise<ConversationRecord | undefined> {
|
|
107
|
+
// The row lock makes a racing adopt wait and then find the conversation no longer no one's.
|
|
108
|
+
const rows: Row[] = await this.#sql`
|
|
109
|
+
UPDATE conversations SET visibility = 'private', principal_id = ${principalId}
|
|
110
|
+
WHERE key = ${key} AND visibility = 'shared' AND principal_id IS NULL
|
|
111
|
+
RETURNING *`;
|
|
112
|
+
const [row] = rows;
|
|
113
|
+
return row ? recordOf(row) : this.get(key);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
async setTitle(
|
|
104
117
|
key: ChannelKey,
|
|
105
118
|
title: string | undefined,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
precheckPlugin,
|
|
14
14
|
scheduleStorePlugin,
|
|
15
15
|
} from "./builtin/stores.ts";
|
|
16
|
+
import { warnDeprecations } from "./config/access.ts";
|
|
16
17
|
import {
|
|
17
18
|
type ResolvedConfig,
|
|
18
19
|
type RoundtableConfig,
|
|
@@ -23,17 +24,23 @@ import { ConfigError } from "./domain/errors.ts";
|
|
|
23
24
|
import { JudgeError } from "./errors.ts";
|
|
24
25
|
import type { RoundtableOptions } from "./host.ts";
|
|
25
26
|
import type { ListenerConfig } from "./http/listeners.ts";
|
|
27
|
+
import { identityPlugin } from "./identity/identity-plugin.ts";
|
|
26
28
|
import { type JudgeModel, piJudgeModel } from "./judging/model-judge.ts";
|
|
27
29
|
import { createLogger, type LogEntry, type Logger } from "./log.ts";
|
|
28
30
|
import { formatModelRef, type ModelRef } from "./models.ts";
|
|
29
31
|
import { ErrorReporter } from "./ops/error-reporter.ts";
|
|
30
32
|
import type { RoundtablePlugin } from "./plugin.ts";
|
|
33
|
+
import {
|
|
34
|
+
BRIDGE_MEMORY_FIX,
|
|
35
|
+
bridgeMemoryProblem,
|
|
36
|
+
configuredCrowd,
|
|
37
|
+
onClaudeBridge,
|
|
38
|
+
} from "./runtime/bridge-guard.ts";
|
|
31
39
|
import {
|
|
32
40
|
type AgentSessionsSlot,
|
|
33
41
|
agentSessionsSlot,
|
|
34
42
|
runtimePlugin,
|
|
35
43
|
} from "./runtime/runtime-plugin.ts";
|
|
36
|
-
import { speakerPolicy } from "./speakers.ts";
|
|
37
44
|
import { toolTiers } from "./tool-tiers.ts";
|
|
38
45
|
|
|
39
46
|
/** What `defineRoundtable` returns: give both to `new Roundtable(options, plugins)`. */
|
|
@@ -111,6 +118,19 @@ function publicListener(
|
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The owner the Discord parts act for: the primary owner, by the Discord user id among their
|
|
123
|
+
* identities, until those parts follow each turn's principal.
|
|
124
|
+
*/
|
|
125
|
+
export function discordOwnerOf(
|
|
126
|
+
config: ResolvedConfig,
|
|
127
|
+
): ResolvedConfig["owner"] {
|
|
128
|
+
return {
|
|
129
|
+
...config.owner,
|
|
130
|
+
id: config.primaryOwner.discordId ?? config.owner.id,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
/** The Discord bot and the agent server that lives there, each in its place in the plugin list. */
|
|
115
135
|
interface DiscordAssembly {
|
|
116
136
|
discord: RoundtablePlugin;
|
|
@@ -129,12 +149,8 @@ function discordAssembly(
|
|
|
129
149
|
errorReporter: ErrorReporter | undefined;
|
|
130
150
|
},
|
|
131
151
|
): DiscordAssembly {
|
|
132
|
-
const { name
|
|
133
|
-
const
|
|
134
|
-
owners: [owner.id],
|
|
135
|
-
...(config.speakers.admins ? { admins: config.speakers.admins } : {}),
|
|
136
|
-
...(config.speakers.members ? { members: config.speakers.members } : {}),
|
|
137
|
-
});
|
|
152
|
+
const { name } = config;
|
|
153
|
+
const owner = discordOwnerOf(config);
|
|
138
154
|
const sharedPrompt = config.prompts.shared
|
|
139
155
|
? readPrompt(config.prompts.shared, "prompts.shared")
|
|
140
156
|
: readPrompt(join(ASSETS, "prompts", "shared.md"), "prompts.shared");
|
|
@@ -147,12 +163,15 @@ function discordAssembly(
|
|
|
147
163
|
token: discord.token,
|
|
148
164
|
ownerId: owner.id,
|
|
149
165
|
ownerName: owner.name,
|
|
150
|
-
speakers,
|
|
151
166
|
rootCommand: discord.rootCommand,
|
|
152
167
|
dataDir: config.dataDir,
|
|
153
168
|
...(discord.refusalHint === undefined
|
|
154
169
|
? {}
|
|
155
170
|
: { refusalHint: discord.refusalHint }),
|
|
171
|
+
channelContext: discord.channelContext,
|
|
172
|
+
...(discord.freshMarker === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { freshMarker: discord.freshMarker }),
|
|
156
175
|
}),
|
|
157
176
|
admin: discord.admin ? [discordAdminPlugin({ owner })] : [],
|
|
158
177
|
skills: config.skills
|
|
@@ -170,7 +189,6 @@ function discordAssembly(
|
|
|
170
189
|
entryChannelId: discord.entryChannel,
|
|
171
190
|
owner,
|
|
172
191
|
assistant: name,
|
|
173
|
-
speakers,
|
|
174
192
|
modelRuntime: shared.modelRuntime,
|
|
175
193
|
dataDir: config.dataDir,
|
|
176
194
|
model: config.model,
|
|
@@ -180,6 +198,7 @@ function discordAssembly(
|
|
|
180
198
|
scratchDir: config.scratchDir,
|
|
181
199
|
shellUser: userInfo().username,
|
|
182
200
|
prompts: { shared: sharedPrompt, guest },
|
|
201
|
+
...(discord.agentMemory ? { memory: discord.agentMemory } : {}),
|
|
183
202
|
avatarListener: "public",
|
|
184
203
|
// resolveConfig refuses Discord without a public address.
|
|
185
204
|
avatarUrl: config.http?.publicUrl ?? "",
|
|
@@ -207,6 +226,7 @@ export async function defineRoundtable(
|
|
|
207
226
|
): Promise<DefinedRoundtable> {
|
|
208
227
|
const config = resolveConfig(input);
|
|
209
228
|
const { name, owner, discord } = config;
|
|
229
|
+
const crowd = configuredCrowd(config.access, config.plugins);
|
|
210
230
|
// Nothing here touches the process: the host applies the environment when it runs.
|
|
211
231
|
const modelRuntime =
|
|
212
232
|
overrides.modelRuntime ??
|
|
@@ -219,7 +239,7 @@ export async function defineRoundtable(
|
|
|
219
239
|
? new ErrorReporter({ destination: config.ops, app: name })
|
|
220
240
|
: undefined;
|
|
221
241
|
const { errorSink } = overrides;
|
|
222
|
-
const logger =
|
|
242
|
+
const logger: Logger =
|
|
223
243
|
overrides.logger ??
|
|
224
244
|
createLogger(
|
|
225
245
|
config.slug,
|
|
@@ -230,6 +250,12 @@ export async function defineRoundtable(
|
|
|
230
250
|
}
|
|
231
251
|
: undefined,
|
|
232
252
|
);
|
|
253
|
+
if (config.memory && crowd && onClaudeBridge(config.model))
|
|
254
|
+
logger.warn(
|
|
255
|
+
{},
|
|
256
|
+
`${bridgeMemoryProblem(formatModelRef(config.model), crowd)} ${BRIDGE_MEMORY_FIX}`,
|
|
257
|
+
);
|
|
258
|
+
warnDeprecations(logger, config.deprecations);
|
|
233
259
|
// The agent server hands the runtime its per-agent settings once it sets up.
|
|
234
260
|
const agentSessions = agentSessionsSlot();
|
|
235
261
|
const assembly = discord
|
|
@@ -250,16 +276,18 @@ export async function defineRoundtable(
|
|
|
250
276
|
agentDir: config.agentDir,
|
|
251
277
|
},
|
|
252
278
|
database: { url: config.databaseUrl },
|
|
253
|
-
toolTiers: toolTiers(config.toolTiers),
|
|
279
|
+
toolTiers: toolTiers(config.toolTiers, logger),
|
|
254
280
|
listeners: [
|
|
255
281
|
...(config.http ? [publicListener(config.http)] : []),
|
|
256
282
|
...(overrides.listeners ?? []),
|
|
257
283
|
],
|
|
258
284
|
judgeModel: judgeThrough(modelRuntime, config.judge.model),
|
|
259
285
|
apiKey: (provider) => registry.getApiKeyForProvider(provider),
|
|
286
|
+
drain: { limitMs: config.drainMs },
|
|
260
287
|
...(overrides.aborted ? { aborted: overrides.aborted } : {}),
|
|
261
288
|
},
|
|
262
289
|
plugins: [
|
|
290
|
+
identityPlugin({ rules: config.access, plugins: config.plugins }),
|
|
263
291
|
...(config.memory ? [memoryPlugin({ owner })] : []),
|
|
264
292
|
scheduleStorePlugin(),
|
|
265
293
|
precheckPlugin(),
|
|
@@ -271,6 +299,7 @@ export async function defineRoundtable(
|
|
|
271
299
|
agentDir: config.agentDir,
|
|
272
300
|
dataDir: config.dataDir,
|
|
273
301
|
delegation: config.delegation,
|
|
302
|
+
perPrincipal: config.background.perPrincipal,
|
|
274
303
|
// A conversation's reports are the modules' to deliver.
|
|
275
304
|
...(errorReporter && "conversation" in errorReporter.destination
|
|
276
305
|
? { errorReporter }
|
|
@@ -290,6 +319,8 @@ export async function defineRoundtable(
|
|
|
290
319
|
agents: agentSessions,
|
|
291
320
|
interimText: config.interimText,
|
|
292
321
|
interimPrimaryChars: config.interimPrimaryChars,
|
|
322
|
+
memory: config.memory,
|
|
323
|
+
...(crowd ? { crowd } : {}),
|
|
293
324
|
}),
|
|
294
325
|
...(assembly ? [assembly.agentServer, assembly.seeds] : []),
|
|
295
326
|
...config.plugins,
|
|
@@ -47,7 +47,7 @@ export function agentPanel(options: AgentPanelOptions): AgentPanel {
|
|
|
47
47
|
interaction.isButton() &&
|
|
48
48
|
interaction.customId.startsWith(AGENT_BUTTON_PREFIX)
|
|
49
49
|
) {
|
|
50
|
-
if (!guard.
|
|
50
|
+
if (!(await guard.allows(interaction))) return true;
|
|
51
51
|
await guard.run(interaction, () => commands.button(interaction));
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
@@ -55,7 +55,7 @@ export function agentPanel(options: AgentPanelOptions): AgentPanel {
|
|
|
55
55
|
interaction.isModalSubmit() &&
|
|
56
56
|
interaction.customId.startsWith(AGENT_MODAL_PREFIX)
|
|
57
57
|
) {
|
|
58
|
-
if (!guard.
|
|
58
|
+
if (!(await guard.allows(interaction))) return true;
|
|
59
59
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
60
60
|
await guard.run(interaction, () => commands.submit(interaction));
|
|
61
61
|
return true;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { messages } from "../i18n/index.ts";
|
|
2
|
+
import type { PromptScope } from "../interactions/prompts.ts";
|
|
3
|
+
import { type Tier, tierAtLeast } from "../speakers.ts";
|
|
4
|
+
import type {
|
|
5
|
+
DiscordOwners,
|
|
6
|
+
DiscordOwnersOptions,
|
|
7
|
+
DiscordUser,
|
|
8
|
+
} from "./discord-owners.ts";
|
|
9
|
+
|
|
10
|
+
/** The people a card is for. */
|
|
11
|
+
export interface Audience {
|
|
12
|
+
/** Whether this user may answer, as they are when they press. */
|
|
13
|
+
allows(user: DiscordUser): Promise<boolean>;
|
|
14
|
+
/** The users the card mentions in a thread, in order. */
|
|
15
|
+
mentions: readonly string[];
|
|
16
|
+
/** Shown on the card; empty for none. */
|
|
17
|
+
note: string;
|
|
18
|
+
/** What someone else who presses it is told. */
|
|
19
|
+
refusal: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The primary owner, who answers every card the owners may, and who else holds a tier. */
|
|
23
|
+
export type CardAudienceOptions = DiscordOwnersOptions;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whom a card is for, by the prompt scope of the turn that asks. The speaker's principal answers
|
|
27
|
+
* their own card on any of their Discord identities that is still theirs when they press when,
|
|
28
|
+
* for an approval, they hold its tier, checked again then, and, for an owner-tier one, they are
|
|
29
|
+
* an owner; the card mentions the identity that spoke where it is one of those, or else all of
|
|
30
|
+
* them. Where the scope
|
|
31
|
+
* escalates to the owners, every owner with a Discord identity answers it too, and a card the
|
|
32
|
+
* speaker cannot answer, or whose principal has no Discord identity, is theirs alone; where it
|
|
33
|
+
* escalates to no one, that card goes to no one. Without a scope, a card is the owners'.
|
|
34
|
+
*/
|
|
35
|
+
export class CardAudiences {
|
|
36
|
+
readonly #options: CardAudienceOptions;
|
|
37
|
+
readonly #owners: DiscordOwners;
|
|
38
|
+
|
|
39
|
+
constructor(options: CardAudienceOptions, owners: DiscordOwners) {
|
|
40
|
+
this.#options = options;
|
|
41
|
+
this.#owners = owners;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Who may approve a call held at `minTier`; undefined when no one may. */
|
|
45
|
+
approval(
|
|
46
|
+
scope: PromptScope | undefined,
|
|
47
|
+
minTier: Tier,
|
|
48
|
+
): Promise<Audience | undefined> {
|
|
49
|
+
return this.#audience(scope, minTier, {
|
|
50
|
+
note: messages().cardApproversNote,
|
|
51
|
+
refusal: messages().cardApproversRefusal,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Who may answer a question; undefined when no one may. */
|
|
56
|
+
question(scope: PromptScope | undefined): Promise<Audience | undefined> {
|
|
57
|
+
return this.#audience(scope, undefined, {
|
|
58
|
+
note: messages().cardAskerNote,
|
|
59
|
+
refusal: messages().cardAskerRefusal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async #audience(
|
|
64
|
+
scope: PromptScope | undefined,
|
|
65
|
+
minTier: Tier | undefined,
|
|
66
|
+
words: { note(...userIds: string[]): string; refusal: string },
|
|
67
|
+
): Promise<Audience | undefined> {
|
|
68
|
+
const { ownerId } = this.#options;
|
|
69
|
+
const escalates = scope?.escalate !== "none";
|
|
70
|
+
const own = scope && (await this.#ownIdentities(scope, minTier));
|
|
71
|
+
if (scope && own) {
|
|
72
|
+
const primary = own.includes(ownerId);
|
|
73
|
+
const mentions = own.includes(scope.speakerId) ? [scope.speakerId] : own;
|
|
74
|
+
return {
|
|
75
|
+
allows: async (user) => {
|
|
76
|
+
// Read the links again: one moved or unlinked since the card was posted is no longer theirs.
|
|
77
|
+
if (
|
|
78
|
+
own.includes(user.id) &&
|
|
79
|
+
(await this.#stillOwn(scope.principalId, user.id))
|
|
80
|
+
)
|
|
81
|
+
return (
|
|
82
|
+
(primary && user.id === ownerId) ||
|
|
83
|
+
this.#owners.holds(user, minTier, scope.principalId)
|
|
84
|
+
);
|
|
85
|
+
return escalates && this.#owners.isOwner(user);
|
|
86
|
+
},
|
|
87
|
+
mentions,
|
|
88
|
+
note: primary ? "" : words.note(...mentions),
|
|
89
|
+
refusal: primary ? messages().cardOwnerOnly : words.refusal,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!escalates) return undefined;
|
|
93
|
+
return {
|
|
94
|
+
allows: (user) => this.#owners.isOwner(user),
|
|
95
|
+
mentions: await this.#owners.ids(),
|
|
96
|
+
note: "",
|
|
97
|
+
refusal: messages().cardOwnerOnly,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether the Discord user is still one of the principal's identities; false when that cannot be read. */
|
|
102
|
+
async #stillOwn(principalId: string, userId: string): Promise<boolean> {
|
|
103
|
+
return (
|
|
104
|
+
(await this.#discordIdentities(principalId))?.includes(userId) ?? false
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The principal's Discord user ids; undefined, and logged, when they cannot be read. */
|
|
109
|
+
async #discordIdentities(principalId: string): Promise<string[] | undefined> {
|
|
110
|
+
const { identity, logger } = this.#options;
|
|
111
|
+
if (!identity) return undefined;
|
|
112
|
+
try {
|
|
113
|
+
return (await identity.identities(principalId))
|
|
114
|
+
.filter((link) => link.provider === "discord")
|
|
115
|
+
.map((link) => link.subject);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.warn(
|
|
118
|
+
{ principal: principalId, err: error },
|
|
119
|
+
"could not read the speaker's identities; none counts as theirs, so their card is the owners'",
|
|
120
|
+
);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The Discord identities of the scope's principal when they may answer their own card: at the
|
|
127
|
+
* tier it needs and, for an owner-tier call, an owner; undefined when they may not, or have
|
|
128
|
+
* none.
|
|
129
|
+
*/
|
|
130
|
+
async #ownIdentities(
|
|
131
|
+
scope: PromptScope,
|
|
132
|
+
minTier: Tier | undefined,
|
|
133
|
+
): Promise<string[] | undefined> {
|
|
134
|
+
if (
|
|
135
|
+
!this.#options.identity ||
|
|
136
|
+
(minTier && !tierAtLeast(scope.tier, minTier))
|
|
137
|
+
)
|
|
138
|
+
return undefined;
|
|
139
|
+
// A turn whose tier defaulted to owner is not an owner's: its owner-tier call is the owners'.
|
|
140
|
+
if (
|
|
141
|
+
minTier === "owner" &&
|
|
142
|
+
!(await this.#owners.isOwnerPrincipal(scope.principalId))
|
|
143
|
+
)
|
|
144
|
+
return undefined;
|
|
145
|
+
const own = await this.#discordIdentities(scope.principalId);
|
|
146
|
+
return own && own.length > 0 ? own : undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|