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.
- package/CHANGELOG.md +12 -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 +8 -4
- 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
|
@@ -13,7 +13,7 @@ test.skipIf(!url)(
|
|
|
13
13
|
try {
|
|
14
14
|
for (const migration of visitCounter.migrations ?? []) {
|
|
15
15
|
await migration.up(sql);
|
|
16
|
-
await migration.up(sql); //
|
|
16
|
+
await migration.up(sql); // Written to be idempotent, so a second run changes nothing.
|
|
17
17
|
}
|
|
18
18
|
const harness = await testPlugin(visitCounter, { database: sql });
|
|
19
19
|
expect(await harness.runTool("visit_count", { place: "lab" })).toBe(
|
package/examples/migrations.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { definePlugin, defineTool } from "pi-roundtable";
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Migrations create the plugin's tables before any setup runs
|
|
6
|
-
*
|
|
5
|
+
* Migrations create the plugin's tables before any setup runs. Each one runs once and is recorded
|
|
6
|
+
* in a ledger. Table names are shared with every other plugin: prefix them.
|
|
7
7
|
*/
|
|
8
8
|
export const visitCounter = definePlugin({
|
|
9
9
|
name: "visit-counter",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "A plugin-driven Pi agent server for Discord",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"type": "git",
|
|
35
35
|
"url": "git+https://github.com/wayne930242/pi-roundtable.git"
|
|
36
36
|
},
|
|
37
|
+
"homepage": "https://pi-roundtable.wayneh.tw",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/wayne930242/pi-roundtable/issues"
|
|
40
|
+
},
|
|
37
41
|
"publishConfig": {
|
|
38
42
|
"access": "public",
|
|
39
43
|
"provenance": true
|
|
@@ -45,11 +49,11 @@
|
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
51
|
"@babel/parser": "7.29.9",
|
|
48
|
-
"@earendil-works/pi-ai": ">=0.99.2 <
|
|
49
|
-
"@earendil-works/pi-coding-agent": ">=0.99.2 <
|
|
52
|
+
"@earendil-works/pi-ai": ">=0.99.2 <2",
|
|
53
|
+
"@earendil-works/pi-coding-agent": ">=0.99.2 <2",
|
|
50
54
|
"canvas": "3.2.3",
|
|
51
55
|
"discord.js": "14.27.0",
|
|
52
|
-
"pi-mcp-adapter": "
|
|
56
|
+
"pi-mcp-adapter": "5.0.0",
|
|
53
57
|
"pino": "10.3.1",
|
|
54
58
|
"typebox": "1.3.34",
|
|
55
59
|
"unpdf": "1.8.1"
|
package/src/cli/cli.ts
CHANGED
|
@@ -79,7 +79,9 @@ export function processEnvironment(): CliEnvironment {
|
|
|
79
79
|
roundtable.listen();
|
|
80
80
|
await roundtable.run();
|
|
81
81
|
},
|
|
82
|
+
// pi-lens-ignore: no-console-except-error — the command line's printed output is its product
|
|
82
83
|
out: (line) => console.log(line),
|
|
84
|
+
// pi-lens-ignore: no-console-except-error — the command line's printed output is its product
|
|
83
85
|
err: (line) => console.error(line),
|
|
84
86
|
};
|
|
85
87
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -7,6 +7,7 @@ runCli(process.argv.slice(2), processEnvironment()).then(
|
|
|
7
7
|
process.exitCode = code;
|
|
8
8
|
},
|
|
9
9
|
(error: unknown) => {
|
|
10
|
+
// pi-lens-ignore: no-console-except-error — the entry point reports a failed boot on stderr; no logger exists yet
|
|
10
11
|
console.error(error instanceof Error ? error.message : String(error));
|
|
11
12
|
// A boot that failed has stopped what it started; nothing may keep the process up.
|
|
12
13
|
process.exit(1);
|
package/src/cli/project.ts
CHANGED
|
@@ -89,6 +89,8 @@ export class Project {
|
|
|
89
89
|
let value: unknown = raw.value;
|
|
90
90
|
for (const key of path) {
|
|
91
91
|
if (typeof value !== "object" || value === null) return undefined;
|
|
92
|
+
// A read only: nothing is assigned by key, and every caller passes literal keys.
|
|
93
|
+
// nosemgrep: javascript.lang.security.audit.prototype-pollution.prototype-pollution-loop.prototype-pollution-loop
|
|
92
94
|
value = (value as Record<string, unknown>)[key];
|
|
93
95
|
}
|
|
94
96
|
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
package/src/cli/runtime.ts
CHANGED
|
@@ -26,7 +26,7 @@ export async function openModelRuntime(
|
|
|
26
26
|
): Promise<ModelRuntime> {
|
|
27
27
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
28
28
|
const authPath = join(agentDir, "auth.json");
|
|
29
|
-
return
|
|
29
|
+
return ModelRuntime.create(
|
|
30
30
|
existsSync(authPath)
|
|
31
31
|
? { authPath, refreshOnCreate: false }
|
|
32
32
|
: { credentials: emptyCredentials, refreshOnCreate: false },
|
|
@@ -47,7 +47,7 @@ export async function assemble(
|
|
|
47
47
|
config: Parameters<typeof defineRoundtable>[0],
|
|
48
48
|
agentDir: string,
|
|
49
49
|
): Promise<DefinedRoundtable> {
|
|
50
|
-
return
|
|
50
|
+
return defineRoundtable(config, {
|
|
51
51
|
modelRuntime: await openModelRuntime(agentDir),
|
|
52
52
|
});
|
|
53
53
|
}
|
package/src/cli/templates.ts
CHANGED
|
@@ -114,15 +114,16 @@ export function renderProject(
|
|
|
114
114
|
};
|
|
115
115
|
const skeleton = filesUnder(dir)
|
|
116
116
|
.map((file) => relative(dir, file))
|
|
117
|
-
.
|
|
118
|
-
(path)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
.flatMap((path) =>
|
|
118
|
+
path.startsWith(`${PLUGIN_DIR}/`) || path.startsWith(`${OFFICIAL_DIR}/`)
|
|
119
|
+
? []
|
|
120
|
+
: [
|
|
121
|
+
{
|
|
122
|
+
path: targetOf(path),
|
|
123
|
+
content: fill(readFileSync(join(dir, path), "utf8"), values),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
);
|
|
126
127
|
return [...skeleton, ...renderPlugin(pluginNames("hello"), dir)];
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -39,8 +39,7 @@ export function agentSystemPrompt(input: {
|
|
|
39
39
|
];
|
|
40
40
|
if (group) {
|
|
41
41
|
const others = group.members
|
|
42
|
-
.
|
|
43
|
-
.map((m) => `"${m.displayName}"`)
|
|
42
|
+
.flatMap((m) => (m.name === agent.name ? [] : [`"${m.displayName}"`]))
|
|
44
43
|
.join(", ");
|
|
45
44
|
parts.push(
|
|
46
45
|
`You are now speaking in the group chat "${group.group.displayName}" with ${w.name} and ${others}. Each of your turns shows the group messages since your last turn. Speak only for yourself and add what your role brings; do not repeat what others said. To hand the next word to a member, mention them as @display name.`,
|
|
@@ -155,8 +155,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
155
155
|
#activeNames(): string {
|
|
156
156
|
return (
|
|
157
157
|
this.agents()
|
|
158
|
-
.
|
|
159
|
-
.map((a) => a.name)
|
|
158
|
+
.flatMap((a) => (a.status === "active" ? [a.name] : []))
|
|
160
159
|
.join(", ") || "none"
|
|
161
160
|
);
|
|
162
161
|
}
|
|
@@ -115,9 +115,11 @@ export class FakeChannels implements AgentChannels {
|
|
|
115
115
|
this.webhooksRemoved.push(channelId);
|
|
116
116
|
}
|
|
117
117
|
texts(channelId: string) {
|
|
118
|
-
return this.posts
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
return this.posts.flatMap((p) =>
|
|
119
|
+
p.channelId === channelId
|
|
120
|
+
? [`${p.post.name}: ${p.post.chunks.join("")}`]
|
|
121
|
+
: [],
|
|
122
|
+
);
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
|
|
@@ -10,6 +10,7 @@ const BADGE = { x: 396, y: 396, radius: 64 };
|
|
|
10
10
|
const hsl = (h: number, s: number, l: number): string =>
|
|
11
11
|
`hsl(${Math.round(h) % 360},${s}%,${l}%)`;
|
|
12
12
|
|
|
13
|
+
// pi-lens-ignore: hardcoded-url — the SVG XML namespace, a fixed identifier rather than an address that is fetched
|
|
13
14
|
const layer = (body: string): Buffer =>
|
|
14
15
|
Buffer.from(
|
|
15
16
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${SIDE}" height="${SIDE}" viewBox="0 0 ${SIDE} ${SIDE}">${body}</svg>`,
|
|
@@ -43,7 +43,7 @@ export class GroupMessages {
|
|
|
43
43
|
const rows: MessageRow[] = await this.#sql`
|
|
44
44
|
SELECT id, author, author_name, text, created_at FROM agent_group_messages
|
|
45
45
|
WHERE guild_id = ${this.#guild} AND group_name = ${group} ORDER BY id DESC LIMIT ${limit}`;
|
|
46
|
-
return rows.
|
|
46
|
+
return rows.toReversed().map(toMessage);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Group messages the member has not received yet, its own excluded; the newest `limit` are carried. */
|
|
@@ -15,20 +15,21 @@ const PROMPT_CHARS_FOR_SCORING = 1_200;
|
|
|
15
15
|
const escapeRegExp = (value: string) =>
|
|
16
16
|
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
17
|
|
|
18
|
+
const mentions = (text: string, label: string) =>
|
|
19
|
+
// The label is an agent's name, escaped so every character of it matches literally.
|
|
20
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
|
|
21
|
+
new RegExp(`@${escapeRegExp(label)}(?![\\p{L}\\p{N}_-])`, "iu").test(text);
|
|
22
|
+
|
|
18
23
|
/** Members a text names with `@display name` or `@name`, in group order. */
|
|
19
24
|
export function mentionedMembers(
|
|
20
25
|
text: string,
|
|
21
26
|
members: readonly Agent[],
|
|
22
27
|
): string[] {
|
|
23
|
-
return members
|
|
24
|
-
.
|
|
25
|
-
[member.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
),
|
|
29
|
-
),
|
|
30
|
-
)
|
|
31
|
-
.map((member) => member.name);
|
|
28
|
+
return members.flatMap((member) =>
|
|
29
|
+
[member.displayName, member.name].some((label) => mentions(text, label))
|
|
30
|
+
? [member.name]
|
|
31
|
+
: [],
|
|
32
|
+
);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
RECENT_FOR_SCORING,
|
|
13
13
|
} from "./group-round.ts";
|
|
14
14
|
import { channelIdOf, groupScope, groupSessionKey } from "./team-keys.ts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
BACKLOG_LIMIT,
|
|
17
|
+
type Chain,
|
|
18
|
+
type TurnExtra,
|
|
19
|
+
type TurnHost,
|
|
20
|
+
} from "./team-turn-types.ts";
|
|
16
21
|
|
|
17
22
|
/** A group channel's rounds: scoring, then the chosen members answer one at a time. */
|
|
18
23
|
export class GroupTurns {
|
|
@@ -99,19 +104,19 @@ export class GroupTurns {
|
|
|
99
104
|
// Delivered once the session has the text, so a message never comes twice.
|
|
100
105
|
if (backlog.lastId !== undefined)
|
|
101
106
|
await store.advanceCursor(group.name, name, backlog.lastId);
|
|
107
|
+
// Attachments and a confirmation belong to a member's first turn of the round only.
|
|
108
|
+
const extra: TurnExtra = { interactive: true };
|
|
109
|
+
if (first) {
|
|
110
|
+
extra.attachments = attachments;
|
|
111
|
+
if (approvals.get(name)) extra.confirmed = true;
|
|
112
|
+
}
|
|
102
113
|
const result = await this.#host.turn(
|
|
103
114
|
member,
|
|
104
115
|
groupScope(member, group),
|
|
105
116
|
channel,
|
|
106
117
|
groupTurnText(group, member, backlog),
|
|
107
118
|
chain,
|
|
108
|
-
|
|
109
|
-
? {
|
|
110
|
-
attachments,
|
|
111
|
-
interactive: true,
|
|
112
|
-
...(approvals.get(name) ? { confirmed: true } : {}),
|
|
113
|
-
}
|
|
114
|
-
: { interactive: true },
|
|
119
|
+
extra,
|
|
115
120
|
);
|
|
116
121
|
replies += 1;
|
|
117
122
|
last = member;
|
|
@@ -54,17 +54,16 @@ export function layoutText(
|
|
|
54
54
|
layout: readonly CategoryLayout[],
|
|
55
55
|
store: TeamStore,
|
|
56
56
|
): string {
|
|
57
|
-
const lines = layout
|
|
58
|
-
|
|
59
|
-
.map(
|
|
60
|
-
|
|
61
|
-
(id)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
57
|
+
const lines = layout.flatMap((c) => {
|
|
58
|
+
if (!categoryKind(c.name)) return [];
|
|
59
|
+
const shown = c.channelIds.map(
|
|
60
|
+
(id) =>
|
|
61
|
+
store.agentByChannel(id)?.name ??
|
|
62
|
+
store.groupByChannel(id)?.name ??
|
|
63
|
+
`<#${id}>`,
|
|
64
|
+
);
|
|
65
|
+
return [`- ${c.name}: ${shown.join(", ") || "empty"}`];
|
|
66
|
+
});
|
|
68
67
|
return `Categories, in server order:\n${lines.join("\n") || "none"}`;
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -122,9 +121,9 @@ export function planArrangement(
|
|
|
122
121
|
channelIds: [...channelIds, ...(existing ? stay(existing) : [])],
|
|
123
122
|
};
|
|
124
123
|
});
|
|
125
|
-
const rest = current
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const rest = current.flatMap((c) =>
|
|
125
|
+
ahead.some((a) => a.id === c.id) ? [] : [{ ...c, channelIds: stay(c) }],
|
|
126
|
+
);
|
|
128
127
|
const layout = [...ahead, ...rest];
|
|
129
128
|
const remove = layout.flatMap((c) =>
|
|
130
129
|
c.id && c.channelIds.length === 0 && categoryKind(c.name) ? [c.id] : [],
|
|
@@ -61,12 +61,11 @@ export async function teamStatus(ctx: TeamContext): Promise<TeamStatus> {
|
|
|
61
61
|
schedules: home ? (await schedules.forChannel(home)).length : 0,
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
const groups: GroupStatus[] = store
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return {
|
|
64
|
+
const groups: GroupStatus[] = store.groups().flatMap((group) => {
|
|
65
|
+
if (group.status !== "active") return [];
|
|
66
|
+
const lastActive = turns.lastGroupActive(group.name);
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
70
69
|
name: group.name,
|
|
71
70
|
displayName: group.displayName,
|
|
72
71
|
channelId: group.channelId,
|
|
@@ -74,7 +73,8 @@ export async function teamStatus(ctx: TeamContext): Promise<TeamStatus> {
|
|
|
74
73
|
host: store.agent(group.host)?.displayName ?? group.host,
|
|
75
74
|
busy: queue.size(discordKey(group.channelId)),
|
|
76
75
|
...(lastActive ? { lastActive } : {}),
|
|
77
|
-
}
|
|
78
|
-
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
79
|
return { agents, groups };
|
|
80
80
|
}
|
|
@@ -9,7 +9,7 @@ import type { AgentTurnScope } from "../domain/ports.ts";
|
|
|
9
9
|
import { messages } from "../i18n/index.ts";
|
|
10
10
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
11
11
|
import { thinkingLine } from "../presentation/thinking-line.ts";
|
|
12
|
-
import { settleTurn } from "../routing/settle-turn.ts";
|
|
12
|
+
import { endOf, settleTurn } from "../routing/settle-turn.ts";
|
|
13
13
|
import type { Speaker } from "../speakers.ts";
|
|
14
14
|
import { toolTiers } from "../tool-tiers.ts";
|
|
15
15
|
import { AgentMessages } from "./agent-messages.ts";
|
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
|
|
32
32
|
export { MAX_CHAIN_MESSAGES, type TeamTurnsOptions };
|
|
33
33
|
|
|
34
|
+
/** What a failed turn posts: the stopped notice for a stop, the failure notice otherwise. */
|
|
35
|
+
function noticeOf(result: { stopped?: true }): string {
|
|
36
|
+
return result.stopped ? messages().stoppedNotice : messages().failureNotice;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* The agent team's turns: owner messages, background turns, group rounds, and messages between
|
|
36
41
|
* agents, each posted under the answering agent's name.
|
|
@@ -199,7 +204,7 @@ export class TeamTurns {
|
|
|
199
204
|
}
|
|
200
205
|
this.#options.events?.turnEnded({
|
|
201
206
|
...turn,
|
|
202
|
-
result: result
|
|
207
|
+
result: endOf(result),
|
|
203
208
|
});
|
|
204
209
|
try {
|
|
205
210
|
if (!result.ok && !result.stopped) {
|
|
@@ -214,13 +219,7 @@ export class TeamTurns {
|
|
|
214
219
|
: undefined;
|
|
215
220
|
await this.#post(postTo, this.#current(agent), {
|
|
216
221
|
...(thinking ? { thinking } : {}),
|
|
217
|
-
chunks: result.ok
|
|
218
|
-
? splitReply(result.text)
|
|
219
|
-
: [
|
|
220
|
-
result.stopped
|
|
221
|
-
? messages().stoppedNotice
|
|
222
|
-
: messages().failureNotice,
|
|
223
|
-
],
|
|
222
|
+
chunks: result.ok ? splitReply(result.text) : [noticeOf(result)],
|
|
224
223
|
});
|
|
225
224
|
} catch (error) {
|
|
226
225
|
logger.error({ agent: agent.name, err: error }, "agent reply not posted");
|
|
@@ -76,7 +76,24 @@ function judgeThrough(modelRuntime: ModelRuntime, ref: ModelRef): JudgeModel {
|
|
|
76
76
|
throw new JudgeError(
|
|
77
77
|
`the judge's model ${formatModelRef(ref)} is not available; log in to its provider or change judge.model`,
|
|
78
78
|
);
|
|
79
|
-
return
|
|
79
|
+
return piJudgeModel(modelRuntime, model)(system, prompt);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The listener `config.http` names: its unix socket when it has one, else its TCP port. */
|
|
84
|
+
function publicListener(
|
|
85
|
+
http: ReturnType<typeof resolveConfig>["http"],
|
|
86
|
+
): ListenerConfig {
|
|
87
|
+
if (http.socketPath)
|
|
88
|
+
return {
|
|
89
|
+
id: "public",
|
|
90
|
+
socketPath: http.socketPath,
|
|
91
|
+
...(http.socketMode === undefined ? {} : { mode: http.socketMode }),
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
id: "public",
|
|
95
|
+
port: http.port,
|
|
96
|
+
...(http.hostname ? { hostname: http.hostname } : {}),
|
|
80
97
|
};
|
|
81
98
|
}
|
|
82
99
|
|
|
@@ -138,24 +155,7 @@ export async function defineRoundtable(
|
|
|
138
155
|
},
|
|
139
156
|
database: { url: config.databaseUrl },
|
|
140
157
|
toolTiers: toolTiers(config.toolTiers),
|
|
141
|
-
listeners: [
|
|
142
|
-
config.http.socketPath
|
|
143
|
-
? {
|
|
144
|
-
id: "public",
|
|
145
|
-
socketPath: config.http.socketPath,
|
|
146
|
-
...(config.http.socketMode === undefined
|
|
147
|
-
? {}
|
|
148
|
-
: { mode: config.http.socketMode }),
|
|
149
|
-
}
|
|
150
|
-
: {
|
|
151
|
-
id: "public",
|
|
152
|
-
port: config.http.port,
|
|
153
|
-
...(config.http.hostname
|
|
154
|
-
? { hostname: config.http.hostname }
|
|
155
|
-
: {}),
|
|
156
|
-
},
|
|
157
|
-
...(overrides.listeners ?? []),
|
|
158
|
-
],
|
|
158
|
+
listeners: [publicListener(config.http), ...(overrides.listeners ?? [])],
|
|
159
159
|
judgeModel: judgeThrough(modelRuntime, config.judge.model),
|
|
160
160
|
apiKey: (provider) => registry.getApiKeyForProvider(provider),
|
|
161
161
|
...(overrides.aborted ? { aborted: overrides.aborted } : {}),
|
package/src/core/define.ts
CHANGED
|
@@ -120,20 +120,20 @@ export function defineTool<Schema extends TObject>(
|
|
|
120
120
|
}),
|
|
121
121
|
};
|
|
122
122
|
const hold = spec.hold;
|
|
123
|
-
|
|
123
|
+
const contribution = {
|
|
124
124
|
name,
|
|
125
125
|
minTier: spec.minTier,
|
|
126
126
|
agent: spec.agent ?? true,
|
|
127
127
|
session,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
};
|
|
129
|
+
if (!hold) return contribution;
|
|
130
|
+
return {
|
|
131
|
+
...contribution,
|
|
132
|
+
hold: {
|
|
133
|
+
name: `tool:${name}`,
|
|
134
|
+
describe: (tool, input) =>
|
|
135
|
+
tool === name ? hold(input as Static<Schema>) : undefined,
|
|
136
|
+
},
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -90,13 +90,12 @@ export function operationPermissions(
|
|
|
90
90
|
channel: OwnerChannel,
|
|
91
91
|
): bigint[] {
|
|
92
92
|
const needed = OPERATION_PERMISSIONS[operation];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: [...needed];
|
|
93
|
+
if (!channel.isThread()) return [...needed];
|
|
94
|
+
return needed.map((flag) =>
|
|
95
|
+
flag === PermissionFlagsBits.SendMessages
|
|
96
|
+
? PermissionFlagsBits.SendMessagesInThreads
|
|
97
|
+
: flag,
|
|
98
|
+
);
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
interface ChannelFile {
|
|
@@ -127,6 +126,16 @@ function serializeMessage(message: Message) {
|
|
|
127
126
|
};
|
|
128
127
|
}
|
|
129
128
|
|
|
129
|
+
function uploadOf(upload: ChannelFile) {
|
|
130
|
+
return {
|
|
131
|
+
attachment: Buffer.from(upload.dataBase64, "base64"),
|
|
132
|
+
name: upload.filename,
|
|
133
|
+
...(upload.description === undefined
|
|
134
|
+
? {}
|
|
135
|
+
: { description: upload.description }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
function messagePayload(
|
|
131
140
|
args: Record<string, unknown>,
|
|
132
141
|
): Pick<MessageCreateOptions, "content" | "allowedMentions" | "files"> {
|
|
@@ -134,17 +143,7 @@ function messagePayload(
|
|
|
134
143
|
return {
|
|
135
144
|
...(args.content !== undefined ? { content: String(args.content) } : {}),
|
|
136
145
|
allowedMentions: { parse: [] },
|
|
137
|
-
...(uploads
|
|
138
|
-
? {
|
|
139
|
-
files: uploads.map((upload) => ({
|
|
140
|
-
attachment: Buffer.from(upload.dataBase64, "base64"),
|
|
141
|
-
name: upload.filename,
|
|
142
|
-
...(upload.description === undefined
|
|
143
|
-
? {}
|
|
144
|
-
: { description: upload.description }),
|
|
145
|
-
})),
|
|
146
|
-
}
|
|
147
|
-
: {}),
|
|
146
|
+
...(uploads ? { files: uploads.map(uploadOf) } : {}),
|
|
148
147
|
};
|
|
149
148
|
}
|
|
150
149
|
|
|
@@ -393,6 +392,9 @@ export function discordChannelExecutor(
|
|
|
393
392
|
"INVALID_CHANNEL: a thread takes its parent channel's permissions",
|
|
394
393
|
);
|
|
395
394
|
return editPermissions(channel, tool, args);
|
|
395
|
+
default:
|
|
396
|
+
// A message operation, handled below.
|
|
397
|
+
break;
|
|
396
398
|
}
|
|
397
399
|
const message = await channel.messages.fetch(String(args.messageId));
|
|
398
400
|
if (tool === "discord_edit_message")
|
|
@@ -262,6 +262,12 @@ const DISCORD_EPOCH = 1420070400000;
|
|
|
262
262
|
const BASE64 =
|
|
263
263
|
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
264
264
|
|
|
265
|
+
/** The `=` padding a base64 string ends with, which carries no bytes. */
|
|
266
|
+
function paddingOf(data: string): number {
|
|
267
|
+
if (data.endsWith("==")) return 2;
|
|
268
|
+
return data.endsWith("=") ? 1 : 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
/** Checks and fills defaults into a tool's arguments; throws ChannelToolError otherwise. */
|
|
266
272
|
export function parseChannelTool(
|
|
267
273
|
tool: string,
|
|
@@ -307,9 +313,7 @@ export function parseChannelTool(
|
|
|
307
313
|
if (!BASE64.test(upload.dataBase64))
|
|
308
314
|
throw new ChannelToolError("INVALID_CHANNEL_BASE64");
|
|
309
315
|
const data = upload.dataBase64;
|
|
310
|
-
bytes +=
|
|
311
|
-
(data.length / 4) * 3 -
|
|
312
|
-
(data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
|
|
316
|
+
bytes += (data.length / 4) * 3 - paddingOf(data);
|
|
313
317
|
if (bytes > CHANNEL_UPLOAD_MAX_BYTES)
|
|
314
318
|
throw new ChannelToolError("INVALID_CHANNEL_UPLOAD_SIZE");
|
|
315
319
|
}
|
|
@@ -22,6 +22,16 @@ function attachmentRefs(
|
|
|
22
22
|
}));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function referenceOf(referenced: Message, mention: RegExp) {
|
|
26
|
+
return {
|
|
27
|
+
text: referenced.content.replace(mention, "").trim(),
|
|
28
|
+
attachments: attachmentRefs(referenced),
|
|
29
|
+
...(referenced.webhookId
|
|
30
|
+
? { webhookName: referenced.author.username }
|
|
31
|
+
: {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/**
|
|
26
36
|
* The facts of one Discord message, as the neutral `InboundMessage`; undefined for the bot's own
|
|
27
37
|
* messages and before the client is ready. A forward reports the channel it came from as a
|
|
@@ -32,6 +42,8 @@ export async function toInbound(
|
|
|
32
42
|
{ botId, applicationId }: BotIdentity,
|
|
33
43
|
): Promise<InboundMessage | undefined> {
|
|
34
44
|
if (!botId || message.author.id === botId) return undefined;
|
|
45
|
+
// botId is the bot's own Discord user id from the client, a snowflake, not message text.
|
|
46
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
|
|
35
47
|
const mention = new RegExp(`<@!?${botId}>`, "g");
|
|
36
48
|
const reference = message.reference;
|
|
37
49
|
// A forward also has a reference, to a message elsewhere; its copy is the snapshot.
|
|
@@ -84,16 +96,6 @@ export async function toInbound(
|
|
|
84
96
|
},
|
|
85
97
|
}
|
|
86
98
|
: {}),
|
|
87
|
-
...(referenced
|
|
88
|
-
? {
|
|
89
|
-
reference: {
|
|
90
|
-
text: referenced.content.replace(mention, "").trim(),
|
|
91
|
-
attachments: attachmentRefs(referenced),
|
|
92
|
-
...(referenced.webhookId
|
|
93
|
-
? { webhookName: referenced.author.username }
|
|
94
|
-
: {}),
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
: {}),
|
|
99
|
+
...(referenced ? { reference: referenceOf(referenced, mention) } : {}),
|
|
98
100
|
};
|
|
99
101
|
}
|
|
@@ -362,9 +362,9 @@ export class OwnerCards implements InteractionModule {
|
|
|
362
362
|
}
|
|
363
363
|
if (interaction.isStringSelectMenu() && action === "pick") {
|
|
364
364
|
const values = interaction.values;
|
|
365
|
-
const choices = question.options
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
const choices = question.options.flatMap((option, i) =>
|
|
366
|
+
values.includes(String(i)) ? [option.label] : [],
|
|
367
|
+
);
|
|
368
368
|
if (values.includes(OTHER)) {
|
|
369
369
|
card.picked = choices;
|
|
370
370
|
await interaction.showModal(answerModal(id));
|
|
@@ -64,18 +64,22 @@ export class OwnerThreads {
|
|
|
64
64
|
permissions.has(PermissionFlagsBits.ManageThreads))
|
|
65
65
|
);
|
|
66
66
|
};
|
|
67
|
-
return threads
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
return threads.flatMap((t) =>
|
|
68
|
+
sees(owner, t) && sees(bot, t)
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
id: t.id,
|
|
72
|
+
name: t.name,
|
|
73
|
+
parentId: t.parentId,
|
|
74
|
+
private: t.type === ChannelType.PrivateThread,
|
|
75
|
+
archived: t.archived,
|
|
76
|
+
locked: t.locked,
|
|
77
|
+
messageCount: t.messageCount,
|
|
78
|
+
createdAt: t.createdAt?.toISOString() ?? null,
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
: [],
|
|
82
|
+
);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
async create(args: Record<string, unknown>) {
|