pi-roundtable 0.26.3 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +23 -0
- package/README.zh-TW.md +31 -8
- package/docs/plugins.md +176 -7
- package/package.json +1 -1
- package/src/core/agents/agent-book.ts +34 -1
- package/src/core/agents/agent-claim.ts +25 -5
- package/src/core/agents/agent-dashboard.ts +23 -2
- package/src/core/agents/agent-messages.ts +28 -10
- package/src/core/agents/agent-ports.ts +10 -0
- package/src/core/agents/agent-prompt.ts +17 -1
- package/src/core/agents/agent-rules.ts +11 -1
- package/src/core/agents/agent-schema.ts +22 -0
- package/src/core/agents/agent-statements.ts +16 -2
- package/src/core/agents/agent-store.ts +30 -20
- package/src/core/agents/agent-team-fake-runtime.ts +48 -0
- package/src/core/agents/agent-team-fixture.ts +32 -44
- package/src/core/agents/agent-team.ts +69 -74
- package/src/core/agents/agent-tool-schema.ts +84 -0
- package/src/core/agents/agent-tools.ts +50 -85
- package/src/core/agents/group-turns.ts +132 -41
- package/src/core/agents/helper-create.ts +180 -0
- package/src/core/agents/helper-ops.ts +33 -0
- package/src/core/agents/helper-test-kit.ts +72 -0
- package/src/core/agents/helper-tools.ts +104 -0
- package/src/core/agents/helper-work.ts +101 -0
- package/src/core/agents/message-target.ts +15 -23
- package/src/core/agents/routes.ts +347 -0
- package/src/core/agents/team-approval.ts +32 -0
- package/src/core/agents/team-editing.ts +97 -32
- package/src/core/agents/team-helpers.ts +216 -0
- package/src/core/agents/team-lifecycle.ts +26 -1
- package/src/core/agents/team-options.ts +8 -0
- package/src/core/agents/team-prompt.ts +75 -0
- package/src/core/agents/team-selection.ts +31 -13
- package/src/core/agents/team-status.ts +6 -0
- package/src/core/agents/team-text.ts +20 -4
- package/src/core/agents/team-turn-types.ts +12 -1
- package/src/core/agents/team-turns.ts +25 -31
- package/src/core/builtin/agent-server.ts +37 -2
- package/src/core/builtin/modules.ts +4 -4
- package/src/core/config/adapters.ts +12 -0
- package/src/core/config/agent-team.ts +2 -0
- package/src/core/config/config-schema.ts +4 -0
- package/src/core/config/config.ts +48 -2
- package/src/core/config/helpers.ts +60 -0
- package/src/core/config/ops.ts +8 -0
- package/src/core/config/routing.ts +296 -0
- package/src/core/contract/channels.ts +73 -2
- package/src/core/contract/surface.ts +31 -1
- package/src/core/define-roundtable.ts +8 -1
- package/src/core/discord/agent-discord.ts +67 -6
- package/src/core/discord/discord-surface.ts +24 -24
- package/src/core/discord/message-changes.ts +66 -0
- package/src/core/discord/poster/poster-checks.ts +62 -8
- package/src/core/discord/poster/poster.ts +41 -0
- package/src/core/discord/poster/rest-poster-read.ts +130 -0
- package/src/core/discord/poster/rest-poster.ts +38 -120
- package/src/core/domain/round.ts +31 -0
- package/src/core/host-registration.ts +15 -0
- package/src/core/host.ts +6 -0
- package/src/core/i18n/conversation.ts +3 -0
- package/src/core/i18n/dashboard.ts +4 -0
- package/src/core/ops/error-reporter.ts +4 -0
- package/src/core/ops/ops-alerts.ts +102 -6
- package/src/core/plugin.ts +17 -0
- package/src/core/registry/contributions.ts +1 -26
- package/src/core/registry/surface-service.ts +41 -0
- package/src/core/routing/channel-router.ts +54 -3
- package/src/core/routing/claim-events.ts +42 -0
- package/src/core/routing/message-observers.ts +86 -0
- package/src/core/routing/peer-bots.ts +149 -0
- package/src/core/routing/surface-port.ts +4 -0
- package/src/core/services.ts +7 -6
- package/src/core/testing/fake-poster-guild.ts +1 -1
- package/src/core/testing/fake-poster-reads.ts +96 -0
- package/src/core/testing/fake-poster.ts +62 -94
- package/src/core/tool-set.snapshot.json +8 -0
- package/src/discord/index.ts +2 -0
- package/src/index.ts +18 -1
- package/src/kit/channels.ts +1 -0
- package/src/kit/index.ts +5 -0
- package/src/kit/ops-alerts.ts +4 -0
- package/src/testing.ts +18 -1
|
@@ -5,7 +5,7 @@ import type { ThinkingSetting } from "../models.ts";
|
|
|
5
5
|
import type { AgentDirectory } from "../services.ts";
|
|
6
6
|
import { zonedStamp } from "../time.ts";
|
|
7
7
|
import type { ChannelMessage } from "./agent-ports.ts";
|
|
8
|
-
import { characterOf } from "./agent-rules.ts";
|
|
8
|
+
import { characterOf, isHelper } from "./agent-rules.ts";
|
|
9
9
|
import { describeThinking } from "./agent-settings.ts";
|
|
10
10
|
import type { Agent } from "./agent-store.ts";
|
|
11
11
|
|
|
@@ -34,10 +34,21 @@ export function channelIdFromRef(channel: string): string {
|
|
|
34
34
|
|
|
35
35
|
/** Every agent and group as agent_list shows them, before the category layout. */
|
|
36
36
|
export function teamListText(store: Pick<AgentDirectory, "agents" | "groups">) {
|
|
37
|
-
const
|
|
37
|
+
const all = store.agents();
|
|
38
|
+
const line = (a: Agent, indent: string) => {
|
|
38
39
|
const channel = a.channelId ? `<#${a.channelId}>` : "no channel";
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const temp = isHelper(a)
|
|
41
|
+
? ` (temp, of ${a.parent}, ${a.finishedAt ? "finished" : "working"})`
|
|
42
|
+
: "";
|
|
43
|
+
return `${indent}- ${a.name}${temp} "${a.displayName}" — ${channel}, ${a.status}`;
|
|
44
|
+
};
|
|
45
|
+
// A temp knight is listed under its parent.
|
|
46
|
+
const agents = all
|
|
47
|
+
.filter((a) => !isHelper(a))
|
|
48
|
+
.flatMap((a) => [
|
|
49
|
+
line(a, ""),
|
|
50
|
+
...all.filter((h) => h.parent === a.name).map((h) => line(h, " ")),
|
|
51
|
+
]);
|
|
41
52
|
const groups = store
|
|
42
53
|
.groups()
|
|
43
54
|
.map(
|
|
@@ -63,6 +74,11 @@ export function agentDetails(
|
|
|
63
74
|
`Name: ${agent.name}`,
|
|
64
75
|
`Display name: ${agent.displayName}`,
|
|
65
76
|
`Status: ${agent.status}`,
|
|
77
|
+
...(isHelper(agent)
|
|
78
|
+
? [
|
|
79
|
+
`Temp knight of: ${agent.parent} (${agent.finishedAt ? "finished" : "working"})`,
|
|
80
|
+
]
|
|
81
|
+
: []),
|
|
66
82
|
`Channel: ${agent.channelId ? `<#${agent.channelId}>` : "none"}`,
|
|
67
83
|
...(conversations.length === 0
|
|
68
84
|
? []
|
|
@@ -22,6 +22,8 @@ import type { ToolTiers } from "../tool-tiers.ts";
|
|
|
22
22
|
import type { AgentChannels, AgentTurnRunner } from "./agent-ports.ts";
|
|
23
23
|
import type { Agent, PgAgentStore } from "./agent-store.ts";
|
|
24
24
|
import type { RelevanceScorer } from "./group-round.ts";
|
|
25
|
+
import type { HelperWork } from "./helper-work.ts";
|
|
26
|
+
import type { Routes } from "./routes.ts";
|
|
25
27
|
|
|
26
28
|
/** Agent messages without an owner message in between (spec behavior 13). */
|
|
27
29
|
export const MAX_CHAIN_MESSAGES = 8;
|
|
@@ -62,9 +64,14 @@ export interface TeamTurnsOptions {
|
|
|
62
64
|
*/
|
|
63
65
|
surfaces?: Pick<
|
|
64
66
|
SurfacePort,
|
|
65
|
-
"sendReply" | "interim" | "roomSurfaces" | "of" | "progress"
|
|
67
|
+
"sendReply" | "interim" | "roomSurfaces" | "of" | "progress" | "groupRound"
|
|
66
68
|
>;
|
|
67
69
|
studio: Pick<AvatarStudio, "url">;
|
|
70
|
+
/**
|
|
71
|
+
* The host's routing table, which says where work no person started lands. Without one the team
|
|
72
|
+
* builds the defaults, every place being what the host chose before `routing` existed.
|
|
73
|
+
*/
|
|
74
|
+
routes?: Routes;
|
|
68
75
|
/** Set once the runtime exists, which itself needs the team's tools. */
|
|
69
76
|
runtime: () => AgentTurnRunner;
|
|
70
77
|
/** The tools every agent turn runs with. */
|
|
@@ -150,4 +157,8 @@ export interface TurnHost {
|
|
|
150
157
|
/** Whether the speaker's tier holds every tool of the held actions. */
|
|
151
158
|
mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean;
|
|
152
159
|
turnChannel(scope: AgentTurnScope): ChannelKey;
|
|
160
|
+
/** The routing table. */
|
|
161
|
+
readonly routes: Routes;
|
|
162
|
+
/** What keeps a temp knight working; a turn or a message to one brackets itself with it. */
|
|
163
|
+
readonly helpers: Pick<HelperWork, "begin" | "end">;
|
|
153
164
|
}
|
|
@@ -15,12 +15,14 @@ import { isQuiet, replyBody } from "../presentation/reply-body.ts";
|
|
|
15
15
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
16
16
|
import { withReplyFiles } from "../reply-files.ts";
|
|
17
17
|
import { endOf, settleTurn } from "../routing/settle-turn.ts";
|
|
18
|
-
import {
|
|
19
|
-
import { type Speaker, tierAtLeast } from "../speakers.ts";
|
|
18
|
+
import type { Speaker } from "../speakers.ts";
|
|
20
19
|
import { toolTiers } from "../tool-tiers.ts";
|
|
21
20
|
import { AgentMessages } from "./agent-messages.ts";
|
|
22
21
|
import type { Agent } from "./agent-store.ts";
|
|
23
22
|
import { GroupTurns } from "./group-turns.ts";
|
|
23
|
+
import { HelperWork } from "./helper-work.ts";
|
|
24
|
+
import { type Routes, routesOf } from "./routes.ts";
|
|
25
|
+
import { mayApprove } from "./team-approval.ts";
|
|
24
26
|
import {
|
|
25
27
|
agentOfChannel,
|
|
26
28
|
channelIdOf,
|
|
@@ -28,8 +30,6 @@ import {
|
|
|
28
30
|
coordinatorOf,
|
|
29
31
|
discordIdOf,
|
|
30
32
|
discordKey,
|
|
31
|
-
keysOf,
|
|
32
|
-
roomScope,
|
|
33
33
|
scopeOf,
|
|
34
34
|
} from "./team-keys.ts";
|
|
35
35
|
import {
|
|
@@ -49,6 +49,9 @@ export { MAX_CHAIN_MESSAGES, type TeamTurnsOptions };
|
|
|
49
49
|
*/
|
|
50
50
|
export class TeamTurns {
|
|
51
51
|
readonly #options: TeamTurnsOptions;
|
|
52
|
+
readonly routes: Routes;
|
|
53
|
+
/** What keeps each temp knight working, and so when it is finished. */
|
|
54
|
+
readonly helpers: HelperWork;
|
|
52
55
|
/** The chain of each running turn, by session key, read by message_agent. */
|
|
53
56
|
readonly #chains = new Map<ChannelKey, Chain>();
|
|
54
57
|
/** The channel each working agent's turn posts in. */
|
|
@@ -63,8 +66,19 @@ export class TeamTurns {
|
|
|
63
66
|
|
|
64
67
|
constructor(options: TeamTurnsOptions) {
|
|
65
68
|
this.#options = options;
|
|
69
|
+
this.routes = routesOf(options);
|
|
70
|
+
this.helpers = new HelperWork({
|
|
71
|
+
store: options.store,
|
|
72
|
+
queue: options.queue,
|
|
73
|
+
runtime: options.runtime,
|
|
74
|
+
roomSurfaces: () => options.surfaces?.roomSurfaces() ?? [],
|
|
75
|
+
changed: () => options.changed(),
|
|
76
|
+
logger: options.logger,
|
|
77
|
+
});
|
|
66
78
|
const host: TurnHost = {
|
|
67
79
|
options,
|
|
80
|
+
routes: this.routes,
|
|
81
|
+
helpers: this.helpers,
|
|
68
82
|
chains: this.#chains,
|
|
69
83
|
turn: (agent, scope, postTo, text, chain, extra) =>
|
|
70
84
|
this.#turn(agent, scope, postTo, text, chain, extra),
|
|
@@ -151,29 +165,8 @@ export class TeamTurns {
|
|
|
151
165
|
);
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
/**
|
|
155
|
-
* Whether the speaker may approve the held actions by a message, call by call: an owner, as the
|
|
156
|
-
* agents' channels are shared, or, for a call whose tool named its approvers, one of those
|
|
157
|
-
* principals, or, for any other call, the principal whose turn held them (by their id, for
|
|
158
|
-
* actions held before 0.9). Their tier must now hold every call. Nobody else in the channel
|
|
159
|
-
* may, so a call that named approvers is not its speaker's to approve unless named. The gate
|
|
160
|
-
* checks the named approvers again when the turn begins, whoever asked for the release.
|
|
161
|
-
*/
|
|
162
168
|
#mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean {
|
|
163
|
-
|
|
164
|
-
pending.principalId !== undefined
|
|
165
|
-
? speaker.principalId === pending.principalId
|
|
166
|
-
: speaker.id === pending.speakerId;
|
|
167
|
-
const tiers = this.#options.toolTiers ?? toolTiers();
|
|
168
|
-
// Nothing held names an approver to go by: only the owners and the speaker whose turn held it.
|
|
169
|
-
if (pending.calls.length === 0) return speaker.tier === "owner" || theirs;
|
|
170
|
-
return pending.calls.every(
|
|
171
|
-
(call) =>
|
|
172
|
-
(speaker.tier === "owner" ||
|
|
173
|
-
(call.approverIds ? mayApproveNamed(speaker, call) : theirs)) &&
|
|
174
|
-
tiers.allows(speaker.tier, call.tool) &&
|
|
175
|
-
(!call.minTier || tierAtLeast(speaker.tier, call.minTier)),
|
|
176
|
-
);
|
|
169
|
+
return mayApprove(this.#options.toolTiers ?? toolTiers(), speaker, pending);
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
/** The surfaces that host rooms; none for a team without a surface port. */
|
|
@@ -207,6 +200,8 @@ export class TeamTurns {
|
|
|
207
200
|
): Promise<TurnResult> {
|
|
208
201
|
const { logger, startTyping, showStop } = this.#options;
|
|
209
202
|
const stopTyping = startTyping(postTo);
|
|
203
|
+
// A temp knight is working for as long as a turn of it runs.
|
|
204
|
+
this.helpers.begin(agent.name);
|
|
210
205
|
// A group round has several sessions; only an agent's own channel maps to one.
|
|
211
206
|
const hideStop = scope.group ? undefined : showStop(postTo);
|
|
212
207
|
this.#chains.set(scope.session, chain);
|
|
@@ -250,6 +245,7 @@ export class TeamTurns {
|
|
|
250
245
|
this.#posting.delete(scope.session);
|
|
251
246
|
this.#working.delete(agent.name);
|
|
252
247
|
this.#active(agent.name, scope.group);
|
|
248
|
+
this.helpers.end(agent.name);
|
|
253
249
|
}
|
|
254
250
|
this.#options.events?.turnEnded({
|
|
255
251
|
...turn,
|
|
@@ -450,11 +446,7 @@ export class TeamTurns {
|
|
|
450
446
|
const home =
|
|
451
447
|
entryChannelId !== undefined
|
|
452
448
|
? discordKey(entryChannelId)
|
|
453
|
-
:
|
|
454
|
-
store,
|
|
455
|
-
coordinator.name,
|
|
456
|
-
this.#options.surfaces?.roomSurfaces() ?? [],
|
|
457
|
-
)[0];
|
|
449
|
+
: this.routes.primary(coordinator.name);
|
|
458
450
|
if (!home)
|
|
459
451
|
throw new AgentError(`${coordinator.name} has no room to post in`);
|
|
460
452
|
await this.#post(home, coordinator, { chunks: splitReply(text) });
|
|
@@ -478,6 +470,8 @@ export class TeamTurns {
|
|
|
478
470
|
* One owner message in a group channel: scored, then answered by the chosen members one at
|
|
479
471
|
* a time, each seeing every group message since its last turn. Call inside the queue.
|
|
480
472
|
*/
|
|
473
|
+
/** Ends the group channel's running round on Stop: the sessions to stop, or undefined when none runs. */
|
|
474
|
+
readonly stopRound = (channel: ChannelKey) => this.#groups.stopRound(channel);
|
|
481
475
|
answerGroup(
|
|
482
476
|
channel: ChannelKey,
|
|
483
477
|
speaker: Speaker,
|
|
@@ -9,8 +9,11 @@ import { DiscordAgentTeam } from "../agents/agent-team.ts";
|
|
|
9
9
|
import { AGENT_TOOLS } from "../agents/agent-tools.ts";
|
|
10
10
|
import { FileAvatarStudio } from "../agents/avatar-studio.ts";
|
|
11
11
|
import { RelevanceScorer } from "../agents/group-round.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { Routes } from "../agents/routes.ts";
|
|
13
|
+
import { agentOfChannel, discordKey } from "../agents/team-keys.ts";
|
|
13
14
|
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
15
|
+
import type { ResolvedHelpers } from "../config/helpers.ts";
|
|
16
|
+
import type { ResolvedRouting } from "../config/routing.ts";
|
|
14
17
|
import type { AgentSessions } from "../contract/runtime.ts";
|
|
15
18
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
16
19
|
import { ConfigError } from "../domain/errors.ts";
|
|
@@ -116,6 +119,10 @@ export interface AgentServerOptions {
|
|
|
116
119
|
avatarNeutral?: string;
|
|
117
120
|
/** Reports the process's own errors to an agent; without one nothing is reported. */
|
|
118
121
|
errorReporter?: ErrorReporter;
|
|
122
|
+
/** The checked `routing` block: where work no person started lands. Without it every place is what it was before `routing`. */
|
|
123
|
+
routing?: ResolvedRouting;
|
|
124
|
+
/** The temp knights' limits; `false` leaves `helper_create` and `helper_end` out. Without it the defaults apply. */
|
|
125
|
+
helpers?: ResolvedHelpers | false;
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
/** The name of the agent server's plugin, as `serviceStarted` events name it. */
|
|
@@ -147,6 +154,9 @@ const AGENT_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
147
154
|
agent_get: "member",
|
|
148
155
|
message_agent: "member",
|
|
149
156
|
channel_read: "member",
|
|
157
|
+
// A knight may make temp knights for whoever is talking to it; the limits hold the cost.
|
|
158
|
+
helper_create: "member",
|
|
159
|
+
helper_end: "member",
|
|
150
160
|
};
|
|
151
161
|
|
|
152
162
|
/** The tables the agent server keeps: its agents and groups. */
|
|
@@ -186,6 +196,7 @@ export function agentServerPlugin(
|
|
|
186
196
|
const teamId = options.guildId ?? LOCAL_TEAM_ID;
|
|
187
197
|
/** Set once the plugin has set up; the preflight reads it after every plugin linked. */
|
|
188
198
|
let hostsRooms: (() => boolean) | undefined;
|
|
199
|
+
let linkRoutes: (() => void) | undefined;
|
|
189
200
|
return {
|
|
190
201
|
name: AGENT_SERVER_PLUGIN,
|
|
191
202
|
// The agent server's own tables; the host runs them before any setup, in this order. The
|
|
@@ -200,6 +211,8 @@ export function agentServerPlugin(
|
|
|
200
211
|
throw new ConfigError(
|
|
201
212
|
"the agents need a surface that hosts their rooms, such as pi-roundtable-web, and none is installed. Without Discord channels there is nowhere to talk to them; add a plugin whose chat surface declares agentRooms: true, or configure discord.",
|
|
202
213
|
);
|
|
214
|
+
// The surfaces the rules name exist only once every plugin has registered its own.
|
|
215
|
+
linkRoutes?.();
|
|
203
216
|
},
|
|
204
217
|
setup: async (context) => {
|
|
205
218
|
// Absent on a host without Discord: then the team has no channels or dashboard.
|
|
@@ -261,7 +274,21 @@ export function agentServerPlugin(
|
|
|
261
274
|
: undefined;
|
|
262
275
|
const sharedPrompt = shared ? () => shared.text() : undefined;
|
|
263
276
|
const guestPrompt = guest ? () => guest.text() : undefined;
|
|
277
|
+
const routes = new Routes({
|
|
278
|
+
routing: options.routing,
|
|
279
|
+
store,
|
|
280
|
+
roomSurfaces: () => surfaces.roomSurfaces(),
|
|
281
|
+
latest: (agent) => store.conversations.latest(agent),
|
|
282
|
+
logger,
|
|
283
|
+
});
|
|
284
|
+
// The surfaces the rules name are registered by the time every plugin has set up.
|
|
285
|
+
linkRoutes = () =>
|
|
286
|
+
routes.validate({
|
|
287
|
+
names: surfaces.names?.() ?? [],
|
|
288
|
+
discordChannels: connection !== undefined,
|
|
289
|
+
});
|
|
264
290
|
const built: DiscordAgentTeam = new DiscordAgentTeam({
|
|
291
|
+
routes,
|
|
265
292
|
guildId: teamId,
|
|
266
293
|
...(options.entryChannelId !== undefined
|
|
267
294
|
? { entryChannelId: options.entryChannelId }
|
|
@@ -319,6 +346,7 @@ export function agentServerPlugin(
|
|
|
319
346
|
: {}),
|
|
320
347
|
...(skills ? { skills } : {}),
|
|
321
348
|
modes: () => context.services.find(MODES),
|
|
349
|
+
...(options.helpers !== undefined ? { helpers: options.helpers } : {}),
|
|
322
350
|
principals: () => context.services.find(IDENTITY),
|
|
323
351
|
...(threads ? { threads } : {}),
|
|
324
352
|
logger,
|
|
@@ -349,6 +377,7 @@ export function agentServerPlugin(
|
|
|
349
377
|
runtime: running,
|
|
350
378
|
approvals: confirmations,
|
|
351
379
|
avatars: studio,
|
|
380
|
+
routes,
|
|
352
381
|
workspace: {
|
|
353
382
|
workDir: options.workDir,
|
|
354
383
|
...(agentSessions.scratchDir
|
|
@@ -373,7 +402,7 @@ export function agentServerPlugin(
|
|
|
373
402
|
options.errorReporter?.connect({
|
|
374
403
|
channelOf: (name) =>
|
|
375
404
|
store.agent(name)?.status === "active"
|
|
376
|
-
?
|
|
405
|
+
? routes.primary(name)
|
|
377
406
|
: undefined,
|
|
378
407
|
post: (channel, text) =>
|
|
379
408
|
surfaces.sendReply(channel, { chunks: splitReply(text) }),
|
|
@@ -411,6 +440,12 @@ export function agentServerPlugin(
|
|
|
411
440
|
team: built,
|
|
412
441
|
runtime: running,
|
|
413
442
|
surface: context.surfaces,
|
|
443
|
+
integrationChannel: (origin) => {
|
|
444
|
+
const agent = agentOfChannel(store, surfaces, origin);
|
|
445
|
+
return agent
|
|
446
|
+
? routes.of(agent.name, "integration", origin)
|
|
447
|
+
: origin;
|
|
448
|
+
},
|
|
414
449
|
attachmentDir,
|
|
415
450
|
...(options.attachmentDownload
|
|
416
451
|
? { attachmentDownload: options.attachmentDownload }
|
|
@@ -233,20 +233,20 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
233
233
|
reportsReach = () => {
|
|
234
234
|
if (!surfaces.of(channel))
|
|
235
235
|
throw new ConfigError(
|
|
236
|
-
`config
|
|
236
|
+
`config ${reporter.configKey}: no chat surface serves ${JSON.stringify(channel)}, so its error reports could never be posted. Name a conversation of a surface a plugin brings, or leave ops out.`,
|
|
237
237
|
);
|
|
238
238
|
if (!conversations.owns(channel))
|
|
239
239
|
throw new ConfigError(
|
|
240
|
-
`config
|
|
240
|
+
`config ${reporter.configKey}: no plugin's conversations own ${JSON.stringify(channel)}, so nothing answers its error reports. Name a conversation a plugin's claim owns, or leave ops out.`,
|
|
241
241
|
);
|
|
242
242
|
if (!conversations.takesBackground(channel))
|
|
243
243
|
throw new ConfigError(
|
|
244
|
-
`config
|
|
244
|
+
`config ${reporter.configKey}: the claim that owns ${JSON.stringify(channel)} takes no background turns, so nothing answers its error reports. Name a conversation whose claim takes background turns, or leave ops out.`,
|
|
245
245
|
);
|
|
246
246
|
// A report turn is the system principal's, which a conversation private to its person refuses.
|
|
247
247
|
if (!conversations.takesSystemReports(channel))
|
|
248
248
|
throw new ConfigError(
|
|
249
|
-
`config
|
|
249
|
+
`config ${reporter.configKey}: the claim that owns ${JSON.stringify(channel)} keeps its conversations private to their people, so it takes no system error reports and each would only be logged. Name a conversation of a surface whose conversations are shared, such as a Discord channel, or report to an agent with ops.agent.`,
|
|
250
250
|
);
|
|
251
251
|
};
|
|
252
252
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelContextOptions } from "../contract/channel-context.ts";
|
|
2
2
|
import { ConfigError } from "../domain/errors.ts";
|
|
3
|
+
import { type PeerBotsConfig, peerBotsShape } from "../routing/peer-bots.ts";
|
|
3
4
|
import {
|
|
4
5
|
bool,
|
|
5
6
|
guarded,
|
|
@@ -51,6 +52,14 @@ export interface DiscordConfig {
|
|
|
51
52
|
* for the agent server and every plugin's claim.
|
|
52
53
|
*/
|
|
53
54
|
channelContext?: ChannelContextOptions | false;
|
|
55
|
+
/**
|
|
56
|
+
* The other bots whose mentions and replies the host answers like a guest's, so separate bots
|
|
57
|
+
* can talk to each other. Explicit Discord user ids only, each for every server or limited to
|
|
58
|
+
* some; there is no wildcard and no "all bots", and left out, no bot is answered. A peer is
|
|
59
|
+
* served only as far as `access` serves its identity (`discord:<id>`), and a channel answers at
|
|
60
|
+
* most `maxRounds` (default 3) of their messages in a row until a person posts.
|
|
61
|
+
*/
|
|
62
|
+
peerBots?: PeerBotsConfig;
|
|
54
63
|
/**
|
|
55
64
|
* The divider the assistant posts in a server channel or thread when its conversation starts
|
|
56
65
|
* over (a new conversation by an owner's command), which ends the window channel context reads;
|
|
@@ -101,6 +110,7 @@ export interface ResolvedDiscord {
|
|
|
101
110
|
agentSpeakers?: "everyone" | "owners";
|
|
102
111
|
/** As configured: `{}` when left out, so the defaults apply, or `false`. */
|
|
103
112
|
channelContext: NonNullable<DiscordConfig["channelContext"]>;
|
|
113
|
+
peerBots?: PeerBotsConfig;
|
|
104
114
|
freshMarker?: string | false;
|
|
105
115
|
formatting?: string | false;
|
|
106
116
|
cardGraceSeconds: number;
|
|
@@ -133,6 +143,7 @@ export function resolvedDiscord(
|
|
|
133
143
|
? {}
|
|
134
144
|
: { agentSpeakers: discord.agentSpeakers }),
|
|
135
145
|
channelContext: discord.channelContext ?? {},
|
|
146
|
+
...(discord.peerBots === undefined ? {} : { peerBots: discord.peerBots }),
|
|
136
147
|
...(discord.freshMarker === undefined
|
|
137
148
|
? {}
|
|
138
149
|
: { freshMarker: discord.freshMarker }),
|
|
@@ -180,6 +191,7 @@ export const discordShape = shape({
|
|
|
180
191
|
agentMemory: optional(oneOf<"everyone" | "owners">("everyone", "owners")),
|
|
181
192
|
agentSpeakers: optional(oneOf<"everyone" | "owners">("everyone", "owners")),
|
|
182
193
|
channelContext: optional(orOff(channelContextShape)),
|
|
194
|
+
peerBots: optional(peerBotsShape),
|
|
183
195
|
freshMarker: optional(orOff(text)),
|
|
184
196
|
formatting: optional(orOff(text)),
|
|
185
197
|
cardGraceSeconds: optional(integer(1, 86_400)),
|
|
@@ -46,6 +46,7 @@ export function checkAgentTeamKeys(
|
|
|
46
46
|
interface AgentOnlyKeys {
|
|
47
47
|
agents?: readonly unknown[];
|
|
48
48
|
skills?: unknown;
|
|
49
|
+
helpers?: unknown;
|
|
49
50
|
ops?: { agent?: string; conversation?: string };
|
|
50
51
|
discord?: {
|
|
51
52
|
agentMemory?: unknown;
|
|
@@ -63,6 +64,7 @@ export function checkAgentServerOff(config: AgentOnlyKeys): void {
|
|
|
63
64
|
const used: [string, boolean][] = [
|
|
64
65
|
["agents", (config.agents?.length ?? 0) > 0],
|
|
65
66
|
["skills", config.skills !== undefined && config.skills !== false],
|
|
67
|
+
["helpers", config.helpers !== undefined && config.helpers !== false],
|
|
66
68
|
["ops.agent", config.ops?.agent !== undefined],
|
|
67
69
|
["discord.agentMemory", config.discord?.agentMemory !== undefined],
|
|
68
70
|
["discord.agentSpeakers", config.discord?.agentSpeakers !== undefined],
|
|
@@ -8,8 +8,10 @@ import { discordShape } from "./adapters.ts";
|
|
|
8
8
|
import { attachmentsShape } from "./attachments.ts";
|
|
9
9
|
import type { Pronouns } from "./config.ts";
|
|
10
10
|
import { conversationsShape } from "./conversations.ts";
|
|
11
|
+
import { helpersField } from "./helpers.ts";
|
|
11
12
|
import { limitsShape } from "./limits.ts";
|
|
12
13
|
import { profilesShape } from "./profiles.ts";
|
|
14
|
+
import { routingShape } from "./routing.ts";
|
|
13
15
|
import {
|
|
14
16
|
bool,
|
|
15
17
|
characterInput,
|
|
@@ -142,6 +144,8 @@ export const configSchema = shape({
|
|
|
142
144
|
skills: optional(skillsField),
|
|
143
145
|
memory: optional(bool),
|
|
144
146
|
ops: optional(shape({ agent: optional(text), conversation: optional(text) })),
|
|
147
|
+
routing: routingShape,
|
|
148
|
+
helpers: optional(helpersField),
|
|
145
149
|
drainSeconds: optional(integer(1, 3_600)),
|
|
146
150
|
...limitsShape,
|
|
147
151
|
interimText: optional(oneOf<InterimTextMode>("on", "off")),
|
|
@@ -30,9 +30,19 @@ import { checkAgentServerOff, checkAgentTeamKeys } from "./agent-team.ts";
|
|
|
30
30
|
import type { AttachmentsConfig } from "./attachments.ts";
|
|
31
31
|
import { configSchema } from "./config-schema.ts";
|
|
32
32
|
import type { ConversationsConfig } from "./conversations.ts";
|
|
33
|
+
import {
|
|
34
|
+
type HelpersConfig,
|
|
35
|
+
helpersOf,
|
|
36
|
+
type ResolvedHelpers,
|
|
37
|
+
} from "./helpers.ts";
|
|
33
38
|
import { type LimitsConfig, limitsOf, type ResolvedLimits } from "./limits.ts";
|
|
34
|
-
import { opsOf, type ResolvedOps } from "./ops.ts";
|
|
39
|
+
import { opsOf, opsOfPlace, type ResolvedOps } from "./ops.ts";
|
|
35
40
|
import { type ProfilesConfig, profileRulesOf } from "./profiles.ts";
|
|
41
|
+
import {
|
|
42
|
+
type ResolvedRouting,
|
|
43
|
+
type RoutingConfig,
|
|
44
|
+
routingOf,
|
|
45
|
+
} from "./routing.ts";
|
|
36
46
|
import type { SkillsConfig } from "./skills-config.ts";
|
|
37
47
|
|
|
38
48
|
export type { DiscordConfig };
|
|
@@ -217,8 +227,22 @@ export interface RoundtableConfig extends LimitsConfig {
|
|
|
217
227
|
* `discord`), or to a conversation, by its key `<surface>:<id>`, as a visible message and a
|
|
218
228
|
* report turn its claim answers. The host does not start when no chat surface serves the
|
|
219
229
|
* conversation or no plugin's claim owns it; the web chat takes no error reports.
|
|
230
|
+
* @deprecated An alias of `routing.places.ops` since 0.27, which also accepts an agent whose
|
|
231
|
+
* primary place is not Discord; removed in 1.0. Setting both stops the start.
|
|
220
232
|
*/
|
|
221
233
|
ops?: { agent: string } | { conversation: string };
|
|
234
|
+
/**
|
|
235
|
+
* Where events no person waits on land: the agents' primary places, where a webhook's report,
|
|
236
|
+
* a `message_agent` call and an opening task go, and the named places such as `ops`. Left out,
|
|
237
|
+
* every one lands where it did before the key existed.
|
|
238
|
+
*/
|
|
239
|
+
routing?: RoutingConfig;
|
|
240
|
+
/**
|
|
241
|
+
* Temp knights: helper knights an agent makes for a task with `helper_create` and clears with
|
|
242
|
+
* `helper_end`. Left out, the defaults apply (4 per knight, 12 on the host, finished ones
|
|
243
|
+
* archived after 24 hours); `false` leaves the tools out.
|
|
244
|
+
*/
|
|
245
|
+
helpers?: false | HelpersConfig;
|
|
222
246
|
/**
|
|
223
247
|
* Seconds a shutdown waits for running turns before stopping them; default 180. Give the
|
|
224
248
|
* service manager a stop timeout a minute longer, such as systemd's `TimeoutStopSec`.
|
|
@@ -235,6 +259,10 @@ export interface RoundtableConfig extends LimitsConfig {
|
|
|
235
259
|
plugins?: RoundtablePlugin[];
|
|
236
260
|
}
|
|
237
261
|
|
|
262
|
+
/** Logged once when the top-level `ops` is set: it is an alias of the `ops` place. */
|
|
263
|
+
export const OPS_DEPRECATION =
|
|
264
|
+
"`ops` is an alias of `routing.places.ops` since 0.27 and goes away in 1.0; move it there";
|
|
265
|
+
|
|
238
266
|
/** Logged once when `avatar` is set: agents are composed characters and no picture is drawn from it. */
|
|
239
267
|
export const AVATAR_DEPRECATION =
|
|
240
268
|
"agents are composed characters since 0.23; `avatar` is ignored";
|
|
@@ -310,6 +338,12 @@ export interface ResolvedConfig extends ResolvedLimits {
|
|
|
310
338
|
skills: false | SkillsConfig;
|
|
311
339
|
memory: boolean;
|
|
312
340
|
ops?: ResolvedOps;
|
|
341
|
+
/** The config key `ops` came from, as error messages name it. */
|
|
342
|
+
opsKey: "ops" | "routing.places.ops";
|
|
343
|
+
/** The checked `routing` block; undefined when the configuration has none. */
|
|
344
|
+
routing?: ResolvedRouting;
|
|
345
|
+
/** The temp knights' limits, or `false` when the tools are left out. */
|
|
346
|
+
helpers: ResolvedHelpers | false;
|
|
313
347
|
drainMs: number;
|
|
314
348
|
interimText: InterimTextMode;
|
|
315
349
|
interimPrimaryChars: number;
|
|
@@ -353,7 +387,15 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
353
387
|
checkAgentTeamKeys(config, discord !== undefined);
|
|
354
388
|
const slug =
|
|
355
389
|
discord?.rootCommand ?? name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
356
|
-
const
|
|
390
|
+
const routing = routingOf(config.routing, {
|
|
391
|
+
withDiscord: discord !== undefined,
|
|
392
|
+
agentServer:
|
|
393
|
+
agentServer &&
|
|
394
|
+
(discord !== undefined || (config.agents?.length ?? 0) > 0),
|
|
395
|
+
topLevelOps: config.ops !== undefined,
|
|
396
|
+
});
|
|
397
|
+
const ops =
|
|
398
|
+
opsOf(config.ops, discord !== undefined) ?? opsOfPlace(routing?.places.ops);
|
|
357
399
|
const access = accessOf(config, discord !== undefined);
|
|
358
400
|
const { primaryOwner } = access;
|
|
359
401
|
return {
|
|
@@ -364,6 +406,7 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
364
406
|
deprecations: [
|
|
365
407
|
...access.deprecations,
|
|
366
408
|
...(config.avatar ? [AVATAR_DEPRECATION] : []),
|
|
409
|
+
...(config.ops ? [OPS_DEPRECATION] : []),
|
|
367
410
|
],
|
|
368
411
|
owner: {
|
|
369
412
|
id: primaryOwner.id,
|
|
@@ -444,6 +487,9 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
444
487
|
: false,
|
|
445
488
|
memory: config.memory ?? true,
|
|
446
489
|
...(ops ? { ops } : {}),
|
|
490
|
+
opsKey: config.ops ? "ops" : "routing.places.ops",
|
|
491
|
+
...(routing ? { routing } : {}),
|
|
492
|
+
helpers: agentServer ? helpersOf(config.helpers) : false,
|
|
447
493
|
drainMs: (config.drainSeconds ?? DRAIN_LIMIT_MS / 1_000) * 1_000,
|
|
448
494
|
...limitsOf(config),
|
|
449
495
|
interimText: config.interimText ?? "on",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
import { type Field, integer, optional, orOff, shape } from "./schema.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Temp knights: helper knights an agent makes for a task and clears when they are done. Left out,
|
|
6
|
+
* the defaults below apply; `false` stops the tools `helper_create` and `helper_end` being offered.
|
|
7
|
+
*/
|
|
8
|
+
export interface HelpersConfig {
|
|
9
|
+
/** The most active temp knights one knight has at once, 1 to 8; default 4. */
|
|
10
|
+
perParent?: number;
|
|
11
|
+
/** The most active temp knights on the whole host, 1 to 50; default 12. */
|
|
12
|
+
perHost?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Hours a finished temp knight stays before the host archives it, 1 to 720; `false` never does.
|
|
15
|
+
* Default 24. An archived one can be restored from the archive page.
|
|
16
|
+
*/
|
|
17
|
+
archiveFinishedAfterHours?: number | false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What the team reads: every limit filled in. */
|
|
21
|
+
export interface ResolvedHelpers {
|
|
22
|
+
perParent: number;
|
|
23
|
+
perHost: number;
|
|
24
|
+
archiveFinishedAfterHours: number | false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_HELPERS: ResolvedHelpers = Object.freeze({
|
|
28
|
+
perParent: 4,
|
|
29
|
+
perHost: 12,
|
|
30
|
+
archiveFinishedAfterHours: 24,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const afterHours: Field<number | false> = orOff(integer(1, 720));
|
|
34
|
+
|
|
35
|
+
export const helpersField = orOff(
|
|
36
|
+
shape({
|
|
37
|
+
perParent: optional(integer(1, 8)),
|
|
38
|
+
perHost: optional(integer(1, 50)),
|
|
39
|
+
archiveFinishedAfterHours: optional(afterHours),
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** The checked `helpers` block with the defaults filled in; `false` when it is switched off. */
|
|
44
|
+
export function helpersOf(
|
|
45
|
+
config: HelpersConfig | false | undefined,
|
|
46
|
+
): ResolvedHelpers | false {
|
|
47
|
+
if (config === false) return false;
|
|
48
|
+
const resolved: ResolvedHelpers = {
|
|
49
|
+
perParent: config?.perParent ?? DEFAULT_HELPERS.perParent,
|
|
50
|
+
perHost: config?.perHost ?? DEFAULT_HELPERS.perHost,
|
|
51
|
+
archiveFinishedAfterHours:
|
|
52
|
+
config?.archiveFinishedAfterHours ??
|
|
53
|
+
DEFAULT_HELPERS.archiveFinishedAfterHours,
|
|
54
|
+
};
|
|
55
|
+
if (resolved.perParent > resolved.perHost)
|
|
56
|
+
throw new ConfigError(
|
|
57
|
+
`config helpers.perParent: ${resolved.perParent} is more than helpers.perHost (${resolved.perHost}), which counts all of them. Lower perParent or raise perHost.`,
|
|
58
|
+
);
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
package/src/core/config/ops.ts
CHANGED
|
@@ -32,3 +32,11 @@ export function opsOf(
|
|
|
32
32
|
);
|
|
33
33
|
return { conversation: conversation as ChannelKey };
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/** The error reports' destination that `routing.places.ops` names: a conversation, or an agent's primary place. */
|
|
37
|
+
export function opsOfPlace(
|
|
38
|
+
place: ChannelKey | { agent: string } | undefined,
|
|
39
|
+
): ResolvedOps | undefined {
|
|
40
|
+
if (place === undefined) return undefined;
|
|
41
|
+
return typeof place === "string" ? { conversation: place } : place;
|
|
42
|
+
}
|