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,5 +1,65 @@
|
|
|
1
|
+
import type { ChannelContextOptions } from "../contract/channel-context.ts";
|
|
1
2
|
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
bool,
|
|
5
|
+
integer,
|
|
6
|
+
number,
|
|
7
|
+
oneOf,
|
|
8
|
+
optional,
|
|
9
|
+
orOff,
|
|
10
|
+
shape,
|
|
11
|
+
text,
|
|
12
|
+
} from "./schema.ts";
|
|
13
|
+
|
|
14
|
+
/** The Discord bot and its guild, as the top-level `discord` or `discord()` from `pi-roundtable/discord` gives them. */
|
|
15
|
+
export interface DiscordConfig {
|
|
16
|
+
/** The bot token; keep it in `.env`, not in this file. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** The agent server's guild id. */
|
|
19
|
+
guild: string;
|
|
20
|
+
/** The channel the coordinator lives in. */
|
|
21
|
+
entryChannel: string;
|
|
22
|
+
/** The root slash command, without the slash; default the lowercase assistant name. */
|
|
23
|
+
rootCommand?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the owner's conversations have Discord administration tools; default true. `false`
|
|
26
|
+
* leaves the `discord-admin` addon out.
|
|
27
|
+
*/
|
|
28
|
+
admin?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Text appended as it is to the refusal a non-owner gets from the root command, such as a
|
|
31
|
+
* pointer to the commands anyone may use; include the space or punctuation your language
|
|
32
|
+
* needs before it. Default none.
|
|
33
|
+
*/
|
|
34
|
+
refusalHint?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Whose turns in the agents' channels load memory: `"everyone"` (default) lets each speaker
|
|
37
|
+
* read their own, `"owners"` only the owners, so a guest's turn has no memory in its prompt and
|
|
38
|
+
* no memory tools, and never makes claude-bridge refuse the owner afterwards.
|
|
39
|
+
*/
|
|
40
|
+
agentMemory?: "everyone" | "owners";
|
|
41
|
+
/**
|
|
42
|
+
* What a turn addressed to the assistant in a server channel reads of the messages posted there
|
|
43
|
+
* since its last post, mentions or not; on with the defaults when left out. `false` turns it off
|
|
44
|
+
* for the agent server and every plugin's claim.
|
|
45
|
+
*/
|
|
46
|
+
channelContext?: ChannelContextOptions | false;
|
|
47
|
+
/**
|
|
48
|
+
* The divider the assistant posts in a server channel or thread when its conversation starts
|
|
49
|
+
* over (a new conversation by an owner's command), which ends the window channel context reads;
|
|
50
|
+
* never in a direct message. Default `─── new conversation ───`; `false` posts none.
|
|
51
|
+
*/
|
|
52
|
+
freshMarker?: string | false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What `discord.channelContext` takes besides `false`. */
|
|
56
|
+
const channelContextShape = shape({
|
|
57
|
+
fetch: optional(integer(1, 100)),
|
|
58
|
+
keep: optional(integer(1, 100)),
|
|
59
|
+
similarity: optional(number(0, 1)),
|
|
60
|
+
messageChars: optional(integer(1, 4000)),
|
|
61
|
+
botMessageChars: optional(integer(1, 4000)),
|
|
62
|
+
});
|
|
3
63
|
|
|
4
64
|
/** The Discord settings, at the top level or in a Discord adapter. */
|
|
5
65
|
export const discordShape = shape({
|
|
@@ -9,6 +69,9 @@ export const discordShape = shape({
|
|
|
9
69
|
rootCommand: optional(text),
|
|
10
70
|
admin: optional(bool),
|
|
11
71
|
refusalHint: optional(text),
|
|
72
|
+
agentMemory: optional(oneOf<"everyone" | "owners">("everyone", "owners")),
|
|
73
|
+
channelContext: optional(orOff(channelContextShape)),
|
|
74
|
+
freshMarker: optional(orOff(text)),
|
|
12
75
|
});
|
|
13
76
|
|
|
14
77
|
/** The adapters `adapters` takes, by the name their factory gives them. */
|
|
@@ -4,6 +4,7 @@ import type { AgentSeed } from "../agents/agent-rules.ts";
|
|
|
4
4
|
import { ConfigError } from "../domain/errors.ts";
|
|
5
5
|
import type { InterimTextMode } from "../domain/interim.ts";
|
|
6
6
|
import { isLocale, type Locale } from "../i18n/index.ts";
|
|
7
|
+
import type { AccessRules } from "../identity/access-policy.ts";
|
|
7
8
|
import type { OwnerIdentity } from "../identity.ts";
|
|
8
9
|
import {
|
|
9
10
|
type ModelRef,
|
|
@@ -11,11 +12,13 @@ import {
|
|
|
11
12
|
THINKING_LEVELS,
|
|
12
13
|
type ThinkingLevel,
|
|
13
14
|
} from "../models.ts";
|
|
15
|
+
import type { PerPrincipalLimits } from "../modules/background/personal-target.ts";
|
|
14
16
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
15
17
|
import { PRIMARY_CHARS } from "../runtime/interim-text.ts";
|
|
16
18
|
import type { ChannelKey } from "../sessions.ts";
|
|
17
19
|
import type { Tier, TierMembers } from "../speakers.ts";
|
|
18
|
-
import {
|
|
20
|
+
import { type AccessConfig, accessOf, accessShape } from "./access.ts";
|
|
21
|
+
import { type DiscordConfig, discordShape, liftAdapters } from "./adapters.ts";
|
|
19
22
|
import {
|
|
20
23
|
bool,
|
|
21
24
|
guarded,
|
|
@@ -30,6 +33,8 @@ import {
|
|
|
30
33
|
text,
|
|
31
34
|
} from "./schema.ts";
|
|
32
35
|
|
|
36
|
+
export type { DiscordConfig };
|
|
37
|
+
|
|
33
38
|
/** How prompts refer back to the owner. */
|
|
34
39
|
export type Pronouns = "he" | "she" | "they";
|
|
35
40
|
|
|
@@ -43,29 +48,6 @@ export interface TierConfig {
|
|
|
43
48
|
everyone?: boolean;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
/** The Discord bot and its guild, as the top-level `discord` or `discord()` from `pi-roundtable/discord` gives them. */
|
|
47
|
-
export interface DiscordConfig {
|
|
48
|
-
/** The bot token; keep it in `.env`, not in this file. */
|
|
49
|
-
token: string;
|
|
50
|
-
/** The agent server's guild id. */
|
|
51
|
-
guild: string;
|
|
52
|
-
/** The channel the coordinator lives in. */
|
|
53
|
-
entryChannel: string;
|
|
54
|
-
/** The root slash command, without the slash; default the lowercase assistant name. */
|
|
55
|
-
rootCommand?: string;
|
|
56
|
-
/**
|
|
57
|
-
* Whether the owner's conversations have Discord administration tools; default true. `false`
|
|
58
|
-
* leaves the `discord-admin` addon out.
|
|
59
|
-
*/
|
|
60
|
-
admin?: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* Text appended as it is to the refusal a non-owner gets from the root command, such as a
|
|
63
|
-
* pointer to the commands anyone may use; include the space or punctuation your language
|
|
64
|
-
* needs before it. Default none.
|
|
65
|
-
*/
|
|
66
|
-
refusalHint?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
51
|
/** A chat network the host talks through, as its adapter's factory makes it; `adapter` names the network. */
|
|
70
52
|
export interface AdapterConfig {
|
|
71
53
|
readonly adapter: string;
|
|
@@ -77,12 +59,21 @@ export interface DiscordAdapterConfig extends AdapterConfig {
|
|
|
77
59
|
readonly discord: DiscordConfig;
|
|
78
60
|
}
|
|
79
61
|
|
|
62
|
+
/** The configuration's `background`: each person's share of the host's background work. */
|
|
63
|
+
export interface BackgroundConfig {
|
|
64
|
+
perPrincipal?: PerPrincipalLimits;
|
|
65
|
+
}
|
|
66
|
+
|
|
80
67
|
/** What `roundtable.config.ts` gives `defineRoundtable`. */
|
|
81
68
|
export interface RoundtableConfig {
|
|
82
69
|
/** The assistant's display name; default "Roundtable". */
|
|
83
70
|
name?: string;
|
|
84
|
-
/**
|
|
85
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The one owner, as 0.8 wrote it: their id, which is both their principal and their Discord
|
|
73
|
+
* user id, their name, and pronouns. Deprecated, going away in 0.10: write `access` instead,
|
|
74
|
+
* never both; `roundtable upgrade` rewrites it.
|
|
75
|
+
*/
|
|
76
|
+
owner?: { id: string; name: string; pronouns?: Pronouns };
|
|
86
77
|
/**
|
|
87
78
|
* The Discord bot, its guild, and the agent server that lives there. Leave it out for a host
|
|
88
79
|
* without Discord: no Discord plugins, no agent server or agents, no skills, and no tool that
|
|
@@ -113,8 +104,19 @@ export interface RoundtableConfig {
|
|
|
113
104
|
locale?: Locale;
|
|
114
105
|
/** The IANA time zone schedules and stamps use; default "UTC". */
|
|
115
106
|
timeZone?: string;
|
|
116
|
-
/** Who may talk to the agents besides the owner, and
|
|
107
|
+
/** Who may talk to the agents besides the owner, by Discord user and role ids, as 0.8 wrote it. Deprecated with `owner`: write `access` instead. */
|
|
117
108
|
speakers?: { admins?: TierConfig; members?: TierConfig };
|
|
109
|
+
/**
|
|
110
|
+
* Who the host serves: the owners, the first the primary one, and the admins and members by
|
|
111
|
+
* identity (`<provider>:<subject>`), role (`<surface>:role:<name>`), or everyone. Required
|
|
112
|
+
* unless the deprecated `owner` is given, and never with it.
|
|
113
|
+
*/
|
|
114
|
+
access?: AccessConfig;
|
|
115
|
+
/**
|
|
116
|
+
* Each person's limits across all their conversations: `perPrincipal.schedules` they may keep and
|
|
117
|
+
* `perPrincipal.delegations` running at once. Unset, only each conversation's, as in 0.8.
|
|
118
|
+
*/
|
|
119
|
+
background?: BackgroundConfig;
|
|
118
120
|
toolTiers?: Record<string, Tier>;
|
|
119
121
|
/** Prompt files, read once at start; `shared` starts every agent's prompt. */
|
|
120
122
|
prompts?: { shared: string; guest?: string };
|
|
@@ -187,11 +189,13 @@ const isPlugin = (value: unknown): value is RoundtablePlugin =>
|
|
|
187
189
|
|
|
188
190
|
const schema = shape({
|
|
189
191
|
name: optional(text),
|
|
190
|
-
owner:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
owner: optional(
|
|
193
|
+
shape({
|
|
194
|
+
id: text,
|
|
195
|
+
name: text,
|
|
196
|
+
pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
195
199
|
discord: optional(discordShape),
|
|
196
200
|
database: shape({ url: text }),
|
|
197
201
|
dataDir: text,
|
|
@@ -216,6 +220,17 @@ const schema = shape({
|
|
|
216
220
|
speakers: optional(
|
|
217
221
|
shape({ admins: optional(tierMembers), members: optional(tierMembers) }),
|
|
218
222
|
),
|
|
223
|
+
access: optional(accessShape),
|
|
224
|
+
background: optional(
|
|
225
|
+
shape({
|
|
226
|
+
perPrincipal: optional(
|
|
227
|
+
shape({
|
|
228
|
+
schedules: optional(integer(1, 10_000)),
|
|
229
|
+
delegations: optional(integer(1, 1_000)),
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
219
234
|
toolTiers: optional(record(oneOf<Tier>("owner", "admin", "member"))),
|
|
220
235
|
prompts: optional(shape({ shared: text, guest: optional(text) })),
|
|
221
236
|
agents: optional(
|
|
@@ -255,12 +270,6 @@ const schema = shape({
|
|
|
255
270
|
),
|
|
256
271
|
});
|
|
257
272
|
|
|
258
|
-
const PRONOUNS: Record<Pronouns, OwnerIdentity["pronouns"]> = {
|
|
259
|
-
he: { subject: "he", object: "him", possessive: "his" },
|
|
260
|
-
she: { subject: "she", object: "her", possessive: "her" },
|
|
261
|
-
they: { subject: "they", object: "them", possessive: "their" },
|
|
262
|
-
};
|
|
263
|
-
|
|
264
273
|
/** The configuration with every default filled in. */
|
|
265
274
|
export interface ResolvedConfig {
|
|
266
275
|
name: string;
|
|
@@ -269,7 +278,17 @@ export interface ResolvedConfig {
|
|
|
269
278
|
* with Discord. `discord.rootCommand` when Discord is on, else the lowercase assistant name.
|
|
270
279
|
*/
|
|
271
280
|
slug: string;
|
|
281
|
+
/**
|
|
282
|
+
* The primary owner, `access.owners[0]` (or the 0.8 `owner`): their principal id, name, and
|
|
283
|
+
* pronouns, and the Discord user id the Discord parts act for until 0.10.
|
|
284
|
+
*/
|
|
285
|
+
primaryOwner: OwnerIdentity & { id: string; discordId?: string };
|
|
286
|
+
/** The primary owner as 0.8 knew them, `id` their principal id. Deprecated: read `primaryOwner`. */
|
|
272
287
|
owner: OwnerIdentity & { id: string };
|
|
288
|
+
/** Who the host serves, the 0.8 `owner` and `speakers` converted when they are what the configuration gives. */
|
|
289
|
+
access: AccessRules;
|
|
290
|
+
/** What the configuration wrote in a deprecated form, which the host logs once. */
|
|
291
|
+
deprecations: string[];
|
|
273
292
|
/** Undefined on a host without Discord. */
|
|
274
293
|
discord?: {
|
|
275
294
|
token: string;
|
|
@@ -278,6 +297,10 @@ export interface ResolvedConfig {
|
|
|
278
297
|
rootCommand: string;
|
|
279
298
|
admin: boolean;
|
|
280
299
|
refusalHint?: string;
|
|
300
|
+
agentMemory?: "everyone" | "owners";
|
|
301
|
+
/** As configured: `{}` when left out, so the defaults apply, or `false`. */
|
|
302
|
+
channelContext: NonNullable<DiscordConfig["channelContext"]>;
|
|
303
|
+
freshMarker?: string | false;
|
|
281
304
|
};
|
|
282
305
|
databaseUrl: string;
|
|
283
306
|
dataDir: string;
|
|
@@ -288,7 +311,10 @@ export interface ResolvedConfig {
|
|
|
288
311
|
delegation: { model: ModelRef; thinking: ThinkingLevel };
|
|
289
312
|
locale: Locale;
|
|
290
313
|
timeZone: string;
|
|
314
|
+
/** The 0.8 `speakers`; empty when the configuration writes `access`. Deprecated: read `access`. */
|
|
291
315
|
speakers: { admins?: TierMembers; members?: TierMembers };
|
|
316
|
+
/** Each person's limits across their conversations; a limit left out is unset. */
|
|
317
|
+
background: { perPrincipal: PerPrincipalLimits };
|
|
292
318
|
toolTiers: Record<string, Tier>;
|
|
293
319
|
prompts: { shared?: string; guest?: string };
|
|
294
320
|
agents: AgentSeed[];
|
|
@@ -378,13 +404,16 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
378
404
|
const slug =
|
|
379
405
|
discord?.rootCommand ?? name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
380
406
|
const ops = opsOf(config.ops, discord !== undefined);
|
|
407
|
+
const access = accessOf(config, discord !== undefined);
|
|
408
|
+
const { primaryOwner } = access;
|
|
381
409
|
return {
|
|
382
410
|
name,
|
|
383
411
|
slug,
|
|
412
|
+
...access,
|
|
384
413
|
owner: {
|
|
385
|
-
id:
|
|
386
|
-
name:
|
|
387
|
-
pronouns:
|
|
414
|
+
id: primaryOwner.id,
|
|
415
|
+
name: primaryOwner.name,
|
|
416
|
+
pronouns: primaryOwner.pronouns,
|
|
388
417
|
},
|
|
389
418
|
...(discord
|
|
390
419
|
? {
|
|
@@ -397,6 +426,13 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
397
426
|
...(discord.refusalHint === undefined
|
|
398
427
|
? {}
|
|
399
428
|
: { refusalHint: discord.refusalHint }),
|
|
429
|
+
...(discord.agentMemory === undefined
|
|
430
|
+
? {}
|
|
431
|
+
: { agentMemory: discord.agentMemory }),
|
|
432
|
+
channelContext: discord.channelContext ?? {},
|
|
433
|
+
...(discord.freshMarker === undefined
|
|
434
|
+
? {}
|
|
435
|
+
: { freshMarker: discord.freshMarker }),
|
|
400
436
|
},
|
|
401
437
|
}
|
|
402
438
|
: {}),
|
|
@@ -423,6 +459,7 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
423
459
|
...(config.speakers?.admins ? { admins: config.speakers.admins } : {}),
|
|
424
460
|
...(config.speakers?.members ? { members: config.speakers.members } : {}),
|
|
425
461
|
},
|
|
462
|
+
background: { perPrincipal: { ...config.background?.perPrincipal } },
|
|
426
463
|
toolTiers: config.toolTiers ?? {},
|
|
427
464
|
prompts: config.prompts ?? {},
|
|
428
465
|
agents: config.agents ?? [],
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// What a surface tells a turn about the channel around the message it answers. Discord fills it
|
|
2
|
+
// (`DISCORD.channelContext`); the types and the way a turn carries it are any surface's.
|
|
3
|
+
|
|
4
|
+
/** How much of a channel a turn reads as its channel context; every member has a default. */
|
|
5
|
+
export interface ChannelContextOptions {
|
|
6
|
+
/** How many messages before the addressed one are fetched; default 50, at most 100 (Discord's page). */
|
|
7
|
+
fetch?: number;
|
|
8
|
+
/** How many of the window's messages are kept, the newest; default 15. */
|
|
9
|
+
keep?: number;
|
|
10
|
+
/**
|
|
11
|
+
* How alike (0 to 1) two consecutive messages of one author must be to count as one, the later
|
|
12
|
+
* kept, such as a message and its corrected repost; default 0.8.
|
|
13
|
+
*/
|
|
14
|
+
similarity?: number;
|
|
15
|
+
/** Where a message's text is cut; default 500 characters. */
|
|
16
|
+
messageChars?: number;
|
|
17
|
+
/** Where another bot's message is cut; default 80 characters. */
|
|
18
|
+
botMessageChars?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One channel message a turn reads around the message that addressed the assistant. */
|
|
22
|
+
export interface ChannelContextMessage {
|
|
23
|
+
id: string;
|
|
24
|
+
authorId: string;
|
|
25
|
+
/** How the author appears in the channel. */
|
|
26
|
+
authorName: string;
|
|
27
|
+
/** Another bot, or an integration such as a webhook that is not the assistant's. */
|
|
28
|
+
bot: boolean;
|
|
29
|
+
/** One of the host's owners. */
|
|
30
|
+
owner: boolean;
|
|
31
|
+
/** The text, with mentions by name and stickers and files as text, cut where the options say. */
|
|
32
|
+
text: string;
|
|
33
|
+
at: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What was said in a server channel since the assistant last posted there, before the message that
|
|
38
|
+
* addressed it: public channel text, not anyone's private memory, and not from the turn's speaker.
|
|
39
|
+
*/
|
|
40
|
+
export interface ChannelContext {
|
|
41
|
+
/** Oldest first; never empty. */
|
|
42
|
+
readonly messages: readonly ChannelContextMessage[];
|
|
43
|
+
/** The delimited block `withChannelContext` appends to a turn's text. */
|
|
44
|
+
readonly text: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The turn's text, then its channel context when there is one, as `withReference` adds a replied-to message. */
|
|
48
|
+
export function withChannelContext(
|
|
49
|
+
text: string,
|
|
50
|
+
context: ChannelContext | undefined,
|
|
51
|
+
): string {
|
|
52
|
+
return context ? `${text}\n\n${context.text}` : text;
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Locale } from "../i18n/index.ts";
|
|
2
|
+
import type { ActorFacts } from "../identity/actor-facts.ts";
|
|
2
3
|
import type { ChannelKey } from "../sessions.ts";
|
|
3
|
-
import type { Tier } from "../speakers.ts";
|
|
4
|
+
import type { Speaker, Tier } from "../speakers.ts";
|
|
4
5
|
|
|
5
6
|
/** A file attached to a chat message, as the surface reports it; nothing is downloaded yet. */
|
|
6
7
|
export interface AttachmentRef {
|
|
@@ -16,6 +17,21 @@ export interface InboundMessage {
|
|
|
16
17
|
channel: ChannelKey;
|
|
17
18
|
/** Unique per surface; keeps the files of different messages apart. */
|
|
18
19
|
messageId: string;
|
|
20
|
+
/**
|
|
21
|
+
* Who wrote it, as the surface reports them: the router resolves them to `speaker` through the
|
|
22
|
+
* identity service. A surface that leaves it out has it read from `authorId`, `authorName`, and
|
|
23
|
+
* `authorRoleIds` under its own name as the provider, with a `deprecated` warning; that goes
|
|
24
|
+
* away in 1.0.
|
|
25
|
+
*/
|
|
26
|
+
actor?: ActorFacts;
|
|
27
|
+
/**
|
|
28
|
+
* Who the author is, with their principal and tier, as the router resolved them before any
|
|
29
|
+
* claim admits the message: undefined when the access rules serve no one by them, and for a bot
|
|
30
|
+
* or an integration, which are not resolved. Only the router sets it; a surface's own value is
|
|
31
|
+
* dropped. A claim that serves only people the host serves answers only when it is set.
|
|
32
|
+
*/
|
|
33
|
+
speaker?: Speaker;
|
|
34
|
+
/** The id the surface knows the author by; who they are to the host is `speaker`. */
|
|
19
35
|
authorId: string;
|
|
20
36
|
/** How the author appears in the channel, for example a server nickname. */
|
|
21
37
|
authorName: string;
|
|
@@ -77,10 +93,22 @@ export interface BackgroundTarget {
|
|
|
77
93
|
name: string;
|
|
78
94
|
/** How lists such as `/<root> schedule` name it, in the host's locale. */
|
|
79
95
|
label(locale: Locale): string;
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Limits of the schedules made for it; absent, nothing may be scheduled for it. `perPrincipal`,
|
|
98
|
+
* when set, holds each person to that many of its schedules across all their conversations.
|
|
99
|
+
*/
|
|
100
|
+
schedules?: {
|
|
101
|
+
perChannel: number;
|
|
102
|
+
perPrincipal?: number;
|
|
103
|
+
promptChars: number;
|
|
104
|
+
aheadDays: number;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Limits of the delegated tasks reporting to it; absent, nothing may be delegated to it.
|
|
108
|
+
* `maxRunningPerPrincipal`, when set, holds each person to that many running at once across
|
|
109
|
+
* all their conversations.
|
|
110
|
+
*/
|
|
111
|
+
delegation?: { maxRunning: number; maxRunningPerPrincipal?: number };
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
/** A turn nobody wrote in the channel: a schedule's, or a report of work done elsewhere. */
|
|
@@ -91,18 +119,31 @@ export interface BackgroundTurn {
|
|
|
91
119
|
* plugin contributes, and a claim skips one it does not serve.
|
|
92
120
|
*/
|
|
93
121
|
target: string;
|
|
94
|
-
author: { id: string; name: string };
|
|
95
122
|
/**
|
|
96
|
-
*
|
|
97
|
-
* the
|
|
123
|
+
* Whom the turn is for: the principal who set up the work, and the id and name they are shown
|
|
124
|
+
* by, such as the identity they spoke as. The system principal is the host's alone.
|
|
125
|
+
*/
|
|
126
|
+
author: { principalId: string; id: string; name: string };
|
|
127
|
+
/**
|
|
128
|
+
* The tier the turn asks to run at, its creator's when they set it up. It runs at that tier or
|
|
129
|
+
* the principal's own now, whichever is lower.
|
|
98
130
|
*/
|
|
99
|
-
tier
|
|
131
|
+
tier: Tier;
|
|
100
132
|
turnId: string;
|
|
101
133
|
text: string;
|
|
102
134
|
/** It delivers a report the owner is waiting for, so it may ask them on cards. */
|
|
103
135
|
report?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Who the turn runs as, set by the router alone once it checked the author: their principal
|
|
138
|
+
* exists and is not disabled, and the tier is capped at theirs. A claim runs the turn as this
|
|
139
|
+
* speaker; one the caller sets is replaced.
|
|
140
|
+
*/
|
|
141
|
+
speaker?: Speaker;
|
|
104
142
|
}
|
|
105
143
|
|
|
144
|
+
/** Who a background turn runs as, once checked; or why it may not run now. */
|
|
145
|
+
export type BackgroundRunsAs = { speaker: Speaker } | { skipped: string };
|
|
146
|
+
|
|
106
147
|
/**
|
|
107
148
|
* Whose conversation a channel holds, as the claim that owns it names it when `startFresh` says
|
|
108
149
|
* whose it was: any string, such as "owner" or "study". The host reads none of them, so a plugin
|
|
@@ -125,6 +166,12 @@ export type Admission =
|
|
|
125
166
|
};
|
|
126
167
|
/** The turn, run in the channel's queue. */
|
|
127
168
|
run(): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Called instead of `run` when the router drops the message the claim admitted, because
|
|
171
|
+
* its author, once recorded, was not who they were when admitted; frees what the claim
|
|
172
|
+
* holds for it.
|
|
173
|
+
*/
|
|
174
|
+
dropped?(): void;
|
|
128
175
|
/** The log line when the turn throws. */
|
|
129
176
|
failure: string;
|
|
130
177
|
}
|
|
@@ -161,13 +208,32 @@ export interface ChannelClaim {
|
|
|
161
208
|
deleteConversation?(channel: ChannelKey): Promise<void>;
|
|
162
209
|
/** Reports of work started in an owned channel stay in it instead of opening a thread. */
|
|
163
210
|
postsInPlace?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Whether the claim's `background` answers the host's own turns (`SYSTEM_PRINCIPAL`), such as
|
|
213
|
+
* `ops.conversation`'s error reports, in its conversations; true when left out. A claim whose
|
|
214
|
+
* conversations are each private to the person who opened them, such as the web chat's, says
|
|
215
|
+
* false, and a host whose `ops.conversation` names one of them does not start.
|
|
216
|
+
*/
|
|
217
|
+
takesSystemReports?: boolean;
|
|
164
218
|
}
|
|
165
219
|
|
|
166
220
|
/** The conversations of every claimed channel, through the channel queue. */
|
|
167
221
|
export interface ConversationPort {
|
|
168
222
|
/** Never rejects; resolves when the message's turn is done or dropped. */
|
|
169
223
|
handle(message: InboundMessage): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* Runs a turn nobody wrote, as its author's principal at the lower of its tier and theirs; it
|
|
226
|
+
* is skipped, with the reason, when the principal is unknown, disabled, holds no tier, or was
|
|
227
|
+
* last seen too long ago (`access.backgroundStaleDays`), or when the turn names no principal or
|
|
228
|
+
* tier. Never rejects.
|
|
229
|
+
*/
|
|
170
230
|
background(turn: BackgroundTurn): Promise<ScheduledOutcome>;
|
|
231
|
+
/**
|
|
232
|
+
* Who a background turn would run as now, checked as `background` checks it, without running
|
|
233
|
+
* it: for work that must not happen for an author who may not run the turn, such as a
|
|
234
|
+
* schedule's precheck. Throws when the identity service cannot tell.
|
|
235
|
+
*/
|
|
236
|
+
runsAs(turn: BackgroundTurn): Promise<BackgroundRunsAs>;
|
|
171
237
|
/** A contributed background target, read when used; undefined when no plugin contributes it. */
|
|
172
238
|
target(name: string): BackgroundTarget | undefined;
|
|
173
239
|
/** Waits for the channel's running turn, then starts its conversation over; says whose it was. */
|
|
@@ -182,6 +248,8 @@ export interface ConversationPort {
|
|
|
182
248
|
owns(channel: ChannelKey): boolean;
|
|
183
249
|
/** Whether the claim that owns the channel runs background turns there; false when none owns it. */
|
|
184
250
|
takesBackground(channel: ChannelKey): boolean;
|
|
251
|
+
/** Whether the claim that owns the channel takes the host's own turns; false when none owns it. */
|
|
252
|
+
takesSystemReports(channel: ChannelKey): boolean;
|
|
185
253
|
}
|
|
186
254
|
|
|
187
255
|
/** The host's channel queue, as plugins use it. */
|
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
TranscriptEntry,
|
|
6
6
|
TurnResult,
|
|
7
7
|
} from "../domain/conversation.ts";
|
|
8
|
-
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
9
8
|
import type { TurnRequest } from "../domain/ports.ts";
|
|
9
|
+
import type { PromptScope, Prompts } from "../interactions/prompts.ts";
|
|
10
10
|
import type { Logger } from "../log.ts";
|
|
11
11
|
import type { ThinkingSetting } from "../models.ts";
|
|
12
12
|
import type { HostEnv, LinkedSessions } from "../plugin.ts";
|
|
@@ -51,11 +51,18 @@ export interface AgentRuntime {
|
|
|
51
51
|
startFresh(conversation: ChannelKey): Promise<void>;
|
|
52
52
|
/** Like `startFresh`, but removes the conversation and every archive of it for good; called between turns. */
|
|
53
53
|
deleteConversation(conversation: ChannelKey): Promise<void>;
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The conversation's held actions known in memory: those of a turn run since startup, and those
|
|
56
|
+
* `heldActions` or `recentTranscript` restored from the store for it. After a restart, ask one of
|
|
57
|
+
* those first.
|
|
58
|
+
*/
|
|
55
59
|
pendingConfirmation(
|
|
56
60
|
conversation: ChannelKey,
|
|
57
61
|
): PendingConfirmation | undefined;
|
|
58
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The conversation's held actions, restored from the store after a restart, so that
|
|
64
|
+
* `pendingConfirmation` knows them too, as `recentTranscript` does. Expired ones count as none.
|
|
65
|
+
*/
|
|
59
66
|
heldActions(
|
|
60
67
|
conversation: ChannelKey,
|
|
61
68
|
): Promise<PendingConfirmation | undefined>;
|
|
@@ -96,6 +103,13 @@ export interface AgentSessions {
|
|
|
96
103
|
modelOf(name: string): { model: string; thinking: ThinkingSetting };
|
|
97
104
|
/** The channel the scope's turns run in: the group's for a seat in one, else the agent's own. */
|
|
98
105
|
turnChannel(scope: AgentTurnScope): ChannelKey;
|
|
106
|
+
/**
|
|
107
|
+
* Whose turns in an agent's conversation load memory: `"everyone"` (the default) lets each
|
|
108
|
+
* speaker read their own, `"owners"` only the owners, so a speaker below the owner tier gets no
|
|
109
|
+
* memory in their prompt and no memory tools, and their turn never carries private memory into
|
|
110
|
+
* the conversation's history.
|
|
111
|
+
*/
|
|
112
|
+
memory?: "everyone" | "owners";
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
/** Where a runtime keeps each conversation's held actions, so they survive a restart. */
|
|
@@ -124,13 +138,15 @@ export interface RuntimeDeps {
|
|
|
124
138
|
/** What each tool needs; plugin tools are added when the host links, so ask at use time. */
|
|
125
139
|
toolTiers: ToolTiers;
|
|
126
140
|
/**
|
|
127
|
-
* The
|
|
128
|
-
*
|
|
141
|
+
* The approval and question prompts in a conversation for a turn's scope, made with
|
|
142
|
+
* `promptScope(speaker, visibility)`, from its chat surface; undefined when the surface has
|
|
143
|
+
* none, and the action then waits for a message that approves it. A `Speaker`, the 0.8 form,
|
|
144
|
+
* is read as theirs in a shared conversation, with a deprecation warning, until 1.0.
|
|
129
145
|
*/
|
|
130
146
|
prompts(
|
|
131
147
|
conversation: ChannelKey,
|
|
132
|
-
|
|
133
|
-
):
|
|
148
|
+
scope?: PromptScope | Speaker,
|
|
149
|
+
): Prompts | undefined;
|
|
134
150
|
/**
|
|
135
151
|
* The agent server's per-agent settings, for agent turns. The runtime is built before the agent
|
|
136
152
|
* server, so read this when a turn runs, not in the factory; it stays undefined on a host without
|
|
@@ -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,
|