pi-roundtable 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +39 -11
  3. package/README.zh-TW.md +35 -14
  4. package/docs/plugins.md +258 -167
  5. package/examples/migrations.test.ts +1 -1
  6. package/examples/migrations.ts +2 -2
  7. package/package.json +8 -4
  8. package/src/cli/cli.ts +2 -0
  9. package/src/cli/main.ts +1 -0
  10. package/src/cli/project.ts +2 -0
  11. package/src/cli/runtime.ts +2 -2
  12. package/src/cli/templates.ts +10 -9
  13. package/src/core/agents/agent-prompt.ts +1 -2
  14. package/src/core/agents/agent-store.ts +1 -2
  15. package/src/core/agents/agent-team-fixture.ts +5 -3
  16. package/src/core/agents/fallback-avatar.ts +1 -0
  17. package/src/core/agents/group-messages.ts +1 -1
  18. package/src/core/agents/group-round.ts +10 -9
  19. package/src/core/agents/group-turns.ts +13 -8
  20. package/src/core/agents/team-layout.ts +13 -14
  21. package/src/core/agents/team-status.ts +8 -8
  22. package/src/core/agents/team-turns.ts +8 -9
  23. package/src/core/define-roundtable.ts +19 -19
  24. package/src/core/define.ts +10 -10
  25. package/src/core/discord/channel-executor.ts +20 -18
  26. package/src/core/discord/channel-operations.ts +7 -3
  27. package/src/core/discord/inbound-message.ts +13 -11
  28. package/src/core/discord/owner-cards.ts +3 -3
  29. package/src/core/discord/owner-discord-threads.ts +16 -12
  30. package/src/core/discord/owner-discord.ts +5 -4
  31. package/src/core/host.ts +1 -0
  32. package/src/core/log.ts +1 -0
  33. package/src/core/modules/schedules/scheduler.ts +3 -0
  34. package/src/core/modules/skills/skill-link.ts +13 -8
  35. package/src/core/modules/skills/skill-listing.ts +2 -4
  36. package/src/core/modules/skills/skill-registry.ts +3 -2
  37. package/src/core/modules/skills/skill-store.ts +3 -2
  38. package/src/core/registry/contributions.ts +1 -1
  39. package/src/core/routing/conversation-turns.ts +2 -2
  40. package/src/core/routing/settle-turn.ts +8 -0
  41. package/src/core/runtime/compaction-tiers.ts +9 -10
  42. package/src/core/runtime/turn-answer.ts +10 -12
  43. package/src/core/shared/session-messages.ts +4 -4
@@ -153,9 +153,9 @@ export class DiscordOwnerOps implements OwnerOperations {
153
153
  displayName: m.displayName,
154
154
  nickname: m.nickname,
155
155
  bot: m.user.bot,
156
- roles: m.roles.cache
157
- .filter((r) => r.id !== guild.id)
158
- .map((r) => r.name),
156
+ roles: [...m.roles.cache.values()].flatMap((r) =>
157
+ r.id === guild.id ? [] : [r.name],
158
+ ),
159
159
  }));
160
160
  }
161
161
  case "discord_create_role": {
@@ -233,8 +233,9 @@ export class DiscordOwnerOps implements OwnerOperations {
233
233
  await guild.members.unban(userId, reason);
234
234
  return { userId, banned: false };
235
235
  }
236
+ default:
237
+ throw refuse("INVALID_TOOL", tool);
236
238
  }
237
- throw refuse("INVALID_TOOL", tool);
238
239
  }
239
240
 
240
241
  /** Servers the owner and the assistant share, with what each of them may do there. */
package/src/core/host.ts CHANGED
@@ -109,6 +109,7 @@ async function startInBackground(
109
109
  ): Promise<void> {
110
110
  const { services, servicePlugins, handlers } = registry;
111
111
  await Promise.all(
112
+ // pi-lens-ignore: array-callback-return — an async callback returns its promise on every path; map only starts the tasks for Promise.all, and the early return skips a service with no background start
112
113
  services.map(async (service) => {
113
114
  if (!service.startInBackground) return;
114
115
  const plugin = servicePlugins.get(service) ?? "unknown";
package/src/core/log.ts CHANGED
@@ -2,6 +2,7 @@ import pino from "pino";
2
2
 
3
3
  /** One log method: fields and a message, or a message alone. */
4
4
  export interface LogFn {
5
+ // pi-lens-ignore: no-bare-object-param — LogFn is public API and pino's own call signature takes any object; a narrower type would refuse loggers that pass an interface-typed value
5
6
  (fields: object, message?: string): void;
6
7
  (message: string): void;
7
8
  }
@@ -31,6 +31,8 @@ function statusText(outcome: ScheduledOutcome): string {
31
31
  return `failed: ${outcome.error}`.slice(0, 300);
32
32
  case "skipped":
33
33
  return `skipped: ${outcome.reason}`;
34
+ default:
35
+ return outcome satisfies never;
34
36
  }
35
37
  }
36
38
 
@@ -99,6 +101,7 @@ export class Scheduler {
99
101
 
100
102
  /** Resolves once every started run has finished. */
101
103
  async idle(): Promise<void> {
104
+ // pi-lens-ignore: no-single-promise-in-promise-methods — the spread holds every run in flight, none to many; it is not one promise
102
105
  await Promise.all([...this.#running]);
103
106
  }
104
107
 
@@ -4,6 +4,18 @@ import { AgentError } from "../../domain/errors.ts";
4
4
  import { checkRepoName } from "./repo-name.ts";
5
5
  import { checkSkillName, readSkillFile } from "./skill-rules.ts";
6
6
 
7
+ /** The child folders of `dir` that hold a SKILL.md, as paths from the repository root. */
8
+ function skillFoldersUnder(dir: string, relative: string): string[] {
9
+ const prefix = relative ? `${relative}/` : "";
10
+ const folders = readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
11
+ entry.isDirectory() && existsSync(join(dir, entry.name, "SKILL.md"))
12
+ ? [`${prefix}${entry.name}`]
13
+ : [],
14
+ );
15
+ // pi-lens-ignore: no-sort-without-comparator — folder paths sort by code unit, the same order on every machine
16
+ return folders.sort();
17
+ }
18
+
7
19
  /**
8
20
  * The skills a link would register: the one at a repository path, or every child folder with a
9
21
  * SKILL.md (behavior 15). Throws one error listing every problem, so nothing is half linked.
@@ -28,14 +40,7 @@ export function planLinkedSkills(
28
40
  throw new AgentError(`${repo} has no folder ${relative || "."}.`);
29
41
  const folders = existsSync(join(dir, "SKILL.md"))
30
42
  ? [relative]
31
- : readdirSync(dir, { withFileTypes: true })
32
- .filter(
33
- (entry) =>
34
- entry.isDirectory() &&
35
- existsSync(join(dir, entry.name, "SKILL.md")),
36
- )
37
- .map((entry) => (relative ? `${relative}/${entry.name}` : entry.name))
38
- .sort();
43
+ : skillFoldersUnder(dir, relative);
39
44
  if (folders.length === 0)
40
45
  throw new AgentError(
41
46
  `${repo}:${relative || "."} has no SKILL.md, nor any folder with one.`,
@@ -42,8 +42,7 @@ export function skillCatalog(
42
42
  : { description: one.description, file: one.file }),
43
43
  groups: store
44
44
  .groups()
45
- .filter((g) => g.skills.includes(name))
46
- .map((g) => g.name),
45
+ .flatMap((g) => (g.skills.includes(name) ? [g.name] : [])),
47
46
  carriers: store.carriers(name),
48
47
  };
49
48
  });
@@ -93,8 +92,7 @@ export function skillListText(
93
92
  continue;
94
93
  const groups = store
95
94
  .groups()
96
- .filter((g) => g.skills.includes(name))
97
- .map((g) => g.name);
95
+ .flatMap((g) => (g.skills.includes(name) ? [g.name] : []));
98
96
  const carriers = store.carriers(name);
99
97
  const missing = "reason" in one ? ` — missing: ${one.reason}` : "";
100
98
  rows.push(
@@ -80,8 +80,9 @@ export class StoredSkillRegistry implements SkillRegistry {
80
80
  linkedFrom(repo: string): string[] {
81
81
  return this.#options.store
82
82
  .skills()
83
- .filter((entry) => entry.kind === "linked" && entry.repo === repo)
84
- .map((entry) => entry.name);
83
+ .flatMap((entry) =>
84
+ entry.kind === "linked" && entry.repo === repo ? [entry.name] : [],
85
+ );
85
86
  }
86
87
 
87
88
  /** The registered skills an agent carries, as the dashboard lists them. */
@@ -177,9 +177,9 @@ export class SkillStore {
177
177
 
178
178
  /** The agents carrying a skill. */
179
179
  carriers(skill: string): string[] {
180
+ // pi-lens-ignore: no-sort-without-comparator — agent names sort by code unit, the same order on every machine
180
181
  return [...this.#carried]
181
- .filter(([, skills]) => skills.includes(skill))
182
- .map(([agent]) => agent)
182
+ .flatMap(([agent, skills]) => (skills.includes(skill) ? [agent] : []))
183
183
  .sort();
184
184
  }
185
185
 
@@ -260,6 +260,7 @@ export class SkillStore {
260
260
 
261
261
  /** Replaces the registered skills an agent carries. */
262
262
  async setCarried(agent: string, skills: readonly string[]): Promise<void> {
263
+ // pi-lens-ignore: no-sort-without-comparator — skill names sort by code unit, the same order on every machine
263
264
  const sorted = [...new Set(skills)].sort();
264
265
  await this.#sql.begin(async (tx) => {
265
266
  await tx`DELETE FROM agent_skills WHERE guild_id = ${this.#guild} AND agent = ${agent}`;
@@ -403,7 +403,7 @@ export function linkSessions(registry: Registry): LinkedSessions {
403
403
  prompt: registry.prompt,
404
404
  persona: (kind) => personas.get(kind)?.prompt(),
405
405
  requiredTools: [...new Set(registry.requiredTools)],
406
- agentTools: registry.tools.filter((t) => t.agent).map((t) => t.name),
406
+ agentTools: registry.tools.flatMap((t) => (t.agent ? [t.name] : [])),
407
407
  agentSelection: () => mergeSelections(registry.agentSelections),
408
408
  };
409
409
  }
@@ -9,7 +9,7 @@ import { splitReply } from "../presentation/reply-splitter.ts";
9
9
  import { thinkingLine } from "../presentation/thinking-line.ts";
10
10
  import type { ChannelKey, ToolSelection, TurnSelection } from "../sessions.ts";
11
11
  import type { Speaker } from "../speakers.ts";
12
- import { settleTurn } from "./settle-turn.ts";
12
+ import { endOf, settleTurn } from "./settle-turn.ts";
13
13
 
14
14
  /** One turn of a conversation, as a claim asks for it. */
15
15
  export interface ConversationTurnInput {
@@ -106,7 +106,7 @@ export function conversationTurns(
106
106
  }
107
107
  events.turnEnded({
108
108
  ...turn,
109
- result: result.ok ? "ok" : result.stopped ? "stopped" : "failed",
109
+ result: endOf(result),
110
110
  });
111
111
  try {
112
112
  if (!result.ok && !result.stopped)
@@ -1,5 +1,7 @@
1
1
  import type { ScheduledOutcome } from "../contract/channels.ts";
2
+ import type { TurnResult } from "../domain/conversation.ts";
2
3
  import { AgentRunError } from "../domain/errors.ts";
4
+ import type { TurnEndEvent } from "../plugin.ts";
3
5
 
4
6
  /** Runs a turn; a thrown error becomes its failed result, labelled `<what> crashed`. */
5
7
  export async function settleTurn<R>(
@@ -16,6 +18,12 @@ export async function settleTurn<R>(
16
18
  }
17
19
  }
18
20
 
21
+ /** How a turn ended, as the `turnEnded` event reports it. */
22
+ export function endOf(result: TurnResult): TurnEndEvent["result"] {
23
+ if (result.ok) return "ok";
24
+ return result.stopped ? "stopped" : "failed";
25
+ }
26
+
19
27
  /** The result of a turn nobody wrote, as the scheduler and reports see it. */
20
28
  export function outcome(
21
29
  result: { ok: true } | { ok: false; error: { message: string } },
@@ -182,19 +182,18 @@ export class CompactionTiers {
182
182
  event: string,
183
183
  handler: Handler,
184
184
  ) => () => void;
185
- const on = ((event: string, handler: Handler) =>
186
- register(
187
- event,
188
- event === "session_before_compact"
189
- ? (compaction, ctx) =>
190
- gate(compaction as SessionBeforeCompactEvent)
191
- ? handler(compaction, ctx)
192
- : undefined
193
- : handler,
194
- )) as ExtensionAPI["on"];
185
+ const on = ((event: string, handler: Handler) => {
186
+ if (event !== "session_before_compact") return register(event, handler);
187
+ return register(event, (compaction, ctx) =>
188
+ gate(compaction as SessionBeforeCompactEvent)
189
+ ? handler(compaction, ctx)
190
+ : undefined,
191
+ );
192
+ }) as ExtensionAPI["on"];
195
193
  return compactor(
196
194
  new Proxy(pi, {
197
195
  get: (target, prop, receiver) =>
196
+ // pi-lens-ignore: no-reflect-get — a Proxy get trap: the receiver keeps a getter's `this` on the proxy, which target[prop] would not
198
197
  prop === "on" ? on : Reflect.get(target, prop, receiver),
199
198
  }),
200
199
  );
@@ -34,15 +34,13 @@ export function thinkingOf(messages: readonly Message[]): string {
34
34
  ? (message.content as (MessagePart | undefined)[])
35
35
  : [],
36
36
  )
37
- .filter(
38
- (
39
- part,
40
- ): part is { type: "thinking"; thinking: string; redacted?: boolean } =>
41
- part?.type === "thinking" &&
42
- typeof part.thinking === "string" &&
43
- part.redacted !== true,
37
+ .flatMap((part) =>
38
+ part?.type === "thinking" &&
39
+ typeof part.thinking === "string" &&
40
+ part.redacted !== true
41
+ ? [part.thinking.trim()]
42
+ : [],
44
43
  )
45
- .map((part) => part.thinking.trim())
46
44
  .filter(Boolean)
47
45
  .join("\n\n");
48
46
  }
@@ -60,11 +58,11 @@ export function turnAnswer(
60
58
  const text = (
61
59
  steered
62
60
  ? messages
63
- .filter(
64
- (message): message is Assistant =>
65
- isAssistant(message) && message.stopReason === "stop",
61
+ .flatMap((message) =>
62
+ isAssistant(message) && message.stopReason === "stop"
63
+ ? [textOf(message.content).trim()]
64
+ : [],
66
65
  )
67
- .map((message) => textOf(message.content).trim())
68
66
  .filter(Boolean)
69
67
  .join("\n\n")
70
68
  : textOf(last.message.content)
@@ -11,11 +11,11 @@ export function textOf(content: unknown): string {
11
11
  if (typeof content === "string") return content;
12
12
  if (!Array.isArray(content)) return "";
13
13
  return content
14
- .filter(
15
- (part): part is { type: "text"; text: string } =>
16
- part?.type === "text" && typeof part.text === "string",
14
+ .flatMap((part) =>
15
+ part?.type === "text" && typeof part.text === "string"
16
+ ? [part.text as string]
17
+ : [],
17
18
  )
18
- .map((part) => part.text)
19
19
  .join("\n");
20
20
  }
21
21