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
|
@@ -31,11 +31,12 @@ export interface AgentRoutes {
|
|
|
31
31
|
*/
|
|
32
32
|
integration?: "origin" | "primary" | (string & {});
|
|
33
33
|
/**
|
|
34
|
-
* The conversation a `message_agent` call reaches when it names none. `"
|
|
35
|
-
*
|
|
34
|
+
* The conversation a `message_agent` call reaches when it names none. `"origin-surface"`
|
|
35
|
+
* (default) is the agent's place on the sender's surface, else its primary place; `"latest"` the
|
|
36
|
+
* one the agent last ran a turn in on the sender's surface, else as `"origin-surface"`;
|
|
36
37
|
* `"primary"` the agent's primary place; a surface name the agent's place there.
|
|
37
38
|
*/
|
|
38
|
-
agentMessage?: "latest" | "primary" | (string & {});
|
|
39
|
+
agentMessage?: "origin-surface" | "latest" | "primary" | (string & {});
|
|
39
40
|
/**
|
|
40
41
|
* Where an agent's opening task (`agent_create task`) runs. `"origin-surface"` (default) is the
|
|
41
42
|
* agent's place on the surface the creation came from, else its primary place; `"primary"`
|
|
@@ -86,7 +87,7 @@ const DECLARED_PLACES: readonly string[] = ["ops"];
|
|
|
86
87
|
export const ROUTE_WORDS = {
|
|
87
88
|
primary: ["auto"],
|
|
88
89
|
integration: ["origin", "primary"],
|
|
89
|
-
agentMessage: ["latest", "primary"],
|
|
90
|
+
agentMessage: ["origin-surface", "latest", "primary"],
|
|
90
91
|
opening: ["origin-surface", "primary"],
|
|
91
92
|
} as const;
|
|
92
93
|
|
|
@@ -28,6 +28,7 @@ import { AgentError } from "../domain/errors.ts";
|
|
|
28
28
|
import type { InterimPosts } from "../domain/interim.ts";
|
|
29
29
|
import { messages } from "../i18n/index.ts";
|
|
30
30
|
import type { Logger } from "../log.ts";
|
|
31
|
+
import { type IntercomTexts, intercomPost } from "./intercom-cards.ts";
|
|
31
32
|
|
|
32
33
|
const DASHBOARD_NAME = "dashboard";
|
|
33
34
|
|
|
@@ -42,10 +43,13 @@ export class DiscordAgentChannels implements AgentChannels {
|
|
|
42
43
|
readonly #webhooks = new Map<string, Promise<Webhook>>();
|
|
43
44
|
/** The parent channel of each thread the agents live in, and null for a channel that is none. */
|
|
44
45
|
readonly #parents = new Map<string, string | null>();
|
|
46
|
+
/** Where the cards' whole texts are kept for their open buttons; without it a card posts as text. */
|
|
47
|
+
readonly #texts: IntercomTexts | undefined;
|
|
45
48
|
|
|
46
|
-
constructor(client: Client, guildId: string) {
|
|
49
|
+
constructor(client: Client, guildId: string, texts?: IntercomTexts) {
|
|
47
50
|
this.#client = client;
|
|
48
51
|
this.#guildId = guildId;
|
|
52
|
+
this.#texts = texts;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -77,6 +81,13 @@ export class DiscordAgentChannels implements AgentChannels {
|
|
|
77
81
|
...(route.threadId ? { threadId: route.threadId } : {}),
|
|
78
82
|
};
|
|
79
83
|
try {
|
|
84
|
+
if (post.intercom && this.#texts) {
|
|
85
|
+
await webhook.send({
|
|
86
|
+
...as,
|
|
87
|
+
...intercomPost(post.intercom, this.#texts),
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
80
91
|
if (post.thinking) await webhook.send({ ...as, content: post.thinking });
|
|
81
92
|
for (const chunk of post.chunks)
|
|
82
93
|
await webhook.send({ ...as, content: chunk });
|
|
@@ -35,6 +35,8 @@ export interface CardCopiesOptions {
|
|
|
35
35
|
setInterval?: typeof setInterval;
|
|
36
36
|
/** Replaceable in tests, with `setInterval`. */
|
|
37
37
|
clearInterval?: typeof clearInterval;
|
|
38
|
+
/** What the texts are, as the log names them; default "card copy texts". */
|
|
39
|
+
what?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -69,7 +71,7 @@ export class CardCopies {
|
|
|
69
71
|
} catch (error) {
|
|
70
72
|
logger.warn(
|
|
71
73
|
{ path, err: error },
|
|
72
|
-
"card copy texts not read;
|
|
74
|
+
`${options.what ?? "card copy texts"} not read; buttons of earlier cards will say they are gone`,
|
|
73
75
|
);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -172,7 +174,10 @@ export class CardCopies {
|
|
|
172
174
|
await chmod(temporary, 0o600);
|
|
173
175
|
await rename(temporary, path);
|
|
174
176
|
} catch (error) {
|
|
175
|
-
logger.warn(
|
|
177
|
+
logger.warn(
|
|
178
|
+
{ path, err: error },
|
|
179
|
+
`${this.#options.what ?? "card copy texts"} not saved`,
|
|
180
|
+
);
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
183
|
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
Events,
|
|
7
7
|
GatewayIntentBits,
|
|
8
8
|
type Interaction,
|
|
9
|
-
type Message,
|
|
10
9
|
MessageFlags,
|
|
11
10
|
Partials,
|
|
12
11
|
type REST,
|
|
@@ -47,18 +46,17 @@ import { DiscordThreadHost } from "./dispatch-thread-host.ts";
|
|
|
47
46
|
import { DISCORD_FORMATTING, pickFormatting } from "./formatting.ts";
|
|
48
47
|
import { DEFAULT_FRESH_MARKER, postFreshMarker } from "./fresh-marker.ts";
|
|
49
48
|
import { lateAnswerMessage } from "./inbound-message.ts";
|
|
49
|
+
import type { IntercomTexts } from "./intercom-cards.ts";
|
|
50
50
|
import { listenForMessages, reportChanges } from "./message-changes.ts";
|
|
51
51
|
import type { CardChannel, LateTurn } from "./owner-cards.ts";
|
|
52
52
|
import { DiscordOwnerOps } from "./owner-discord.ts";
|
|
53
|
-
import {
|
|
53
|
+
import { showStopPanel } from "./stop-button.ts";
|
|
54
54
|
|
|
55
55
|
const PREFIX = "discord:";
|
|
56
56
|
/** Discord's error code for a channel that does not exist or cannot be seen. */
|
|
57
57
|
const UNKNOWN_CHANNEL = 10003;
|
|
58
58
|
|
|
59
59
|
const TYPING_REFRESH_MS = 8_000;
|
|
60
|
-
/** A turn shorter than this shows no stop button, so quick answers do not flicker one. */
|
|
61
|
-
const STOP_PANEL_DELAY_MS = 5_000;
|
|
62
60
|
|
|
63
61
|
export interface DiscordSurfaceOptions {
|
|
64
62
|
token: string;
|
|
@@ -73,6 +71,8 @@ export interface DiscordSurfaceOptions {
|
|
|
73
71
|
freshMarker?: string | false;
|
|
74
72
|
/** What the model is told about how replies render here; `false` tells it nothing. Default: `DISCORD_FORMATTING`. */
|
|
75
73
|
formatting?: string | false;
|
|
74
|
+
/** Where the agents' message cards keep their whole texts for the open button; without it a card posts as text. */
|
|
75
|
+
intercomTexts?: IntercomTexts;
|
|
76
76
|
/** Reads the channel context of an addressed message; without it the surface has none. */
|
|
77
77
|
channelContext?: (
|
|
78
78
|
message: InboundMessage,
|
|
@@ -157,7 +157,11 @@ export class DiscordSurface
|
|
|
157
157
|
|
|
158
158
|
/** The agent server's channels and webhooks. */
|
|
159
159
|
agentChannels(guildId: string): AgentChannels {
|
|
160
|
-
return new DiscordAgentChannels(
|
|
160
|
+
return new DiscordAgentChannels(
|
|
161
|
+
this.#client,
|
|
162
|
+
guildId,
|
|
163
|
+
this.#options.intercomTexts,
|
|
164
|
+
);
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
/** Threads for background dispatches, in the channel that started each. */
|
|
@@ -365,33 +369,12 @@ export class DiscordSurface
|
|
|
365
369
|
}
|
|
366
370
|
|
|
367
371
|
showStop(channel: ChannelKey): () => void {
|
|
368
|
-
const { logger } = this.#options;
|
|
369
372
|
// An outside agent's conversation has no Discord channel to show a button in.
|
|
370
373
|
if (!channel.startsWith(PREFIX)) return () => undefined;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
.delete()
|
|
376
|
-
.catch((error: unknown) =>
|
|
377
|
-
logger.warn({ channel, err: error }, "stop panel not removed"),
|
|
378
|
-
);
|
|
379
|
-
const timer = setTimeout(() => {
|
|
380
|
-
this.#sendable(channel)
|
|
381
|
-
.then((target): Promise<Message> => target.send(stopPanel()))
|
|
382
|
-
.then((message) => {
|
|
383
|
-
if (hidden) void remove(message);
|
|
384
|
-
else panel = message;
|
|
385
|
-
})
|
|
386
|
-
.catch((error: unknown) =>
|
|
387
|
-
logger.warn({ channel, err: error }, "stop panel not shown"),
|
|
388
|
-
);
|
|
389
|
-
}, STOP_PANEL_DELAY_MS);
|
|
390
|
-
return () => {
|
|
391
|
-
hidden = true;
|
|
392
|
-
clearTimeout(timer);
|
|
393
|
-
if (panel) void remove(panel);
|
|
394
|
-
};
|
|
374
|
+
return showStopPanel(
|
|
375
|
+
() => this.#sendable(channel),
|
|
376
|
+
(error, what) => this.#options.logger.warn({ channel, err: error }, what),
|
|
377
|
+
);
|
|
395
378
|
}
|
|
396
379
|
|
|
397
380
|
async react(
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionRowBuilder,
|
|
3
|
+
AttachmentBuilder,
|
|
4
|
+
ButtonBuilder,
|
|
5
|
+
ButtonStyle,
|
|
6
|
+
type Interaction,
|
|
7
|
+
MessageFlags,
|
|
8
|
+
} from "discord.js";
|
|
9
|
+
import type { IntercomCard } from "../domain/conversation.ts";
|
|
10
|
+
import { messages } from "../i18n/index.ts";
|
|
11
|
+
import {
|
|
12
|
+
intercomFolds,
|
|
13
|
+
intercomHeading,
|
|
14
|
+
intercomSummary,
|
|
15
|
+
} from "../presentation/intercom.ts";
|
|
16
|
+
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
17
|
+
import type { InteractionModule } from "./interaction-module.ts";
|
|
18
|
+
|
|
19
|
+
/** The custom id of a card's open button starts with this; the text's id follows. */
|
|
20
|
+
export const INTERCOM_BUTTON_PREFIX = "roundtable:intercom:";
|
|
21
|
+
/** The most messages an opened card's text takes; a longer text comes as one file. */
|
|
22
|
+
const MAX_PARTS = 5;
|
|
23
|
+
|
|
24
|
+
/** The whole texts of the cards, by the id their open button carries; `CardCopies` keeps them. */
|
|
25
|
+
export interface IntercomTexts {
|
|
26
|
+
remember(id: string, text: string): void;
|
|
27
|
+
recall(id: string): string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A card's message: one line of small text, its heading and summary, and an open button when the
|
|
32
|
+
* summary leaves part of the text out and the text was kept under `id`.
|
|
33
|
+
*/
|
|
34
|
+
export function intercomMessage(card: IntercomCard, id: string | undefined) {
|
|
35
|
+
const line = messages().intercomLine(
|
|
36
|
+
intercomHeading(card),
|
|
37
|
+
intercomSummary(card.text),
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
content: `-# ${line}`,
|
|
41
|
+
allowedMentions: { parse: [] as never[] },
|
|
42
|
+
...(id === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: {
|
|
45
|
+
components: [
|
|
46
|
+
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
47
|
+
new ButtonBuilder()
|
|
48
|
+
.setCustomId(`${INTERCOM_BUTTON_PREFIX}${id}`)
|
|
49
|
+
.setLabel(messages().intercomExpand)
|
|
50
|
+
.setStyle(ButtonStyle.Secondary),
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
withComponents: true,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A card's message, its whole text kept in `texts` for the open button when the summary leaves part out. */
|
|
59
|
+
export function intercomPost(card: IntercomCard, texts: IntercomTexts) {
|
|
60
|
+
if (!intercomFolds(card)) return intercomMessage(card, undefined);
|
|
61
|
+
const id = crypto.randomUUID();
|
|
62
|
+
texts.remember(id, card.text);
|
|
63
|
+
return intercomMessage(card, id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a press of the open button answers, seen only by whoever pressed: the whole text in
|
|
68
|
+
* messages within Discord's limit, or one file when it would take more than `MAX_PARTS`.
|
|
69
|
+
*/
|
|
70
|
+
export function intercomReplies(text: string) {
|
|
71
|
+
const flags = MessageFlags.Ephemeral as const;
|
|
72
|
+
const allowedMentions = { parse: [] as never[] };
|
|
73
|
+
const parts = splitReply(text);
|
|
74
|
+
if (parts.length <= MAX_PARTS)
|
|
75
|
+
return parts.map((content) => ({ content, flags, allowedMentions }));
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
content: messages().cardCopyFile,
|
|
79
|
+
files: [
|
|
80
|
+
new AttachmentBuilder(Buffer.from(text, "utf8"), {
|
|
81
|
+
name: "message.md",
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
flags,
|
|
85
|
+
allowedMentions,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Answers a press of a card's open button with the card's whole text, to whoever pressed; a text
|
|
92
|
+
* no longer kept is said to be gone.
|
|
93
|
+
*/
|
|
94
|
+
export function intercomButtonModule(
|
|
95
|
+
texts: Pick<IntercomTexts, "recall">,
|
|
96
|
+
): InteractionModule {
|
|
97
|
+
return {
|
|
98
|
+
commands: () => [],
|
|
99
|
+
handle: async (interaction: Interaction) => {
|
|
100
|
+
if (
|
|
101
|
+
!interaction.isButton() ||
|
|
102
|
+
!interaction.customId.startsWith(INTERCOM_BUTTON_PREFIX)
|
|
103
|
+
)
|
|
104
|
+
return false;
|
|
105
|
+
const text = texts.recall(
|
|
106
|
+
interaction.customId.slice(INTERCOM_BUTTON_PREFIX.length),
|
|
107
|
+
);
|
|
108
|
+
if (!text) {
|
|
109
|
+
await interaction.reply({
|
|
110
|
+
content: messages().intercomGone,
|
|
111
|
+
flags: MessageFlags.Ephemeral,
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const [first, ...rest] = intercomReplies(text);
|
|
116
|
+
await interaction.reply(first ?? { content: text });
|
|
117
|
+
for (const part of rest) await interaction.followUp(part);
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -45,6 +45,9 @@ export function askPrompts(args: {
|
|
|
45
45
|
? {
|
|
46
46
|
wait: {
|
|
47
47
|
pending: "pending",
|
|
48
|
+
...(wait.graceMs === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { graceMs: wait.graceMs }),
|
|
48
51
|
late: (value, by) =>
|
|
49
52
|
wait.late({
|
|
50
53
|
kind: "question",
|
|
@@ -80,6 +83,9 @@ export function askPrompts(args: {
|
|
|
80
83
|
? {
|
|
81
84
|
wait: {
|
|
82
85
|
pending: "pending",
|
|
86
|
+
...(wait.graceMs === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { graceMs: wait.graceMs }),
|
|
83
89
|
late: (value, by) =>
|
|
84
90
|
wait.late({
|
|
85
91
|
kind: "questionnaire",
|
|
@@ -123,6 +129,9 @@ export function askPrompts(args: {
|
|
|
123
129
|
? {
|
|
124
130
|
wait: {
|
|
125
131
|
pending: "pending",
|
|
132
|
+
...(wait.graceMs === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: { graceMs: wait.graceMs }),
|
|
126
135
|
late: (value, by) =>
|
|
127
136
|
wait.late({
|
|
128
137
|
kind: "questionnaire",
|
|
@@ -120,6 +120,8 @@ export interface CardSpec<T> {
|
|
|
120
120
|
by: Answerer,
|
|
121
121
|
prompt: string,
|
|
122
122
|
): ReturnType<PromptWait["late"]>;
|
|
123
|
+
/** The turn's own grace period, in ms, in place of the cards' `graceMs`; see `PromptWait.graceMs`. */
|
|
124
|
+
graceMs?: number;
|
|
123
125
|
};
|
|
124
126
|
/** Hears who approved the card, before it resolves approved in its turn. */
|
|
125
127
|
approved?(by: Answerer, prompt: string): void;
|
|
@@ -196,10 +196,11 @@ export class OwnerCards implements InteractionModule {
|
|
|
196
196
|
cancelled: T,
|
|
197
197
|
card: CardSpec<T>,
|
|
198
198
|
): Promise<T> {
|
|
199
|
-
const { logger,
|
|
199
|
+
const { logger, resume } = this.#options;
|
|
200
200
|
if (signal?.aborted || card.supersede?.aborted)
|
|
201
201
|
return Promise.resolve(cancelled);
|
|
202
202
|
const wait = resume ? card.wait : undefined;
|
|
203
|
+
const graceMs = wait?.graceMs ?? this.#options.graceMs ?? CARD_GRACE_MS;
|
|
203
204
|
const id = randomUUID();
|
|
204
205
|
if (card.copy !== undefined) this.#copies.remember(id, card.copy);
|
|
205
206
|
const sections = [
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
ButtonBuilder,
|
|
4
4
|
ButtonStyle,
|
|
5
5
|
type Interaction,
|
|
6
|
+
type Message,
|
|
6
7
|
MessageFlags,
|
|
8
|
+
type SendableChannels,
|
|
7
9
|
} from "discord.js";
|
|
8
10
|
import type { ConversationPort } from "../contract/channels.ts";
|
|
9
11
|
import { channelKey } from "../contract/surface.ts";
|
|
@@ -11,6 +13,8 @@ import { messages } from "../i18n/index.ts";
|
|
|
11
13
|
import type { CommandGuard, InteractionModule } from "./interaction-module.ts";
|
|
12
14
|
|
|
13
15
|
export const STOP_BUTTON_ID = "roundtable:stop";
|
|
16
|
+
/** A turn shorter than this shows no stop button, so quick answers do not flicker one. */
|
|
17
|
+
const STOP_PANEL_DELAY_MS = 5_000;
|
|
14
18
|
|
|
15
19
|
/** The message a long turn shows: a stop button only an owner may press. */
|
|
16
20
|
export function stopPanel() {
|
|
@@ -28,6 +32,37 @@ export function stopPanel() {
|
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Shows the stop panel in the channel `sendable` finds once the turn has run for
|
|
37
|
+
* `STOP_PANEL_DELAY_MS`; the returned function hides it, or keeps it from showing. A failure is
|
|
38
|
+
* handed to `warn` with what failed, never thrown.
|
|
39
|
+
*/
|
|
40
|
+
export function showStopPanel(
|
|
41
|
+
sendable: () => Promise<SendableChannels>,
|
|
42
|
+
warn: (error: unknown, what: string) => void,
|
|
43
|
+
): () => void {
|
|
44
|
+
let hidden = false;
|
|
45
|
+
let panel: Message | undefined;
|
|
46
|
+
const remove = (message: Message) =>
|
|
47
|
+
message
|
|
48
|
+
.delete()
|
|
49
|
+
.catch((error: unknown) => warn(error, "stop panel not removed"));
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
sendable()
|
|
52
|
+
.then((target): Promise<Message> => target.send(stopPanel()))
|
|
53
|
+
.then((message) => {
|
|
54
|
+
if (hidden) void remove(message);
|
|
55
|
+
else panel = message;
|
|
56
|
+
})
|
|
57
|
+
.catch((error: unknown) => warn(error, "stop panel not shown"));
|
|
58
|
+
}, STOP_PANEL_DELAY_MS);
|
|
59
|
+
return () => {
|
|
60
|
+
hidden = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (panel) void remove(panel);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
export interface StopButtonOptions {
|
|
32
67
|
guard: Pick<CommandGuard, "allows">;
|
|
33
68
|
/** Stops the running turn of the channel the button was pressed in, through its claim. */
|
|
@@ -72,6 +72,19 @@ export interface PostAuthor {
|
|
|
72
72
|
agent?: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A message between two agents of the team, which a surface that shows cards draws as a small card
|
|
77
|
+
* that opens to the whole text.
|
|
78
|
+
*/
|
|
79
|
+
export interface IntercomCard {
|
|
80
|
+
/** `sent`: a request the author sent; `answered`: the answer that came back; `failed`: none came. */
|
|
81
|
+
kind: "sent" | "answered" | "failed";
|
|
82
|
+
/** The agent the request went to, which also gives the answer: its stable name and the name it shows. */
|
|
83
|
+
peer: { name: string; displayName: string };
|
|
84
|
+
/** The whole text: the request, the answer, or why no answer came. */
|
|
85
|
+
text: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export interface OutboundReply {
|
|
76
89
|
/** The assistant's thinking for this turn, posted as a quiet line before the card. */
|
|
77
90
|
thinking?: string;
|
|
@@ -88,4 +101,9 @@ export interface OutboundReply {
|
|
|
88
101
|
author?: PostAuthor;
|
|
89
102
|
/** Set only on the reply for a turn that ended in an error. A surface may show it; Discord ignores it. */
|
|
90
103
|
failed?: true;
|
|
104
|
+
/**
|
|
105
|
+
* Set on a message between agents: a surface that shows cards draws it in place of `chunks`,
|
|
106
|
+
* which carry the same as a short line and the text for a surface that shows none.
|
|
107
|
+
*/
|
|
108
|
+
intercom?: IntercomCard;
|
|
91
109
|
}
|
|
@@ -10,6 +10,13 @@ export interface InterimMessage {
|
|
|
10
10
|
*/
|
|
11
11
|
export interface InterimPosts {
|
|
12
12
|
post(text: string): Promise<InterimMessage>;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the turn's progress message names the tools it called, on a line of its own; true
|
|
15
|
+
* unless false. A surface that shows the tools elsewhere, such as a progress pane, sets false:
|
|
16
|
+
* the progress message then carries only the turn's narration, and a run that wrote none posts
|
|
17
|
+
* nothing.
|
|
18
|
+
*/
|
|
19
|
+
tools?: boolean;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/** Whether a turn posts its intermediate text as it goes; "on" by default. */
|
|
@@ -23,6 +23,11 @@ export interface TurnTimeout {
|
|
|
23
23
|
tools: [string, number][];
|
|
24
24
|
/** How long the timeout card waits for an answer. */
|
|
25
25
|
graceMs: number;
|
|
26
|
+
/**
|
|
27
|
+
* The card goes on by itself when no one answers it in `graceMs`, as Continue would: the
|
|
28
|
+
* conversation is in yolo mode and the turn is short of the long-running cap.
|
|
29
|
+
*/
|
|
30
|
+
autoContinue?: true;
|
|
26
31
|
end: TurnTimeoutEnd;
|
|
27
32
|
/** No timeout card could be shown, as in a schedule's turn, so nobody was asked whether to go on. */
|
|
28
33
|
unasked?: true;
|
|
@@ -24,10 +24,17 @@ export function conversationEn(ctx: CatalogContext) {
|
|
|
24
24
|
delegationTask: (task: string) => `📋 Task:\n${task}`,
|
|
25
25
|
delegationDone: (report: string) => `✅ Done\n\n${report}`,
|
|
26
26
|
delegationFailed: (error: string) => `❌ Failed: ${error}`,
|
|
27
|
-
agentNoReply: (message: string) => `❌ No reply: ${message}`,
|
|
28
27
|
messageDelivered: (displayName: string, text: string) =>
|
|
29
28
|
`📨 To **${displayName}**:\n${text}`,
|
|
30
|
-
|
|
29
|
+
intercomSent: (displayName: string) => `→ To **${displayName}**`,
|
|
30
|
+
intercomAnswered: (displayName: string) => `← From **${displayName}**`,
|
|
31
|
+
intercomFailed: (displayName: string) =>
|
|
32
|
+
`⚠️ No answer from **${displayName}**`,
|
|
33
|
+
intercomLine: (heading: string, summary: string) =>
|
|
34
|
+
summary ? `${heading}: ${summary}` : heading,
|
|
35
|
+
intercomExpand: "Show all",
|
|
36
|
+
intercomGone:
|
|
37
|
+
"The full text of this message is no longer kept; it is still in the agents' conversations.",
|
|
31
38
|
thinkingAuto: "auto",
|
|
32
39
|
confirmTitle: (asker: string) => `${asker} wants to run this action`,
|
|
33
40
|
holdGeneric: (tool: string) => `run ${tool}`,
|
|
@@ -57,10 +64,15 @@ export function conversationZhTW(
|
|
|
57
64
|
delegationTask: (task: string) => `📋 任務:\n${task}`,
|
|
58
65
|
delegationDone: (report: string) => `✅ 完成\n\n${report}`,
|
|
59
66
|
delegationFailed: (error: string) => `❌ 失敗:${error}`,
|
|
60
|
-
agentNoReply: (message: string) => `❌ 沒有回覆:${message}`,
|
|
61
67
|
messageDelivered: (displayName: string, text: string) =>
|
|
62
68
|
`📨 給 **${displayName}**:\n${text}`,
|
|
63
|
-
|
|
69
|
+
intercomSent: (displayName: string) => `→ 給 **${displayName}**`,
|
|
70
|
+
intercomAnswered: (displayName: string) => `← 來自 **${displayName}**`,
|
|
71
|
+
intercomFailed: (displayName: string) => `⚠️ **${displayName}** 沒有回覆`,
|
|
72
|
+
intercomLine: (heading: string, summary: string) =>
|
|
73
|
+
summary ? `${heading}:${summary}` : heading,
|
|
74
|
+
intercomExpand: "展開全文",
|
|
75
|
+
intercomGone: "這則對講的全文已經不在了;夥伴們的對話裡還找得到。",
|
|
64
76
|
thinkingAuto: "自動",
|
|
65
77
|
confirmTitle: (asker: string) => `${asker} 要執行這個動作`,
|
|
66
78
|
holdGeneric: (tool: string) => `執行 ${tool}`,
|
|
@@ -51,7 +51,9 @@ export function turnTimeoutEn() {
|
|
|
51
51
|
`It reached ${limit(t)} and has worked ${durationEn(t.workedMs)}.`,
|
|
52
52
|
`Now: ${doing(t)}.`,
|
|
53
53
|
tools(t),
|
|
54
|
-
|
|
54
|
+
t.autoContinue
|
|
55
|
+
? `It keeps working while this card is open. Continue gives it another ${durationEn(budgetMs)}; Cancel stops it. Yolo mode is on: with no answer in ${durationEn(t.graceMs)} it keeps going by itself.`
|
|
56
|
+
: `It keeps working while this card is open. Continue gives it another ${durationEn(budgetMs)}; Cancel stops it. With no answer in ${durationEn(t.graceMs)} it stops.`,
|
|
55
57
|
]
|
|
56
58
|
.filter(Boolean)
|
|
57
59
|
.join("\n"),
|
|
@@ -64,6 +66,8 @@ export function turnTimeoutEn() {
|
|
|
64
66
|
turnTimeoutFinished: "✅ The turn finished; nothing to answer.",
|
|
65
67
|
turnTimeoutUnanswered: (t: TurnTimeout) =>
|
|
66
68
|
`⏱️ No answer in ${durationEn(t.graceMs)}; the turn was stopped.`,
|
|
69
|
+
turnTimeoutAutoContinued: (t: TurnTimeout) =>
|
|
70
|
+
`⏱️ No answer in ${durationEn(t.graceMs)}; yolo mode is on, so the turn continued automatically.`,
|
|
67
71
|
turnTimedOutNotice: (t: TurnTimeout) => {
|
|
68
72
|
const what =
|
|
69
73
|
t.running.length > 0
|
|
@@ -105,7 +109,9 @@ export function turnTimeoutZhTW(): ReturnType<typeof turnTimeoutEn> {
|
|
|
105
109
|
`已達到${limit(t)},已工作 ${durationZh(t.workedMs)}。`,
|
|
106
110
|
`現在:${doing(t)}。`,
|
|
107
111
|
tools(t),
|
|
108
|
-
|
|
112
|
+
t.autoContinue
|
|
113
|
+
? `這張卡片開著時它會繼續工作。按「繼續」再給它 ${durationZh(budgetMs)};按「取消」停止。Yolo 模式已開啟:${durationZh(t.graceMs)}內沒有回答,它會自己繼續。`
|
|
114
|
+
: `這張卡片開著時它會繼續工作。按「繼續」再給它 ${durationZh(budgetMs)};按「取消」停止。${durationZh(t.graceMs)}內沒有回答就會停止。`,
|
|
109
115
|
]
|
|
110
116
|
.filter(Boolean)
|
|
111
117
|
.join("\n"),
|
|
@@ -117,6 +123,8 @@ export function turnTimeoutZhTW(): ReturnType<typeof turnTimeoutEn> {
|
|
|
117
123
|
turnTimeoutFinished: "✅ 這一輪已完成,不需要回答。",
|
|
118
124
|
turnTimeoutUnanswered: (t: TurnTimeout) =>
|
|
119
125
|
`⏱️ ${durationZh(t.graceMs)}內沒有回答,這一輪已停止。`,
|
|
126
|
+
turnTimeoutAutoContinued: (t: TurnTimeout) =>
|
|
127
|
+
`⏱️ ${durationZh(t.graceMs)}內沒有回答;Yolo 模式已開啟,這一輪已自動繼續。`,
|
|
120
128
|
turnTimedOutNotice: (t: TurnTimeout) => {
|
|
121
129
|
const what =
|
|
122
130
|
t.running.length > 0
|
|
@@ -202,6 +202,11 @@ export type LateAnswer = { by: string } & (
|
|
|
202
202
|
*/
|
|
203
203
|
export interface PromptWait {
|
|
204
204
|
late(answer: LateAnswer): string | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* How long this turn waits before `pending`, in ms, in place of the surface's own grace period;
|
|
207
|
+
* a yolo turn asks with a shorter one. A surface that keeps no grace period ignores it.
|
|
208
|
+
*/
|
|
209
|
+
graceMs?: number;
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
/**
|
|
@@ -8,6 +8,16 @@ export interface Autonomy {
|
|
|
8
8
|
allow: readonly string[];
|
|
9
9
|
/** Hold classes or rule names yolo never approves, such as `shell.push` or `tool:repo_push`. */
|
|
10
10
|
card: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* How long an `ask_user` card holds a yolo turn before it goes on with its own best choice, in
|
|
13
|
+
* ms; the card stays open. Default `DEFAULT_YOLO_QUESTION_GRACE_MS`, 30 seconds.
|
|
14
|
+
*/
|
|
15
|
+
questionGraceMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How long the timeout card waits in a yolo conversation before the turn goes on as if
|
|
18
|
+
* Continue were pressed, in ms. Default `DEFAULT_YOLO_TIMEOUT_CARD_GRACE_MS`, one minute.
|
|
19
|
+
*/
|
|
20
|
+
timeoutCardGraceMs?: number;
|
|
11
21
|
};
|
|
12
22
|
/** The most one long-running turn works in all before it asks or stops, in ms. */
|
|
13
23
|
longRunningMaxMs: number;
|
|
@@ -16,8 +26,19 @@ export interface Autonomy {
|
|
|
16
26
|
/** The longest a long-running turn works without a person: four hours. */
|
|
17
27
|
export const DEFAULT_LONG_RUNNING_MS = 4 * 60 * 60_000;
|
|
18
28
|
|
|
29
|
+
/** How long a yolo turn waits on its `ask_user` card before it goes on: 30 seconds. */
|
|
30
|
+
export const DEFAULT_YOLO_QUESTION_GRACE_MS = 30_000;
|
|
31
|
+
|
|
32
|
+
/** How long a yolo turn's timeout card waits before the turn goes on by itself: one minute. */
|
|
33
|
+
export const DEFAULT_YOLO_TIMEOUT_CARD_GRACE_MS = 60_000;
|
|
34
|
+
|
|
19
35
|
/** Yolo approves nothing beyond its built-in classes, and a long-running turn stops at four hours. */
|
|
20
36
|
export const DEFAULT_AUTONOMY: Autonomy = Object.freeze({
|
|
21
|
-
yolo: Object.freeze({
|
|
37
|
+
yolo: Object.freeze({
|
|
38
|
+
allow: [],
|
|
39
|
+
card: [],
|
|
40
|
+
questionGraceMs: DEFAULT_YOLO_QUESTION_GRACE_MS,
|
|
41
|
+
timeoutCardGraceMs: DEFAULT_YOLO_TIMEOUT_CARD_GRACE_MS,
|
|
42
|
+
}),
|
|
22
43
|
longRunningMaxMs: DEFAULT_LONG_RUNNING_MS,
|
|
23
44
|
});
|