pi-roundtable 0.27.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/docs/plugins.md +54 -22
- package/package.json +1 -1
- package/src/characters/compose.ts +11 -0
- package/src/characters/parts-face.ts +23 -2
- package/src/core/agents/agent-messages.ts +37 -92
- package/src/core/agents/agent-ports.ts +3 -0
- package/src/core/agents/agent-prompt.ts +2 -16
- package/src/core/agents/agent-rules.ts +27 -0
- package/src/core/agents/agent-team.ts +5 -0
- package/src/core/agents/agent-tools.ts +18 -10
- package/src/core/agents/helper-create.ts +3 -24
- package/src/core/agents/helper-modes.ts +64 -0
- package/src/core/agents/message-target.ts +20 -18
- package/src/core/agents/routes.ts +42 -24
- package/src/core/agents/team-editing.ts +5 -5
- package/src/core/agents/team-helpers.ts +76 -5
- package/src/core/agents/team-keys.ts +14 -0
- package/src/core/agents/team-turn-types.ts +7 -13
- package/src/core/agents/team-turns.ts +9 -6
- package/src/core/builtin/agent-server.ts +3 -4
- package/src/core/builtin/discord.ts +35 -8
- package/src/core/builtin/modules.ts +15 -15
- package/src/core/config/adapters.ts +2 -1
- package/src/core/config/limits.ts +35 -2
- package/src/core/config/routing.ts +5 -4
- package/src/core/discord/agent-discord.ts +12 -1
- package/src/core/discord/card-copies.ts +7 -2
- package/src/core/discord/direct-channel.ts +1 -0
- package/src/core/discord/discord-surface.ts +13 -30
- package/src/core/discord/intercom-cards.ts +121 -0
- package/src/core/discord/owner-card-asks.ts +9 -0
- package/src/core/discord/owner-card-types.ts +2 -0
- package/src/core/discord/owner-cards.ts +2 -1
- package/src/core/discord/stop-button.ts +35 -0
- package/src/core/domain/conversation.ts +18 -0
- package/src/core/domain/interim.ts +7 -0
- package/src/core/domain/turn-timeout.ts +5 -0
- package/src/core/i18n/conversation.ts +16 -4
- package/src/core/i18n/turn-timeout.ts +10 -2
- package/src/core/interactions/prompts.ts +5 -0
- package/src/core/modes/autonomy.ts +22 -1
- package/src/core/modes/conversation-modes.ts +135 -23
- package/src/core/modes/mode-inheritance.ts +78 -0
- package/src/core/modes/modes-plugin.ts +1 -1
- package/src/core/modules/notify/notify.ts +3 -2
- package/src/core/modules/notify/session-notify.ts +21 -4
- package/src/core/modules/skills/skill-contract.ts +13 -0
- package/src/core/modules/skills/skill-registry.ts +3 -1
- package/src/core/modules/skills/skill-store.ts +4 -3
- package/src/core/presence/delivery-router.ts +165 -0
- package/src/core/presence/direct-channels.ts +39 -68
- package/src/core/presentation/intercom.ts +44 -0
- package/src/core/runtime/ask-questions.ts +21 -4
- package/src/core/runtime/extensions/ask-user-text.ts +36 -3
- package/src/core/runtime/extensions/ask-user.ts +21 -1
- package/src/core/runtime/interim-text.ts +7 -4
- package/src/core/runtime/pi-agent-runtime.ts +2 -2
- package/src/core/runtime/prompt-slot.ts +11 -1
- package/src/core/runtime/session-factory.ts +11 -6
- package/src/core/runtime/turn-budget.ts +34 -3
- package/src/core/runtime/turn-limits.ts +13 -3
- package/src/index.ts +7 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ChannelKey } from "../domain/conversation.ts";
|
|
1
|
+
import type { ChannelKey, IntercomCard } from "../domain/conversation.ts";
|
|
2
2
|
import { AgentError, IdentityError } from "../domain/errors.ts";
|
|
3
3
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
4
4
|
import { HostStoppingError } from "../errors.ts";
|
|
5
|
-
import { messages } from "../i18n/index.ts";
|
|
6
5
|
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
6
|
+
import { intercomPlain } from "../presentation/intercom.ts";
|
|
7
7
|
import { isQuiet } from "../presentation/reply-body.ts";
|
|
8
8
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
9
9
|
import { addressee, type Speaker } from "../speakers.ts";
|
|
@@ -14,14 +14,13 @@ import {
|
|
|
14
14
|
agentReplyText,
|
|
15
15
|
chainLimitReason,
|
|
16
16
|
deliveredMessagePost,
|
|
17
|
-
returnedAnswerPost,
|
|
18
17
|
} from "./agent-prompt.ts";
|
|
18
|
+
import { answersTo } from "./agent-rules.ts";
|
|
19
19
|
import type { Agent } from "./agent-store.ts";
|
|
20
20
|
import { conversationLabel, messageTarget } from "./message-target.ts";
|
|
21
21
|
import { discordIdOf, discordKey, scopeOf } from "./team-keys.ts";
|
|
22
22
|
import {
|
|
23
23
|
type Chain,
|
|
24
|
-
type Exchange,
|
|
25
24
|
MAX_CHAIN_MESSAGES,
|
|
26
25
|
type TurnHost,
|
|
27
26
|
} from "./team-turn-types.ts";
|
|
@@ -80,35 +79,25 @@ export class AgentMessages {
|
|
|
80
79
|
);
|
|
81
80
|
// A temp knight talks only to the conversation it was made from (and to the owners), so one
|
|
82
81
|
// person's helper session is not open to another person's turn.
|
|
83
|
-
if (target.parent
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`"${target.name}" is a temp knight of "${target.parent}" and answers only the conversation that made it; ask "${target.parent}" there.`,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
82
|
+
if (!answersTo(target, caller.name, parent, chain.speaker))
|
|
83
|
+
throw new AgentError(
|
|
84
|
+
`"${target.name}" is a temp knight of "${target.parent}" and answers only the conversation that made it; ask "${target.parent}" there.`,
|
|
85
|
+
);
|
|
90
86
|
chain.messages += 1;
|
|
91
87
|
// A temp knight is working from the moment a message is handed to it until it has answered.
|
|
92
88
|
this.#host.helpers.begin(target.name);
|
|
93
|
-
// The
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
// The request shows in the sender's turn channel as a small card under the sender's name.
|
|
90
|
+
this.#card(parent, sender, {
|
|
91
|
+
kind: "sent",
|
|
92
|
+
peer: this.#peer(target),
|
|
93
|
+
text,
|
|
94
|
+
});
|
|
96
95
|
void queue
|
|
97
96
|
.run(targetChannel, async () =>
|
|
98
|
-
this.#
|
|
99
|
-
sender,
|
|
100
|
-
target,
|
|
101
|
-
text,
|
|
102
|
-
chain,
|
|
103
|
-
await exchange,
|
|
104
|
-
targetChannel,
|
|
105
|
-
parent,
|
|
106
|
-
),
|
|
97
|
+
this.#exchange(sender, target, text, chain, targetChannel, parent),
|
|
107
98
|
)
|
|
108
99
|
.catch((error: unknown) => {
|
|
109
100
|
if (error instanceof HostStoppingError) {
|
|
110
|
-
// Refused before it started: the thread it opened has nothing more to hold.
|
|
111
|
-
void exchange.then((opened) => opened?.thread.close());
|
|
112
101
|
this.#keep({
|
|
113
102
|
kind: "message",
|
|
114
103
|
sender: sender.name,
|
|
@@ -139,69 +128,36 @@ export class AgentMessages {
|
|
|
139
128
|
return `Sent to "${target.displayName}" in its ${conversationLabel(targetChannel)}. It works there; its answer comes back to you as a new turn.`;
|
|
140
129
|
}
|
|
141
130
|
|
|
131
|
+
/** The other agent of a card, as it is named now. */
|
|
132
|
+
#peer(agent: Agent): IntercomCard["peer"] {
|
|
133
|
+
const current = this.#host.current(agent);
|
|
134
|
+
return { name: current.name, displayName: current.displayName };
|
|
135
|
+
}
|
|
136
|
+
|
|
142
137
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* rejects.
|
|
138
|
+
* Posts a card of the exchange in the channel under `as`'s name: the card where the channel's
|
|
139
|
+
* surface shows cards, and its heading and text where it shows none. Never rejects.
|
|
146
140
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
): Promise<Exchange | undefined> {
|
|
153
|
-
const thread = await this.#host.options.threads?.open(
|
|
154
|
-
parent,
|
|
155
|
-
`→ ${target.displayName}`,
|
|
156
|
-
);
|
|
157
|
-
if (!thread) return undefined;
|
|
158
|
-
await this.#host
|
|
159
|
-
.post(parent, this.#host.current(sender), {
|
|
160
|
-
chunks: splitReply(
|
|
161
|
-
deliveredMessagePost(this.#host.current(target), text),
|
|
162
|
-
),
|
|
163
|
-
threadId: thread.id,
|
|
141
|
+
#card(channel: ChannelKey, as: Agent, card: IntercomCard): Promise<void> {
|
|
142
|
+
return this.#host
|
|
143
|
+
.post(channel, this.#host.current(as), {
|
|
144
|
+
chunks: splitReply(intercomPlain(card)),
|
|
145
|
+
intercom: card,
|
|
164
146
|
})
|
|
165
147
|
.catch((error: unknown) =>
|
|
166
148
|
this.#host.options.logger.warn(
|
|
167
|
-
{ err: error },
|
|
168
|
-
"agent
|
|
149
|
+
{ channel, kind: card.kind, err: error },
|
|
150
|
+
"agent message card not posted",
|
|
169
151
|
),
|
|
170
152
|
);
|
|
171
|
-
return { parent, thread };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Runs the target's turn, then posts its answer and wakes the sender; closes the thread either way. */
|
|
175
|
-
async #deliver(
|
|
176
|
-
sender: Agent,
|
|
177
|
-
target: Agent,
|
|
178
|
-
text: string,
|
|
179
|
-
chain: Chain,
|
|
180
|
-
exchange: Exchange | undefined,
|
|
181
|
-
targetChannel: ChannelKey,
|
|
182
|
-
senderChannel: ChannelKey,
|
|
183
|
-
): Promise<void> {
|
|
184
|
-
try {
|
|
185
|
-
await this.#exchange(
|
|
186
|
-
sender,
|
|
187
|
-
target,
|
|
188
|
-
text,
|
|
189
|
-
chain,
|
|
190
|
-
exchange,
|
|
191
|
-
targetChannel,
|
|
192
|
-
senderChannel,
|
|
193
|
-
);
|
|
194
|
-
} finally {
|
|
195
|
-
await exchange?.thread.close();
|
|
196
|
-
}
|
|
197
153
|
}
|
|
198
154
|
|
|
155
|
+
/** Runs the target's turn, then shows its answer in the sender's channel and wakes the sender. */
|
|
199
156
|
async #exchange(
|
|
200
157
|
sender: Agent,
|
|
201
158
|
target: Agent,
|
|
202
159
|
text: string,
|
|
203
160
|
chain: Chain,
|
|
204
|
-
exchange: Exchange | undefined,
|
|
205
161
|
targetChannel: ChannelKey,
|
|
206
162
|
senderChannel: ChannelKey,
|
|
207
163
|
): Promise<void> {
|
|
@@ -223,9 +179,6 @@ export class AgentMessages {
|
|
|
223
179
|
chain,
|
|
224
180
|
{ unattended: true },
|
|
225
181
|
);
|
|
226
|
-
const thread = exchange?.thread;
|
|
227
|
-
if (thread && !result.ok)
|
|
228
|
-
await thread.close(messages().agentNoReply(result.error.message));
|
|
229
182
|
// A target with nothing to answer sends nothing back and wakes nobody.
|
|
230
183
|
if (isQuiet(result)) {
|
|
231
184
|
logger.info(
|
|
@@ -236,29 +189,22 @@ export class AgentMessages {
|
|
|
236
189
|
}
|
|
237
190
|
const current = this.#host.options.store.agent(sender.name);
|
|
238
191
|
if (current?.status !== "active") return;
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
.catch((error: unknown) =>
|
|
247
|
-
logger.error({ err: error }, "agent answer not posted back"),
|
|
248
|
-
);
|
|
249
|
-
await thread?.close();
|
|
192
|
+
// The answer, or why none came, shows in the sender's channel as a small card.
|
|
193
|
+
await this.#card(senderChannel, target, {
|
|
194
|
+
kind: result.ok ? "answered" : "failed",
|
|
195
|
+
peer: this.#peer(target),
|
|
196
|
+
text: result.ok ? result.text : result.error.message,
|
|
197
|
+
});
|
|
250
198
|
const followUp = result.ok
|
|
251
199
|
? agentReplyText(
|
|
252
200
|
addressee(chain.speaker, this.#host.options.owner),
|
|
253
201
|
this.#host.current(target),
|
|
254
202
|
result.text,
|
|
255
|
-
thread?.mention,
|
|
256
203
|
)
|
|
257
204
|
: agentFailureText(
|
|
258
205
|
addressee(chain.speaker, this.#host.options.owner),
|
|
259
206
|
this.#host.current(target),
|
|
260
207
|
result.error.message,
|
|
261
|
-
thread?.mention,
|
|
262
208
|
);
|
|
263
209
|
// The answer is a report back to the sender, so its turn may ask the owner on cards.
|
|
264
210
|
void queue
|
|
@@ -407,7 +353,7 @@ export class AgentMessages {
|
|
|
407
353
|
await this.#followUp(sender, entry.channel, entry.text, chain);
|
|
408
354
|
return;
|
|
409
355
|
}
|
|
410
|
-
// Its
|
|
356
|
+
// Its request card was posted when it was sent; the answer's card follows as it would have.
|
|
411
357
|
this.#host.helpers.begin(target.name);
|
|
412
358
|
try {
|
|
413
359
|
await this.#exchange(
|
|
@@ -415,7 +361,6 @@ export class AgentMessages {
|
|
|
415
361
|
target,
|
|
416
362
|
entry.text,
|
|
417
363
|
chain,
|
|
418
|
-
undefined,
|
|
419
364
|
entry.channel,
|
|
420
365
|
origin,
|
|
421
366
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentRuntime, ContextUse } from "../contract/runtime.ts";
|
|
2
|
+
import type { IntercomCard } from "../domain/conversation.ts";
|
|
2
3
|
import type { InterimPosts } from "../domain/interim.ts";
|
|
3
4
|
import type { ThinkingSetting } from "../models.ts";
|
|
4
5
|
|
|
@@ -24,6 +25,8 @@ export interface AgentPost {
|
|
|
24
25
|
files?: { name: string; data: Uint8Array }[];
|
|
25
26
|
/** A thread of the channel to post in instead, such as a dispatch's. */
|
|
26
27
|
threadId?: string;
|
|
28
|
+
/** A message between agents, shown as a small card in place of `chunks` where the channel can. */
|
|
29
|
+
intercom?: IntercomCard;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -134,16 +134,8 @@ export function agentReplyText(
|
|
|
134
134
|
who: OwnerIdentity,
|
|
135
135
|
from: Agent,
|
|
136
136
|
text: string,
|
|
137
|
-
thread?: string,
|
|
138
137
|
): string {
|
|
139
|
-
return `(The agent "${from.displayName}" (\`${from.name}\`) answered your message. Continue with it and tell ${who.name} what matters; your reply is posted in your channel
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Where the exchange was posted, when it had a thread. */
|
|
143
|
-
function threadNote(thread: string | undefined): string {
|
|
144
|
-
return thread
|
|
145
|
-
? ` The exchange is in the thread ${thread}, now archived; link it when useful.`
|
|
146
|
-
: "";
|
|
138
|
+
return `(The agent "${from.displayName}" (\`${from.name}\`) answered your message. Continue with it and tell ${who.name} what matters; your reply is posted in your channel.)\n\n${text}`;
|
|
147
139
|
}
|
|
148
140
|
|
|
149
141
|
/** What the sender receives when the target could not answer. */
|
|
@@ -151,9 +143,8 @@ export function agentFailureText(
|
|
|
151
143
|
who: OwnerIdentity,
|
|
152
144
|
from: Agent,
|
|
153
145
|
reason: string,
|
|
154
|
-
thread?: string,
|
|
155
146
|
): string {
|
|
156
|
-
return `(The agent "${from.displayName}" (\`${from.name}\`) could not answer your message: ${reason}. Tell ${who.name} briefly, or try again later
|
|
147
|
+
return `(The agent "${from.displayName}" (\`${from.name}\`) could not answer your message: ${reason}. Tell ${who.name} briefly, or try again later.)`;
|
|
157
148
|
}
|
|
158
149
|
|
|
159
150
|
/** A new agent's first turn. */
|
|
@@ -192,11 +183,6 @@ export function deliveredMessagePost(to: Agent, text: string): string {
|
|
|
192
183
|
return messages().messageDelivered(to.displayName, text);
|
|
193
184
|
}
|
|
194
185
|
|
|
195
|
-
/** How the target's answer appears in the sender's channel. */
|
|
196
|
-
export function returnedAnswerPost(text: string): string {
|
|
197
|
-
return messages().answerReturned(text);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
186
|
/** Why a chain of agent messages stops without the owner in between. */
|
|
201
187
|
export function chainLimitReason(who: OwnerIdentity): string {
|
|
202
188
|
const o = ownerWords(who);
|
|
@@ -42,6 +42,33 @@ export interface Agent {
|
|
|
42
42
|
export const isHelper = (agent: Pick<Agent, "parent"> | undefined): boolean =>
|
|
43
43
|
agent?.parent !== undefined;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Whether `sender`, writing from `from` in a run `speaker` started, may message `target`: a temp
|
|
47
|
+
* knight answers only its knight in the conversation that made it, and the owners.
|
|
48
|
+
*/
|
|
49
|
+
export const answersTo = (
|
|
50
|
+
target: Pick<Agent, "parent" | "origin">,
|
|
51
|
+
sender: string,
|
|
52
|
+
from: string,
|
|
53
|
+
speaker: { tier: string },
|
|
54
|
+
): boolean =>
|
|
55
|
+
target.parent === undefined ||
|
|
56
|
+
speaker.tier === "owner" ||
|
|
57
|
+
(sender === target.parent && target.origin === from);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The surface a temp knight was made on, such as "discord" or the web app's, read off its origin;
|
|
61
|
+
* undefined for a permanent knight and for a temp knight made before the origin was kept. A temp
|
|
62
|
+
* knight lives on that surface; its place on another is made only when someone uses it there.
|
|
63
|
+
*/
|
|
64
|
+
export const helperSurface = (
|
|
65
|
+
agent: Pick<Agent, "parent" | "origin"> | undefined,
|
|
66
|
+
): string | undefined => {
|
|
67
|
+
const origin = agent?.parent === undefined ? undefined : agent.origin;
|
|
68
|
+
const colon = origin?.indexOf(":") ?? -1;
|
|
69
|
+
return origin && colon > 0 ? origin.slice(0, colon) : undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
45
72
|
export interface AgentGroup {
|
|
46
73
|
name: string;
|
|
47
74
|
displayName: string;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import type { HelperTeam } from "./helper-ops.ts";
|
|
20
20
|
import { type Routes, routesOf } from "./routes.ts";
|
|
21
21
|
import { TeamEditing } from "./team-editing.ts";
|
|
22
|
+
import type { TeamHelpers } from "./team-helpers.ts";
|
|
22
23
|
import {
|
|
23
24
|
channelOwner,
|
|
24
25
|
coordinatorOf,
|
|
@@ -321,6 +322,10 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
321
322
|
return this.#turns.message(...args);
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
reach(...args: Parameters<TeamHelpers["reach"]>): Promise<void> {
|
|
326
|
+
return this.#lifecycle.helpers.reach(...args);
|
|
327
|
+
}
|
|
328
|
+
|
|
324
329
|
async read(
|
|
325
330
|
channel: string,
|
|
326
331
|
around: string | undefined,
|
|
@@ -123,7 +123,7 @@ export interface AgentOps {
|
|
|
123
123
|
avatar(name: string, look: CharacterInput): Promise<string>;
|
|
124
124
|
/** Queues the message and returns at once. */
|
|
125
125
|
/**
|
|
126
|
-
* Hands a request to another agent, in
|
|
126
|
+
* Hands a request to another agent, in its conversation on the sender's surface, or in the
|
|
127
127
|
* `conversation` named: "discord", "web" (or a room surface's name), or one of its conversation keys.
|
|
128
128
|
*/
|
|
129
129
|
message(
|
|
@@ -132,6 +132,15 @@ export interface AgentOps {
|
|
|
132
132
|
text: string,
|
|
133
133
|
conversation?: string,
|
|
134
134
|
): string;
|
|
135
|
+
/**
|
|
136
|
+
* Readies the place a message is about to reach: a temp knight made in the web app gets its
|
|
137
|
+
* Discord thread when the caller may message it and names "discord". Never rejects.
|
|
138
|
+
*/
|
|
139
|
+
reach?(
|
|
140
|
+
caller: AgentTurnScope,
|
|
141
|
+
to: string,
|
|
142
|
+
conversation?: string,
|
|
143
|
+
): Promise<void>;
|
|
135
144
|
createGroup(input: {
|
|
136
145
|
name: string;
|
|
137
146
|
displayName: string;
|
|
@@ -425,24 +434,23 @@ function toolDefs(
|
|
|
425
434
|
name: MESSAGE_AGENT_TOOL,
|
|
426
435
|
label: "Message agent",
|
|
427
436
|
description:
|
|
428
|
-
'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
|
|
437
|
+
'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 its conversation on the surface you are writing from (its room from a web room, its Discord channel from Discord), so nothing crosses surfaces unless you name it. Give conversation to choose another: "discord", "web", or one of the conversation keys agent_get lists for it. The request shows in its conversation, and the request and the answer show as small cards in yours.',
|
|
429
438
|
parameters: Type.Object({
|
|
430
439
|
to: NAME,
|
|
431
440
|
text: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
432
441
|
conversation: Type.Optional(
|
|
433
442
|
Type.String({
|
|
434
443
|
description:
|
|
435
|
-
'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
|
|
444
|
+
'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 its conversation on the surface you are writing from.',
|
|
436
445
|
}),
|
|
437
446
|
),
|
|
438
447
|
}),
|
|
439
|
-
run: (i) =>
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
),
|
|
448
|
+
run: async (i) => {
|
|
449
|
+
const to = requiredString(i, "to");
|
|
450
|
+
const conversation = optionalString(i, "conversation");
|
|
451
|
+
await ops.reach?.(scope, to, conversation);
|
|
452
|
+
return ops.message(scope, to, requiredString(i, "text"), conversation);
|
|
453
|
+
},
|
|
446
454
|
});
|
|
447
455
|
return defs;
|
|
448
456
|
}
|
|
@@ -2,12 +2,11 @@ import type { CharacterInput } from "../../characters/index.ts";
|
|
|
2
2
|
import { helpersOf } from "../config/helpers.ts";
|
|
3
3
|
import { AgentError } from "../domain/errors.ts";
|
|
4
4
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
5
|
-
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
6
5
|
import type { Speaker } from "../speakers.ts";
|
|
7
6
|
import { toolTiers } from "../tool-tiers.ts";
|
|
8
7
|
import { isHelper } from "./agent-rules.ts";
|
|
9
8
|
import type { Agent } from "./agent-store.ts";
|
|
10
|
-
import {
|
|
9
|
+
import { followOrigin } from "./helper-modes.ts";
|
|
11
10
|
import type { TeamContext } from "./team-options.ts";
|
|
12
11
|
|
|
13
12
|
/** The team's way of making an agent, which `createHelper` uses to make a temp knight. */
|
|
@@ -98,10 +97,10 @@ export async function createHelper(
|
|
|
98
97
|
);
|
|
99
98
|
const helper = made.agent;
|
|
100
99
|
try {
|
|
101
|
-
await
|
|
100
|
+
await followOrigin(ctx, caller, speaker, helper);
|
|
102
101
|
ctx.turns.message(caller, helper.name, task);
|
|
103
102
|
} catch (error) {
|
|
104
|
-
// The
|
|
103
|
+
// The modes or the task could not be handed over (a chain at its cap): the temp
|
|
105
104
|
// knight is not left idle, and its thread is closed.
|
|
106
105
|
await ctx.archiveHelpers([helper.name]);
|
|
107
106
|
throw error;
|
|
@@ -158,23 +157,3 @@ async function makeRoom(
|
|
|
158
157
|
await ctx.archiveHelpers([oldest.name]);
|
|
159
158
|
return ` To make room, "${oldest.displayName}" (${oldest.name}), your oldest finished temp knight, was archived.`;
|
|
160
159
|
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Turns long-running on for the temp knight's conversations when the conversation that asked for
|
|
164
|
-
* it has it on, so it works as long as its parent may. This adds no permission: an owner turned
|
|
165
|
-
* it on there. Only a turn of an owner can carry that over, as for any change of modes.
|
|
166
|
-
*/
|
|
167
|
-
async function inheritLongRunning(
|
|
168
|
-
ctx: TeamContext,
|
|
169
|
-
caller: AgentTurnScope,
|
|
170
|
-
speaker: Speaker,
|
|
171
|
-
helper: Agent,
|
|
172
|
-
): Promise<void> {
|
|
173
|
-
const { store, surfaces } = ctx.options;
|
|
174
|
-
const modes = ctx.options.modes?.();
|
|
175
|
-
if (!modes?.peek(ctx.turns.turnChannel(caller)).longRunning) return;
|
|
176
|
-
if (speaker.tier !== "owner" || speaker.principalId === SYSTEM_PRINCIPAL)
|
|
177
|
-
return;
|
|
178
|
-
for (const key of keysOf(store, helper.name, surfaces?.roomSurfaces() ?? []))
|
|
179
|
-
await modes.set(key, { longRunning: true }, speaker);
|
|
180
|
-
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
2
|
+
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
3
|
+
import type { ModeOrigins } from "../modes/mode-inheritance.ts";
|
|
4
|
+
import type { AgentDirectory } from "../services.ts";
|
|
5
|
+
import type { Speaker } from "../speakers.ts";
|
|
6
|
+
import { isHelper } from "./agent-rules.ts";
|
|
7
|
+
import type { Agent } from "./agent-store.ts";
|
|
8
|
+
import { agentOfChannel, groupOfChannel, keysOf } from "./team-keys.ts";
|
|
9
|
+
import type { TeamContext } from "./team-options.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Makes the temp knight's conversations follow the modes of the conversation that asked for it,
|
|
13
|
+
* live, so turning yolo or long-running on or off there applies to it at once. This adds no
|
|
14
|
+
* permission: an owner turns the modes on there. Only a turn of an owner can carry them over, as
|
|
15
|
+
* for any change of modes; any other turn's temp knight starts with its own, both off, whatever
|
|
16
|
+
* an earlier agent of the same name followed.
|
|
17
|
+
*/
|
|
18
|
+
export async function followOrigin(
|
|
19
|
+
ctx: TeamContext,
|
|
20
|
+
caller: AgentTurnScope,
|
|
21
|
+
speaker: Speaker,
|
|
22
|
+
helper: Agent,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const { store, surfaces } = ctx.options;
|
|
25
|
+
const modes = ctx.options.modes?.();
|
|
26
|
+
if (!modes) return;
|
|
27
|
+
const keys = keysOf(store, helper.name, surfaces?.roomSurfaces() ?? []);
|
|
28
|
+
if (speaker.tier !== "owner" || speaker.principalId === SYSTEM_PRINCIPAL) {
|
|
29
|
+
for (const key of keys) await modes.forget(key);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await modes.follow(keys, ctx.turns.turnChannel(caller), speaker);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the agent server says of a conversation that follows another's modes: it still does while
|
|
37
|
+
* it is a temp knight's conversation made from that origin, named after the temp knight's parent;
|
|
38
|
+
* the origin is gone once the agent or group whose conversation it is (else the parent) is
|
|
39
|
+
* archived or deleted.
|
|
40
|
+
*/
|
|
41
|
+
export function modeOrigins(
|
|
42
|
+
store: Pick<
|
|
43
|
+
AgentDirectory,
|
|
44
|
+
"agent" | "agentByChannel" | "group" | "groupByChannel"
|
|
45
|
+
>,
|
|
46
|
+
hosts: { roomSurfaces(): readonly string[] },
|
|
47
|
+
): ModeOrigins {
|
|
48
|
+
return {
|
|
49
|
+
originOf: (channel, origin) => {
|
|
50
|
+
const helper = agentOfChannel(store, hosts, channel);
|
|
51
|
+
if (!helper || !isHelper(helper) || helper.origin !== origin)
|
|
52
|
+
return undefined;
|
|
53
|
+
const parent = store.agent(helper.parent as string);
|
|
54
|
+
const owner =
|
|
55
|
+
agentOfChannel(store, hosts, origin) ??
|
|
56
|
+
groupOfChannel(store, hosts, origin) ??
|
|
57
|
+
parent;
|
|
58
|
+
return {
|
|
59
|
+
name: parent?.displayName ?? (helper.parent as string),
|
|
60
|
+
alive: owner?.status === "active",
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -3,19 +3,22 @@ import { AgentError } from "../domain/errors.ts";
|
|
|
3
3
|
import type { AgentDirectory } from "../services.ts";
|
|
4
4
|
import type { Agent } from "./agent-store.ts";
|
|
5
5
|
import { Routes } from "./routes.ts";
|
|
6
|
-
import { agentRoomKey,
|
|
6
|
+
import { agentRoomKey, keysOf } from "./team-keys.ts";
|
|
7
7
|
|
|
8
8
|
/** What the choice of a message's conversation reads. */
|
|
9
9
|
export interface MessageTargetContext {
|
|
10
10
|
store: Pick<AgentDirectory, "agent" | "group">;
|
|
11
11
|
/** The surfaces that host agent rooms. */
|
|
12
12
|
roomSurfaces: readonly string[];
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The conversation the target last ran a turn in, if it ran one since the host started; read
|
|
15
|
+
* only under the `"latest"` routing rule, and only on the sender's surface.
|
|
16
|
+
*/
|
|
14
17
|
latest: ChannelKey | undefined;
|
|
15
|
-
/** The conversation the sender writes from. */
|
|
18
|
+
/** The conversation the sender writes from: the origin whose surface the message stays on. */
|
|
16
19
|
from: ChannelKey;
|
|
17
20
|
/** The host's routing table; without it the defaults every host had before `routing`. */
|
|
18
|
-
routes?: Pick<Routes, "of">;
|
|
21
|
+
routes?: Pick<Routes, "of" | "placeOn">;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/** The word for the web app's conversation, accepted when exactly one surface hosts rooms. */
|
|
@@ -24,9 +27,8 @@ const WEB = "web";
|
|
|
24
27
|
/**
|
|
25
28
|
* The conversation of `target` that a message is delivered into. With `ask` (a surface name such
|
|
26
29
|
* as "discord", "web", or the surface of a room, or one of the target's conversation keys) that
|
|
27
|
-
* one; otherwise the conversation the routing table picks (`Routes.of`): by default the
|
|
28
|
-
*
|
|
29
|
-
* it has run none yet, the sender's own surface, so a message between rooms stays among the rooms.
|
|
30
|
+
* one; otherwise the conversation the routing table picks (`Routes.of`): by default the target's
|
|
31
|
+
* place on the sender's own surface, so a message never crosses surfaces unless named.
|
|
30
32
|
*/
|
|
31
33
|
export function messageTarget(
|
|
32
34
|
ctx: MessageTargetContext,
|
|
@@ -35,25 +37,25 @@ export function messageTarget(
|
|
|
35
37
|
): ChannelKey {
|
|
36
38
|
const keys = keysOf(ctx.store, target.name, ctx.roomSurfaces);
|
|
37
39
|
const has = keys.length > 0 ? keys.join(", ") : "none";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const routes =
|
|
41
|
+
ctx.routes ??
|
|
42
|
+
new Routes({
|
|
43
|
+
store: ctx.store,
|
|
44
|
+
roomSurfaces: () => ctx.roomSurfaces,
|
|
45
|
+
latest: () => ctx.latest,
|
|
46
|
+
});
|
|
47
|
+
if (ask === undefined || ask.trim() === "")
|
|
46
48
|
return routes.of(target.name, "agentMessage", ctx.from);
|
|
47
|
-
}
|
|
48
49
|
const wanted = ask.trim();
|
|
49
50
|
const exact = keys.find((key) => key === wanted);
|
|
50
51
|
if (exact !== undefined) return exact;
|
|
51
52
|
if (wanted === "discord") {
|
|
52
|
-
|
|
53
|
+
const place = routes.placeOn(target.name, "discord");
|
|
54
|
+
if (!place)
|
|
53
55
|
throw new AgentError(
|
|
54
56
|
`${target.name} has no Discord channel; its conversations: ${has}.`,
|
|
55
57
|
);
|
|
56
|
-
return
|
|
58
|
+
return place;
|
|
57
59
|
}
|
|
58
60
|
const surface = ctx.roomSurfaces.includes(wanted)
|
|
59
61
|
? wanted
|