pi-roundtable 0.1.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
import { nearest } from "../registry/contributions.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One field of a configuration: how to check a value and what to say when it is wrong. A
|
|
6
|
+
* checker returns the value it accepts, so a field may also normalize.
|
|
7
|
+
*/
|
|
8
|
+
export interface Field<T> {
|
|
9
|
+
/** What the field holds, for the message: `a string`, `one of a, b`. */
|
|
10
|
+
describe: string;
|
|
11
|
+
check(value: unknown, path: string): T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const wrong = (path: string, wanted: string, got: unknown): ConfigError =>
|
|
15
|
+
new ConfigError(
|
|
16
|
+
`config ${path}: expected ${wanted}, got ${
|
|
17
|
+
got === undefined ? "nothing" : JSON.stringify(got)
|
|
18
|
+
}. Fix the value in roundtable.config.ts.`,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/** A non-empty string. */
|
|
22
|
+
export const text: Field<string> = {
|
|
23
|
+
describe: "a non-empty string",
|
|
24
|
+
check: (value, path) => {
|
|
25
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
26
|
+
throw wrong(path, "a non-empty string", value);
|
|
27
|
+
return value;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True or false. */
|
|
32
|
+
export const bool: Field<boolean> = {
|
|
33
|
+
describe: "true or false",
|
|
34
|
+
check: (value, path) => {
|
|
35
|
+
if (typeof value !== "boolean") throw wrong(path, "true or false", value);
|
|
36
|
+
return value;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** An integer from `min` to `max`. */
|
|
41
|
+
export const integer = (min: number, max: number): Field<number> => ({
|
|
42
|
+
describe: `an integer from ${min} to ${max}`,
|
|
43
|
+
check: (value, path) => {
|
|
44
|
+
if (
|
|
45
|
+
!Number.isInteger(value) ||
|
|
46
|
+
(value as number) < min ||
|
|
47
|
+
(value as number) > max
|
|
48
|
+
)
|
|
49
|
+
throw wrong(path, `an integer from ${min} to ${max}`, value);
|
|
50
|
+
return value as number;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** A number from `min` to `max`. */
|
|
55
|
+
export const number = (min: number, max: number): Field<number> => ({
|
|
56
|
+
describe: `a number from ${min} to ${max}`,
|
|
57
|
+
check: (value, path) => {
|
|
58
|
+
if (
|
|
59
|
+
typeof value !== "number" ||
|
|
60
|
+
Number.isNaN(value) ||
|
|
61
|
+
value < min ||
|
|
62
|
+
value > max
|
|
63
|
+
)
|
|
64
|
+
throw wrong(path, `a number from ${min} to ${max}`, value);
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** One of the listed strings. */
|
|
70
|
+
export const oneOf = <T extends string>(
|
|
71
|
+
...allowed: readonly T[]
|
|
72
|
+
): Field<T> => ({
|
|
73
|
+
describe: `one of ${allowed.join(", ")}`,
|
|
74
|
+
check: (value, path) => {
|
|
75
|
+
if (typeof value !== "string" || !allowed.includes(value as T))
|
|
76
|
+
throw wrong(path, `one of ${allowed.join(", ")}`, value);
|
|
77
|
+
return value as T;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** A list whose items each pass `item`. */
|
|
82
|
+
export const list = <T>(item: Field<T>): Field<T[]> => ({
|
|
83
|
+
describe: `a list of ${item.describe}`,
|
|
84
|
+
check: (value, path) => {
|
|
85
|
+
if (!Array.isArray(value))
|
|
86
|
+
throw wrong(path, `a list of ${item.describe}`, value);
|
|
87
|
+
return value.map((entry, index) => item.check(entry, `${path}[${index}]`));
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** An object whose every value passes `item`, keyed by any string. */
|
|
92
|
+
export const record = <T>(item: Field<T>): Field<Record<string, T>> => ({
|
|
93
|
+
describe: `an object of ${item.describe}`,
|
|
94
|
+
check: (value, path) => {
|
|
95
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
96
|
+
throw wrong(path, `an object of ${item.describe}`, value);
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(value).map(([key, entry]) => [
|
|
99
|
+
key,
|
|
100
|
+
item.check(entry, `${path}.${key}`),
|
|
101
|
+
]),
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** A value a caller-supplied predicate accepts, such as a plugin object. */
|
|
107
|
+
export const guarded = <T>(
|
|
108
|
+
describe: string,
|
|
109
|
+
accepts: (value: unknown) => value is T,
|
|
110
|
+
): Field<T> => ({
|
|
111
|
+
describe,
|
|
112
|
+
check: (value, path) => {
|
|
113
|
+
if (!accepts(value)) throw wrong(path, describe, value);
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** A field that may be left out. */
|
|
119
|
+
export type Optional<T> = { optional: Field<T> };
|
|
120
|
+
|
|
121
|
+
export const optional = <T>(field: Field<T>): Optional<T> => ({
|
|
122
|
+
optional: field,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
type Shape = Record<string, Field<unknown> | Optional<unknown>>;
|
|
126
|
+
|
|
127
|
+
type Checked<S extends Shape> = {
|
|
128
|
+
[K in keyof S as S[K] extends Optional<unknown>
|
|
129
|
+
? never
|
|
130
|
+
: K]: S[K] extends Field<infer T> ? T : never;
|
|
131
|
+
} & {
|
|
132
|
+
[K in keyof S as S[K] extends Optional<unknown>
|
|
133
|
+
? K
|
|
134
|
+
: never]?: S[K] extends Optional<infer T> ? T : never;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const isOptional = (
|
|
138
|
+
field: Field<unknown> | Optional<unknown>,
|
|
139
|
+
): field is Optional<unknown> => "optional" in field;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* An object with the named fields. A key the shape does not name is an error that names the
|
|
143
|
+
* nearest key that is; a missing required key names the key.
|
|
144
|
+
*/
|
|
145
|
+
export function shape<S extends Shape>(fields: S): Field<Checked<S>> {
|
|
146
|
+
const keys = Object.keys(fields);
|
|
147
|
+
return {
|
|
148
|
+
describe: `an object with ${keys.join(", ")}`,
|
|
149
|
+
check: (value, path) => {
|
|
150
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
151
|
+
throw wrong(path, "an object", value);
|
|
152
|
+
const given = value as Record<string, unknown>;
|
|
153
|
+
for (const key of Object.keys(given)) {
|
|
154
|
+
if (keys.includes(key)) continue;
|
|
155
|
+
const meant = nearest(key, keys);
|
|
156
|
+
throw new ConfigError(
|
|
157
|
+
`config ${path === "" ? key : `${path}.${key}`}: unknown key.${
|
|
158
|
+
meant ? ` Did you mean "${meant}"?` : ""
|
|
159
|
+
} The keys here are ${keys.join(", ")}.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const out: Record<string, unknown> = {};
|
|
163
|
+
for (const key of keys) {
|
|
164
|
+
const field = fields[key];
|
|
165
|
+
if (!field) continue;
|
|
166
|
+
const at = path === "" ? key : `${path}.${key}`;
|
|
167
|
+
const raw = given[key];
|
|
168
|
+
if (isOptional(field)) {
|
|
169
|
+
if (raw !== undefined) out[key] = field.optional.check(raw, at);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (raw === undefined)
|
|
173
|
+
throw new ConfigError(
|
|
174
|
+
`config ${at}: required, expected ${field.describe}. Add it to roundtable.config.ts.`,
|
|
175
|
+
);
|
|
176
|
+
out[key] = field.check(raw, at);
|
|
177
|
+
}
|
|
178
|
+
return out as Checked<S>;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
2
|
+
import type { Tier } from "../speakers.ts";
|
|
3
|
+
|
|
4
|
+
/** A file attached to a chat message, as the surface reports it; nothing is downloaded yet. */
|
|
5
|
+
export interface AttachmentRef {
|
|
6
|
+
url: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** MIME type when the surface knows it. */
|
|
9
|
+
contentType?: string;
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The facts a surface reports about one incoming message; it never decides who may talk. */
|
|
14
|
+
export interface InboundMessage {
|
|
15
|
+
channel: ChannelKey;
|
|
16
|
+
/** Unique per surface; keeps the files of different messages apart. */
|
|
17
|
+
messageId: string;
|
|
18
|
+
authorId: string;
|
|
19
|
+
/** How the author appears in the channel, for example a server nickname. */
|
|
20
|
+
authorName: string;
|
|
21
|
+
authorIsBot: boolean;
|
|
22
|
+
/** The roles the author holds in the message's server; absent in direct messages. */
|
|
23
|
+
authorRoleIds?: readonly string[];
|
|
24
|
+
/** The webhook that posted the message; its name is `authorName`. */
|
|
25
|
+
webhookId?: string;
|
|
26
|
+
/** The webhook is one the assistant posts through itself, such as an agent's voice. */
|
|
27
|
+
ownWebhook?: boolean;
|
|
28
|
+
isDirect: boolean;
|
|
29
|
+
/** The server the message was posted in; absent in direct messages. */
|
|
30
|
+
guildId?: string;
|
|
31
|
+
mentionsBot: boolean;
|
|
32
|
+
/** The message replies to one of the bot's messages. */
|
|
33
|
+
repliesToBot: boolean;
|
|
34
|
+
/** Message text with the bot mention removed. */
|
|
35
|
+
text: string;
|
|
36
|
+
attachments: AttachmentRef[];
|
|
37
|
+
/** The message this one replies to, as far as the surface can read it. */
|
|
38
|
+
reference?: {
|
|
39
|
+
text: string;
|
|
40
|
+
attachments: AttachmentRef[];
|
|
41
|
+
/** The name a webhook posted the referenced message under, such as an agent's. */
|
|
42
|
+
webhookName?: string;
|
|
43
|
+
};
|
|
44
|
+
/** The message the author forwarded with this one; its attachments are in `attachments`. */
|
|
45
|
+
forwarded?: {
|
|
46
|
+
text: string;
|
|
47
|
+
/** The source channel, as `<#id>`, and a link to the original message. */
|
|
48
|
+
channelMention: string;
|
|
49
|
+
url: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reactions on a message sent during a turn: added to that turn, or waiting for its own. */
|
|
54
|
+
export const STEERED_MARK = "↪️";
|
|
55
|
+
export const QUEUED_MARK = "⏳";
|
|
56
|
+
|
|
57
|
+
/** How a turn nobody wrote in the channel ended. */
|
|
58
|
+
export type ScheduledOutcome =
|
|
59
|
+
| { status: "ran" }
|
|
60
|
+
| { status: "failed"; error: string }
|
|
61
|
+
| { status: "skipped"; reason: string };
|
|
62
|
+
|
|
63
|
+
/** A turn nobody wrote in the channel: a schedule's, or a report of work done elsewhere. */
|
|
64
|
+
export interface BackgroundTurn {
|
|
65
|
+
channel: ChannelKey;
|
|
66
|
+
/** Whose turn a schedule asked for; a claim skips a mode it does not serve. */
|
|
67
|
+
mode: "owner" | "party";
|
|
68
|
+
author: { id: string; name: string };
|
|
69
|
+
/**
|
|
70
|
+
* The tier the turn runs at, which is its creator's when a person set it up; absent for
|
|
71
|
+
* the operator's own automation, which runs at the owner's.
|
|
72
|
+
*/
|
|
73
|
+
tier?: Tier;
|
|
74
|
+
turnId: string;
|
|
75
|
+
text: string;
|
|
76
|
+
/** It delivers a report the owner is waiting for, so it may ask him on cards. */
|
|
77
|
+
report?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What a claim does with a message in a channel it owns. */
|
|
81
|
+
export type Admission =
|
|
82
|
+
| {
|
|
83
|
+
kind: "turn";
|
|
84
|
+
/**
|
|
85
|
+
* When the channel is busy: whether the message is marked, and first offered to the
|
|
86
|
+
* running turn with `steer`, which says whether the turn took it.
|
|
87
|
+
*/
|
|
88
|
+
busy?: {
|
|
89
|
+
steer?(): Promise<boolean>;
|
|
90
|
+
react(emoji: string): Promise<void>;
|
|
91
|
+
unreact(emoji: string): Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
/** The turn, run in the channel's queue. */
|
|
94
|
+
run(): Promise<void>;
|
|
95
|
+
/** The log line when the turn throws. */
|
|
96
|
+
failure: string;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
kind: "background";
|
|
100
|
+
turn: BackgroundTurn;
|
|
101
|
+
/** Called when the turn did not run. */
|
|
102
|
+
unanswered(outcome: ScheduledOutcome): void;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One plugin's channels. The router asks the claims by descending priority, then registration
|
|
107
|
+
* order, and the first that owns a channel decides everything there: a message it does not
|
|
108
|
+
* admit is dropped, never passed to a lower claim. Each operation runs inside the channel's
|
|
109
|
+
* queue, so none of them queues itself.
|
|
110
|
+
*/
|
|
111
|
+
export interface ChannelClaim {
|
|
112
|
+
name: string;
|
|
113
|
+
priority: number;
|
|
114
|
+
/** Whether the claim owns the channel; `guildId` is the message's server, when routing one. */
|
|
115
|
+
owns(channel: ChannelKey, guildId?: string): boolean;
|
|
116
|
+
/** What to do with a message in an owned channel; undefined drops it. */
|
|
117
|
+
admit(message: InboundMessage): Admission | undefined;
|
|
118
|
+
/** A background turn in an owned channel; a claim without one skips them. */
|
|
119
|
+
background?(turn: BackgroundTurn): Promise<ScheduledOutcome>;
|
|
120
|
+
/** Starts the channel's conversation over, saying whose it was. */
|
|
121
|
+
startFresh(channel: ChannelKey): Promise<string>;
|
|
122
|
+
/** Removes the channel's conversation for good; a claim without one refuses. */
|
|
123
|
+
deleteConversation?(channel: ChannelKey): Promise<void>;
|
|
124
|
+
/** Reports of work started in an owned channel stay in it instead of opening a thread. */
|
|
125
|
+
postsInPlace?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The conversations of every claimed channel, through the channel queue. */
|
|
129
|
+
export interface ConversationPort {
|
|
130
|
+
/** Never rejects; resolves when the message's turn is done or dropped. */
|
|
131
|
+
handle(message: InboundMessage): Promise<void>;
|
|
132
|
+
background(turn: BackgroundTurn): Promise<ScheduledOutcome>;
|
|
133
|
+
/** Waits for the channel's running turn, then starts its conversation over; says whose it was. */
|
|
134
|
+
startFresh(channel: ChannelKey): Promise<string>;
|
|
135
|
+
/** `busy` while a turn runs or waits in the channel. */
|
|
136
|
+
deleteConversation(channel: ChannelKey): Promise<"deleted" | "busy">;
|
|
137
|
+
/** Stops the channel's running turn; false when none runs. */
|
|
138
|
+
stop(channel: ChannelKey): boolean;
|
|
139
|
+
/** Whether reports of work started in the channel stay in it, by its owner's policy. */
|
|
140
|
+
postsInPlace(channel: ChannelKey): boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The host's channel queue, as plugins use it. */
|
|
144
|
+
export interface QueuePort {
|
|
145
|
+
run<T>(channel: ChannelKey, task: () => Promise<T>): Promise<T>;
|
|
146
|
+
/** Tasks of the channel running or waiting. */
|
|
147
|
+
size(channel: ChannelKey): number;
|
|
148
|
+
/** Channels with a task running or waiting. */
|
|
149
|
+
busy(): ChannelKey[];
|
|
150
|
+
/** Called whenever a channel's count of running and waiting tasks changes. */
|
|
151
|
+
onChange(listener: (channel: ChannelKey) => void): void;
|
|
152
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
APIApplicationCommandSubcommandGroupOption,
|
|
3
|
+
APIApplicationCommandSubcommandOption,
|
|
4
|
+
Interaction,
|
|
5
|
+
InteractionContextType,
|
|
6
|
+
RESTPostAPIChatInputApplicationCommandsJSONBody,
|
|
7
|
+
} from "discord.js";
|
|
8
|
+
|
|
9
|
+
/** A group of slash commands and the components they post, handled together. */
|
|
10
|
+
export interface InteractionModule {
|
|
11
|
+
/** Top-level commands of its own; never the configured root command. */
|
|
12
|
+
commands(): RESTPostAPIChatInputApplicationCommandsJSONBody[];
|
|
13
|
+
/** Resolves true when this module handled the interaction. */
|
|
14
|
+
handle(interaction: Interaction): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A subcommand or subcommand group under the root command. */
|
|
18
|
+
export type RootOption =
|
|
19
|
+
| APIApplicationCommandSubcommandOption
|
|
20
|
+
| APIApplicationCommandSubcommandGroupOption;
|
|
21
|
+
|
|
22
|
+
/** A plugin's interactions: its module, and the subcommands it adds under the root command. */
|
|
23
|
+
export interface InteractionContribution {
|
|
24
|
+
module: InteractionModule;
|
|
25
|
+
rootOptions?: readonly RootOption[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The one root command every plugin's subcommands go under, such as `/roundtable`. */
|
|
29
|
+
export interface CommandRoot {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
contexts: readonly InteractionContextType[];
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** A choice among named options; the answer is one option's name. */
|
|
2
|
+
export interface ChoiceQuestion {
|
|
3
|
+
type: "choice";
|
|
4
|
+
instructions: string;
|
|
5
|
+
criteria: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChoiceAnswer {
|
|
9
|
+
choice: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A yes-or-no question; the answer is the probability of yes. */
|
|
14
|
+
export interface YesNoQuestion {
|
|
15
|
+
type: "yesno";
|
|
16
|
+
instructions: string;
|
|
17
|
+
criteria?: { true?: string; false?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** An ordinal question; the answer is a probability for each criterion, in order. */
|
|
21
|
+
export interface ScoreQuestion {
|
|
22
|
+
type: "score";
|
|
23
|
+
instructions: string;
|
|
24
|
+
criteria: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Small judgments about a state: whether a reply approves held actions, how hard a turn is,
|
|
29
|
+
* which agents a group message concerns. Every method throws when it cannot answer; callers
|
|
30
|
+
* fall back to their own default.
|
|
31
|
+
*/
|
|
32
|
+
export interface Judge {
|
|
33
|
+
askYesNo(
|
|
34
|
+
state: Record<string, unknown>,
|
|
35
|
+
questions: Record<string, YesNoQuestion>,
|
|
36
|
+
): Promise<Record<string, number>>;
|
|
37
|
+
askChoice(
|
|
38
|
+
state: Record<string, unknown>,
|
|
39
|
+
name: string,
|
|
40
|
+
question: ChoiceQuestion,
|
|
41
|
+
): Promise<ChoiceAnswer>;
|
|
42
|
+
/** Probabilities indexed like the criteria. */
|
|
43
|
+
askScore(
|
|
44
|
+
state: Record<string, unknown>,
|
|
45
|
+
name: string,
|
|
46
|
+
question: ScoreQuestion,
|
|
47
|
+
): Promise<number[]>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** An image to draw from. */
|
|
51
|
+
export interface ReferenceImage {
|
|
52
|
+
/** Base64 bytes. */
|
|
53
|
+
data: string;
|
|
54
|
+
mimeType: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Draws one image from a prompt and reference images; returns PNG or other decodable bytes. */
|
|
58
|
+
export type ImageDrawer = (
|
|
59
|
+
prompt: string,
|
|
60
|
+
references: readonly ReferenceImage[],
|
|
61
|
+
) => Promise<Uint8Array>;
|
|
62
|
+
|
|
63
|
+
/** The replaceable parts the core runs on; a plugin provides at most the ones it replaces. */
|
|
64
|
+
export interface Providers {
|
|
65
|
+
judge: Judge;
|
|
66
|
+
images: ImageDrawer;
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
|
|
3
|
+
/** A table of the agent server and the columns that make a row unique inside one guild. */
|
|
4
|
+
export interface GuildTable {
|
|
5
|
+
table: string;
|
|
6
|
+
key: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Serializes concurrent boots of the same migration. */
|
|
10
|
+
const LOCK = 7284912301;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gives every row of the tables its guild and makes the guild part of the primary key, so
|
|
14
|
+
* names are unique per guild. Rows without a guild belong to `guildId`, the guild the process
|
|
15
|
+
* is configured for; rows of another guild are never touched. Idempotent, and all or nothing.
|
|
16
|
+
*/
|
|
17
|
+
export async function scopeToGuild(
|
|
18
|
+
sql: SQL,
|
|
19
|
+
guildId: string,
|
|
20
|
+
tables: readonly GuildTable[],
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
await sql.begin(async (tx) => {
|
|
23
|
+
await tx`SELECT pg_advisory_xact_lock(${LOCK})`;
|
|
24
|
+
for (const { table, key } of tables) {
|
|
25
|
+
await tx.unsafe(
|
|
26
|
+
`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS guild_id text`,
|
|
27
|
+
);
|
|
28
|
+
await tx.unsafe(
|
|
29
|
+
`UPDATE ${table} SET guild_id = $1 WHERE guild_id IS NULL`,
|
|
30
|
+
[guildId],
|
|
31
|
+
);
|
|
32
|
+
await tx.unsafe(
|
|
33
|
+
`ALTER TABLE ${table} ALTER COLUMN guild_id SET NOT NULL`,
|
|
34
|
+
);
|
|
35
|
+
await tx.unsafe(`
|
|
36
|
+
DO $migration$
|
|
37
|
+
DECLARE pk text;
|
|
38
|
+
BEGIN
|
|
39
|
+
SELECT c.conname INTO pk FROM pg_constraint c
|
|
40
|
+
WHERE c.conrelid = '${table}'::regclass AND c.contype = 'p'
|
|
41
|
+
AND NOT EXISTS (
|
|
42
|
+
SELECT 1 FROM pg_attribute a
|
|
43
|
+
WHERE a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey)
|
|
44
|
+
AND a.attname = 'guild_id');
|
|
45
|
+
IF pk IS NOT NULL THEN
|
|
46
|
+
EXECUTE format('ALTER TABLE ${table} DROP CONSTRAINT %I', pk);
|
|
47
|
+
ALTER TABLE ${table} ADD PRIMARY KEY (guild_id, ${key.join(", ")});
|
|
48
|
+
END IF;
|
|
49
|
+
END
|
|
50
|
+
$migration$`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|