pi-roundtable 0.5.0 → 0.5.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.
- package/CHANGELOG.md +6 -0
- package/README.md +39 -11
- package/README.zh-TW.md +35 -14
- package/docs/plugins.md +258 -167
- package/examples/migrations.test.ts +1 -1
- package/examples/migrations.ts +2 -2
- package/package.json +7 -3
- package/src/cli/cli.ts +2 -0
- package/src/cli/main.ts +1 -0
- package/src/cli/project.ts +2 -0
- package/src/cli/runtime.ts +2 -2
- package/src/cli/templates.ts +10 -9
- package/src/core/agents/agent-prompt.ts +1 -2
- package/src/core/agents/agent-store.ts +1 -2
- package/src/core/agents/agent-team-fixture.ts +5 -3
- package/src/core/agents/fallback-avatar.ts +1 -0
- package/src/core/agents/group-messages.ts +1 -1
- package/src/core/agents/group-round.ts +10 -9
- package/src/core/agents/group-turns.ts +13 -8
- package/src/core/agents/team-layout.ts +13 -14
- package/src/core/agents/team-status.ts +8 -8
- package/src/core/agents/team-turns.ts +8 -9
- package/src/core/define-roundtable.ts +19 -19
- package/src/core/define.ts +10 -10
- package/src/core/discord/channel-executor.ts +20 -18
- package/src/core/discord/channel-operations.ts +7 -3
- package/src/core/discord/inbound-message.ts +13 -11
- package/src/core/discord/owner-cards.ts +3 -3
- package/src/core/discord/owner-discord-threads.ts +16 -12
- package/src/core/discord/owner-discord.ts +5 -4
- package/src/core/host.ts +1 -0
- package/src/core/log.ts +1 -0
- package/src/core/modules/schedules/scheduler.ts +3 -0
- package/src/core/modules/skills/skill-link.ts +13 -8
- package/src/core/modules/skills/skill-listing.ts +2 -4
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +3 -2
- package/src/core/registry/contributions.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -2
- package/src/core/routing/settle-turn.ts +8 -0
- package/src/core/runtime/compaction-tiers.ts +9 -10
- package/src/core/runtime/turn-answer.ts +10 -12
- 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
|
-
.
|
|
158
|
-
|
|
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
|
-
:
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
84
|
-
|
|
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
|
-
.
|
|
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.
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
? (compaction, ctx)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
part
|
|
42
|
-
|
|
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
|
-
.
|
|
64
|
-
(message)
|
|
65
|
-
|
|
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
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|