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.
Files changed (65) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +2 -0
  3. package/README.zh-TW.md +2 -0
  4. package/docs/plugins.md +54 -22
  5. package/package.json +1 -1
  6. package/src/characters/compose.ts +11 -0
  7. package/src/characters/parts-face.ts +23 -2
  8. package/src/core/agents/agent-messages.ts +37 -92
  9. package/src/core/agents/agent-ports.ts +3 -0
  10. package/src/core/agents/agent-prompt.ts +2 -16
  11. package/src/core/agents/agent-rules.ts +27 -0
  12. package/src/core/agents/agent-team.ts +5 -0
  13. package/src/core/agents/agent-tools.ts +18 -10
  14. package/src/core/agents/helper-create.ts +3 -24
  15. package/src/core/agents/helper-modes.ts +64 -0
  16. package/src/core/agents/message-target.ts +20 -18
  17. package/src/core/agents/routes.ts +42 -24
  18. package/src/core/agents/team-editing.ts +5 -5
  19. package/src/core/agents/team-helpers.ts +76 -5
  20. package/src/core/agents/team-keys.ts +14 -0
  21. package/src/core/agents/team-turn-types.ts +7 -13
  22. package/src/core/agents/team-turns.ts +9 -6
  23. package/src/core/builtin/agent-server.ts +3 -4
  24. package/src/core/builtin/discord.ts +35 -8
  25. package/src/core/builtin/modules.ts +15 -15
  26. package/src/core/config/adapters.ts +2 -1
  27. package/src/core/config/limits.ts +35 -2
  28. package/src/core/config/routing.ts +5 -4
  29. package/src/core/discord/agent-discord.ts +12 -1
  30. package/src/core/discord/card-copies.ts +7 -2
  31. package/src/core/discord/direct-channel.ts +1 -0
  32. package/src/core/discord/discord-surface.ts +13 -30
  33. package/src/core/discord/intercom-cards.ts +121 -0
  34. package/src/core/discord/owner-card-asks.ts +9 -0
  35. package/src/core/discord/owner-card-types.ts +2 -0
  36. package/src/core/discord/owner-cards.ts +2 -1
  37. package/src/core/discord/stop-button.ts +35 -0
  38. package/src/core/domain/conversation.ts +18 -0
  39. package/src/core/domain/interim.ts +7 -0
  40. package/src/core/domain/turn-timeout.ts +5 -0
  41. package/src/core/i18n/conversation.ts +16 -4
  42. package/src/core/i18n/turn-timeout.ts +10 -2
  43. package/src/core/interactions/prompts.ts +5 -0
  44. package/src/core/modes/autonomy.ts +22 -1
  45. package/src/core/modes/conversation-modes.ts +135 -23
  46. package/src/core/modes/mode-inheritance.ts +78 -0
  47. package/src/core/modes/modes-plugin.ts +1 -1
  48. package/src/core/modules/notify/notify.ts +3 -2
  49. package/src/core/modules/notify/session-notify.ts +21 -4
  50. package/src/core/modules/skills/skill-contract.ts +13 -0
  51. package/src/core/modules/skills/skill-registry.ts +3 -1
  52. package/src/core/modules/skills/skill-store.ts +4 -3
  53. package/src/core/presence/delivery-router.ts +165 -0
  54. package/src/core/presence/direct-channels.ts +39 -68
  55. package/src/core/presentation/intercom.ts +44 -0
  56. package/src/core/runtime/ask-questions.ts +21 -4
  57. package/src/core/runtime/extensions/ask-user-text.ts +36 -3
  58. package/src/core/runtime/extensions/ask-user.ts +21 -1
  59. package/src/core/runtime/interim-text.ts +7 -4
  60. package/src/core/runtime/pi-agent-runtime.ts +2 -2
  61. package/src/core/runtime/prompt-slot.ts +11 -1
  62. package/src/core/runtime/session-factory.ts +11 -6
  63. package/src/core/runtime/turn-budget.ts +34 -3
  64. package/src/core/runtime/turn-limits.ts +13 -3
  65. 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. `"latest"` (default) is
35
- * the one the agent last ran a turn in, never carrying a message from a room out to Discord;
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; copy buttons of earlier cards will say they are gone",
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({ path, err: error }, "card copy texts not saved");
177
+ logger.warn(
178
+ { path, err: error },
179
+ `${this.#options.what ?? "card copy texts"} not saved`,
180
+ );
176
181
  }
177
182
  }
178
183
  }
@@ -36,6 +36,7 @@ export function discordDirectChannel(
36
36
  };
37
37
  return {
38
38
  name: "discord",
39
+ surface: "discord",
39
40
  label: "a direct message on Discord",
40
41
  knows: async (principalId) => (await userOf(principalId)) !== undefined,
41
42
  reaches: async (principalId) => {
@@ -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 { stopPanel } from "./stop-button.ts";
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(this.#client, guildId);
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
- let hidden = false;
372
- let panel: Message | undefined;
373
- const remove = (message: Message) =>
374
- message
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, graceMs = CARD_GRACE_MS, resume } = this.#options;
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
- answerReturned: (text: string) => `↩️ Reply:\n${text}`,
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
- answerReturned: (text: string) => `↩️ 回覆:\n${text}`,
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
- `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.`,
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
- `這張卡片開著時它會繼續工作。按「繼續」再給它 ${durationZh(budgetMs)};按「取消」停止。${durationZh(t.graceMs)}內沒有回答就會停止。`,
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({ allow: [], card: [] }),
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
  });