pi-roundtable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DispatchThread,
|
|
3
|
+
DispatchThreads,
|
|
4
|
+
} from "../../discord/dispatch-threads.ts";
|
|
5
|
+
import type { ChannelKey } from "../../domain/conversation.ts";
|
|
6
|
+
import { DelegationError } from "../../domain/errors.ts";
|
|
7
|
+
import { messages } from "../../i18n/index.ts";
|
|
8
|
+
import type { Logger } from "../../log.ts";
|
|
9
|
+
import type { Tier } from "../../speakers.ts";
|
|
10
|
+
import { timeZone } from "../../time.ts";
|
|
11
|
+
|
|
12
|
+
export type DelegationMode = "owner" | "party";
|
|
13
|
+
|
|
14
|
+
export interface DelegationJob {
|
|
15
|
+
id: number;
|
|
16
|
+
/** Where the report comes back as a turn. */
|
|
17
|
+
channel: ChannelKey;
|
|
18
|
+
/** The channel of the owner's turn that started it, which hosts its thread; party jobs have none. */
|
|
19
|
+
origin?: ChannelKey;
|
|
20
|
+
mode: DelegationMode;
|
|
21
|
+
/** Who asked; a party report is answered as this person's turn. */
|
|
22
|
+
author: { id: string; name: string; tier?: Tier };
|
|
23
|
+
title: string;
|
|
24
|
+
task: string;
|
|
25
|
+
startedAt: Date;
|
|
26
|
+
/** Where its report is posted, once opened; absent where no thread could open. */
|
|
27
|
+
thread?: DispatchThread;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DelegationOutcome =
|
|
31
|
+
| { ok: true; report: string }
|
|
32
|
+
| { ok: false; error: string };
|
|
33
|
+
|
|
34
|
+
export interface DelegationWorker {
|
|
35
|
+
run(task: string, signal: AbortSignal): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DelegatorOptions {
|
|
39
|
+
worker: DelegationWorker;
|
|
40
|
+
/** Posts the finished job back in its channel; never rejects. */
|
|
41
|
+
deliver: (job: DelegationJob, outcome: DelegationOutcome) => Promise<void>;
|
|
42
|
+
/** Each job's thread in its origin; without it every job reports in its channel only. */
|
|
43
|
+
threads?: Pick<DispatchThreads, "open">;
|
|
44
|
+
logger: Logger;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
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
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Runs delegated tasks in the background and hands each result back to its channel. Jobs live
|
|
57
|
+
* in memory: a restart drops the running ones, and the channel never hears back from them.
|
|
58
|
+
*/
|
|
59
|
+
export class Delegator {
|
|
60
|
+
readonly #options: DelegatorOptions;
|
|
61
|
+
readonly #running = new Map<
|
|
62
|
+
number,
|
|
63
|
+
{ job: DelegationJob; done: Promise<void> }
|
|
64
|
+
>();
|
|
65
|
+
#nextId = 1;
|
|
66
|
+
|
|
67
|
+
constructor(options: DelegatorOptions) {
|
|
68
|
+
this.#options = options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Starts a job and returns at once; throws DelegationError on bad input or a full channel. */
|
|
72
|
+
start(
|
|
73
|
+
request: Omit<DelegationJob, "id" | "startedAt" | "thread">,
|
|
74
|
+
): DelegationJob {
|
|
75
|
+
const title = request.title.trim();
|
|
76
|
+
const task = request.task.trim();
|
|
77
|
+
if (!title || !task)
|
|
78
|
+
throw new DelegationError("title and task are required");
|
|
79
|
+
if (task.length > MAX_TASK_CHARS)
|
|
80
|
+
throw new DelegationError(
|
|
81
|
+
`the task is ${task.length} characters; keep it within ${MAX_TASK_CHARS}`,
|
|
82
|
+
);
|
|
83
|
+
const busy = [...this.#running.values()].filter(
|
|
84
|
+
({ job }) => job.channel === request.channel,
|
|
85
|
+
).length;
|
|
86
|
+
const limit = RUNNING_LIMIT[request.mode];
|
|
87
|
+
if (busy >= limit)
|
|
88
|
+
throw new DelegationError(
|
|
89
|
+
`this channel already has ${busy} delegated tasks running; wait for one to report back`,
|
|
90
|
+
);
|
|
91
|
+
const job: DelegationJob = {
|
|
92
|
+
...request,
|
|
93
|
+
title,
|
|
94
|
+
task,
|
|
95
|
+
id: this.#nextId++,
|
|
96
|
+
startedAt: new Date(),
|
|
97
|
+
};
|
|
98
|
+
const done = this.#run(job).finally(() => this.#running.delete(job.id));
|
|
99
|
+
this.#running.set(job.id, { job, done });
|
|
100
|
+
return job;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The channel of each job still running, one entry per job. */
|
|
104
|
+
runningChannels(): ChannelKey[] {
|
|
105
|
+
return [...this.#running.values()].map(({ job }) => job.channel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Resolves once every started job has reported. */
|
|
109
|
+
async idle(): Promise<void> {
|
|
110
|
+
await Promise.all([...this.#running.values()].map(({ done }) => done));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #run(job: DelegationJob): Promise<void> {
|
|
114
|
+
const {
|
|
115
|
+
worker,
|
|
116
|
+
deliver,
|
|
117
|
+
threads,
|
|
118
|
+
logger,
|
|
119
|
+
timeoutMs = 20 * 60_000,
|
|
120
|
+
} = this.#options;
|
|
121
|
+
logger.info(
|
|
122
|
+
{ job: job.id, channel: job.channel, mode: job.mode },
|
|
123
|
+
"delegated task started",
|
|
124
|
+
);
|
|
125
|
+
const thread = await threads?.open(
|
|
126
|
+
job.origin,
|
|
127
|
+
job.title,
|
|
128
|
+
messages().delegationTask(job.task),
|
|
129
|
+
);
|
|
130
|
+
if (thread) job.thread = thread;
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
133
|
+
let outcome: DelegationOutcome;
|
|
134
|
+
try {
|
|
135
|
+
outcome = {
|
|
136
|
+
ok: true,
|
|
137
|
+
report: await worker.run(job.task, controller.signal),
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
outcome = {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
|
+
};
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
logger.info(
|
|
148
|
+
{ job: job.id, ok: outcome.ok, ms: Date.now() - job.startedAt.getTime() },
|
|
149
|
+
"delegated task finished",
|
|
150
|
+
);
|
|
151
|
+
await thread?.close(
|
|
152
|
+
outcome.ok
|
|
153
|
+
? messages().delegationDone(outcome.report)
|
|
154
|
+
: messages().delegationFailed(outcome.error),
|
|
155
|
+
);
|
|
156
|
+
await deliver(job, outcome).catch((error: unknown) =>
|
|
157
|
+
logger.error({ job: job.id, err: error }, "delegated report not posted"),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** What the asking agent receives when its delegated task reports back. */
|
|
163
|
+
export function delegatedTurnText(
|
|
164
|
+
job: DelegationJob,
|
|
165
|
+
outcome: DelegationOutcome,
|
|
166
|
+
localTime: string,
|
|
167
|
+
): string {
|
|
168
|
+
const head = [
|
|
169
|
+
`## Delegated task #${job.id}: ${job.title}`,
|
|
170
|
+
...(job.thread
|
|
171
|
+
? [
|
|
172
|
+
`Its task and report are in the thread ${job.thread.mention}, now archived; link it when useful.`,
|
|
173
|
+
]
|
|
174
|
+
: []),
|
|
175
|
+
].join("\n");
|
|
176
|
+
if (!outcome.ok)
|
|
177
|
+
return [
|
|
178
|
+
head,
|
|
179
|
+
`The task handed to the worker model at ${localTime} ${messages().zoneTime(timeZone())} failed: ${outcome.error}. Nobody wrote a new message: tell ${job.author.name} it failed and what you can do instead.`,
|
|
180
|
+
].join("\n");
|
|
181
|
+
return [
|
|
182
|
+
head,
|
|
183
|
+
`The task handed to the worker model at ${localTime} ${messages().zoneTime(timeZone())} has reported back. Nobody wrote a new message: answer ${job.author.name} with it in your own words, keeping the source links.`,
|
|
184
|
+
"",
|
|
185
|
+
"### Task",
|
|
186
|
+
job.task,
|
|
187
|
+
"",
|
|
188
|
+
"### Report",
|
|
189
|
+
outcome.report,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
createAgentSession,
|
|
4
|
+
DefaultResourceLoader,
|
|
5
|
+
type ModelRuntime,
|
|
6
|
+
SessionManager,
|
|
7
|
+
SettingsManager,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
formatModelRef,
|
|
11
|
+
type ModelRef,
|
|
12
|
+
type ThinkingLevel,
|
|
13
|
+
} from "../../models.ts";
|
|
14
|
+
import { runWorkerTask } from "../../runtime/worker-task.ts";
|
|
15
|
+
import { packageDir } from "../../shared/package-dir.ts";
|
|
16
|
+
import { profileToolsExtension } from "../../shared/profile-tools.ts";
|
|
17
|
+
|
|
18
|
+
const WEB_TOOLS = ["web_search", "fetch_content", "get_search_content"];
|
|
19
|
+
|
|
20
|
+
const WORKER_PROMPT =
|
|
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
|
+
|
|
23
|
+
export interface SolWorkerOptions {
|
|
24
|
+
/** Shared with the rest of the host, so the Codex login refreshes in one place. */
|
|
25
|
+
modelRuntime: ModelRuntime;
|
|
26
|
+
agentDir: string;
|
|
27
|
+
workDir: string;
|
|
28
|
+
model: ModelRef;
|
|
29
|
+
thinking: ThinkingLevel;
|
|
30
|
+
}
|
|
31
|
+
|
|
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;
|
|
35
|
+
readonly #webAccessPath: string;
|
|
36
|
+
|
|
37
|
+
constructor(options: SolWorkerOptions) {
|
|
38
|
+
this.#options = options;
|
|
39
|
+
this.#webAccessPath = packageDir("pi-web-access");
|
|
40
|
+
mkdirSync(options.workDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async run(task: string, signal: AbortSignal): Promise<string> {
|
|
44
|
+
const { modelRuntime, agentDir, workDir, model, thinking } = this.#options;
|
|
45
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
46
|
+
cwd: workDir,
|
|
47
|
+
agentDir,
|
|
48
|
+
noExtensions: true,
|
|
49
|
+
noSkills: true,
|
|
50
|
+
noPromptTemplates: true,
|
|
51
|
+
noThemes: true,
|
|
52
|
+
noContextFiles: true,
|
|
53
|
+
additionalExtensionPaths: [this.#webAccessPath],
|
|
54
|
+
extensionFactories: [
|
|
55
|
+
{ name: "web-only", factory: profileToolsExtension(() => WEB_TOOLS) },
|
|
56
|
+
],
|
|
57
|
+
appendSystemPrompt: [WORKER_PROMPT],
|
|
58
|
+
});
|
|
59
|
+
await resourceLoader.reload();
|
|
60
|
+
const { session } = await createAgentSession({
|
|
61
|
+
cwd: workDir,
|
|
62
|
+
agentDir,
|
|
63
|
+
thinkingLevel: thinking,
|
|
64
|
+
modelRuntime,
|
|
65
|
+
resourceLoader,
|
|
66
|
+
sessionManager: SessionManager.inMemory(workDir),
|
|
67
|
+
settingsManager: SettingsManager.inMemory({}),
|
|
68
|
+
noTools: "builtin",
|
|
69
|
+
});
|
|
70
|
+
return runWorkerTask(session, {
|
|
71
|
+
modelRuntime,
|
|
72
|
+
model: formatModelRef(model),
|
|
73
|
+
task,
|
|
74
|
+
signal,
|
|
75
|
+
aborted: "the worker ran out of time",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type TObject, Type } from "typebox";
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_TOOLS,
|
|
5
|
+
ChannelToolError,
|
|
6
|
+
} from "../../discord/channel-operations.ts";
|
|
7
|
+
import { assistantName } from "../../i18n/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
type OwnerIdentity,
|
|
10
|
+
type OwnerWords,
|
|
11
|
+
ownerWords,
|
|
12
|
+
} from "../../identity.ts";
|
|
13
|
+
import { textToolsExtension } from "../../runtime/text-tools.ts";
|
|
14
|
+
|
|
15
|
+
const id = Type.String({ pattern: "^\\d{1,20}$" });
|
|
16
|
+
const guild = { guildId: id };
|
|
17
|
+
const member = { guildId: id, userId: id };
|
|
18
|
+
const reason = Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
maxLength: 400,
|
|
21
|
+
description: "Shown in the server's audit log.",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
const roleFields = {
|
|
25
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 100 })),
|
|
26
|
+
color: Type.Optional(
|
|
27
|
+
Type.String({ pattern: "^#[0-9a-fA-F]{6}$", description: "#rrggbb" }),
|
|
28
|
+
),
|
|
29
|
+
hoist: Type.Optional(
|
|
30
|
+
Type.Boolean({ description: "Show members separately in the list." }),
|
|
31
|
+
),
|
|
32
|
+
mentionable: Type.Optional(Type.Boolean()),
|
|
33
|
+
permissions: Type.Optional(
|
|
34
|
+
Type.Array(Type.String(), {
|
|
35
|
+
maxItems: 60,
|
|
36
|
+
description:
|
|
37
|
+
"Every permission the role grants, as discord.js PermissionFlagsBits names.",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
const strict = { additionalProperties: false } as const;
|
|
42
|
+
const autoArchive = Type.Union(
|
|
43
|
+
[60, 1440, 4320, 10080].map((v) => Type.Literal(v)),
|
|
44
|
+
{
|
|
45
|
+
description: "Minutes of inactivity before the thread archives.",
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
interface ServerTool {
|
|
50
|
+
label: string;
|
|
51
|
+
/** The tool's description, naming the owner in `o`'s words. */
|
|
52
|
+
describe(o: OwnerWords): string;
|
|
53
|
+
parameters: TObject;
|
|
54
|
+
}
|
|
55
|
+
|
|
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> = {
|
|
58
|
+
discord_list_servers: {
|
|
59
|
+
label: "List Discord servers",
|
|
60
|
+
describe: (o) =>
|
|
61
|
+
`List the Discord servers that ${o.name} and ${assistantName()} share, with the server permissions each of them holds there.`,
|
|
62
|
+
parameters: Type.Object({}, strict),
|
|
63
|
+
},
|
|
64
|
+
discord_list_channels: {
|
|
65
|
+
label: "List channels",
|
|
66
|
+
describe: (o) =>
|
|
67
|
+
`List a server's channels and categories that both ${o.name} and ${assistantName()} can see.`,
|
|
68
|
+
parameters: Type.Object(guild, strict),
|
|
69
|
+
},
|
|
70
|
+
discord_list_roles: {
|
|
71
|
+
label: "List roles",
|
|
72
|
+
describe: () =>
|
|
73
|
+
"List a server's roles, highest first, with their permissions.",
|
|
74
|
+
parameters: Type.Object(guild, strict),
|
|
75
|
+
},
|
|
76
|
+
discord_find_members: {
|
|
77
|
+
label: "Find members",
|
|
78
|
+
describe: () =>
|
|
79
|
+
"Find server members whose username or server nickname starts with the query.",
|
|
80
|
+
parameters: Type.Object(
|
|
81
|
+
{
|
|
82
|
+
...guild,
|
|
83
|
+
query: Type.String({ minLength: 1, maxLength: 100 }),
|
|
84
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 25 })),
|
|
85
|
+
},
|
|
86
|
+
strict,
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
discord_create_channel: {
|
|
90
|
+
label: "Create channel",
|
|
91
|
+
describe: () =>
|
|
92
|
+
"Create a text, voice, or announcement channel, or a category.",
|
|
93
|
+
parameters: Type.Object(
|
|
94
|
+
{
|
|
95
|
+
...guild,
|
|
96
|
+
name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
97
|
+
type: Type.Union(
|
|
98
|
+
["text", "voice", "announcement", "category"].map((v) =>
|
|
99
|
+
Type.Literal(v),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
parentId: Type.Optional(id),
|
|
103
|
+
topic: Type.Optional(Type.String({ maxLength: 1024 })),
|
|
104
|
+
reason,
|
|
105
|
+
},
|
|
106
|
+
strict,
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
discord_delete_channel: {
|
|
110
|
+
label: "Delete channel",
|
|
111
|
+
describe: () => "Delete a channel, category, or thread.",
|
|
112
|
+
parameters: Type.Object({ channelId: id, reason }, strict),
|
|
113
|
+
},
|
|
114
|
+
discord_list_threads: {
|
|
115
|
+
label: "List threads",
|
|
116
|
+
describe: (o) =>
|
|
117
|
+
`List a server's active threads that both ${o.name} and ${assistantName()} can see, optionally only those under one channel; with archived, list that channel's archived threads instead. The message tools take a thread's id as channelId.`,
|
|
118
|
+
parameters: Type.Object(
|
|
119
|
+
{
|
|
120
|
+
...guild,
|
|
121
|
+
parentId: Type.Optional(id),
|
|
122
|
+
archived: Type.Optional(
|
|
123
|
+
Type.Boolean({ description: "Needs parentId." }),
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
strict,
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
discord_create_thread: {
|
|
130
|
+
label: "Start thread",
|
|
131
|
+
describe: (o) =>
|
|
132
|
+
`Start a thread in a text or announcement channel, from one of its messages or on its own. A private thread (text channels only, not from a message) gets ${o.name} added. Post in it with discord_send_message using the thread's id.`,
|
|
133
|
+
parameters: Type.Object(
|
|
134
|
+
{
|
|
135
|
+
channelId: id,
|
|
136
|
+
name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
137
|
+
messageId: Type.Optional(id),
|
|
138
|
+
private: Type.Optional(Type.Boolean()),
|
|
139
|
+
autoArchiveMinutes: Type.Optional(autoArchive),
|
|
140
|
+
reason,
|
|
141
|
+
},
|
|
142
|
+
strict,
|
|
143
|
+
),
|
|
144
|
+
},
|
|
145
|
+
discord_edit_thread: {
|
|
146
|
+
label: "Edit thread",
|
|
147
|
+
describe: () =>
|
|
148
|
+
"Rename a thread, archive or unarchive it, lock or unlock it, or change when it auto-archives.",
|
|
149
|
+
parameters: Type.Object(
|
|
150
|
+
{
|
|
151
|
+
threadId: id,
|
|
152
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 100 })),
|
|
153
|
+
archived: Type.Optional(Type.Boolean()),
|
|
154
|
+
locked: Type.Optional(
|
|
155
|
+
Type.Boolean({
|
|
156
|
+
description: "Only members who can manage threads may post.",
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
autoArchiveMinutes: Type.Optional(autoArchive),
|
|
160
|
+
reason,
|
|
161
|
+
},
|
|
162
|
+
strict,
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
discord_create_role: {
|
|
166
|
+
label: "Create role",
|
|
167
|
+
describe: () => "Create a role.",
|
|
168
|
+
parameters: Type.Object(
|
|
169
|
+
{
|
|
170
|
+
...guild,
|
|
171
|
+
...roleFields,
|
|
172
|
+
name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
173
|
+
reason,
|
|
174
|
+
},
|
|
175
|
+
strict,
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
discord_edit_role: {
|
|
179
|
+
label: "Edit role",
|
|
180
|
+
describe: () =>
|
|
181
|
+
"Change a role's name, color, display, or permissions; permissions replaces the whole set.",
|
|
182
|
+
parameters: Type.Object(
|
|
183
|
+
{ ...guild, roleId: id, ...roleFields, reason },
|
|
184
|
+
strict,
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
discord_delete_role: {
|
|
188
|
+
label: "Delete role",
|
|
189
|
+
describe: () => "Delete a role.",
|
|
190
|
+
parameters: Type.Object({ ...guild, roleId: id, reason }, strict),
|
|
191
|
+
},
|
|
192
|
+
discord_add_member_role: {
|
|
193
|
+
label: "Give role",
|
|
194
|
+
describe: () => "Give a member a role.",
|
|
195
|
+
parameters: Type.Object({ ...member, roleId: id, reason }, strict),
|
|
196
|
+
},
|
|
197
|
+
discord_remove_member_role: {
|
|
198
|
+
label: "Take role",
|
|
199
|
+
describe: () => "Take a role from a member.",
|
|
200
|
+
parameters: Type.Object({ ...member, roleId: id, reason }, strict),
|
|
201
|
+
},
|
|
202
|
+
discord_set_nickname: {
|
|
203
|
+
label: "Set nickname",
|
|
204
|
+
describe: () =>
|
|
205
|
+
`Set a member's server nickname; an empty nickname clears it. ${assistantName()}'s own id changes its name in that server.`,
|
|
206
|
+
parameters: Type.Object(
|
|
207
|
+
{ ...member, nickname: Type.String({ maxLength: 32 }), reason },
|
|
208
|
+
strict,
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
discord_timeout_member: {
|
|
212
|
+
label: "Time out member",
|
|
213
|
+
describe: () =>
|
|
214
|
+
"Stop a member from talking for some minutes; 0 lifts a timeout.",
|
|
215
|
+
parameters: Type.Object(
|
|
216
|
+
{
|
|
217
|
+
...member,
|
|
218
|
+
minutes: Type.Integer({ minimum: 0, maximum: 40320 }),
|
|
219
|
+
reason,
|
|
220
|
+
},
|
|
221
|
+
strict,
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
discord_kick_member: {
|
|
225
|
+
label: "Kick member",
|
|
226
|
+
describe: () => "Remove a member from the server; they can join again.",
|
|
227
|
+
parameters: Type.Object({ ...member, reason }, strict),
|
|
228
|
+
},
|
|
229
|
+
discord_ban_member: {
|
|
230
|
+
label: "Ban member",
|
|
231
|
+
describe: () =>
|
|
232
|
+
"Ban a user from the server, optionally deleting their recent messages.",
|
|
233
|
+
parameters: Type.Object(
|
|
234
|
+
{
|
|
235
|
+
...member,
|
|
236
|
+
deleteMessageSeconds: Type.Optional(
|
|
237
|
+
Type.Integer({ minimum: 0, maximum: 604800 }),
|
|
238
|
+
),
|
|
239
|
+
reason,
|
|
240
|
+
},
|
|
241
|
+
strict,
|
|
242
|
+
),
|
|
243
|
+
},
|
|
244
|
+
discord_unban_member: {
|
|
245
|
+
label: "Unban member",
|
|
246
|
+
describe: () => "Lift a user's ban.",
|
|
247
|
+
parameters: Type.Object({ ...member, reason }, strict),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** Every Discord tool of the owner's `discord` profile. */
|
|
252
|
+
export const DISCORD_OWNER_TOOLS = [
|
|
253
|
+
...Object.keys(CHANNEL_TOOLS),
|
|
254
|
+
...Object.keys(SERVER_TOOLS),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
/** Runs one Discord tool for the owner; throws ChannelToolError for a refused call. */
|
|
258
|
+
export interface OwnerDiscord {
|
|
259
|
+
run(tool: string, args: Record<string, unknown>): Promise<unknown>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function discordAdminExtension(
|
|
263
|
+
discord: OwnerDiscord,
|
|
264
|
+
owner: OwnerIdentity,
|
|
265
|
+
): ExtensionFactory {
|
|
266
|
+
const o = ownerWords(owner);
|
|
267
|
+
const specs = [
|
|
268
|
+
...Object.entries(CHANNEL_TOOLS).map(([name, spec]) => ({
|
|
269
|
+
name,
|
|
270
|
+
label: name,
|
|
271
|
+
description: spec.description,
|
|
272
|
+
parameters: spec.schema,
|
|
273
|
+
})),
|
|
274
|
+
...Object.entries(SERVER_TOOLS).map(([name, spec]) => ({
|
|
275
|
+
name,
|
|
276
|
+
label: spec.label,
|
|
277
|
+
description: spec.describe(o),
|
|
278
|
+
parameters: spec.parameters,
|
|
279
|
+
})),
|
|
280
|
+
];
|
|
281
|
+
return textToolsExtension(
|
|
282
|
+
specs.map((spec) => ({
|
|
283
|
+
...spec,
|
|
284
|
+
run: async (input) => JSON.stringify(await discord.run(spec.name, input)),
|
|
285
|
+
})),
|
|
286
|
+
ChannelToolError,
|
|
287
|
+
);
|
|
288
|
+
}
|