pi-roundtable 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +10 -6
  3. package/README.zh-TW.md +144 -0
  4. package/docs/plugins.md +1448 -66
  5. package/examples/echo-runtime.test.ts +122 -0
  6. package/examples/echo-runtime.ts +107 -0
  7. package/examples/events.test.ts +1 -0
  8. package/examples/events.ts +2 -2
  9. package/examples/fake-surface.test.ts +180 -0
  10. package/examples/fake-surface.ts +109 -0
  11. package/examples/interactions.test.ts +12 -3
  12. package/examples/interactions.ts +19 -19
  13. package/examples/shared-services.test.ts +74 -0
  14. package/examples/shared-services.ts +60 -0
  15. package/examples/study-room.test.ts +69 -0
  16. package/examples/study-room.ts +52 -0
  17. package/examples/support-desk.test.ts +34 -0
  18. package/examples/support-desk.ts +40 -0
  19. package/package.json +11 -3
  20. package/src/cli/checks/database.ts +11 -30
  21. package/src/cli/checks/images.ts +19 -0
  22. package/src/cli/doctor.ts +6 -0
  23. package/src/cli/main.ts +2 -1
  24. package/src/core/agents/agent-claim.ts +42 -16
  25. package/src/core/agents/agent-messages.ts +4 -4
  26. package/src/core/agents/agent-ports.ts +8 -21
  27. package/src/core/agents/agent-prompt.ts +3 -1
  28. package/src/core/agents/agent-store.ts +6 -4
  29. package/src/core/agents/agent-team-fixture.ts +389 -0
  30. package/src/core/agents/agent-team.ts +26 -25
  31. package/src/core/agents/agent-tools.ts +34 -15
  32. package/src/core/agents/avatar-studio.ts +40 -6
  33. package/src/core/agents/fallback-avatar.ts +85 -0
  34. package/src/core/agents/team-editing.ts +49 -10
  35. package/src/core/agents/team-keys.ts +22 -9
  36. package/src/core/agents/team-layout.ts +4 -3
  37. package/src/core/agents/team-lifecycle.ts +28 -2
  38. package/src/core/agents/team-options.ts +9 -5
  39. package/src/core/agents/team-status.ts +4 -4
  40. package/src/core/agents/team-text.ts +8 -6
  41. package/src/core/agents/team-turn-types.ts +4 -4
  42. package/src/core/agents/team-turns.ts +6 -5
  43. package/src/core/builtin/agent-server.ts +187 -121
  44. package/src/core/builtin/discord-admin.ts +41 -0
  45. package/src/core/builtin/discord.ts +65 -40
  46. package/src/core/builtin/modules.ts +67 -58
  47. package/src/core/builtin/session-tool.ts +24 -0
  48. package/src/core/builtin/skills.ts +72 -0
  49. package/src/core/builtin/stores.ts +55 -31
  50. package/src/core/config/config.ts +45 -4
  51. package/src/core/config/schema.ts +6 -0
  52. package/src/core/contract/channels.ts +54 -14
  53. package/src/core/contract/providers.ts +17 -0
  54. package/src/core/contract/runtime.ts +138 -0
  55. package/src/core/contract/services.ts +45 -0
  56. package/src/core/contract/surface.ts +88 -0
  57. package/src/core/db/migrations.ts +118 -3
  58. package/src/core/define-roundtable.ts +70 -46
  59. package/src/core/define.ts +2 -1
  60. package/src/core/discord/agent-commands.ts +39 -14
  61. package/src/core/discord/agent-panel.ts +66 -0
  62. package/src/core/discord/channel-executor.ts +6 -3
  63. package/src/core/discord/channel-operations.ts +14 -11
  64. package/src/core/discord/command-collection.ts +46 -0
  65. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  66. package/src/core/discord/connection.ts +35 -0
  67. package/src/core/discord/discord-surface.ts +32 -94
  68. package/src/core/discord/dispatch-threads.ts +2 -2
  69. package/src/core/discord/inbound-message.ts +99 -0
  70. package/src/core/discord/interaction-module.ts +57 -1
  71. package/src/core/discord/owner-cards.ts +4 -4
  72. package/src/core/discord/owner-command.ts +44 -11
  73. package/src/core/discord/owner-discord.ts +2 -2
  74. package/src/core/discord/schedule-commands.ts +21 -18
  75. package/src/core/discord/stop-button.ts +44 -1
  76. package/src/core/domain/attachment.ts +12 -8
  77. package/src/core/domain/conversation.ts +3 -4
  78. package/src/core/domain/owner-prompts.ts +5 -5
  79. package/src/core/domain/ports.ts +9 -53
  80. package/src/core/freeze.ts +9 -0
  81. package/src/core/host.ts +255 -82
  82. package/src/core/http/listeners.ts +55 -20
  83. package/src/core/i18n/agent-panel.ts +4 -0
  84. package/src/core/i18n/index.ts +12 -2
  85. package/src/core/i18n/owner.ts +7 -1
  86. package/src/core/i18n/schedules.ts +12 -6
  87. package/src/core/identity.ts +1 -1
  88. package/src/core/judging/effort-judge.ts +25 -3
  89. package/src/core/log.ts +21 -3
  90. package/src/core/models.ts +2 -2
  91. package/src/core/modules/background/background-turns.ts +6 -4
  92. package/src/core/modules/delegation/delegate.ts +3 -2
  93. package/src/core/modules/delegation/delegator.ts +15 -13
  94. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  95. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  96. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  97. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  98. package/src/core/modules/memory/owner-memory.ts +9 -13
  99. package/src/core/modules/schedules/recurrence.ts +5 -3
  100. package/src/core/modules/schedules/schedule-store.ts +11 -11
  101. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  102. package/src/core/modules/schedules/scheduler.ts +7 -2
  103. package/src/core/modules/schedules/schedules.ts +9 -3
  104. package/src/core/modules/skills/skill-registry.ts +3 -2
  105. package/src/core/modules/skills/skill-store.ts +2 -15
  106. package/src/core/modules/skills/skill-tools.ts +10 -4
  107. package/src/core/ops/error-reporter.ts +1 -1
  108. package/src/core/plugin.ts +232 -28
  109. package/src/core/registry/contributions.ts +173 -14
  110. package/src/core/registry/providers.ts +35 -5
  111. package/src/core/registry/services.ts +229 -0
  112. package/src/core/routing/channel-queue.ts +5 -0
  113. package/src/core/routing/channel-router.ts +21 -7
  114. package/src/core/routing/conversation-turns.ts +139 -0
  115. package/src/core/routing/message-text.ts +9 -2
  116. package/src/core/routing/surface-port.ts +39 -0
  117. package/src/core/runtime/conversation-sessions.ts +4 -3
  118. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  119. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  120. package/src/core/runtime/mcp.ts +1 -1
  121. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  122. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  123. package/src/core/runtime/prompt-slot.ts +8 -3
  124. package/src/core/runtime/runtime-types.ts +9 -35
  125. package/src/core/runtime/session-factory.ts +21 -5
  126. package/src/core/runtime/text-tools.ts +1 -3
  127. package/src/core/services.ts +238 -90
  128. package/src/core/sessions.ts +3 -2
  129. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  130. package/src/core/shared/delegate-tool.ts +4 -3
  131. package/src/core/shared/schedule-tools.ts +22 -13
  132. package/src/core/shared/session-messages.ts +1 -1
  133. package/src/core/speakers.ts +6 -38
  134. package/src/core/testing/database.ts +1 -7
  135. package/src/core/testing/eager-catalog.ts +30 -0
  136. package/src/core/testing/locale.ts +23 -5
  137. package/src/core/testing/modules.ts +105 -41
  138. package/src/core/testing/test-host.ts +279 -0
  139. package/src/core/testing/tool-set.ts +64 -0
  140. package/src/core/time.ts +6 -1
  141. package/src/core/tool-set.snapshot.json +288 -0
  142. package/src/core/tool-tiers.ts +17 -57
  143. package/src/discord/index.ts +64 -0
  144. package/src/index.ts +181 -10
  145. package/src/kit/channels.ts +14 -0
  146. package/src/kit/domain.ts +18 -0
  147. package/src/kit/holds.ts +4 -0
  148. package/src/kit/index.ts +129 -0
  149. package/src/kit/judging.ts +10 -0
  150. package/src/kit/memory.ts +3 -0
  151. package/src/kit/mirror.ts +19 -0
  152. package/src/kit/presentation.ts +7 -0
  153. package/src/kit/shell.ts +7 -0
  154. package/src/kit/skills.ts +8 -0
  155. package/src/kit/support.ts +21 -0
  156. package/src/kit/threads.ts +7 -0
  157. package/src/kit/tools.ts +14 -0
  158. package/src/kit/worker.ts +16 -0
  159. package/src/testing.ts +394 -34
  160. package/examples/guide.test.ts +0 -52
  161. package/src/cli/add-plugin.test.ts +0 -107
  162. package/src/cli/checks/basic.test.ts +0 -276
  163. package/src/cli/checks/database.test.ts +0 -117
  164. package/src/cli/checks/discord.test.ts +0 -200
  165. package/src/cli/cli.test.ts +0 -122
  166. package/src/cli/config-edit.test.ts +0 -130
  167. package/src/cli/doctor.test.ts +0 -184
  168. package/src/cli/init.test.ts +0 -127
  169. package/src/cli/size.test.ts +0 -9
  170. package/src/cli/templates.test.ts +0 -129
  171. package/src/cli/testing/fixtures.ts +0 -106
  172. package/src/core/agents/agent-claim.test.ts +0 -142
  173. package/src/core/agents/agent-dashboard.test.ts +0 -120
  174. package/src/core/agents/agent-guild.test.ts +0 -124
  175. package/src/core/agents/agent-store.test.ts +0 -205
  176. package/src/core/agents/avatar-studio.test.ts +0 -88
  177. package/src/core/agents/group-round.test.ts +0 -143
  178. package/src/core/agents/owner-identity.test.ts +0 -172
  179. package/src/core/agents/team-turns.test.ts +0 -278
  180. package/src/core/attachments/attachments.test.ts +0 -85
  181. package/src/core/boundary.test.ts +0 -45
  182. package/src/core/builtin/modules.test.ts +0 -127
  183. package/src/core/config/config.test.ts +0 -135
  184. package/src/core/contract/discord.ts +0 -33
  185. package/src/core/db/migrations.test.ts +0 -237
  186. package/src/core/define-roundtable.test.ts +0 -134
  187. package/src/core/define.test.ts +0 -144
  188. package/src/core/discord/agent-commands.test.ts +0 -58
  189. package/src/core/discord/agent-discord.test.ts +0 -70
  190. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  191. package/src/core/discord/dispatch-threads.test.ts +0 -94
  192. package/src/core/discord/owner-cards.test.ts +0 -435
  193. package/src/core/discord/owner-discord.test.ts +0 -325
  194. package/src/core/domain/expression.ts +0 -21
  195. package/src/core/domain/profile.ts +0 -32
  196. package/src/core/drain.test.ts +0 -46
  197. package/src/core/events.test.ts +0 -81
  198. package/src/core/holds.test.ts +0 -54
  199. package/src/core/host.test.ts +0 -536
  200. package/src/core/http/listeners.test.ts +0 -164
  201. package/src/core/i18n/i18n.test.ts +0 -139
  202. package/src/core/identity.test.ts +0 -18
  203. package/src/core/judging/effort-judge.test.ts +0 -112
  204. package/src/core/judging/model-judge.test.ts +0 -126
  205. package/src/core/modules/delegation/delegator.test.ts +0 -170
  206. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  207. package/src/core/modules/schedules/schedule.test.ts +0 -358
  208. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  209. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  210. package/src/core/ops/error-reporter.test.ts +0 -283
  211. package/src/core/presentation/card-cadence.ts +0 -41
  212. package/src/core/presentation/presentation.test.ts +0 -166
  213. package/src/core/public-entry.test.ts +0 -59
  214. package/src/core/registry/contributions.test.ts +0 -215
  215. package/src/core/registry/interactions.test.ts +0 -87
  216. package/src/core/registry/providers.test.ts +0 -103
  217. package/src/core/routing/channel-queue.test.ts +0 -31
  218. package/src/core/routing/channel-router.test.ts +0 -326
  219. package/src/core/routing/conversation-kind.ts +0 -14
  220. package/src/core/routing/settle-turn.test.ts +0 -21
  221. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  222. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  223. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  224. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  225. package/src/core/runtime/session-archive.test.ts +0 -20
  226. package/src/core/runtime/steerable-run.test.ts +0 -264
  227. package/src/core/runtime/text-tools.test.ts +0 -110
  228. package/src/core/runtime/turn-answer.test.ts +0 -72
  229. package/src/core/runtime/worker-task.test.ts +0 -82
  230. package/src/core/services.test.ts +0 -28
  231. package/src/core/sessions.test.ts +0 -85
  232. package/src/core/shared/unix-server.ts +0 -18
  233. package/src/core/size.test.ts +0 -9
  234. package/src/core/speakers.test.ts +0 -78
  235. package/src/core/testing/file-size.ts +0 -34
  236. package/src/core/time.test.ts +0 -166
  237. package/src/core/tool-tiers.test.ts +0 -117
  238. package/src/entries.test.ts +0 -112
  239. package/src/testing.test.ts +0 -156
@@ -2,8 +2,6 @@ import { createEn, type Messages } from "./en.ts";
2
2
  import type { CatalogContext } from "./types.ts";
3
3
  import { createZhTW } from "./zh-tw.ts";
4
4
 
5
- export type { CatalogContext } from "./types.ts";
6
-
7
5
  export type { Messages };
8
6
 
9
7
  export type Locale = "en" | "zh-TW";
@@ -35,6 +33,17 @@ function build(locale: Locale, settings: LocaleSettings): Messages {
35
33
 
36
34
  let active: Messages = build("en", DEFAULTS);
37
35
  let names: CatalogContext = DEFAULTS;
36
+ let current: Locale = "en";
37
+
38
+ /** The catalog of a locale, apart from the process's own; for text that names its locale itself. */
39
+ export function catalogFor(locale: Locale): Messages {
40
+ return build(locale, DEFAULTS);
41
+ }
42
+
43
+ /** The locale `setLocale` chose. */
44
+ export function activeLocale(): Locale {
45
+ return current;
46
+ }
38
47
 
39
48
  /**
40
49
  * Chooses the catalog of Discord text for the whole process; the host calls it once at startup,
@@ -44,6 +53,7 @@ let names: CatalogContext = DEFAULTS;
44
53
  export function setLocale(locale: Locale, settings: LocaleSettings): void {
45
54
  if (!isLocale(locale)) throw new Error(`Unknown locale: ${String(locale)}`);
46
55
  active = build(locale, settings);
56
+ current = locale;
47
57
  names = { assistant: settings.assistant, root: settings.root };
48
58
  }
49
59
 
@@ -13,6 +13,9 @@ export function ownerEn(ctx: CatalogContext) {
13
13
  surfaceActionFailed: "Sorry, that action did not work.",
14
14
  stopNote: "-# Working; press Stop to interrupt.",
15
15
  stopLabel: "Stop",
16
+ stopDone: "Stopped.",
17
+ stopIdle: "Nothing is running right now.",
18
+ stopOwnerOnly: `Only ${ctx.assistant}'s owner can stop it.`,
16
19
  panelPage: (title: string, page: number, pages: number) =>
17
20
  `${title} (${page}/${pages})`,
18
21
  };
@@ -25,10 +28,13 @@ export function ownerZhTW(ctx: CatalogContext): ReturnType<typeof ownerEn> {
25
28
  ownerErrorTitle: "出了點問題",
26
29
  ownerErrorBody: "這次沒能完成,請稍後再試;細節已經記在 log 裡。",
27
30
  ownerRefusalTitle: "只有擁有者能用",
28
- ownerRefusalBody: `\`/${ctx.root}\` 是 ${ctx.assistant} 擁有者的控制台。擲骰請用 \`/roll\` 或 \`/dice\`。`,
31
+ ownerRefusalBody: `\`/${ctx.root}\` 是 ${ctx.assistant} 擁有者的控制台。`,
29
32
  surfaceActionFailed: "抱歉,這個操作沒有成功。",
30
33
  stopNote: "-# 工作中,想中斷就按停止。",
31
34
  stopLabel: "停止",
35
+ stopDone: "已停止。",
36
+ stopIdle: "現在沒有進行中的工作。",
37
+ stopOwnerOnly: `只有 ${ctx.assistant} 的擁有者能停止。`,
32
38
  panelPage: (title: string, page: number, pages: number) =>
33
39
  `${title}(${page}/${pages})`,
34
40
  };
@@ -30,10 +30,13 @@ export function schedulesEn(ctx: CatalogContext) {
30
30
  `Every ${days} days at ${time} (counting from ${startDate})`,
31
31
  scheduleWeekly: (days: readonly string[], time: string) =>
32
32
  `Weekly on ${days.join(", ")} at ${time}`,
33
- scheduleHead: (id: number, title: string, channel: string, mode: string) =>
34
- `**#${id} ${title}** · ${channel} · ${mode}`,
33
+ scheduleHead: (
34
+ id: number,
35
+ title: string,
36
+ channel: string,
37
+ target: string,
38
+ ) => `**#${id} ${title}** · ${channel} · ${target}`,
35
39
  scheduleModeOwner: ctx.assistant,
36
- scheduleModeParty: "party role",
37
40
  scheduleFooter: `Schedules are created and edited by the bot in conversation; here you can review and cancel them. \`/${ctx.root} schedule cancel\` cancels one.`,
38
41
  scheduleGroupDescription: "Schedules",
39
42
  scheduleListDescription: "List the schedules of every channel",
@@ -66,10 +69,13 @@ export function schedulesZhTW(
66
69
  `每 ${days} 天 ${time}(從 ${startDate} 起算)`,
67
70
  scheduleWeekly: (days: readonly string[], time: string) =>
68
71
  `每週${days.join("、")} ${time}`,
69
- scheduleHead: (id: number, title: string, channel: string, mode: string) =>
70
- `**#${id} ${title}** ${channel} ${mode}`,
72
+ scheduleHead: (
73
+ id: number,
74
+ title: string,
75
+ channel: string,
76
+ target: string,
77
+ ) => `**#${id} ${title}** ${channel} ${target}`,
71
78
  scheduleModeOwner: ctx.assistant,
72
- scheduleModeParty: "party 角色",
73
79
  scheduleFooter: `排程由 bot 在對話中建立與修改;這裡可以總覽與取消。\`/${ctx.root} schedule cancel\` 取消。`,
74
80
  scheduleGroupDescription: "排程",
75
81
  scheduleListDescription: "列出所有頻道的排程",
@@ -1,7 +1,7 @@
1
1
  /** Who the agents work for, as prompts and tool descriptions name them. */
2
2
  export interface OwnerIdentity {
3
3
  name: string;
4
- /** How prompts refer back to the owner, such as he, him, his. */
4
+ /** How prompts refer back to the owner, such as they, them, their. */
5
5
  pronouns: { subject: string; object: string; possessive: string };
6
6
  }
7
7
 
@@ -3,8 +3,13 @@ import type { Logger } from "../log.ts";
3
3
  import type { ThinkingLevel } from "../models.ts";
4
4
 
5
5
  /** The levels the judge chooses between, lowest first; criterion i of a brief asks for level i. */
6
- export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"] as const;
7
- export type EffortLevel = (typeof EFFORT_LEVELS)[number];
6
+ export type EffortLevel = "low" | "medium" | "high" | "xhigh";
7
+ const EFFORT_LEVELS: readonly EffortLevel[] = [
8
+ "low",
9
+ "medium",
10
+ "high",
11
+ "xhigh",
12
+ ];
8
13
 
9
14
  /** What the judge is told about the conversation, and one criterion per effort level. */
10
15
  export interface EffortBrief {
@@ -62,8 +67,25 @@ export interface PreviousTurn<Level extends ThinkingLevel> {
62
67
  level?: Level;
63
68
  }
64
69
 
70
+ /** Picks the thinking level of one turn from a message and what the previous turn left. */
71
+ export interface EffortPicker<Fallback extends ThinkingLevel = ThinkingLevel> {
72
+ judge(
73
+ message: string,
74
+ previous?: PreviousTurn<EffortLevel | Fallback>,
75
+ ): Promise<EffortLevel | Fallback>;
76
+ }
77
+
78
+ /** The judge of a conversation's turns, reading each message against the brief. */
79
+ export function effortJudge<Fallback extends ThinkingLevel>(
80
+ options: EffortJudgeOptions<Fallback>,
81
+ ): EffortPicker<Fallback> {
82
+ return new EffortJudge(options);
83
+ }
84
+
65
85
  /** Picks the thinking level of one turn from the judge's reading of its message. */
66
- export class EffortJudge<Fallback extends ThinkingLevel = ThinkingLevel> {
86
+ export class EffortJudge<Fallback extends ThinkingLevel = ThinkingLevel>
87
+ implements EffortPicker<Fallback>
88
+ {
67
89
  readonly #options: EffortJudgeOptions<Fallback>;
68
90
 
69
91
  constructor(options: EffortJudgeOptions<Fallback>) {
package/src/core/log.ts CHANGED
@@ -1,6 +1,24 @@
1
- import pino, { type Logger } from "pino";
1
+ import pino from "pino";
2
2
 
3
- export type { Logger };
3
+ /** One log method: fields and a message, or a message alone. */
4
+ export interface LogFn {
5
+ (fields: object, message?: string): void;
6
+ (message: string): void;
7
+ }
8
+
9
+ /**
10
+ * The logger a plugin writes to. pino satisfies it, so a process that already has a pino logger
11
+ * hands it over as it is.
12
+ */
13
+ export interface Logger {
14
+ debug: LogFn;
15
+ info: LogFn;
16
+ warn: LogFn;
17
+ error: LogFn;
18
+ fatal: LogFn;
19
+ /** A logger that adds these fields to every line it writes. */
20
+ child(fields: Record<string, unknown>): Logger;
21
+ }
4
22
 
5
23
  /** One log line as pino writes it: `level`, `time`, `msg`, `err`, and the logged fields. */
6
24
  export type LogEntry = Record<string, unknown>;
@@ -12,7 +30,7 @@ export type LogEntry = Record<string, unknown>;
12
30
  export function createLogger(
13
31
  app: string,
14
32
  onError?: (entry: LogEntry) => void,
15
- ): Logger {
33
+ ): pino.Logger {
16
34
  const options = {
17
35
  base: { app },
18
36
  level: process.env.LOG_LEVEL ?? "info",
@@ -9,14 +9,14 @@ export type ThinkingLevel =
9
9
  | "high"
10
10
  | "xhigh";
11
11
 
12
- export const THINKING_LEVELS: readonly ThinkingLevel[] = [
12
+ export const THINKING_LEVELS: readonly ThinkingLevel[] = Object.freeze([
13
13
  "off",
14
14
  "minimal",
15
15
  "low",
16
16
  "medium",
17
17
  "high",
18
18
  "xhigh",
19
- ];
19
+ ]);
20
20
 
21
21
  /** An agent's or the assistant's thinking setting: a fixed level, or the judge's choice each turn. */
22
22
  export const AUTO_THINKING = "auto";
@@ -1,9 +1,11 @@
1
+ import { OWNER_TARGET } from "../../agents/agent-claim.ts";
1
2
  import type {
2
3
  ConversationPort,
3
4
  ScheduledOutcome,
4
5
  } from "../../contract/channels.ts";
5
6
  import type { ChannelKey } from "../../domain/conversation.ts";
6
7
  import type { Logger } from "../../log.ts";
8
+ import type { BackgroundTurns } from "../../services.ts";
7
9
  import { zonedStamp } from "../../time.ts";
8
10
  import {
9
11
  type DelegationJob,
@@ -24,7 +26,7 @@ export interface BackgroundTurnsOptions {
24
26
  * Turns nobody wrote, each answered in its channel by the claim that owns it: a due schedule's,
25
27
  * a delegated task's report, and the process's own logged error.
26
28
  */
27
- export class BackgroundTurns {
29
+ export class ConversationBackgroundTurns implements BackgroundTurns {
28
30
  readonly #options: BackgroundTurnsOptions;
29
31
 
30
32
  constructor(options: BackgroundTurnsOptions) {
@@ -35,7 +37,7 @@ export class BackgroundTurns {
35
37
  runScheduled(schedule: Schedule, firedAt: Date): Promise<ScheduledOutcome> {
36
38
  return this.#options.conversations.background({
37
39
  channel: schedule.channel,
38
- mode: schedule.mode,
40
+ target: schedule.target,
39
41
  author: { id: schedule.createdById, name: schedule.createdByName },
40
42
  tier: schedule.createdTier,
41
43
  turnId: `schedule-${schedule.id}-${firedAt.getTime()}`,
@@ -50,7 +52,7 @@ export class BackgroundTurns {
50
52
  ): Promise<void> {
51
53
  const outcome = await this.#options.conversations.background({
52
54
  channel: job.channel,
53
- mode: job.mode,
55
+ target: job.target,
54
56
  author: job.author,
55
57
  ...(job.author.tier ? { tier: job.author.tier } : {}),
56
58
  turnId: `delegate-${job.id}-${job.startedAt.getTime()}`,
@@ -68,7 +70,7 @@ export class BackgroundTurns {
68
70
  runErrorReport(channel: ChannelKey, text: string): Promise<ScheduledOutcome> {
69
71
  return this.#options.conversations.background({
70
72
  channel,
71
- mode: "owner",
73
+ target: OWNER_TARGET.name,
72
74
  author: this.#options.system,
73
75
  turnId: `error-${Date.now()}`,
74
76
  text,
@@ -1,11 +1,12 @@
1
1
  import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import { OWNER_TARGET } from "../../agents/agent-claim.ts";
2
3
  import type { ChannelKey } from "../../domain/conversation.ts";
3
4
  import { DelegationError } from "../../domain/errors.ts";
4
5
  import type { OwnerIdentity } from "../../identity.ts";
5
6
  import { textToolsExtension } from "../../runtime/text-tools.ts";
7
+ import type { Delegator } from "../../services.ts";
6
8
  import { DELEGATE_TOOL_SPEC } from "../../shared/delegate-tool.ts";
7
9
  import type { Speaker } from "../../speakers.ts";
8
- import type { Delegator } from "./delegator.ts";
9
10
 
10
11
  export interface OwnerDelegation {
11
12
  delegator: Pick<Delegator, "start">;
@@ -36,7 +37,7 @@ export function delegateExtension(
36
37
  const job = delegation.delegator.start({
37
38
  channel: target,
38
39
  origin,
39
- mode: "owner",
40
+ target: OWNER_TARGET.name,
40
41
  author: speaker() ?? delegation.owner,
41
42
  title,
42
43
  task,
@@ -1,3 +1,4 @@
1
+ import type { BackgroundTarget } from "../../contract/channels.ts";
1
2
  import type {
2
3
  DispatchThread,
3
4
  DispatchThreads,
@@ -6,19 +7,19 @@ import type { ChannelKey } from "../../domain/conversation.ts";
6
7
  import { DelegationError } from "../../domain/errors.ts";
7
8
  import { messages } from "../../i18n/index.ts";
8
9
  import type { Logger } from "../../log.ts";
10
+ import type { Delegator } from "../../services.ts";
9
11
  import type { Tier } from "../../speakers.ts";
10
12
  import { timeZone } from "../../time.ts";
11
13
 
12
- export type DelegationMode = "owner" | "party";
13
-
14
14
  export interface DelegationJob {
15
15
  id: number;
16
16
  /** Where the report comes back as a turn. */
17
17
  channel: ChannelKey;
18
- /** The channel of the owner's turn that started it, which hosts its thread; party jobs have none. */
18
+ /** The channel of the owner's turn that started it, which hosts its thread; absent where no thread opens. */
19
19
  origin?: ChannelKey;
20
- mode: DelegationMode;
21
- /** Who asked; a party report is answered as this person's turn. */
20
+ /** The name of the background target the report is answered as. */
21
+ target: string;
22
+ /** Who asked; the report is answered as this person's turn. */
22
23
  author: { id: string; name: string; tier?: Tier };
23
24
  title: string;
24
25
  task: string;
@@ -37,6 +38,8 @@ export interface DelegationWorker {
37
38
 
38
39
  export interface DelegatorOptions {
39
40
  worker: DelegationWorker;
41
+ /** Resolves a job's target; an unknown one, or one without `delegation`, is refused with a DelegationError. */
42
+ targets(name: string): BackgroundTarget | undefined;
40
43
  /** Posts the finished job back in its channel; never rejects. */
41
44
  deliver: (job: DelegationJob, outcome: DelegationOutcome) => Promise<void>;
42
45
  /** Each job's thread in its origin; without it every job reports in its channel only. */
@@ -46,17 +49,12 @@ export interface DelegatorOptions {
46
49
  }
47
50
 
48
51
  const MAX_TASK_CHARS = 4_000;
49
- /** Jobs one channel may have running at once. */
50
- const RUNNING_LIMIT: Readonly<Record<DelegationMode, number>> = {
51
- owner: 3,
52
- party: 2,
53
- };
54
52
 
55
53
  /**
56
54
  * Runs delegated tasks in the background and hands each result back to its channel. Jobs live
57
55
  * in memory: a restart drops the running ones, and the channel never hears back from them.
58
56
  */
59
- export class Delegator {
57
+ export class DefaultDelegator implements Delegator {
60
58
  readonly #options: DelegatorOptions;
61
59
  readonly #running = new Map<
62
60
  number,
@@ -83,7 +81,11 @@ export class Delegator {
83
81
  const busy = [...this.#running.values()].filter(
84
82
  ({ job }) => job.channel === request.channel,
85
83
  ).length;
86
- const limit = RUNNING_LIMIT[request.mode];
84
+ const limit = this.#options.targets(request.target)?.delegation?.maxRunning;
85
+ if (limit === undefined)
86
+ throw new DelegationError(
87
+ `delegated tasks are not available for ${request.target} conversations`,
88
+ );
87
89
  if (busy >= limit)
88
90
  throw new DelegationError(
89
91
  `this channel already has ${busy} delegated tasks running; wait for one to report back`,
@@ -119,7 +121,7 @@ export class Delegator {
119
121
  timeoutMs = 20 * 60_000,
120
122
  } = this.#options;
121
123
  logger.info(
122
- { job: job.id, channel: job.channel, mode: job.mode },
124
+ { job: job.id, channel: job.channel, target: job.target },
123
125
  "delegated task started",
124
126
  );
125
127
  const thread = await threads?.open(
@@ -12,15 +12,15 @@ import {
12
12
  type ThinkingLevel,
13
13
  } from "../../models.ts";
14
14
  import { runWorkerTask } from "../../runtime/worker-task.ts";
15
+ import { activeToolsExtension } from "../../shared/active-tools.ts";
15
16
  import { packageDir } from "../../shared/package-dir.ts";
16
- import { profileToolsExtension } from "../../shared/profile-tools.ts";
17
17
 
18
18
  const WEB_TOOLS = ["web_search", "fetch_content", "get_search_content"];
19
19
 
20
20
  const WORKER_PROMPT =
21
21
  "You are a research worker. Another agent handed you the task below and will pass your report on. Do it with web search and page reading, preferring primary sources. Report in the task's language, self-contained, with a source link for each claim, and say what you could not verify.";
22
22
 
23
- export interface SolWorkerOptions {
23
+ export interface WebResearchWorkerOptions {
24
24
  /** Shared with the rest of the host, so the Codex login refreshes in one place. */
25
25
  modelRuntime: ModelRuntime;
26
26
  agentDir: string;
@@ -30,11 +30,11 @@ export interface SolWorkerOptions {
30
30
  }
31
31
 
32
32
  /** Runs one task in a fresh, unsaved session that can only search and read the web. */
33
- export class SolWorker {
34
- readonly #options: SolWorkerOptions;
33
+ export class WebResearchWorker {
34
+ readonly #options: WebResearchWorkerOptions;
35
35
  readonly #webAccessPath: string;
36
36
 
37
- constructor(options: SolWorkerOptions) {
37
+ constructor(options: WebResearchWorkerOptions) {
38
38
  this.#options = options;
39
39
  this.#webAccessPath = packageDir("pi-web-access");
40
40
  mkdirSync(options.workDir, { recursive: true });
@@ -52,7 +52,7 @@ export class SolWorker {
52
52
  noContextFiles: true,
53
53
  additionalExtensionPaths: [this.#webAccessPath],
54
54
  extensionFactories: [
55
- { name: "web-only", factory: profileToolsExtension(() => WEB_TOOLS) },
55
+ { name: "web-only", factory: activeToolsExtension(() => WEB_TOOLS) },
56
56
  ],
57
57
  appendSystemPrompt: [WORKER_PROMPT],
58
58
  });
@@ -54,7 +54,7 @@ interface ServerTool {
54
54
  }
55
55
 
56
56
  /** Server-level tools only the owner's agent has; channel tools reuse the outside-agent set. */
57
- export const SERVER_TOOLS: Record<string, ServerTool> = {
57
+ const SERVER_TOOLS: Record<string, ServerTool> = {
58
58
  discord_list_servers: {
59
59
  label: "List Discord servers",
60
60
  describe: (o) =>
@@ -248,19 +248,19 @@ export const SERVER_TOOLS: Record<string, ServerTool> = {
248
248
  },
249
249
  };
250
250
 
251
- /** Every Discord tool of the owner's `discord` profile. */
252
- export const DISCORD_OWNER_TOOLS = [
251
+ /** Every Discord tool the owner's conversations may use: the names the `discord-admin` addon registers. */
252
+ export const DISCORD_ADMIN_TOOLS: readonly string[] = Object.freeze([
253
253
  ...Object.keys(CHANNEL_TOOLS),
254
254
  ...Object.keys(SERVER_TOOLS),
255
- ];
255
+ ]);
256
256
 
257
257
  /** Runs one Discord tool for the owner; throws ChannelToolError for a refused call. */
258
- export interface OwnerDiscord {
258
+ export interface OwnerOperations {
259
259
  run(tool: string, args: Record<string, unknown>): Promise<unknown>;
260
260
  }
261
261
 
262
262
  export function discordAdminExtension(
263
- discord: OwnerDiscord,
263
+ discord: OwnerOperations,
264
264
  owner: OwnerIdentity,
265
265
  ): ExtensionFactory {
266
266
  const o = ownerWords(owner);
@@ -6,7 +6,12 @@ import { assistantName } from "../../i18n/index.ts";
6
6
  const MAX_SHOWN_COMMAND = 600;
7
7
 
8
8
  /** Pi's built-in shell and file tools, which agent sessions activate. */
9
- export const SHELL_TOOLS = ["bash", "read", "edit", "write"] as const;
9
+ export const SHELL_TOOLS = Object.freeze([
10
+ "bash",
11
+ "read",
12
+ "edit",
13
+ "write",
14
+ ] as const);
10
15
 
11
16
  /** Programs that are held whatever their arguments. */
12
17
  const ALWAYS_HELD = new Set([
@@ -377,10 +382,10 @@ export function shellActionNeedingConfirmation(
377
382
  }
378
383
 
379
384
  /** Shell calls of a session with a workspace; sessions without one have no shell. */
380
- export const shellHoldRule: HoldRule = {
385
+ export const shellHoldRule: Readonly<HoldRule> = Object.freeze<HoldRule>({
381
386
  name: "shell",
382
387
  describe: (tool, input, { workspace }) =>
383
388
  workspace === undefined
384
389
  ? undefined
385
390
  : shellActionNeedingConfirmation(tool, input, workspace),
386
- };
391
+ });
@@ -1,22 +1,24 @@
1
1
  import type { SQL } from "bun";
2
2
  import type { Migration } from "../../db/migrations.ts";
3
3
  import { MemoryError } from "../../domain/errors.ts";
4
+ import type { MemoryStore, SpeakerMemory } from "../../services.ts";
4
5
 
5
- export const OWNER_MEMORY_KINDS = ["core", "note", "event"] as const;
6
- export type OwnerMemoryKind = (typeof OWNER_MEMORY_KINDS)[number];
6
+ /** The kinds of memory: a core fact shown every turn, a searchable note, a dated event. */
7
+ export const MEMORY_KINDS = Object.freeze(["core", "note", "event"] as const);
8
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
7
9
 
8
- export interface OwnerMemory {
10
+ export interface Memory {
9
11
  id: number;
10
- kind: OwnerMemoryKind;
12
+ kind: MemoryKind;
11
13
  fact: string;
12
14
  /** YYYY-MM-DD; set for events only. */
13
15
  eventDate: string | null;
14
16
  }
15
17
 
16
- /** What every owner turn carries: all core facts, and events that have not passed yet. */
17
- export interface OwnerPromptMemory {
18
- core: OwnerMemory[];
19
- events: OwnerMemory[];
18
+ /** What every turn carries: all core facts, and events that have not passed yet. */
19
+ export interface PromptMemory {
20
+ core: Memory[];
21
+ events: Memory[];
20
22
  }
21
23
 
22
24
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
@@ -24,12 +26,12 @@ const SEARCH_LIMIT = 20;
24
26
 
25
27
  interface Row {
26
28
  id: string | number;
27
- kind: OwnerMemoryKind;
29
+ kind: MemoryKind;
28
30
  fact: string;
29
31
  event_date: string | null;
30
32
  }
31
33
 
32
- function toMemory(row: Row): OwnerMemory {
34
+ function toMemory(row: Row): Memory {
33
35
  return {
34
36
  id: Number(row.id),
35
37
  kind: row.kind,
@@ -53,12 +55,12 @@ export function searchTerms(query: string): string[] {
53
55
  /** The trimmed fact, after the rules every stored memory follows. */
54
56
  function checkedFact(
55
57
  fact: string,
56
- kind: OwnerMemoryKind,
58
+ kind: MemoryKind,
57
59
  eventDate: string | undefined,
58
60
  ): string {
59
61
  const trimmed = fact.trim();
60
62
  if (!trimmed) throw new MemoryError("a memory fact cannot be empty");
61
- if (!(OWNER_MEMORY_KINDS as readonly string[]).includes(kind))
63
+ if (!(MEMORY_KINDS as readonly string[]).includes(kind))
62
64
  throw new MemoryError(`unknown memory kind: ${kind}`);
63
65
  if (kind === "event" && !(eventDate && DATE.test(eventDate)))
64
66
  throw new MemoryError("an event needs its date as YYYY-MM-DD");
@@ -72,7 +74,7 @@ function checkedFact(
72
74
  * dated events. The store is bound to a speaker and never reads or changes another's; every
73
75
  * agent shares the memory of whoever is speaking.
74
76
  */
75
- export class OwnerMemoryStore {
77
+ export class PgMemoryStore implements MemoryStore, SpeakerMemory {
76
78
  readonly #sql: SQL;
77
79
  /** Whose memory this is: a Discord user id. */
78
80
  readonly speakerId: string;
@@ -108,7 +110,7 @@ export class OwnerMemoryStore {
108
110
  */
109
111
  static migrations(ownerId: string): Migration[] {
110
112
  return [
111
- OwnerMemoryStore.migration,
113
+ PgMemoryStore.migration,
112
114
  {
113
115
  name: "owner-memory-speaker",
114
116
  up: async (sql) => {
@@ -125,25 +127,25 @@ export class OwnerMemoryStore {
125
127
  }
126
128
 
127
129
  /** The store over the host's migrated pool, holding one speaker's memory. */
128
- static attach(sql: SQL, speakerId: string): OwnerMemoryStore {
129
- return new OwnerMemoryStore(sql, speakerId);
130
+ static attach(sql: SQL, speakerId: string): PgMemoryStore {
131
+ return new PgMemoryStore(sql, speakerId);
130
132
  }
131
133
 
132
134
  /** Another speaker's memory in the same database. */
133
- forSpeaker(speakerId: string): OwnerMemoryStore {
135
+ forSpeaker(speakerId: string): PgMemoryStore {
134
136
  return speakerId === this.speakerId
135
137
  ? this
136
- : new OwnerMemoryStore(this.#sql, speakerId);
138
+ : new PgMemoryStore(this.#sql, speakerId);
137
139
  }
138
140
 
139
- async list(): Promise<OwnerMemory[]> {
141
+ async list(): Promise<Memory[]> {
140
142
  const rows: Row[] = await this.#sql`
141
143
  SELECT id, kind, fact, to_char(event_date, 'YYYY-MM-DD') AS event_date
142
144
  FROM owner_memory WHERE speaker_id = ${this.speakerId} ORDER BY id`;
143
145
  return rows.map(toMemory);
144
146
  }
145
147
 
146
- async forPrompt(today: string): Promise<OwnerPromptMemory> {
148
+ async forPrompt(today: string): Promise<PromptMemory> {
147
149
  const rows: Row[] = await this.#sql`
148
150
  SELECT id, kind, fact, to_char(event_date, 'YYYY-MM-DD') AS event_date
149
151
  FROM owner_memory
@@ -159,9 +161,9 @@ export class OwnerMemoryStore {
159
161
 
160
162
  async add(
161
163
  fact: string,
162
- kind: OwnerMemoryKind = "core",
164
+ kind: MemoryKind = "core",
163
165
  eventDate?: string,
164
- ): Promise<OwnerMemory> {
166
+ ): Promise<Memory> {
165
167
  const trimmed = checkedFact(fact, kind, eventDate);
166
168
  const [row]: Row[] = await this.#sql`
167
169
  INSERT INTO owner_memory (speaker_id, fact, kind, event_date)
@@ -175,7 +177,7 @@ export class OwnerMemoryStore {
175
177
  * Memories containing any query term, case-insensitively, most terms matched first,
176
178
  * then newest first.
177
179
  */
178
- async search(query: string, limit = SEARCH_LIMIT): Promise<OwnerMemory[]> {
180
+ async search(query: string, limit = SEARCH_LIMIT): Promise<Memory[]> {
179
181
  const terms = searchTerms(query);
180
182
  if (terms.length === 0) throw new MemoryError("the search query is empty");
181
183
  const rows: (Row & { hits: number })[] = await this.#sql`
@@ -196,8 +198,8 @@ export class OwnerMemoryStore {
196
198
  /** Replaces one memory's text, kind, and date under `add`'s rules; undefined when the id is unknown. */
197
199
  async update(
198
200
  id: number,
199
- change: { fact: string; kind: OwnerMemoryKind; eventDate?: string },
200
- ): Promise<OwnerMemory | undefined> {
201
+ change: { fact: string; kind: MemoryKind; eventDate?: string },
202
+ ): Promise<Memory | undefined> {
201
203
  const trimmed = checkedFact(change.fact, change.kind, change.eventDate);
202
204
  const [row]: Row[] = await this.#sql`
203
205
  UPDATE owner_memory
@@ -2,23 +2,20 @@ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { messages } from "../../i18n/index.ts";
4
4
  import { type OwnerIdentity, ownerWords } from "../../identity.ts";
5
+ import type { MemoryStore } from "../../services.ts";
5
6
  import { toolError, toolText } from "../../shared/tool-result.ts";
6
7
  import type { Speaker } from "../../speakers.ts";
7
8
  import { timeZone, zonedToday } from "../../time.ts";
8
- import type {
9
- OwnerMemory,
10
- OwnerMemoryStore,
11
- OwnerPromptMemory,
12
- } from "./owner-memory-store.ts";
9
+ import type { Memory, PromptMemory } from "./owner-memory-store.ts";
13
10
 
14
- function line(memory: OwnerMemory): string {
11
+ function line(memory: Memory): string {
15
12
  return memory.eventDate
16
13
  ? `- ${memory.eventDate}: ${memory.fact}`
17
14
  : `- ${memory.fact}`;
18
15
  }
19
16
 
20
17
  export function ownerMemorySection(
21
- memory: OwnerPromptMemory,
18
+ memory: PromptMemory,
22
19
  today: string,
23
20
  /** Whose memory it is, when not the owner's. */
24
21
  speaker?: Speaker,
@@ -45,7 +42,9 @@ export function ownerMemorySection(
45
42
  * replaced system prompt.
46
43
  */
47
44
  export function ownerMemoryExtension(
48
- ownerStore: OwnerMemoryStore,
45
+ memories: MemoryStore,
46
+ /** The owner's Discord id: the speaker whose memory a turn reads when no other speaks. */
47
+ ownerId: string,
49
48
  owner: OwnerIdentity,
50
49
  /**
51
50
  * The person the running turn is for, in a session that serves several. Their memory, not
@@ -57,12 +56,9 @@ export function ownerMemoryExtension(
57
56
  /** The running turn's other speaker; undefined when the owner's own memory applies. */
58
57
  const other = (): Speaker | undefined => {
59
58
  const current = speaker?.();
60
- return current && current.id !== ownerStore.speakerId ? current : undefined;
61
- };
62
- const storeOf = () => {
63
- const current = other();
64
- return current ? ownerStore.forSpeaker(current.id) : ownerStore;
59
+ return current && current.id !== ownerId ? current : undefined;
65
60
  };
61
+ const storeOf = () => memories.forSpeaker(other()?.id ?? ownerId);
66
62
  const shared = speaker
67
63
  ? " Whoever is speaking has a memory of their own, shared by every agent; this reads and changes theirs, not someone else's."
68
64
  : "";