pi-roundtable 0.26.1 → 0.26.2
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 +33 -0
- package/docs/plugins.md +12 -7
- package/package.json +1 -1
- package/src/core/agents/agent-claim.ts +1 -0
- package/src/core/agents/agent-conversations.ts +94 -0
- package/src/core/agents/agent-messages.ts +247 -56
- package/src/core/agents/agent-outbox.ts +91 -0
- package/src/core/agents/agent-prompt.ts +40 -1
- package/src/core/agents/agent-schema.ts +6 -0
- package/src/core/agents/agent-store.ts +22 -3
- package/src/core/agents/agent-team-fixture.ts +81 -88
- package/src/core/agents/agent-team-seeds.ts +29 -0
- package/src/core/agents/agent-team.ts +17 -14
- package/src/core/agents/agent-tools.ts +34 -5
- package/src/core/agents/group-turns.ts +1 -1
- package/src/core/agents/message-target.ts +79 -0
- package/src/core/agents/team-editing.ts +30 -2
- package/src/core/agents/team-keys.ts +7 -0
- package/src/core/agents/team-options.ts +3 -0
- package/src/core/agents/team-text.ts +4 -0
- package/src/core/agents/team-turn-types.ts +28 -0
- package/src/core/agents/team-turns.ts +36 -25
- package/src/core/assets/prompts/shared-guest.md +1 -3
- package/src/core/assets/prompts/shared.md +1 -3
- package/src/core/builtin/agent-server.ts +5 -0
- package/src/core/builtin/modules.ts +6 -0
- package/src/core/contract/channels.ts +5 -0
- package/src/core/contract/surface.ts +5 -0
- package/src/core/discord/channel-context.ts +3 -1
- package/src/core/discord/embed-text.ts +45 -0
- package/src/core/discord/inbound-message.ts +7 -2
- package/src/core/domain/ports.ts +17 -0
- package/src/core/domain/turn-timeout.ts +2 -0
- package/src/core/i18n/turn-timeout.ts +8 -2
- package/src/core/modules/background/background-turns.ts +3 -0
- package/src/core/modules/schedules/precheck-catalog.ts +49 -0
- package/src/core/modules/schedules/schedule-store.ts +27 -3
- package/src/core/modules/schedules/schedule-tools.ts +43 -86
- package/src/core/modules/schedules/scheduled-turn-text.ts +31 -0
- package/src/core/modules/schedules/schedules.ts +26 -2
- package/src/core/modules/schedules/time-limit.ts +27 -0
- package/src/core/presentation/reply-body.ts +8 -0
- package/src/core/routing/conversation-turns.ts +21 -2
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +31 -27
- package/src/core/runtime/extensions/hold-lookups.ts +21 -0
- package/src/core/runtime/extensions/late-releases.ts +76 -0
- package/src/core/runtime/pi-agent-runtime.ts +21 -19
- package/src/core/runtime/turn-answer.ts +5 -1
- package/src/core/runtime/turn-budget.ts +1 -0
- package/src/core/runtime/turn-limits.ts +12 -2
- package/src/core/runtime/turn-tools.ts +26 -0
- package/src/core/shared/schedule-tools.ts +25 -1
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "../domain/conversation.ts";
|
|
16
16
|
import { AgentRunError } from "../domain/errors.ts";
|
|
17
17
|
import type { TurnRequest } from "../domain/ports.ts";
|
|
18
|
+
import type { IdentityService } from "../identity/identity-port.ts";
|
|
18
19
|
import { silentLogger } from "../log.ts";
|
|
19
20
|
import { StoredSkillRegistry as SkillRegistry } from "../modules/skills/skill-registry.ts";
|
|
20
21
|
import { SkillStore } from "../modules/skills/skill-store.ts";
|
|
@@ -37,12 +38,12 @@ import type {
|
|
|
37
38
|
CategoryLayout,
|
|
38
39
|
ChannelMessage,
|
|
39
40
|
} from "./agent-ports.ts";
|
|
40
|
-
import type { AgentSeed } from "./agent-rules.ts";
|
|
41
41
|
import { PgAgentStore as AgentStore } from "./agent-store.ts";
|
|
42
42
|
import {
|
|
43
43
|
DiscordAgentTeam as AgentTeam,
|
|
44
44
|
type AgentTeamOptions,
|
|
45
45
|
} from "./agent-team.ts";
|
|
46
|
+
import { seeds } from "./agent-team-seeds.ts";
|
|
46
47
|
import { discordKey } from "./team-keys.ts";
|
|
47
48
|
|
|
48
49
|
export const ENTRY = "1000";
|
|
@@ -261,6 +262,7 @@ export const script: {
|
|
|
261
262
|
};
|
|
262
263
|
let queue: ChannelQueue;
|
|
263
264
|
let team: AgentTeam;
|
|
265
|
+
let build: () => AgentTeam;
|
|
264
266
|
let skillStore: TestStore<SkillStore>;
|
|
265
267
|
let skills: SkillRegistry;
|
|
266
268
|
export const removedSchedules: number[] = [];
|
|
@@ -271,34 +273,6 @@ export const eventLog: string[] = [];
|
|
|
271
273
|
/** What a plugin adds to every agent's tools: a tool of its own and a group of another server's. */
|
|
272
274
|
const PLUGIN_SELECTION = { tools: ["memory_add"], groups: ["workspace"] };
|
|
273
275
|
|
|
274
|
-
/** The team a fresh database starts with: a coordinator in the entry channel, then two specialists. */
|
|
275
|
-
function seeds(entryChannelId: string | undefined): AgentSeed[] {
|
|
276
|
-
return [
|
|
277
|
-
{
|
|
278
|
-
name: "coordinator",
|
|
279
|
-
displayName: "Coordinator",
|
|
280
|
-
...(entryChannelId ? { channelId: entryChannelId } : {}),
|
|
281
|
-
prompt:
|
|
282
|
-
"You are the chief of staff of the owner's agent team. You hand work that belongs to a specialist to that agent with message_agent, one owner per task, and propose a new agent when a lasting responsibility has no owner.",
|
|
283
|
-
character: { class: "navigator", palette: "amber" },
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
name: "doctor",
|
|
287
|
-
displayName: "Doctor",
|
|
288
|
-
prompt:
|
|
289
|
-
"You keep the owner's agents healthy: read an agent's prompt with agent_get, find the instruction that caused a misbehavior, and fix it with agent_update.",
|
|
290
|
-
character: { class: "healer", palette: "rose" },
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
name: "infra",
|
|
294
|
-
displayName: "Infra",
|
|
295
|
-
prompt:
|
|
296
|
-
"You manage the owner's servers. Check before you change: read state first, state what you will change and why, then act.",
|
|
297
|
-
character: { class: "engineer", palette: "purpure" },
|
|
298
|
-
},
|
|
299
|
-
];
|
|
300
|
-
}
|
|
301
|
-
|
|
302
276
|
/**
|
|
303
277
|
* Waits until every queued channel task has settled and every active agent has its avatar,
|
|
304
278
|
* which startup draws in the background; a fixed sleep was too short on a slow runner.
|
|
@@ -337,6 +311,8 @@ export function useTeamFixture(
|
|
|
337
311
|
"agent_groups",
|
|
338
312
|
"agent_group_messages",
|
|
339
313
|
"agent_group_cursors",
|
|
314
|
+
"agent_outbox",
|
|
315
|
+
"agent_latest_conversation",
|
|
340
316
|
"prompt_history",
|
|
341
317
|
"skills",
|
|
342
318
|
"skill_versions",
|
|
@@ -381,68 +357,72 @@ export function useTeamFixture(
|
|
|
381
357
|
script.guest = "";
|
|
382
358
|
queue = channelQueue();
|
|
383
359
|
removedSchedules.length = 0;
|
|
360
|
+
identity.current = undefined;
|
|
384
361
|
stops.length = 0;
|
|
385
362
|
eventLog.length = 0;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
?
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
pluginSelection: () => PLUGIN_SELECTION,
|
|
408
|
-
confirmations: { approves: async (_p, reply) => reply.includes("yes") },
|
|
409
|
-
scorer: { score: async () => script.scores },
|
|
410
|
-
events: {
|
|
411
|
-
turnStarted: (turn) => eventLog.push(`turnStarted ${turn.agent}`),
|
|
412
|
-
turnEnded: (turn) =>
|
|
413
|
-
eventLog.push(`turnEnded ${turn.agent} ${turn.result}`),
|
|
414
|
-
changed: () => eventLog.push("changed"),
|
|
415
|
-
},
|
|
416
|
-
promptSections: () => script.prompt,
|
|
417
|
-
pluginTools: () => script.tools,
|
|
418
|
-
models: {
|
|
419
|
-
defaults: {
|
|
420
|
-
model: "claude-bridge/claude-opus-5-5",
|
|
421
|
-
thinking: "medium",
|
|
363
|
+
build = () =>
|
|
364
|
+
new AgentTeam({
|
|
365
|
+
owner: TEST_OWNER,
|
|
366
|
+
toolTiers: FIXTURE_TIERS,
|
|
367
|
+
principals: () => identity.current,
|
|
368
|
+
shellUser: "mcops",
|
|
369
|
+
guildId: local ? "local" : "1",
|
|
370
|
+
...(local
|
|
371
|
+
? { coordinator: "coordinator" }
|
|
372
|
+
: { entryChannelId: ENTRY, channels }),
|
|
373
|
+
seeds: () => seeds(local ? undefined : ENTRY),
|
|
374
|
+
store,
|
|
375
|
+
surfaces,
|
|
376
|
+
studio: options.studio?.() ?? {
|
|
377
|
+
url: (hash) => `https://x/avatars/${hash ?? "default"}.png`,
|
|
378
|
+
render: async (spec) =>
|
|
379
|
+
new Bun.CryptoHasher("sha256")
|
|
380
|
+
.update(JSON.stringify(spec))
|
|
381
|
+
.digest("hex"),
|
|
382
|
+
draw: async () => "a".repeat(64),
|
|
383
|
+
edit: async () => "b".repeat(64),
|
|
422
384
|
},
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
385
|
+
runtime: () => runtime,
|
|
386
|
+
pluginSelection: () => PLUGIN_SELECTION,
|
|
387
|
+
confirmations: { approves: async (_p, reply) => reply.includes("yes") },
|
|
388
|
+
scorer: { score: async () => script.scores },
|
|
389
|
+
events: {
|
|
390
|
+
turnStarted: (turn) => eventLog.push(`turnStarted ${turn.agent}`),
|
|
391
|
+
turnEnded: (turn) =>
|
|
392
|
+
eventLog.push(`turnEnded ${turn.agent} ${turn.result}`),
|
|
393
|
+
changed: () => eventLog.push("changed"),
|
|
394
|
+
},
|
|
395
|
+
promptSections: () => script.prompt,
|
|
396
|
+
pluginTools: () => script.tools,
|
|
397
|
+
models: {
|
|
398
|
+
defaults: {
|
|
399
|
+
model: "claude-bridge/claude-opus-5-5",
|
|
400
|
+
thinking: "medium",
|
|
401
|
+
},
|
|
402
|
+
usable: async () => USABLE,
|
|
403
|
+
},
|
|
404
|
+
schedules: {
|
|
405
|
+
forChannel: async (channel) =>
|
|
406
|
+
channel === discordKey("2000")
|
|
407
|
+
? [{ id: 7 }]
|
|
408
|
+
: channel === "app:agent.doctor"
|
|
409
|
+
? [{ id: 8 }]
|
|
410
|
+
: [],
|
|
411
|
+
remove: async (id) => removedSchedules.push(id),
|
|
412
|
+
},
|
|
413
|
+
queue,
|
|
414
|
+
startTyping: () => () => undefined,
|
|
415
|
+
showStop: (channel) => {
|
|
416
|
+
stops.push(`show ${channel}`);
|
|
417
|
+
return () => stops.push(`hide ${channel}`);
|
|
418
|
+
},
|
|
419
|
+
workDir: "/tmp/agents/work",
|
|
420
|
+
sharedPrompt: () => script.shared,
|
|
421
|
+
guestPrompt: () => script.guest,
|
|
422
|
+
skills,
|
|
423
|
+
logger: silentLogger(),
|
|
424
|
+
});
|
|
425
|
+
team = build();
|
|
446
426
|
await team.start();
|
|
447
427
|
Object.assign(fx, {
|
|
448
428
|
store,
|
|
@@ -458,6 +438,19 @@ export function useTeamFixture(
|
|
|
458
438
|
}, HOOK_TIMEOUT_MS);
|
|
459
439
|
}
|
|
460
440
|
|
|
441
|
+
/** Starts the team again over the same database, as a restarted host: a new queue, runtime and team. */
|
|
442
|
+
/** The identity service the team's replay asks; none until a test sets one. */
|
|
443
|
+
export const identity: { current?: Pick<IdentityService, "speakerFor"> } = {};
|
|
444
|
+
|
|
445
|
+
export async function restart(): Promise<void> {
|
|
446
|
+
queue = channelQueue();
|
|
447
|
+
runtime = new FakeRuntime();
|
|
448
|
+
team = build();
|
|
449
|
+
await team.start();
|
|
450
|
+
Object.assign(fx, { queue, runtime, team });
|
|
451
|
+
await settle();
|
|
452
|
+
}
|
|
453
|
+
|
|
461
454
|
export const opsGroup = () => {
|
|
462
455
|
const group = store.group("ops");
|
|
463
456
|
if (!group) throw new Error("no ops group");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentSeed } from "./agent-rules.ts";
|
|
2
|
+
|
|
3
|
+
/** The team a fresh database starts with: a coordinator in the entry channel, then two specialists. */
|
|
4
|
+
export function seeds(entryChannelId: string | undefined): AgentSeed[] {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: "coordinator",
|
|
8
|
+
displayName: "Coordinator",
|
|
9
|
+
...(entryChannelId ? { channelId: entryChannelId } : {}),
|
|
10
|
+
prompt:
|
|
11
|
+
"You are the chief of staff of the owner's agent team. You hand work that belongs to a specialist to that agent with message_agent, one owner per task, and propose a new agent when a lasting responsibility has no owner.",
|
|
12
|
+
character: { class: "navigator", palette: "amber" },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "doctor",
|
|
16
|
+
displayName: "Doctor",
|
|
17
|
+
prompt:
|
|
18
|
+
"You keep the owner's agents healthy: read an agent's prompt with agent_get, find the instruction that caused a misbehavior, and fix it with agent_update.",
|
|
19
|
+
character: { class: "healer", palette: "rose" },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "infra",
|
|
23
|
+
displayName: "Infra",
|
|
24
|
+
prompt:
|
|
25
|
+
"You manage the owner's servers. Check before you change: read state first, state what you will change and why, then act.",
|
|
26
|
+
character: { class: "engineer", palette: "purpure" },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
@@ -10,7 +10,7 @@ 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 } from "./agent-prompt.ts";
|
|
13
|
+
import { agentSystemPrompt, placeOf } from "./agent-prompt.ts";
|
|
14
14
|
import type { Agent } from "./agent-store.ts";
|
|
15
15
|
import {
|
|
16
16
|
type AgentOps,
|
|
@@ -150,6 +150,8 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
150
150
|
shellUser,
|
|
151
151
|
scratchDir,
|
|
152
152
|
agentTools,
|
|
153
|
+
surfaces,
|
|
154
|
+
channels,
|
|
153
155
|
} = this.#options;
|
|
154
156
|
const agent = store.agent(scope.name);
|
|
155
157
|
if (!agent) throw new AgentError(`no agent ${scope.name}`);
|
|
@@ -172,6 +174,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
172
174
|
...(scratchDir ? { scratchDir } : {}),
|
|
173
175
|
images: this.#images(),
|
|
174
176
|
...(agentTools ? { agentTools } : {}),
|
|
177
|
+
...placeOf(this.#turns.turnChannel(scope), surfaces, !!channels),
|
|
175
178
|
...(group
|
|
176
179
|
? {
|
|
177
180
|
group: {
|
|
@@ -264,7 +267,12 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
264
267
|
* character in the background.
|
|
265
268
|
*/
|
|
266
269
|
start(): Promise<void> {
|
|
267
|
-
return this.#lifecycle.start();
|
|
270
|
+
return this.#lifecycle.start().then(() => this.#turns.replayMessages());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** What the team still writes that a shutdown must wait for. */
|
|
274
|
+
busy(): string[] {
|
|
275
|
+
return this.#turns.busy();
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
/** Ends the background work the team started. */
|
|
@@ -299,10 +307,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
299
307
|
|
|
300
308
|
// ── Turns, run by TeamTurns ────────────────────────────────────────────
|
|
301
309
|
|
|
302
|
-
/**
|
|
303
|
-
* The owner's message in an agent's channel; call inside the channel's queue. `text`
|
|
304
|
-
* carries the reference, `replyText` is what they typed.
|
|
305
|
-
*/
|
|
310
|
+
/** The owner's message in an agent's channel; call inside its queue. `text` has the reference, `replyText` what they typed. */
|
|
306
311
|
answerOwner(
|
|
307
312
|
channel: ChannelKey,
|
|
308
313
|
speaker: Speaker,
|
|
@@ -321,15 +326,12 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
321
326
|
|
|
322
327
|
/**
|
|
323
328
|
* A schedule's or a report's turn in an agent's channel; call inside its queue. A report's
|
|
324
|
-
* turn is interactive: it may ask the owner on cards.
|
|
329
|
+
* turn is interactive: it may ask the owner on cards. `timeLimitMs` is a schedule's own limit.
|
|
325
330
|
*/
|
|
326
331
|
answerBackground(
|
|
327
|
-
|
|
328
|
-
speaker: Speaker,
|
|
329
|
-
text: string,
|
|
330
|
-
interactive = false,
|
|
332
|
+
...args: Parameters<TeamTurns["answerBackground"]>
|
|
331
333
|
): Promise<TurnResult> {
|
|
332
|
-
return this.#turns.answerBackground(
|
|
334
|
+
return this.#turns.answerBackground(...args);
|
|
333
335
|
}
|
|
334
336
|
|
|
335
337
|
/** One owner message in a group channel, answered by a round of members; call inside the queue. */
|
|
@@ -366,8 +368,8 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
366
368
|
return this.#turns.announce(text);
|
|
367
369
|
}
|
|
368
370
|
|
|
369
|
-
message(
|
|
370
|
-
return this.#turns.message(
|
|
371
|
+
message(...args: Parameters<TeamTurns["message"]>): string {
|
|
372
|
+
return this.#turns.message(...args);
|
|
371
373
|
}
|
|
372
374
|
|
|
373
375
|
async read(
|
|
@@ -430,6 +432,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
430
432
|
agent,
|
|
431
433
|
models.defaults,
|
|
432
434
|
skills?.describeCarried(agent.name),
|
|
435
|
+
store.conversations.mark(agent.name, this.roomsOf(name)),
|
|
433
436
|
);
|
|
434
437
|
}
|
|
435
438
|
|
|
@@ -79,12 +79,23 @@ export interface AgentOps {
|
|
|
79
79
|
task: string;
|
|
80
80
|
category?: string;
|
|
81
81
|
skills?: string[];
|
|
82
|
+
/** Turn long-running mode on for its conversations; an owner's turn only. */
|
|
83
|
+
longRunning?: boolean;
|
|
82
84
|
},
|
|
83
85
|
): Promise<string>;
|
|
84
86
|
/** Changes an agent's look; a field left out stays as it is. */
|
|
85
87
|
avatar(name: string, look: CharacterInput): Promise<string>;
|
|
86
88
|
/** Queues the message and returns at once. */
|
|
87
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Hands a request to another agent, in the conversation it used most recently, or in the
|
|
91
|
+
* `conversation` named: "discord", "web" (or a room surface's name), or one of its conversation keys.
|
|
92
|
+
*/
|
|
93
|
+
message(
|
|
94
|
+
caller: AgentTurnScope,
|
|
95
|
+
to: string,
|
|
96
|
+
text: string,
|
|
97
|
+
conversation?: string,
|
|
98
|
+
): string;
|
|
88
99
|
createGroup(input: {
|
|
89
100
|
name: string;
|
|
90
101
|
displayName: string;
|
|
@@ -112,7 +123,7 @@ const NAME = Type.String({
|
|
|
112
123
|
const DISPLAY = Type.String({
|
|
113
124
|
minLength: 1,
|
|
114
125
|
maxLength: 32,
|
|
115
|
-
description: "The name shown on its
|
|
126
|
+
description: "The name shown on its messages; any language.",
|
|
116
127
|
});
|
|
117
128
|
const PROMPT = Type.String({
|
|
118
129
|
minLength: 1,
|
|
@@ -263,7 +274,7 @@ function toolDefs(
|
|
|
263
274
|
name: "agent_create",
|
|
264
275
|
label: "Create agent",
|
|
265
276
|
description:
|
|
266
|
-
"Create a new agent with its own channel and a character picture; its look follows its role unless class and color are given. It starts at once on the opening task in its channel. Its name is permanent.",
|
|
277
|
+
"Create a new agent with its own channel and a character picture; its look follows its role unless class and color are given. It starts at once on the opening task in its channel, which is stopped after 10 minutes unless long_running is set. Its name is permanent.",
|
|
267
278
|
parameters: Type.Object({
|
|
268
279
|
name: Type.String({
|
|
269
280
|
pattern: "^[a-z0-9-]{1,32}$",
|
|
@@ -280,6 +291,11 @@ function toolDefs(
|
|
|
280
291
|
description: "Its opening task, self-contained.",
|
|
281
292
|
}),
|
|
282
293
|
category: Type.Optional(category("Agents")),
|
|
294
|
+
long_running: Type.Optional(
|
|
295
|
+
Type.Boolean({
|
|
296
|
+
description: `Turn on long-running mode for its conversations: its turns, such as the opening task, go on past the usual 10-minute limit by themselves, up to the host's cap (four hours by default). Set it for an agent whose tasks run long, such as research or large changes.`,
|
|
297
|
+
}),
|
|
298
|
+
),
|
|
283
299
|
...(skillsOn
|
|
284
300
|
? {
|
|
285
301
|
skills: Type.Optional(
|
|
@@ -295,6 +311,7 @@ function toolDefs(
|
|
|
295
311
|
const category = optionalString(i, "category");
|
|
296
312
|
const skills = stringList(i, "skills");
|
|
297
313
|
const look = lookOf(i);
|
|
314
|
+
const longRunning = i.long_running === true;
|
|
298
315
|
return ops.create(scope, {
|
|
299
316
|
name: requiredString(i, "name"),
|
|
300
317
|
displayName: requiredString(i, "display_name"),
|
|
@@ -303,6 +320,7 @@ function toolDefs(
|
|
|
303
320
|
task: requiredString(i, "task"),
|
|
304
321
|
...(category ? { category } : {}),
|
|
305
322
|
...(skills && skills.length > 0 ? { skills } : {}),
|
|
323
|
+
...(longRunning ? { longRunning } : {}),
|
|
306
324
|
});
|
|
307
325
|
},
|
|
308
326
|
},
|
|
@@ -442,13 +460,24 @@ function toolDefs(
|
|
|
442
460
|
name: MESSAGE_AGENT_TOOL,
|
|
443
461
|
label: "Message agent",
|
|
444
462
|
description:
|
|
445
|
-
|
|
463
|
+
'Send a self-contained request to another agent. It returns at once; the agent works in its own conversation and its answer comes back to you as a new turn, in the conversation you are writing from. An agent has one conversation on each surface (Discord, the web app); by default the request lands in the one it used most recently, so work it started in the web app is answered there. Give conversation to choose another: "discord", "web", or one of the conversation keys agent_get lists for it. The exchange shows in its conversation, and a thread in yours.',
|
|
446
464
|
parameters: Type.Object({
|
|
447
465
|
to: NAME,
|
|
448
466
|
text: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
467
|
+
conversation: Type.Optional(
|
|
468
|
+
Type.String({
|
|
469
|
+
description:
|
|
470
|
+
'Which of the agent\'s conversations to deliver into: "discord", "web" (or the name of another surface that hosts rooms), or a key from agent_get. Leave it out to reach the one it used most recently.',
|
|
471
|
+
}),
|
|
472
|
+
),
|
|
449
473
|
}),
|
|
450
474
|
run: (i) =>
|
|
451
|
-
ops.message(
|
|
475
|
+
ops.message(
|
|
476
|
+
scope,
|
|
477
|
+
requiredString(i, "to"),
|
|
478
|
+
requiredString(i, "text"),
|
|
479
|
+
optionalString(i, "conversation"),
|
|
480
|
+
),
|
|
452
481
|
});
|
|
453
482
|
return defs;
|
|
454
483
|
}
|
|
@@ -118,7 +118,7 @@ export class GroupTurns {
|
|
|
118
118
|
if (backlog.lastId !== undefined)
|
|
119
119
|
await store.advanceCursor(log, name, backlog.lastId);
|
|
120
120
|
// Attachments and a confirmation belong to a member's first turn of the round only.
|
|
121
|
-
const extra: TurnExtra = { interactive: true };
|
|
121
|
+
const extra: TurnExtra = { interactive: true, byMessage: true };
|
|
122
122
|
if (first) {
|
|
123
123
|
extra.attachments = attachments;
|
|
124
124
|
if (approvals.get(name)) extra.confirmed = true;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChannelKey } from "../domain/conversation.ts";
|
|
2
|
+
import { AgentError } from "../domain/errors.ts";
|
|
3
|
+
import type { AgentDirectory } from "../services.ts";
|
|
4
|
+
import type { Agent } from "./agent-store.ts";
|
|
5
|
+
import {
|
|
6
|
+
agentRoomKey,
|
|
7
|
+
discordIdOf,
|
|
8
|
+
discordKey,
|
|
9
|
+
keysOf,
|
|
10
|
+
roomSurfaceOf,
|
|
11
|
+
} from "./team-keys.ts";
|
|
12
|
+
|
|
13
|
+
/** What the choice of a message's conversation reads. */
|
|
14
|
+
export interface MessageTargetContext {
|
|
15
|
+
store: Pick<AgentDirectory, "agent" | "group">;
|
|
16
|
+
/** The surfaces that host agent rooms. */
|
|
17
|
+
roomSurfaces: readonly string[];
|
|
18
|
+
/** The conversation the target last ran a turn in, if it ran one since the host started. */
|
|
19
|
+
latest: ChannelKey | undefined;
|
|
20
|
+
/** The conversation the sender writes from. */
|
|
21
|
+
from: ChannelKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The word for the web app's conversation, accepted when exactly one surface hosts rooms. */
|
|
25
|
+
const WEB = "web";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The conversation of `target` that a message is delivered into. With `ask` (a surface name such
|
|
29
|
+
* as "discord", "web", or the surface of a room, or one of the target's conversation keys) that
|
|
30
|
+
* one; otherwise the conversation the target used most recently, except that a message from a room
|
|
31
|
+
* never follows it out to Discord, and, when it has run none yet, the sender's own surface, so a message between rooms stays among the rooms.
|
|
32
|
+
*/
|
|
33
|
+
export function messageTarget(
|
|
34
|
+
ctx: MessageTargetContext,
|
|
35
|
+
target: Agent,
|
|
36
|
+
ask: string | undefined,
|
|
37
|
+
): ChannelKey {
|
|
38
|
+
const keys = keysOf(ctx.store, target.name, ctx.roomSurfaces);
|
|
39
|
+
const has = keys.length > 0 ? keys.join(", ") : "none";
|
|
40
|
+
if (ask === undefined || ask.trim() === "") {
|
|
41
|
+
const via = roomSurfaceOf(ctx.from);
|
|
42
|
+
// From a room, never out to Discord: its content may be for the owners alone.
|
|
43
|
+
const outward =
|
|
44
|
+
via !== undefined &&
|
|
45
|
+
ctx.latest !== undefined &&
|
|
46
|
+
discordIdOf(ctx.latest) !== undefined;
|
|
47
|
+
if (ctx.latest !== undefined && keys.includes(ctx.latest) && !outward)
|
|
48
|
+
return ctx.latest;
|
|
49
|
+
if (via === undefined) {
|
|
50
|
+
if (!target.channelId)
|
|
51
|
+
throw new AgentError(`${target.name} has no channel yet.`);
|
|
52
|
+
return discordKey(target.channelId);
|
|
53
|
+
}
|
|
54
|
+
return agentRoomKey(via, target.name);
|
|
55
|
+
}
|
|
56
|
+
const wanted = ask.trim();
|
|
57
|
+
const exact = keys.find((key) => key === wanted);
|
|
58
|
+
if (exact !== undefined) return exact;
|
|
59
|
+
if (wanted === "discord") {
|
|
60
|
+
if (!target.channelId)
|
|
61
|
+
throw new AgentError(
|
|
62
|
+
`${target.name} has no Discord channel; its conversations: ${has}.`,
|
|
63
|
+
);
|
|
64
|
+
return discordKey(target.channelId);
|
|
65
|
+
}
|
|
66
|
+
const surface = ctx.roomSurfaces.includes(wanted)
|
|
67
|
+
? wanted
|
|
68
|
+
: wanted === WEB && ctx.roomSurfaces.length === 1
|
|
69
|
+
? ctx.roomSurfaces[0]
|
|
70
|
+
: undefined;
|
|
71
|
+
if (surface !== undefined) return agentRoomKey(surface, target.name);
|
|
72
|
+
throw new AgentError(
|
|
73
|
+
`${target.name} has no conversation "${wanted}"; its conversations: ${has}. Give "discord", "web" (or the surface's name), or one of those keys.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How a conversation reads to the model that sent a message there: its surface and key. */
|
|
78
|
+
export const conversationLabel = (key: ChannelKey): string =>
|
|
79
|
+
`${key.slice(0, key.indexOf(":"))} conversation (${key})`;
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
import { parseChannelKey } from "../contract/surface.ts";
|
|
9
9
|
import { AgentError } from "../domain/errors.ts";
|
|
10
10
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
11
|
+
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
12
|
+
import type { ConversationModes } from "../modes/conversation-modes.ts";
|
|
11
13
|
import type { AgentChange } from "../services.ts";
|
|
12
14
|
import { addressee, type Speaker } from "../speakers.ts";
|
|
13
15
|
import { openingTaskText } from "./agent-prompt.ts";
|
|
@@ -95,6 +97,8 @@ export class TeamEditing {
|
|
|
95
97
|
task: string;
|
|
96
98
|
category?: string;
|
|
97
99
|
skills?: string[];
|
|
100
|
+
/** Turn long-running mode on for its conversations; the speaker must be an owner. */
|
|
101
|
+
longRunning?: boolean;
|
|
98
102
|
},
|
|
99
103
|
): Promise<string> {
|
|
100
104
|
this.#refuseSkillsOff(input.skills);
|
|
@@ -149,6 +153,23 @@ export class TeamEditing {
|
|
|
149
153
|
return made.agent;
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The modes to turn long-running on in, once the speaker may: an owner other than the host's
|
|
158
|
+
* own turns, as for any change of a conversation's modes. Refused before anything is made.
|
|
159
|
+
*/
|
|
160
|
+
#longRunning(speaker: Speaker): ConversationModes {
|
|
161
|
+
const modes = this.#ctx.options.modes?.();
|
|
162
|
+
if (!modes)
|
|
163
|
+
throw new AgentError(
|
|
164
|
+
"This host has no conversation modes, so long_running cannot be set. Create the agent without it.",
|
|
165
|
+
);
|
|
166
|
+
if (speaker.tier !== "owner" || speaker.principalId === SYSTEM_PRINCIPAL)
|
|
167
|
+
throw new AgentError(
|
|
168
|
+
"Only an owner's turn may create an agent with long_running on. Create it without, and ask the owner to turn it on.",
|
|
169
|
+
);
|
|
170
|
+
return modes;
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
/** An agent cannot carry skills on a host with skills switched off. */
|
|
153
174
|
#refuseSkillsOff(carried: string[] | undefined): void {
|
|
154
175
|
if (carried && carried.length > 0 && !this.#ctx.options.skills)
|
|
@@ -175,6 +196,7 @@ export class TeamEditing {
|
|
|
175
196
|
task?: string;
|
|
176
197
|
category?: string;
|
|
177
198
|
skills?: string[];
|
|
199
|
+
longRunning?: boolean;
|
|
178
200
|
},
|
|
179
201
|
settings: {
|
|
180
202
|
model?: string | undefined;
|
|
@@ -190,8 +212,10 @@ export class TeamEditing {
|
|
|
190
212
|
character: wanted,
|
|
191
213
|
avatarPrompt: _ignored,
|
|
192
214
|
task,
|
|
215
|
+
longRunning,
|
|
193
216
|
...fields
|
|
194
217
|
} = input;
|
|
218
|
+
const modes = longRunning ? this.#longRunning(who.speaker) : undefined;
|
|
195
219
|
const parent = teamCategory(category, "Agents");
|
|
196
220
|
store.checkNewName(input.name);
|
|
197
221
|
skills?.checkRegistered(carried);
|
|
@@ -233,7 +257,12 @@ export class TeamEditing {
|
|
|
233
257
|
"agent created",
|
|
234
258
|
);
|
|
235
259
|
this.#ctx.changed();
|
|
236
|
-
const
|
|
260
|
+
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
261
|
+
// Before the opening task starts, so its turn already runs under the mode.
|
|
262
|
+
if (modes)
|
|
263
|
+
for (const key of keysOf(store, agent.name, rooms))
|
|
264
|
+
await modes.set(key, { longRunning: true }, who.speaker);
|
|
265
|
+
const avatar = `its picture is the ${character.class} look${modes ? "; long-running mode is on" : ""}`;
|
|
237
266
|
const where = channelId ? ` in <#${channelId}>` : "";
|
|
238
267
|
if (!task)
|
|
239
268
|
return {
|
|
@@ -242,7 +271,6 @@ export class TeamEditing {
|
|
|
242
271
|
};
|
|
243
272
|
// The opening task runs in the room of the surface that asked, else where the agent is first
|
|
244
273
|
// reached: its channel, else its first room.
|
|
245
|
-
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
246
274
|
const home =
|
|
247
275
|
who.openIn !== undefined && rooms.includes(who.openIn)
|
|
248
276
|
? agentRoomKey(who.openIn, agent.name)
|
|
@@ -147,6 +147,13 @@ export function roomScope(agent: Agent, room: ChannelKey): AgentTurnScope {
|
|
|
147
147
|
return { name: agent.name, session: room, home: room };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** The agent's own conversation in the channel: its Discord channel's, or its room's. */
|
|
151
|
+
export function scopeOf(agent: Agent, channel: ChannelKey): AgentTurnScope {
|
|
152
|
+
return discordIdOf(channel) === undefined
|
|
153
|
+
? roomScope(agent, channel)
|
|
154
|
+
: homeScope(agent);
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
/**
|
|
151
158
|
* An agent's conversation inside a group: in a Discord group channel, or without a room, as
|
|
152
159
|
* before; in a room, the room's seat, whose home is the agent's own room on that surface.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
2
2
|
import type { ThinkingSetting } from "../models.ts";
|
|
3
|
+
import type { ConversationModes } from "../modes/conversation-modes.ts";
|
|
3
4
|
import type { AgentTools, PromptSection } from "../plugin.ts";
|
|
4
5
|
import type { AvatarStudio, SkillRegistry } from "../services.ts";
|
|
5
6
|
import type { ToolSelection } from "../sessions.ts";
|
|
@@ -61,6 +62,8 @@ export interface AgentTeamOptions
|
|
|
61
62
|
guestPrompt?: string | (() => string);
|
|
62
63
|
/** The skill registry: what each agent carries. Absent when the skills addon is off. */
|
|
63
64
|
skills?: SkillRegistry;
|
|
65
|
+
/** The conversation modes, read when an agent is created with long-running on; none, none is set. */
|
|
66
|
+
modes?: () => ConversationModes | undefined;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/** What the team's collaborators share: its options, its turns, and its change signal. */
|
|
@@ -57,12 +57,16 @@ export function agentDetails(
|
|
|
57
57
|
agent: Agent,
|
|
58
58
|
defaults: { model: string; thinking: ThinkingSetting },
|
|
59
59
|
skills: string | undefined,
|
|
60
|
+
conversations: readonly string[] = [],
|
|
60
61
|
): string {
|
|
61
62
|
return [
|
|
62
63
|
`Name: ${agent.name}`,
|
|
63
64
|
`Display name: ${agent.displayName}`,
|
|
64
65
|
`Status: ${agent.status}`,
|
|
65
66
|
`Channel: ${agent.channelId ? `<#${agent.channelId}>` : "none"}`,
|
|
67
|
+
...(conversations.length === 0
|
|
68
|
+
? []
|
|
69
|
+
: [`Conversations: ${conversations.join(", ")}`]),
|
|
66
70
|
`Model: ${agent.model ?? `${assistantName()}'s (${defaults.model})`}`,
|
|
67
71
|
`Thinking: ${agent.thinking ?? `${assistantName()}'s (${describeThinking(defaults.thinking)})`}`,
|
|
68
72
|
...(skills === undefined ? [] : [`Skills: ${skills}`]),
|