pi-roundtable 0.7.19 → 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 +140 -0
- package/README.md +72 -15
- package/README.zh-TW.md +65 -13
- package/docs/migrating-0.8.md +132 -0
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +853 -81
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +35 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- 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 +7 -3
- 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 +130 -5
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +28 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +33 -2
- package/src/cli/report.ts +12 -3
- package/src/cli/templates.ts +46 -14
- 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/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +10 -1
- package/src/core/builtin/agent-server.ts +38 -101
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +197 -43
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +120 -0
- package/src/core/config/config.ts +228 -94
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +81 -9
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +31 -11
- package/src/core/contract/surface.ts +35 -6
- package/src/core/conversations/conversation-registry.ts +56 -0
- package/src/core/conversations/conversation-store.ts +126 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +157 -57
- 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-adapter.ts +9 -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 -86
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +10 -0
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +33 -2
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +32 -10
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +11 -9
- 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/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +62 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +226 -17
- package/src/core/routing/conversation-turns.ts +85 -1
- package/src/core/routing/surface-port.ts +35 -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 +32 -10
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +67 -4
- package/src/core/runtime/pi-agent-runtime.ts +143 -135
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +6 -6
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +205 -0
- 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/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +42 -2
- package/src/core/sessions.ts +30 -0
- package/src/core/shared/unix-server.ts +35 -5
- 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 +149 -38
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +460 -0
- package/src/core/testing/test-host.ts +100 -19
- 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 +10 -0
- package/src/index.ts +65 -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 +96 -21
- package/templates/.env.example +1 -1
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +22 -0
- package/templates/adapters/web/package.json.tmpl +26 -0
- package/templates/adapters/web/persona/assistant.md +2 -0
- package/templates/adapters/web/roundtable.config.ts +59 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +14 -2
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ChannelContextOptions } from "../contract/channel-context.ts";
|
|
2
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
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
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The Discord settings, at the top level or in a Discord adapter. */
|
|
65
|
+
export const discordShape = shape({
|
|
66
|
+
token: text,
|
|
67
|
+
guild: text,
|
|
68
|
+
entryChannel: text,
|
|
69
|
+
rootCommand: optional(text),
|
|
70
|
+
admin: optional(bool),
|
|
71
|
+
refusalHint: optional(text),
|
|
72
|
+
agentMemory: optional(oneOf<"everyone" | "owners">("everyone", "owners")),
|
|
73
|
+
channelContext: optional(orOff(channelContextShape)),
|
|
74
|
+
freshMarker: optional(orOff(text)),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** The adapters `adapters` takes, by the name their factory gives them. */
|
|
78
|
+
const ADAPTERS = ["discord"] as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The configuration with its adapters in the places the host reads them: a Discord adapter's
|
|
82
|
+
* settings as the top-level `discord`. Throws a ConfigError naming the adapter by its place.
|
|
83
|
+
*/
|
|
84
|
+
// pi-lens-ignore: no-unknown-returns — the configuration as written is untyped until the schema checks it
|
|
85
|
+
export function liftAdapters(input: unknown): unknown {
|
|
86
|
+
if (typeof input !== "object" || input === null || !("adapters" in input))
|
|
87
|
+
return input;
|
|
88
|
+
const { adapters, ...rest } = input as Record<string, unknown>;
|
|
89
|
+
if (adapters === undefined) return rest;
|
|
90
|
+
if (!Array.isArray(adapters))
|
|
91
|
+
throw new ConfigError(
|
|
92
|
+
`config adapters: expected a list of adapters, such as [discord({ ... })] with discord from pi-roundtable/discord, got ${JSON.stringify(adapters)}. Fix the value in roundtable.config.ts.`,
|
|
93
|
+
);
|
|
94
|
+
const lifted: Record<string, unknown> = { ...rest };
|
|
95
|
+
adapters.forEach((entry: unknown, index) => {
|
|
96
|
+
const at = `adapters[${index}]`;
|
|
97
|
+
const name =
|
|
98
|
+
typeof entry === "object" && entry !== null
|
|
99
|
+
? (entry as { adapter?: unknown }).adapter
|
|
100
|
+
: undefined;
|
|
101
|
+
if (typeof name !== "string")
|
|
102
|
+
throw new ConfigError(
|
|
103
|
+
`config ${at}: expected an adapter, an object its factory makes such as discord({ ... }) from pi-roundtable/discord, got ${JSON.stringify(entry)}. Fix the value in roundtable.config.ts.`,
|
|
104
|
+
);
|
|
105
|
+
if (!(ADAPTERS as readonly string[]).includes(name))
|
|
106
|
+
throw new ConfigError(
|
|
107
|
+
`config ${at}: unknown adapter ${JSON.stringify(name)}. The adapters are ${ADAPTERS.join(", ")}; a chat network that comes as a plugin, such as pi-roundtable-webchat's webChat(), belongs in plugins.`,
|
|
108
|
+
);
|
|
109
|
+
if (lifted.discord !== undefined)
|
|
110
|
+
throw new ConfigError(
|
|
111
|
+
`config ${at}: Discord is configured twice, at the top level or by another adapter. Keep one.`,
|
|
112
|
+
);
|
|
113
|
+
const { adapter: _adapter, ...settings } = entry as Record<string, unknown>;
|
|
114
|
+
lifted.discord = shape({ discord: discordShape }).check(
|
|
115
|
+
settings,
|
|
116
|
+
at,
|
|
117
|
+
).discord;
|
|
118
|
+
});
|
|
119
|
+
return lifted;
|
|
120
|
+
}
|
|
@@ -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,9 +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";
|
|
18
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
16
19
|
import type { Tier, TierMembers } from "../speakers.ts";
|
|
20
|
+
import { type AccessConfig, accessOf, accessShape } from "./access.ts";
|
|
21
|
+
import { type DiscordConfig, discordShape, liftAdapters } from "./adapters.ts";
|
|
17
22
|
import {
|
|
18
23
|
bool,
|
|
19
24
|
guarded,
|
|
@@ -28,6 +33,8 @@ import {
|
|
|
28
33
|
text,
|
|
29
34
|
} from "./schema.ts";
|
|
30
35
|
|
|
36
|
+
export type { DiscordConfig };
|
|
37
|
+
|
|
31
38
|
/** How prompts refer back to the owner. */
|
|
32
39
|
export type Pronouns = "he" | "she" | "they";
|
|
33
40
|
|
|
@@ -41,33 +48,46 @@ export interface TierConfig {
|
|
|
41
48
|
everyone?: boolean;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
/** A chat network the host talks through, as its adapter's factory makes it; `adapter` names the network. */
|
|
52
|
+
export interface AdapterConfig {
|
|
53
|
+
readonly adapter: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What `discord()` from `pi-roundtable/discord` makes: the Discord settings under the adapter's name. */
|
|
57
|
+
export interface DiscordAdapterConfig extends AdapterConfig {
|
|
58
|
+
readonly adapter: "discord";
|
|
59
|
+
readonly discord: DiscordConfig;
|
|
60
|
+
}
|
|
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
|
+
|
|
44
67
|
/** What `roundtable.config.ts` gives `defineRoundtable`. */
|
|
45
68
|
export interface RoundtableConfig {
|
|
46
69
|
/** The assistant's display name; default "Roundtable". */
|
|
47
70
|
name?: string;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*/
|
|
69
|
-
refusalHint?: string;
|
|
70
|
-
};
|
|
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 };
|
|
77
|
+
/**
|
|
78
|
+
* The Discord bot, its guild, and the agent server that lives there. Leave it out for a host
|
|
79
|
+
* without Discord: no Discord plugins, no agent server or agents, no skills, and no tool that
|
|
80
|
+
* messages the owner on Discord; conversations come through the plugins' own surfaces. The same
|
|
81
|
+
* settings may come as `adapters: [discord({...})]` instead, never both.
|
|
82
|
+
*/
|
|
83
|
+
discord?: DiscordConfig;
|
|
84
|
+
/**
|
|
85
|
+
* The chat networks the host talks through, each made by its adapter's factory, such as
|
|
86
|
+
* `discord()` from `pi-roundtable/discord`. A Discord adapter is the same as the top-level
|
|
87
|
+
* `discord`; a chat network that comes as a plugin, such as pi-roundtable-webchat, stays in
|
|
88
|
+
* `plugins`.
|
|
89
|
+
*/
|
|
90
|
+
adapters?: readonly AdapterConfig[];
|
|
71
91
|
database: { url: string };
|
|
72
92
|
/** Where the process keeps its files, such as pictures, attachments, and skills. */
|
|
73
93
|
dataDir: string;
|
|
@@ -84,20 +104,33 @@ export interface RoundtableConfig {
|
|
|
84
104
|
locale?: Locale;
|
|
85
105
|
/** The IANA time zone schedules and stamps use; default "UTC". */
|
|
86
106
|
timeZone?: string;
|
|
87
|
-
/** 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. */
|
|
88
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;
|
|
89
120
|
toolTiers?: Record<string, Tier>;
|
|
90
121
|
/** Prompt files, read once at start; `shared` starts every agent's prompt. */
|
|
91
122
|
prompts?: { shared: string; guest?: string };
|
|
92
|
-
/** The first team, created once; an agent already stored is never overwritten. */
|
|
123
|
+
/** The first team, created once; an agent already stored is never overwritten. Needs `discord`. */
|
|
93
124
|
agents?: AgentSeed[];
|
|
94
125
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
126
|
+
* The `public` listener: the public address that reaches it, and the TCP port (default 3000) or
|
|
127
|
+
* unix socket the process listens on. `socketMode` is the socket file's permission bits
|
|
128
|
+
* (default `0o660`); widen it only for a proxy that runs as another user. With `discord` it is
|
|
129
|
+
* required, with `publicUrl`, the address the agents' avatars are served from; without
|
|
130
|
+
* `discord` leave it out when no plugin serves HTTP, and the host opens no listener.
|
|
98
131
|
*/
|
|
99
|
-
http
|
|
100
|
-
publicUrl
|
|
132
|
+
http?: {
|
|
133
|
+
publicUrl?: string;
|
|
101
134
|
port?: number;
|
|
102
135
|
hostname?: string;
|
|
103
136
|
socketPath?: string;
|
|
@@ -109,13 +142,14 @@ export interface RoundtableConfig {
|
|
|
109
142
|
workDir?: string;
|
|
110
143
|
/**
|
|
111
144
|
* The agents' scratch dir: their shell runs with TMPDIR pointing to it, and writes and removals
|
|
112
|
-
* inside it run without a hold. Default `<os temp dir>/<
|
|
145
|
+
* inside it run without a hold. Default `<os temp dir>/<slug>-scratch`, where the slug is
|
|
146
|
+
* `discord.rootCommand`, or the lowercase assistant name without Discord.
|
|
113
147
|
*/
|
|
114
148
|
scratchDir?: string;
|
|
115
149
|
/**
|
|
116
150
|
* Where the skill registry reads built-in skills and keeps linked repositories. `false` leaves
|
|
117
151
|
* the `skills` addon out: agents carry no skills, and no skill tools exist. Stored skills stay
|
|
118
|
-
* in their tables.
|
|
152
|
+
* in their tables. Without `discord` there are no agents, so the addon is off.
|
|
119
153
|
*/
|
|
120
154
|
skills?: false | { builtinDir?: string; reposDir?: string };
|
|
121
155
|
/**
|
|
@@ -123,8 +157,13 @@ export interface RoundtableConfig {
|
|
|
123
157
|
* `memory` addon out: no memory tools, no memory prompt block, and the table stays as it is.
|
|
124
158
|
*/
|
|
125
159
|
memory?: boolean;
|
|
126
|
-
/**
|
|
127
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Where the process's own errors are reported: to an agent, by name, in its channel (needs
|
|
162
|
+
* `discord`), or to a conversation, by its key `<surface>:<id>`, as a visible message and a
|
|
163
|
+
* report turn its claim answers. The host does not start when no chat surface serves the
|
|
164
|
+
* conversation or no plugin's claim owns it; the web chat takes no error reports.
|
|
165
|
+
*/
|
|
166
|
+
ops?: { agent: string } | { conversation: string };
|
|
128
167
|
/**
|
|
129
168
|
* Whether a turn posts the text it writes before its final answer as it goes: long or
|
|
130
169
|
* structured text as ordinary messages, short narration and the tools called in one small
|
|
@@ -150,19 +189,14 @@ const isPlugin = (value: unknown): value is RoundtablePlugin =>
|
|
|
150
189
|
|
|
151
190
|
const schema = shape({
|
|
152
191
|
name: optional(text),
|
|
153
|
-
owner:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
entryChannel: text,
|
|
162
|
-
rootCommand: optional(text),
|
|
163
|
-
admin: optional(bool),
|
|
164
|
-
refusalHint: optional(text),
|
|
165
|
-
}),
|
|
192
|
+
owner: optional(
|
|
193
|
+
shape({
|
|
194
|
+
id: text,
|
|
195
|
+
name: text,
|
|
196
|
+
pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
discord: optional(discordShape),
|
|
166
200
|
database: shape({ url: text }),
|
|
167
201
|
dataDir: text,
|
|
168
202
|
agentDir: optional(text),
|
|
@@ -186,6 +220,17 @@ const schema = shape({
|
|
|
186
220
|
speakers: optional(
|
|
187
221
|
shape({ admins: optional(tierMembers), members: optional(tierMembers) }),
|
|
188
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
|
+
),
|
|
189
234
|
toolTiers: optional(record(oneOf<Tier>("owner", "admin", "member"))),
|
|
190
235
|
prompts: optional(shape({ shared: text, guest: optional(text) })),
|
|
191
236
|
agents: optional(
|
|
@@ -199,13 +244,15 @@ const schema = shape({
|
|
|
199
244
|
}),
|
|
200
245
|
),
|
|
201
246
|
),
|
|
202
|
-
http:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
247
|
+
http: optional(
|
|
248
|
+
shape({
|
|
249
|
+
publicUrl: optional(text),
|
|
250
|
+
port: optional(integer(1, 65535)),
|
|
251
|
+
hostname: optional(text),
|
|
252
|
+
socketPath: optional(text),
|
|
253
|
+
socketMode: optional(integer(0, 0o777)),
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
209
256
|
avatar: optional(text),
|
|
210
257
|
workDir: optional(text),
|
|
211
258
|
scratchDir: optional(text),
|
|
@@ -213,7 +260,7 @@ const schema = shape({
|
|
|
213
260
|
orOff(shape({ builtinDir: optional(text), reposDir: optional(text) })),
|
|
214
261
|
),
|
|
215
262
|
memory: optional(bool),
|
|
216
|
-
ops: optional(shape({ agent: text })),
|
|
263
|
+
ops: optional(shape({ agent: optional(text), conversation: optional(text) })),
|
|
217
264
|
interimText: optional(oneOf<InterimTextMode>("on", "off")),
|
|
218
265
|
interimPrimaryChars: optional(integer(1, 100_000)),
|
|
219
266
|
plugins: optional(
|
|
@@ -223,23 +270,37 @@ const schema = shape({
|
|
|
223
270
|
),
|
|
224
271
|
});
|
|
225
272
|
|
|
226
|
-
const PRONOUNS: Record<Pronouns, OwnerIdentity["pronouns"]> = {
|
|
227
|
-
he: { subject: "he", object: "him", possessive: "his" },
|
|
228
|
-
she: { subject: "she", object: "her", possessive: "her" },
|
|
229
|
-
they: { subject: "they", object: "them", possessive: "their" },
|
|
230
|
-
};
|
|
231
|
-
|
|
232
273
|
/** The configuration with every default filled in. */
|
|
233
274
|
export interface ResolvedConfig {
|
|
234
275
|
name: string;
|
|
276
|
+
/**
|
|
277
|
+
* The process's short name: the logger's and the scratch dir's, and the root slash command's
|
|
278
|
+
* with Discord. `discord.rootCommand` when Discord is on, else the lowercase assistant name.
|
|
279
|
+
*/
|
|
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`. */
|
|
235
287
|
owner: OwnerIdentity & { id: string };
|
|
236
|
-
|
|
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[];
|
|
292
|
+
/** Undefined on a host without Discord. */
|
|
293
|
+
discord?: {
|
|
237
294
|
token: string;
|
|
238
295
|
guild: string;
|
|
239
296
|
entryChannel: string;
|
|
240
297
|
rootCommand: string;
|
|
241
298
|
admin: boolean;
|
|
242
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;
|
|
243
304
|
};
|
|
244
305
|
databaseUrl: string;
|
|
245
306
|
dataDir: string;
|
|
@@ -250,12 +311,17 @@ export interface ResolvedConfig {
|
|
|
250
311
|
delegation: { model: ModelRef; thinking: ThinkingLevel };
|
|
251
312
|
locale: Locale;
|
|
252
313
|
timeZone: string;
|
|
314
|
+
/** The 0.8 `speakers`; empty when the configuration writes `access`. Deprecated: read `access`. */
|
|
253
315
|
speakers: { admins?: TierMembers; members?: TierMembers };
|
|
316
|
+
/** Each person's limits across their conversations; a limit left out is unset. */
|
|
317
|
+
background: { perPrincipal: PerPrincipalLimits };
|
|
254
318
|
toolTiers: Record<string, Tier>;
|
|
255
319
|
prompts: { shared?: string; guest?: string };
|
|
256
320
|
agents: AgentSeed[];
|
|
257
|
-
http
|
|
258
|
-
|
|
321
|
+
/** Undefined when no `http` is configured, and the host opens no `public` listener. */
|
|
322
|
+
http?: {
|
|
323
|
+
/** Always set with Discord. */
|
|
324
|
+
publicUrl?: string;
|
|
259
325
|
port: number;
|
|
260
326
|
hostname?: string;
|
|
261
327
|
socketPath?: string;
|
|
@@ -267,7 +333,7 @@ export interface ResolvedConfig {
|
|
|
267
333
|
/** `false` when the skills addon is off. */
|
|
268
334
|
skills: false | { builtinDir?: string; reposDir?: string };
|
|
269
335
|
memory: boolean;
|
|
270
|
-
ops?: { agent: string };
|
|
336
|
+
ops?: { agent: string } | { conversation: ChannelKey };
|
|
271
337
|
interimText: InterimTextMode;
|
|
272
338
|
interimPrimaryChars: number;
|
|
273
339
|
plugins: RoundtablePlugin[];
|
|
@@ -282,32 +348,94 @@ function modelOf(value: string, path: string): ModelRef {
|
|
|
282
348
|
return parsed;
|
|
283
349
|
}
|
|
284
350
|
|
|
351
|
+
/** Where the errors go: one of an agent or a conversation; an agent only where the agents live. */
|
|
352
|
+
function opsOf(
|
|
353
|
+
ops: { agent?: string; conversation?: string } | undefined,
|
|
354
|
+
withDiscord: boolean,
|
|
355
|
+
): ResolvedConfig["ops"] {
|
|
356
|
+
if (!ops) return undefined;
|
|
357
|
+
const { agent, conversation } = ops;
|
|
358
|
+
if (agent !== undefined && conversation !== undefined)
|
|
359
|
+
throw new ConfigError(
|
|
360
|
+
"config ops: name an agent or a conversation, not both. Keep the one the reports go to.",
|
|
361
|
+
);
|
|
362
|
+
if (agent !== undefined) {
|
|
363
|
+
if (!withDiscord)
|
|
364
|
+
throw new ConfigError(
|
|
365
|
+
'config ops.agent: the agents live in Discord, which is not configured. Report to a conversation a plugin\'s chat surface serves instead, with ops: { conversation: "<surface>:<id>" }; the web chat takes no error reports.',
|
|
366
|
+
);
|
|
367
|
+
return { agent };
|
|
368
|
+
}
|
|
369
|
+
if (conversation === undefined)
|
|
370
|
+
throw new ConfigError(
|
|
371
|
+
'config ops: name an agent or a conversation, such as ops: { agent: "infra" } or ops: { conversation: "<surface>:<id>" }.',
|
|
372
|
+
);
|
|
373
|
+
if (!/^[^:]+:.+$/.test(conversation))
|
|
374
|
+
throw new ConfigError(
|
|
375
|
+
`config ops.conversation: expected a conversation key <surface>:<id>, got ${JSON.stringify(conversation)}. Write the surface's prefix, a colon, and the conversation's id.`,
|
|
376
|
+
);
|
|
377
|
+
return { conversation: conversation as ChannelKey };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Refuses what only the agent server serves on a host without Discord, naming the key. */
|
|
381
|
+
function refuseAgentServerKeys(config: RoundtableConfig): void {
|
|
382
|
+
if (config.agents && config.agents.length > 0)
|
|
383
|
+
throw new ConfigError(
|
|
384
|
+
"config agents: the agents live in Discord, which is not configured. Configure discord, or remove agents.",
|
|
385
|
+
);
|
|
386
|
+
if (config.skills)
|
|
387
|
+
throw new ConfigError(
|
|
388
|
+
"config skills: the skills are the agents', and the agents live in Discord, which is not configured. Configure discord, or remove skills.",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
285
392
|
/** The configuration checked against its schema, defaults filled; throws a ConfigError naming the key and the fix. */
|
|
286
393
|
export function resolveConfig(input: unknown): ResolvedConfig {
|
|
287
|
-
const config = schema.check(input, "") as RoundtableConfig;
|
|
394
|
+
const config = schema.check(liftAdapters(input), "") as RoundtableConfig;
|
|
288
395
|
const name = config.name ?? "Roundtable";
|
|
289
396
|
const model = modelOf(config.model, "model");
|
|
290
397
|
const thinking = config.thinking ?? "medium";
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
398
|
+
const { discord, http } = config;
|
|
399
|
+
if (discord && !http?.publicUrl)
|
|
400
|
+
throw new ConfigError(
|
|
401
|
+
"config http.publicUrl: required with discord, expected the public address that reaches the bot; Discord fetches the agents' avatars from it. Add it to roundtable.config.ts.",
|
|
402
|
+
);
|
|
403
|
+
if (!discord) refuseAgentServerKeys(config);
|
|
404
|
+
const slug =
|
|
405
|
+
discord?.rootCommand ?? name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
406
|
+
const ops = opsOf(config.ops, discord !== undefined);
|
|
407
|
+
const access = accessOf(config, discord !== undefined);
|
|
408
|
+
const { primaryOwner } = access;
|
|
294
409
|
return {
|
|
295
410
|
name,
|
|
411
|
+
slug,
|
|
412
|
+
...access,
|
|
296
413
|
owner: {
|
|
297
|
-
id:
|
|
298
|
-
name:
|
|
299
|
-
pronouns:
|
|
300
|
-
},
|
|
301
|
-
discord: {
|
|
302
|
-
token: config.discord.token,
|
|
303
|
-
guild: config.discord.guild,
|
|
304
|
-
entryChannel: config.discord.entryChannel,
|
|
305
|
-
rootCommand,
|
|
306
|
-
admin: config.discord.admin ?? true,
|
|
307
|
-
...(config.discord.refusalHint === undefined
|
|
308
|
-
? {}
|
|
309
|
-
: { refusalHint: config.discord.refusalHint }),
|
|
414
|
+
id: primaryOwner.id,
|
|
415
|
+
name: primaryOwner.name,
|
|
416
|
+
pronouns: primaryOwner.pronouns,
|
|
310
417
|
},
|
|
418
|
+
...(discord
|
|
419
|
+
? {
|
|
420
|
+
discord: {
|
|
421
|
+
token: discord.token,
|
|
422
|
+
guild: discord.guild,
|
|
423
|
+
entryChannel: discord.entryChannel,
|
|
424
|
+
rootCommand: slug,
|
|
425
|
+
admin: discord.admin ?? true,
|
|
426
|
+
...(discord.refusalHint === undefined
|
|
427
|
+
? {}
|
|
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 }),
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
: {}),
|
|
311
439
|
databaseUrl: config.database.url,
|
|
312
440
|
dataDir: config.dataDir,
|
|
313
441
|
agentDir: config.agentDir ?? `${config.dataDir}/pi`,
|
|
@@ -331,24 +459,30 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
331
459
|
...(config.speakers?.admins ? { admins: config.speakers.admins } : {}),
|
|
332
460
|
...(config.speakers?.members ? { members: config.speakers.members } : {}),
|
|
333
461
|
},
|
|
462
|
+
background: { perPrincipal: { ...config.background?.perPrincipal } },
|
|
334
463
|
toolTiers: config.toolTiers ?? {},
|
|
335
464
|
prompts: config.prompts ?? {},
|
|
336
465
|
agents: config.agents ?? [],
|
|
337
|
-
http
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
466
|
+
...(http
|
|
467
|
+
? {
|
|
468
|
+
http: {
|
|
469
|
+
...(http.publicUrl ? { publicUrl: http.publicUrl } : {}),
|
|
470
|
+
port: http.port ?? 3000,
|
|
471
|
+
...(http.hostname ? { hostname: http.hostname } : {}),
|
|
472
|
+
...(http.socketPath ? { socketPath: http.socketPath } : {}),
|
|
473
|
+
...(http.socketMode === undefined
|
|
474
|
+
? {}
|
|
475
|
+
: { socketMode: http.socketMode }),
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
: {}),
|
|
346
479
|
...(config.avatar ? { avatar: config.avatar } : {}),
|
|
347
480
|
workDir: config.workDir ?? `${config.dataDir}/work`,
|
|
348
|
-
scratchDir: config.scratchDir ?? join(tmpdir(), `${
|
|
349
|
-
|
|
481
|
+
scratchDir: config.scratchDir ?? join(tmpdir(), `${slug}-scratch`),
|
|
482
|
+
// Without Discord there are no agents to carry skills, so the addon is off.
|
|
483
|
+
skills: config.skills ?? (discord ? {} : false),
|
|
350
484
|
memory: config.memory ?? true,
|
|
351
|
-
...(
|
|
485
|
+
...(ops ? { ops } : {}),
|
|
352
486
|
interimText: config.interimText ?? "on",
|
|
353
487
|
interimPrimaryChars: config.interimPrimaryChars ?? PRIMARY_CHARS,
|
|
354
488
|
plugins: config.plugins ?? [],
|
|
@@ -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
|
+
}
|