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
|
@@ -3,18 +3,14 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { SQL } from "bun";
|
|
6
|
-
import type { ContextUse } from "../contract/runtime.ts";
|
|
7
6
|
import type { ChatSurface } from "../contract/surface.ts";
|
|
8
7
|
import { NO_ATTACHMENTS } from "../domain/attachment.ts";
|
|
9
8
|
import type {
|
|
10
9
|
ChannelKey,
|
|
11
10
|
OutboundReply,
|
|
12
|
-
PendingConfirmation,
|
|
13
11
|
PostAuthor,
|
|
14
|
-
TurnResult,
|
|
15
12
|
} from "../domain/conversation.ts";
|
|
16
|
-
import {
|
|
17
|
-
import type { TurnRequest } from "../domain/ports.ts";
|
|
13
|
+
import type { RoundProgress } from "../domain/round.ts";
|
|
18
14
|
import type { IdentityService } from "../identity/identity-port.ts";
|
|
19
15
|
import { silentLogger } from "../log.ts";
|
|
20
16
|
import { StoredSkillRegistry as SkillRegistry } from "../modules/skills/skill-registry.ts";
|
|
@@ -34,7 +30,6 @@ import type {
|
|
|
34
30
|
AgentCategory,
|
|
35
31
|
AgentChannels,
|
|
36
32
|
AgentPost,
|
|
37
|
-
AgentTurnRunner,
|
|
38
33
|
CategoryLayout,
|
|
39
34
|
ChannelMessage,
|
|
40
35
|
} from "./agent-ports.ts";
|
|
@@ -89,6 +84,24 @@ export class FakeChannels implements AgentChannels {
|
|
|
89
84
|
this.put(id, category);
|
|
90
85
|
return id;
|
|
91
86
|
}
|
|
87
|
+
/** The threads made under a channel for temp knights, by id; `archived` once closed. */
|
|
88
|
+
threads = new Map<
|
|
89
|
+
string,
|
|
90
|
+
{ parent: string; name: string; archived: boolean }
|
|
91
|
+
>();
|
|
92
|
+
async createThread(parentChannelId: string, name: string) {
|
|
93
|
+
const id = String(this.#next++);
|
|
94
|
+
this.threads.set(id, { parent: parentChannelId, name, archived: false });
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
async closeThread(threadId: string) {
|
|
98
|
+
const thread = this.threads.get(threadId);
|
|
99
|
+
if (thread) thread.archived = true;
|
|
100
|
+
}
|
|
101
|
+
async openThread(threadId: string) {
|
|
102
|
+
const thread = this.threads.get(threadId);
|
|
103
|
+
if (thread) thread.archived = false;
|
|
104
|
+
}
|
|
92
105
|
async placeIn(channelId: string, category: AgentCategory) {
|
|
93
106
|
if (this.categoryOf(channelId) === category) return false;
|
|
94
107
|
this.put(channelId, category);
|
|
@@ -140,6 +153,13 @@ class FakeSurfaces {
|
|
|
140
153
|
replies: { channel: ChannelKey; reply: OutboundReply }[] = [];
|
|
141
154
|
/** Interim posts as `channel name/agent: text`, edits included. */
|
|
142
155
|
interims: string[] = [];
|
|
156
|
+
/** What each round told the surface, in order. */
|
|
157
|
+
rounds: { channel: ChannelKey; round: RoundProgress }[] = [];
|
|
158
|
+
async groupRound(channel: ChannelKey, round: RoundProgress) {
|
|
159
|
+
// A surface hears of the rounds of its own rooms only; Discord's channels are not any room's.
|
|
160
|
+
if (this.rooms.includes(channel.slice(0, channel.indexOf(":"))))
|
|
161
|
+
this.rounds.push({ channel, round });
|
|
162
|
+
}
|
|
143
163
|
roomSurfaces() {
|
|
144
164
|
return this.rooms;
|
|
145
165
|
}
|
|
@@ -175,44 +195,9 @@ class FakeSurfaces {
|
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
reply: (request: TurnRequest) => string = (r) => `${r.agent?.name} ok`;
|
|
182
|
-
held = new Map<ChannelKey, PendingConfirmation>();
|
|
183
|
-
fresh: ChannelKey[] = [];
|
|
184
|
-
/** Runs inside a turn, like a tool call the model makes. */
|
|
185
|
-
during?: (request: TurnRequest) => unknown;
|
|
186
|
-
/** The next turn ends as if the owner pressed stop. */
|
|
187
|
-
stopNext = false;
|
|
188
|
-
async runTurn(request: TurnRequest): Promise<TurnResult> {
|
|
189
|
-
this.turns.push(request);
|
|
190
|
-
await this.during?.(request);
|
|
191
|
-
if (this.stopNext) {
|
|
192
|
-
this.stopNext = false;
|
|
193
|
-
return {
|
|
194
|
-
ok: false,
|
|
195
|
-
error: new AgentRunError("stopped by the owner"),
|
|
196
|
-
stopped: true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, text: this.reply(request) };
|
|
200
|
-
}
|
|
201
|
-
async heldActions(session: ChannelKey) {
|
|
202
|
-
return this.held.get(session);
|
|
203
|
-
}
|
|
204
|
-
async startFresh(session: ChannelKey) {
|
|
205
|
-
this.fresh.push(session);
|
|
206
|
-
}
|
|
207
|
-
deleted: ChannelKey[] = [];
|
|
208
|
-
async deleteConversation(session: ChannelKey) {
|
|
209
|
-
this.deleted.push(session);
|
|
210
|
-
}
|
|
211
|
-
usage = new Map<ChannelKey, ContextUse>();
|
|
212
|
-
contextUsage(session: ChannelKey) {
|
|
213
|
-
return this.usage.get(session);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
198
|
+
export { FakeRuntime } from "./agent-team-fake-runtime.ts";
|
|
199
|
+
|
|
200
|
+
import { FakeRuntime } from "./agent-team-fake-runtime.ts";
|
|
216
201
|
|
|
217
202
|
// CI runs the setup below about ten times slower than a laptop, and bun's 5 s hook limit
|
|
218
203
|
// left a timed-out hook running while the next test dropped its tables.
|
|
@@ -297,6 +282,8 @@ export function useTeamFixture(
|
|
|
297
282
|
local?: boolean;
|
|
298
283
|
/** The studio the team renders with, called each test; the default fakes pictures. */
|
|
299
284
|
studio?: () => AgentTeamOptions["studio"];
|
|
285
|
+
/** The temp knights' limits the team is built with; left out, the defaults. */
|
|
286
|
+
helpers?: AgentTeamOptions["helpers"];
|
|
300
287
|
} = {},
|
|
301
288
|
): void {
|
|
302
289
|
const { local = false } = options;
|
|
@@ -420,6 +407,7 @@ export function useTeamFixture(
|
|
|
420
407
|
sharedPrompt: () => script.shared,
|
|
421
408
|
guestPrompt: () => script.guest,
|
|
422
409
|
skills,
|
|
410
|
+
...(options.helpers !== undefined ? { helpers: options.helpers } : {}),
|
|
423
411
|
logger: silentLogger(),
|
|
424
412
|
});
|
|
425
413
|
team = build();
|
|
@@ -10,22 +10,20 @@ import type { ResolvedSkill } from "../modules/skills/skill-rules.ts";
|
|
|
10
10
|
import type { AgentChange, AgentTeam } from "../services.ts";
|
|
11
11
|
import type { TurnSelection } from "../sessions.ts";
|
|
12
12
|
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
13
|
-
import { agentSystemPrompt, placeOf } from "./agent-prompt.ts";
|
|
14
13
|
import type { Agent } from "./agent-store.ts";
|
|
15
14
|
import {
|
|
16
15
|
type AgentOps,
|
|
17
16
|
type AvatarMode,
|
|
18
17
|
agentToolsExtension,
|
|
19
18
|
} from "./agent-tools.ts";
|
|
19
|
+
import type { HelperTeam } from "./helper-ops.ts";
|
|
20
|
+
import { type Routes, routesOf } from "./routes.ts";
|
|
20
21
|
import { TeamEditing } from "./team-editing.ts";
|
|
21
22
|
import {
|
|
22
|
-
agentRoomKey,
|
|
23
23
|
channelOwner,
|
|
24
24
|
coordinatorOf,
|
|
25
25
|
discordIdOf,
|
|
26
|
-
discordKey,
|
|
27
26
|
keysOf,
|
|
28
|
-
roomSurfaceOf,
|
|
29
27
|
} from "./team-keys.ts";
|
|
30
28
|
import {
|
|
31
29
|
layoutText,
|
|
@@ -34,6 +32,7 @@ import {
|
|
|
34
32
|
} from "./team-layout.ts";
|
|
35
33
|
import { TeamLifecycle } from "./team-lifecycle.ts";
|
|
36
34
|
import type { AgentTeamOptions, TeamContext } from "./team-options.ts";
|
|
35
|
+
import { teamSystemPrompt } from "./team-prompt.ts";
|
|
37
36
|
import { teamSelection } from "./team-selection.ts";
|
|
38
37
|
import { type TeamStatus, teamStatus } from "./team-status.ts";
|
|
39
38
|
import {
|
|
@@ -64,14 +63,15 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
64
63
|
readonly #editing: TeamEditing;
|
|
65
64
|
|
|
66
65
|
constructor(options: AgentTeamOptions) {
|
|
67
|
-
|
|
66
|
+
// One routing table for the team's turns and its editing.
|
|
67
|
+
this.#options = { ...options, routes: routesOf(options) };
|
|
68
68
|
this.#turns = new TeamTurns({
|
|
69
|
-
...options,
|
|
69
|
+
...this.#options,
|
|
70
70
|
selection: (scope) => this.selection(scope),
|
|
71
71
|
changed: () => this.#changed(),
|
|
72
72
|
});
|
|
73
73
|
this.#context = {
|
|
74
|
-
options,
|
|
74
|
+
options: this.#options,
|
|
75
75
|
turns: this.#turns,
|
|
76
76
|
changed: () => {
|
|
77
77
|
this.#changed();
|
|
@@ -79,6 +79,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
79
79
|
},
|
|
80
80
|
modelOf: (name) => this.modelOf(name),
|
|
81
81
|
retitle: (group) => this.#lifecycle.retitle(group),
|
|
82
|
+
archiveHelpers: (names) => this.#lifecycle.helpers.archive(names),
|
|
82
83
|
};
|
|
83
84
|
this.#lifecycle = new TeamLifecycle(this.#context);
|
|
84
85
|
this.#editing = new TeamEditing(this.#context);
|
|
@@ -141,60 +142,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
141
142
|
|
|
142
143
|
/** The system prompt addition of an agent session, read before each of its runs. */
|
|
143
144
|
systemPrompt(scope: AgentTurnScope): string {
|
|
144
|
-
|
|
145
|
-
store,
|
|
146
|
-
workDir,
|
|
147
|
-
sharedPrompt,
|
|
148
|
-
guestPrompt,
|
|
149
|
-
owner,
|
|
150
|
-
shellUser,
|
|
151
|
-
scratchDir,
|
|
152
|
-
agentTools,
|
|
153
|
-
surfaces,
|
|
154
|
-
channels,
|
|
155
|
-
} = this.#options;
|
|
156
|
-
const agent = store.agent(scope.name);
|
|
157
|
-
if (!agent) throw new AgentError(`no agent ${scope.name}`);
|
|
158
|
-
const shared =
|
|
159
|
-
typeof sharedPrompt === "function" ? sharedPrompt() : sharedPrompt;
|
|
160
|
-
const guest =
|
|
161
|
-
typeof guestPrompt === "function" ? guestPrompt() : guestPrompt;
|
|
162
|
-
const group = scope.group ? store.group(scope.group) : undefined;
|
|
163
|
-
const coordinator = coordinatorOf(store, this.#options);
|
|
164
|
-
const speaker = this.#turns.speakerOf(scope);
|
|
165
|
-
const base = agentSystemPrompt({
|
|
166
|
-
agent,
|
|
167
|
-
...(speaker ? { speaker } : {}),
|
|
168
|
-
...(coordinator ? { coordinator } : {}),
|
|
169
|
-
shared,
|
|
170
|
-
...(guest ? { guestShared: guest } : {}),
|
|
171
|
-
workDir,
|
|
172
|
-
owner,
|
|
173
|
-
shellUser,
|
|
174
|
-
...(scratchDir ? { scratchDir } : {}),
|
|
175
|
-
images: this.#images(),
|
|
176
|
-
...(agentTools ? { agentTools } : {}),
|
|
177
|
-
...placeOf(this.#turns.turnChannel(scope), surfaces, !!channels),
|
|
178
|
-
...(group
|
|
179
|
-
? {
|
|
180
|
-
group: {
|
|
181
|
-
group,
|
|
182
|
-
members: group.members
|
|
183
|
-
.map((m) => store.agent(m))
|
|
184
|
-
.filter((m): m is Agent => m !== undefined),
|
|
185
|
-
},
|
|
186
|
-
}
|
|
187
|
-
: {}),
|
|
188
|
-
});
|
|
189
|
-
const added = (this.#options.promptSections?.() ?? []).flatMap(
|
|
190
|
-
(section) =>
|
|
191
|
-
section.build({
|
|
192
|
-
agent: { name: agent.name, displayName: agent.displayName },
|
|
193
|
-
speaker,
|
|
194
|
-
scope,
|
|
195
|
-
}) ?? [],
|
|
196
|
-
);
|
|
197
|
-
return [base, ...added.filter((text) => text.trim() !== "")].join("\n\n");
|
|
145
|
+
return teamSystemPrompt(this.#options, this.#turns, this.#images(), scope);
|
|
198
146
|
}
|
|
199
147
|
|
|
200
148
|
/** The model and thinking setting of an agent's next run: its own, or the assistant's. */
|
|
@@ -237,21 +185,12 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
237
185
|
|
|
238
186
|
/**
|
|
239
187
|
* Another active agent's channel, for schedule_list: its room on the surface `near` is on when
|
|
240
|
-
* that surface hosts rooms, otherwise its
|
|
241
|
-
* surface that hosts rooms.
|
|
188
|
+
* that surface hosts rooms, otherwise its primary place.
|
|
242
189
|
*/
|
|
243
190
|
channelOf(name: string, near?: ChannelKey): ChannelKey {
|
|
244
191
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const surface = near === undefined ? undefined : roomSurfaceOf(near);
|
|
248
|
-
if (surface !== undefined && rooms.includes(surface))
|
|
249
|
-
return agentRoomKey(surface, name);
|
|
250
|
-
if (agent.channelId) return discordKey(agent.channelId);
|
|
251
|
-
const room = rooms[0];
|
|
252
|
-
if (room === undefined)
|
|
253
|
-
throw new AgentError(`${name} has no channel yet`);
|
|
254
|
-
return agentRoomKey(room, name);
|
|
192
|
+
this.#options.store.activeAgent(name);
|
|
193
|
+
return this.#turns.routes.near(name, near);
|
|
255
194
|
} catch (error) {
|
|
256
195
|
throw new ScheduleError(
|
|
257
196
|
error instanceof Error ? error.message : String(error),
|
|
@@ -259,6 +198,11 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
259
198
|
}
|
|
260
199
|
}
|
|
261
200
|
|
|
201
|
+
/** The routing table: where work no person started lands. */
|
|
202
|
+
get routes(): Routes {
|
|
203
|
+
return this.#turns.routes;
|
|
204
|
+
}
|
|
205
|
+
|
|
262
206
|
// ── Startup and archive, run by TeamLifecycle ──────────────────────────
|
|
263
207
|
|
|
264
208
|
/**
|
|
@@ -267,7 +211,10 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
267
211
|
* character in the background.
|
|
268
212
|
*/
|
|
269
213
|
start(): Promise<void> {
|
|
270
|
-
return this.#lifecycle.start().then(() =>
|
|
214
|
+
return this.#lifecycle.start().then(() => {
|
|
215
|
+
this.#turns.routes.warnUnknown();
|
|
216
|
+
return this.#turns.replayMessages();
|
|
217
|
+
});
|
|
271
218
|
}
|
|
272
219
|
|
|
273
220
|
/** What the team still writes that a shutdown must wait for. */
|
|
@@ -334,6 +281,8 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
334
281
|
return this.#turns.answerBackground(...args);
|
|
335
282
|
}
|
|
336
283
|
|
|
284
|
+
/** Ends the group channel's running round on Stop: the sessions to stop, or undefined when none runs. */
|
|
285
|
+
readonly stopRound = (channel: ChannelKey) => this.#turns.stopRound(channel);
|
|
337
286
|
/** One owner message in a group channel, answered by a round of members; call inside the queue. */
|
|
338
287
|
answerGroup(
|
|
339
288
|
channel: ChannelKey,
|
|
@@ -460,6 +409,52 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
460
409
|
return this.#editing.create(caller, input);
|
|
461
410
|
}
|
|
462
411
|
|
|
412
|
+
/** A knight makes a temp knight for one task; see `AgentOps.helperCreate`. */
|
|
413
|
+
helperCreate(
|
|
414
|
+
caller: AgentTurnScope,
|
|
415
|
+
input: Parameters<TeamEditing["createHelper"]>[1],
|
|
416
|
+
): Promise<string> {
|
|
417
|
+
return this.#editing.createHelper(caller, input);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** A knight archives its own temp knights; the pages that show the parent are told. */
|
|
421
|
+
helperEnd(
|
|
422
|
+
caller: AgentTurnScope,
|
|
423
|
+
names: readonly string[] | "finished",
|
|
424
|
+
): Promise<string> {
|
|
425
|
+
return this.#lifecycle.helpers.end(caller, names);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
helpersOf(parent: string): Agent[] {
|
|
429
|
+
return this.#lifecycle.helpers.of(parent);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
finishedHelpers(parents?: readonly string[]): Agent[] {
|
|
433
|
+
return this.#lifecycle.helpers.finished(parents);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async archiveHelpers(
|
|
437
|
+
names: readonly string[],
|
|
438
|
+
by: string,
|
|
439
|
+
): Promise<string[]> {
|
|
440
|
+
const archived = await this.#lifecycle.helpers.archive(names);
|
|
441
|
+
this.#options.logger.info({ by, archived }, "temp knights archived");
|
|
442
|
+
return archived;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
restoreHelpers(names: readonly string[]): Promise<string[]> {
|
|
446
|
+
return this.#lifecycle.helpers.restore(names);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Archives the temp knights finished for longer than the host keeps them; runs hourly by itself. */
|
|
450
|
+
sweepFinished(now?: Date): Promise<string[]> {
|
|
451
|
+
return this.#lifecycle.helpers.sweepFinished(now);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
onHelpersEnded(listener: Parameters<HelperTeam["onHelpersEnded"]>[0]): void {
|
|
455
|
+
this.#lifecycle.helpers.onEnded(listener);
|
|
456
|
+
}
|
|
457
|
+
|
|
463
458
|
/** An owner creates an agent outside any agent turn; see `AgentTeam.createAgent`. */
|
|
464
459
|
createAgent(
|
|
465
460
|
by: Speaker,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import {
|
|
3
|
+
CATALOG,
|
|
4
|
+
type CharacterInput,
|
|
5
|
+
CLASS_IDS,
|
|
6
|
+
type PaletteName,
|
|
7
|
+
} from "../../characters/index.ts";
|
|
8
|
+
import { optionalString } from "../runtime/text-tools.ts";
|
|
9
|
+
import { MAX_PROMPT_CHARS } from "./agent-rules.ts";
|
|
10
|
+
|
|
11
|
+
const PALETTE_NAMES = Object.keys(CATALOG.palettes) as PaletteName[];
|
|
12
|
+
|
|
13
|
+
// The parts of the agent tools' input that more than one tool takes.
|
|
14
|
+
export const NAME = Type.String({
|
|
15
|
+
description:
|
|
16
|
+
"An agent's name (lowercase, from agent_list), not its display name.",
|
|
17
|
+
});
|
|
18
|
+
export const DISPLAY = Type.String({
|
|
19
|
+
minLength: 1,
|
|
20
|
+
maxLength: 32,
|
|
21
|
+
description: "The name shown on its messages; any language.",
|
|
22
|
+
});
|
|
23
|
+
export const PROMPT = Type.String({
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: MAX_PROMPT_CHARS,
|
|
26
|
+
description:
|
|
27
|
+
"The agent's role prompt, in English: its job, sources, boundaries, and how it reports. Short and stated positively.",
|
|
28
|
+
});
|
|
29
|
+
const enumOf = (ids: readonly string[], description?: string) =>
|
|
30
|
+
Type.String({ enum: [...ids], ...(description ? { description } : {}) });
|
|
31
|
+
export const CLASS = enumOf(
|
|
32
|
+
CLASS_IDS,
|
|
33
|
+
`The agent's character class, which sets its outfit, hat, and prop. ${CLASS_IDS.map((id) => `${id}: ${CATALOG.classes[id].jobs}`).join("; ")}.`,
|
|
34
|
+
);
|
|
35
|
+
export const COLOR = enumOf(
|
|
36
|
+
PALETTE_NAMES,
|
|
37
|
+
"The agent's color palette. Pick one unlike the other agents' (agent_get shows each look).",
|
|
38
|
+
);
|
|
39
|
+
export const PARTS = Type.Object(
|
|
40
|
+
{
|
|
41
|
+
outfit: Type.Optional(enumOf(CATALOG.outfits)),
|
|
42
|
+
hat: Type.Optional(enumOf(CATALOG.hats)),
|
|
43
|
+
prop: Type.Optional(enumOf(CATALOG.props)),
|
|
44
|
+
hair: Type.Optional(enumOf(CATALOG.hair)),
|
|
45
|
+
beard: Type.Optional(enumOf([...CATALOG.beards, "none"])),
|
|
46
|
+
accessory: Type.Optional(enumOf([...CATALOG.accessories, "none"])),
|
|
47
|
+
build: Type.Optional(enumOf(["slim", "regular", "stout"])),
|
|
48
|
+
skin: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
|
|
49
|
+
hair_color: Type.Optional(
|
|
50
|
+
Type.String({
|
|
51
|
+
pattern: "^#[0-9a-fA-F]{6}$",
|
|
52
|
+
description: "A hex color such as #3b2a1a.",
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
{ description: "Single parts to change; any left out stay as they are." },
|
|
57
|
+
);
|
|
58
|
+
const PART_FIELDS = {
|
|
59
|
+
outfit: "outfit",
|
|
60
|
+
hat: "hat",
|
|
61
|
+
prop: "prop",
|
|
62
|
+
hair: "hair",
|
|
63
|
+
beard: "beard",
|
|
64
|
+
accessory: "accessory",
|
|
65
|
+
build: "build",
|
|
66
|
+
skin: "skin",
|
|
67
|
+
hair_color: "hairColor",
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/** The look a tool call names: class, color as palette, and parts; nothing it leaves out. */
|
|
71
|
+
export function lookOf(input: Record<string, unknown>): CharacterInput {
|
|
72
|
+
const look: Record<string, unknown> = {};
|
|
73
|
+
const klass = optionalString(input, "class");
|
|
74
|
+
const color = optionalString(input, "color");
|
|
75
|
+
if (klass) look.class = klass;
|
|
76
|
+
if (color) look.palette = color;
|
|
77
|
+
const parts = input.parts;
|
|
78
|
+
if (parts !== null && typeof parts === "object")
|
|
79
|
+
for (const [key, field] of Object.entries(PART_FIELDS)) {
|
|
80
|
+
const value = (parts as Record<string, unknown>)[key];
|
|
81
|
+
if (value !== undefined) look[field] = value;
|
|
82
|
+
}
|
|
83
|
+
return look as CharacterInput;
|
|
84
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
CATALOG,
|
|
5
|
-
type CharacterInput,
|
|
6
|
-
CLASS_IDS,
|
|
7
|
-
type PaletteName,
|
|
8
|
-
} from "../../characters/index.ts";
|
|
3
|
+
import type { CharacterInput } from "../../characters/index.ts";
|
|
9
4
|
import { AgentError } from "../domain/errors.ts";
|
|
10
5
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
11
6
|
import { assistantName, messages } from "../i18n/index.ts";
|
|
@@ -19,13 +14,17 @@ import {
|
|
|
19
14
|
textToolsExtension,
|
|
20
15
|
} from "../runtime/text-tools.ts";
|
|
21
16
|
import { timeZone } from "../time.ts";
|
|
17
|
+
import { MAX_GROUP_MEMBERS, MIN_GROUP_MEMBERS } from "./agent-store.ts";
|
|
22
18
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
CLASS,
|
|
20
|
+
COLOR,
|
|
21
|
+
DISPLAY,
|
|
22
|
+
lookOf,
|
|
23
|
+
NAME,
|
|
24
|
+
PARTS,
|
|
25
|
+
PROMPT,
|
|
26
|
+
} from "./agent-tool-schema.ts";
|
|
27
|
+
import { helperToolDefs } from "./helper-tools.ts";
|
|
29
28
|
|
|
30
29
|
/** The tool that hands a request to another agent; a group seat has none. */
|
|
31
30
|
export const MESSAGE_AGENT_TOOL = "message_agent";
|
|
@@ -33,11 +32,15 @@ export const MESSAGE_AGENT_TOOL = "message_agent";
|
|
|
33
32
|
/** The tool that changes an agent's look; every host offers it. */
|
|
34
33
|
export const AVATAR_TOOL = "agent_avatar";
|
|
35
34
|
|
|
35
|
+
/** The tools that make and end temp knights; `helpers: false` leaves them out. */
|
|
36
|
+
export const HELPER_TOOLS = ["helper_create", "helper_end"] as const;
|
|
37
|
+
|
|
36
38
|
export const AGENT_TOOLS = [
|
|
37
39
|
"agent_list",
|
|
38
40
|
"agent_get",
|
|
39
41
|
"agent_update",
|
|
40
42
|
"agent_create",
|
|
43
|
+
...HELPER_TOOLS,
|
|
41
44
|
AVATAR_TOOL,
|
|
42
45
|
MESSAGE_AGENT_TOOL,
|
|
43
46
|
"group_create",
|
|
@@ -47,6 +50,20 @@ export const AGENT_TOOLS = [
|
|
|
47
50
|
"channel_read",
|
|
48
51
|
] as const;
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The tools a temp knight's turns do not get: making knights, temp knights or groups, archiving,
|
|
55
|
+
* arranging channels, and scheduling, which would leave work behind with no one to end it.
|
|
56
|
+
*/
|
|
57
|
+
export const WITHHELD_FROM_HELPERS: readonly string[] = [
|
|
58
|
+
"agent_create",
|
|
59
|
+
...HELPER_TOOLS,
|
|
60
|
+
"group_create",
|
|
61
|
+
"group_update",
|
|
62
|
+
"archive",
|
|
63
|
+
"channel_arrange",
|
|
64
|
+
"schedule_create",
|
|
65
|
+
];
|
|
66
|
+
|
|
50
67
|
/**
|
|
51
68
|
* @deprecated Agents are composed characters and no longer drawn; use `AgentTeam.dress`.
|
|
52
69
|
* Removed in 1.0.
|
|
@@ -83,6 +100,25 @@ export interface AgentOps {
|
|
|
83
100
|
longRunning?: boolean;
|
|
84
101
|
},
|
|
85
102
|
): Promise<string>;
|
|
103
|
+
/** Makes a temp knight of the caller for one task; the answer returns to the caller's conversation. */
|
|
104
|
+
helperCreate(
|
|
105
|
+
caller: AgentTurnScope,
|
|
106
|
+
input: {
|
|
107
|
+
name?: string;
|
|
108
|
+
displayName: string;
|
|
109
|
+
prompt: string;
|
|
110
|
+
task: string;
|
|
111
|
+
character?: CharacterInput;
|
|
112
|
+
skills?: string[];
|
|
113
|
+
model?: string;
|
|
114
|
+
thinking?: string;
|
|
115
|
+
},
|
|
116
|
+
): Promise<string>;
|
|
117
|
+
/** Archives the caller's own temp knights: the named ones, or every one that is finished. */
|
|
118
|
+
helperEnd(
|
|
119
|
+
caller: AgentTurnScope,
|
|
120
|
+
names: readonly string[] | "finished",
|
|
121
|
+
): Promise<string>;
|
|
86
122
|
/** Changes an agent's look; a field left out stays as it is. */
|
|
87
123
|
avatar(name: string, look: CharacterInput): Promise<string>;
|
|
88
124
|
/** Queues the message and returns at once. */
|
|
@@ -116,50 +152,6 @@ export interface AgentOps {
|
|
|
116
152
|
): Promise<string>;
|
|
117
153
|
}
|
|
118
154
|
|
|
119
|
-
const NAME = Type.String({
|
|
120
|
-
description:
|
|
121
|
-
"An agent's name (lowercase, from agent_list), not its display name.",
|
|
122
|
-
});
|
|
123
|
-
const DISPLAY = Type.String({
|
|
124
|
-
minLength: 1,
|
|
125
|
-
maxLength: 32,
|
|
126
|
-
description: "The name shown on its messages; any language.",
|
|
127
|
-
});
|
|
128
|
-
const PROMPT = Type.String({
|
|
129
|
-
minLength: 1,
|
|
130
|
-
maxLength: MAX_PROMPT_CHARS,
|
|
131
|
-
description:
|
|
132
|
-
"The agent's role prompt, in English: its job, sources, boundaries, and how it reports. Short and stated positively.",
|
|
133
|
-
});
|
|
134
|
-
const enumOf = (ids: readonly string[], description?: string) =>
|
|
135
|
-
Type.String({ enum: [...ids], ...(description ? { description } : {}) });
|
|
136
|
-
const CLASS = enumOf(
|
|
137
|
-
CLASS_IDS,
|
|
138
|
-
`The agent's character class, which sets its outfit, hat, and prop. ${CLASS_IDS.map((id) => `${id}: ${CATALOG.classes[id].jobs}`).join("; ")}.`,
|
|
139
|
-
);
|
|
140
|
-
const COLOR = enumOf(
|
|
141
|
-
PALETTE_NAMES,
|
|
142
|
-
"The agent's color palette. Pick one unlike the other agents' (agent_get shows each look).",
|
|
143
|
-
);
|
|
144
|
-
const PARTS = Type.Object(
|
|
145
|
-
{
|
|
146
|
-
outfit: Type.Optional(enumOf(CATALOG.outfits)),
|
|
147
|
-
hat: Type.Optional(enumOf(CATALOG.hats)),
|
|
148
|
-
prop: Type.Optional(enumOf(CATALOG.props)),
|
|
149
|
-
hair: Type.Optional(enumOf(CATALOG.hair)),
|
|
150
|
-
beard: Type.Optional(enumOf([...CATALOG.beards, "none"])),
|
|
151
|
-
accessory: Type.Optional(enumOf([...CATALOG.accessories, "none"])),
|
|
152
|
-
build: Type.Optional(enumOf(["slim", "regular", "stout"])),
|
|
153
|
-
skin: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
|
|
154
|
-
hair_color: Type.Optional(
|
|
155
|
-
Type.String({
|
|
156
|
-
pattern: "^#[0-9a-fA-F]{6}$",
|
|
157
|
-
description: "A hex color such as #3b2a1a.",
|
|
158
|
-
}),
|
|
159
|
-
),
|
|
160
|
-
},
|
|
161
|
-
{ description: "Single parts to change; any left out stay as they are." },
|
|
162
|
-
);
|
|
163
155
|
const category = (kind: "Agents" | "Groups") =>
|
|
164
156
|
Type.String({
|
|
165
157
|
description: `The category its channel opens under: ${kind} (the default) or ${kind}-<suffix>, such as ${kind}-work; made when missing. Keep to the categories agent_list shows unless a new one fits better.`,
|
|
@@ -170,34 +162,6 @@ const MEMBERS = Type.Array(Type.String(), {
|
|
|
170
162
|
description: "Agent names in speaking-tie order.",
|
|
171
163
|
});
|
|
172
164
|
|
|
173
|
-
const PART_FIELDS = {
|
|
174
|
-
outfit: "outfit",
|
|
175
|
-
hat: "hat",
|
|
176
|
-
prop: "prop",
|
|
177
|
-
hair: "hair",
|
|
178
|
-
beard: "beard",
|
|
179
|
-
accessory: "accessory",
|
|
180
|
-
build: "build",
|
|
181
|
-
skin: "skin",
|
|
182
|
-
hair_color: "hairColor",
|
|
183
|
-
} as const;
|
|
184
|
-
|
|
185
|
-
/** The look a tool call names: class, color as palette, and parts; nothing it leaves out. */
|
|
186
|
-
function lookOf(input: Record<string, unknown>): CharacterInput {
|
|
187
|
-
const look: Record<string, unknown> = {};
|
|
188
|
-
const klass = optionalString(input, "class");
|
|
189
|
-
const color = optionalString(input, "color");
|
|
190
|
-
if (klass) look.class = klass;
|
|
191
|
-
if (color) look.palette = color;
|
|
192
|
-
const parts = input.parts;
|
|
193
|
-
if (parts !== null && typeof parts === "object")
|
|
194
|
-
for (const [key, field] of Object.entries(PART_FIELDS)) {
|
|
195
|
-
const value = (parts as Record<string, unknown>)[key];
|
|
196
|
-
if (value !== undefined) look[field] = value;
|
|
197
|
-
}
|
|
198
|
-
return look as CharacterInput;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
165
|
type AgentToolDef = TextToolDef<(typeof AGENT_TOOLS)[number]>;
|
|
202
166
|
|
|
203
167
|
/** What the agent tools offer, by what the host can do. */
|
|
@@ -324,6 +288,7 @@ function toolDefs(
|
|
|
324
288
|
});
|
|
325
289
|
},
|
|
326
290
|
},
|
|
291
|
+
...helperToolDefs(ops, scope, skillsOn),
|
|
327
292
|
{
|
|
328
293
|
name: AVATAR_TOOL,
|
|
329
294
|
label: "Change look",
|
|
@@ -345,7 +310,7 @@ function toolDefs(
|
|
|
345
310
|
{
|
|
346
311
|
name: "group_create",
|
|
347
312
|
label: "Create group",
|
|
348
|
-
description: `Create a group chat channel with 2 to
|
|
313
|
+
description: `Create a group chat channel with 2 to 8 agents. The judge picks which members answer each of ${o.name}'s messages there, one after another. The host answers when nobody else fits.`,
|
|
349
314
|
parameters: Type.Object({
|
|
350
315
|
name: Type.String({ pattern: "^[a-z0-9-]{1,32}$" }),
|
|
351
316
|
display_name: DISPLAY,
|