pi-roundtable 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/README.md +10 -6
- package/README.zh-TW.md +144 -0
- package/docs/plugins.md +1448 -66
- package/examples/echo-runtime.test.ts +122 -0
- package/examples/echo-runtime.ts +107 -0
- package/examples/events.test.ts +1 -0
- package/examples/events.ts +2 -2
- package/examples/fake-surface.test.ts +180 -0
- package/examples/fake-surface.ts +109 -0
- package/examples/interactions.test.ts +12 -3
- package/examples/interactions.ts +19 -19
- package/examples/shared-services.test.ts +74 -0
- package/examples/shared-services.ts +60 -0
- package/examples/study-room.test.ts +69 -0
- package/examples/study-room.ts +52 -0
- package/examples/support-desk.test.ts +34 -0
- package/examples/support-desk.ts +40 -0
- package/package.json +11 -3
- package/src/cli/checks/database.ts +11 -30
- package/src/cli/checks/images.ts +19 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/main.ts +2 -1
- package/src/core/agents/agent-claim.ts +42 -16
- package/src/core/agents/agent-messages.ts +4 -4
- package/src/core/agents/agent-ports.ts +8 -21
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-store.ts +6 -4
- package/src/core/agents/agent-team-fixture.ts +389 -0
- package/src/core/agents/agent-team.ts +26 -25
- package/src/core/agents/agent-tools.ts +34 -15
- package/src/core/agents/avatar-studio.ts +40 -6
- package/src/core/agents/fallback-avatar.ts +85 -0
- package/src/core/agents/team-editing.ts +49 -10
- package/src/core/agents/team-keys.ts +22 -9
- package/src/core/agents/team-layout.ts +4 -3
- package/src/core/agents/team-lifecycle.ts +28 -2
- package/src/core/agents/team-options.ts +9 -5
- package/src/core/agents/team-status.ts +4 -4
- package/src/core/agents/team-text.ts +8 -6
- package/src/core/agents/team-turn-types.ts +4 -4
- package/src/core/agents/team-turns.ts +6 -5
- package/src/core/builtin/agent-server.ts +187 -121
- package/src/core/builtin/discord-admin.ts +41 -0
- package/src/core/builtin/discord.ts +65 -40
- package/src/core/builtin/modules.ts +67 -58
- package/src/core/builtin/session-tool.ts +24 -0
- package/src/core/builtin/skills.ts +72 -0
- package/src/core/builtin/stores.ts +55 -31
- package/src/core/config/config.ts +45 -4
- package/src/core/config/schema.ts +6 -0
- package/src/core/contract/channels.ts +54 -14
- package/src/core/contract/providers.ts +17 -0
- package/src/core/contract/runtime.ts +138 -0
- package/src/core/contract/services.ts +45 -0
- package/src/core/contract/surface.ts +88 -0
- package/src/core/db/migrations.ts +118 -3
- package/src/core/define-roundtable.ts +70 -46
- package/src/core/define.ts +2 -1
- package/src/core/discord/agent-commands.ts +39 -14
- package/src/core/discord/agent-panel.ts +66 -0
- package/src/core/discord/channel-executor.ts +6 -3
- package/src/core/discord/channel-operations.ts +14 -11
- package/src/core/discord/command-collection.ts +46 -0
- package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
- package/src/core/discord/connection.ts +35 -0
- package/src/core/discord/discord-surface.ts +32 -94
- package/src/core/discord/dispatch-threads.ts +2 -2
- package/src/core/discord/inbound-message.ts +99 -0
- package/src/core/discord/interaction-module.ts +57 -1
- package/src/core/discord/owner-cards.ts +4 -4
- package/src/core/discord/owner-command.ts +44 -11
- package/src/core/discord/owner-discord.ts +2 -2
- package/src/core/discord/schedule-commands.ts +21 -18
- package/src/core/discord/stop-button.ts +44 -1
- package/src/core/domain/attachment.ts +12 -8
- package/src/core/domain/conversation.ts +3 -4
- package/src/core/domain/owner-prompts.ts +5 -5
- package/src/core/domain/ports.ts +9 -53
- package/src/core/freeze.ts +9 -0
- package/src/core/host.ts +255 -82
- package/src/core/http/listeners.ts +55 -20
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/index.ts +12 -2
- package/src/core/i18n/owner.ts +7 -1
- package/src/core/i18n/schedules.ts +12 -6
- package/src/core/identity.ts +1 -1
- package/src/core/judging/effort-judge.ts +25 -3
- package/src/core/log.ts +21 -3
- package/src/core/models.ts +2 -2
- package/src/core/modules/background/background-turns.ts +6 -4
- package/src/core/modules/delegation/delegate.ts +3 -2
- package/src/core/modules/delegation/delegator.ts +15 -13
- package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
- package/src/core/modules/discord-admin/discord-admin.ts +6 -6
- package/src/core/modules/host-shell/shell-policy.ts +8 -3
- package/src/core/modules/memory/owner-memory-store.ts +27 -25
- package/src/core/modules/memory/owner-memory.ts +9 -13
- package/src/core/modules/schedules/recurrence.ts +5 -3
- package/src/core/modules/schedules/schedule-store.ts +11 -11
- package/src/core/modules/schedules/schedule-tools.ts +22 -20
- package/src/core/modules/schedules/scheduler.ts +7 -2
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +2 -15
- package/src/core/modules/skills/skill-tools.ts +10 -4
- package/src/core/ops/error-reporter.ts +1 -1
- package/src/core/plugin.ts +232 -28
- package/src/core/registry/contributions.ts +173 -14
- package/src/core/registry/providers.ts +35 -5
- package/src/core/registry/services.ts +229 -0
- package/src/core/routing/channel-queue.ts +5 -0
- package/src/core/routing/channel-router.ts +21 -7
- package/src/core/routing/conversation-turns.ts +139 -0
- package/src/core/routing/message-text.ts +9 -2
- package/src/core/routing/surface-port.ts +39 -0
- package/src/core/runtime/conversation-sessions.ts +4 -3
- package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
- package/src/core/runtime/mcp.ts +1 -1
- package/src/core/runtime/pending-confirmation-store.ts +10 -12
- package/src/core/runtime/pi-agent-runtime.ts +9 -6
- package/src/core/runtime/prompt-slot.ts +8 -3
- package/src/core/runtime/runtime-types.ts +9 -35
- package/src/core/runtime/session-factory.ts +21 -5
- package/src/core/runtime/text-tools.ts +1 -3
- package/src/core/services.ts +238 -90
- package/src/core/sessions.ts +3 -2
- package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
- package/src/core/shared/delegate-tool.ts +4 -3
- package/src/core/shared/schedule-tools.ts +22 -13
- package/src/core/shared/session-messages.ts +1 -1
- package/src/core/speakers.ts +6 -38
- package/src/core/testing/database.ts +1 -7
- package/src/core/testing/eager-catalog.ts +30 -0
- package/src/core/testing/locale.ts +23 -5
- package/src/core/testing/modules.ts +105 -41
- package/src/core/testing/test-host.ts +279 -0
- package/src/core/testing/tool-set.ts +64 -0
- package/src/core/time.ts +6 -1
- package/src/core/tool-set.snapshot.json +288 -0
- package/src/core/tool-tiers.ts +17 -57
- package/src/discord/index.ts +64 -0
- package/src/index.ts +181 -10
- package/src/kit/channels.ts +14 -0
- package/src/kit/domain.ts +18 -0
- package/src/kit/holds.ts +4 -0
- package/src/kit/index.ts +129 -0
- package/src/kit/judging.ts +10 -0
- package/src/kit/memory.ts +3 -0
- package/src/kit/mirror.ts +19 -0
- package/src/kit/presentation.ts +7 -0
- package/src/kit/shell.ts +7 -0
- package/src/kit/skills.ts +8 -0
- package/src/kit/support.ts +21 -0
- package/src/kit/threads.ts +7 -0
- package/src/kit/tools.ts +14 -0
- package/src/kit/worker.ts +16 -0
- package/src/testing.ts +394 -34
- package/examples/guide.test.ts +0 -52
- package/src/cli/add-plugin.test.ts +0 -107
- package/src/cli/checks/basic.test.ts +0 -276
- package/src/cli/checks/database.test.ts +0 -117
- package/src/cli/checks/discord.test.ts +0 -200
- package/src/cli/cli.test.ts +0 -122
- package/src/cli/config-edit.test.ts +0 -130
- package/src/cli/doctor.test.ts +0 -184
- package/src/cli/init.test.ts +0 -127
- package/src/cli/size.test.ts +0 -9
- package/src/cli/templates.test.ts +0 -129
- package/src/cli/testing/fixtures.ts +0 -106
- package/src/core/agents/agent-claim.test.ts +0 -142
- package/src/core/agents/agent-dashboard.test.ts +0 -120
- package/src/core/agents/agent-guild.test.ts +0 -124
- package/src/core/agents/agent-store.test.ts +0 -205
- package/src/core/agents/avatar-studio.test.ts +0 -88
- package/src/core/agents/group-round.test.ts +0 -143
- package/src/core/agents/owner-identity.test.ts +0 -172
- package/src/core/agents/team-turns.test.ts +0 -278
- package/src/core/attachments/attachments.test.ts +0 -85
- package/src/core/boundary.test.ts +0 -45
- package/src/core/builtin/modules.test.ts +0 -127
- package/src/core/config/config.test.ts +0 -135
- package/src/core/contract/discord.ts +0 -33
- package/src/core/db/migrations.test.ts +0 -237
- package/src/core/define-roundtable.test.ts +0 -134
- package/src/core/define.test.ts +0 -144
- package/src/core/discord/agent-commands.test.ts +0 -58
- package/src/core/discord/agent-discord.test.ts +0 -70
- package/src/core/discord/dispatch-thread-host.test.ts +0 -114
- package/src/core/discord/dispatch-threads.test.ts +0 -94
- package/src/core/discord/owner-cards.test.ts +0 -435
- package/src/core/discord/owner-discord.test.ts +0 -325
- package/src/core/domain/expression.ts +0 -21
- package/src/core/domain/profile.ts +0 -32
- package/src/core/drain.test.ts +0 -46
- package/src/core/events.test.ts +0 -81
- package/src/core/holds.test.ts +0 -54
- package/src/core/host.test.ts +0 -536
- package/src/core/http/listeners.test.ts +0 -164
- package/src/core/i18n/i18n.test.ts +0 -139
- package/src/core/identity.test.ts +0 -18
- package/src/core/judging/effort-judge.test.ts +0 -112
- package/src/core/judging/model-judge.test.ts +0 -126
- package/src/core/modules/delegation/delegator.test.ts +0 -170
- package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
- package/src/core/modules/schedules/schedule.test.ts +0 -358
- package/src/core/modules/skills/skill-kind.test.ts +0 -55
- package/src/core/modules/skills/skill-registry.test.ts +0 -376
- package/src/core/ops/error-reporter.test.ts +0 -283
- package/src/core/presentation/card-cadence.ts +0 -41
- package/src/core/presentation/presentation.test.ts +0 -166
- package/src/core/public-entry.test.ts +0 -59
- package/src/core/registry/contributions.test.ts +0 -215
- package/src/core/registry/interactions.test.ts +0 -87
- package/src/core/registry/providers.test.ts +0 -103
- package/src/core/routing/channel-queue.test.ts +0 -31
- package/src/core/routing/channel-router.test.ts +0 -326
- package/src/core/routing/conversation-kind.ts +0 -14
- package/src/core/routing/settle-turn.test.ts +0 -21
- package/src/core/runtime/compaction-tiers.test.ts +0 -227
- package/src/core/runtime/extensions/ask-user.test.ts +0 -114
- package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
- package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
- package/src/core/runtime/session-archive.test.ts +0 -20
- package/src/core/runtime/steerable-run.test.ts +0 -264
- package/src/core/runtime/text-tools.test.ts +0 -110
- package/src/core/runtime/turn-answer.test.ts +0 -72
- package/src/core/runtime/worker-task.test.ts +0 -82
- package/src/core/services.test.ts +0 -28
- package/src/core/sessions.test.ts +0 -85
- package/src/core/shared/unix-server.ts +0 -18
- package/src/core/size.test.ts +0 -9
- package/src/core/speakers.test.ts +0 -78
- package/src/core/testing/file-size.ts +0 -34
- package/src/core/time.test.ts +0 -166
- package/src/core/tool-tiers.test.ts +0 -117
- package/src/entries.test.ts +0 -112
- package/src/testing.test.ts +0 -156
package/src/core/host.ts
CHANGED
|
@@ -1,40 +1,63 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
2
|
import type { ConversationPort } from "./contract/channels.ts";
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
3
|
+
import type { SurfacePort } from "./contract/surface.ts";
|
|
4
|
+
import { openPool, runMigrations } from "./db/migrations.ts";
|
|
5
5
|
import { type DrainOptions, waitUntilIdle } from "./drain.ts";
|
|
6
6
|
import { MigrationError, NotLinkedError, PluginError } from "./errors.ts";
|
|
7
7
|
import { EventBus } from "./events.ts";
|
|
8
8
|
import { HttpListeners, type ListenerConfig } from "./http/listeners.ts";
|
|
9
|
+
import { type Locale, setLocale } from "./i18n/index.ts";
|
|
9
10
|
import type { JudgeModel } from "./judging/model-judge.ts";
|
|
10
11
|
import type { Logger } from "./log.ts";
|
|
11
12
|
import type {
|
|
12
|
-
|
|
13
|
+
HostEnv,
|
|
13
14
|
LinkedSessions,
|
|
14
15
|
RoundtablePlugin,
|
|
16
|
+
Service,
|
|
17
|
+
ServiceStartedEvent,
|
|
15
18
|
} from "./plugin.ts";
|
|
19
|
+
import { refuseRemovedFields } from "./plugin.ts";
|
|
16
20
|
import {
|
|
17
21
|
collectContributions,
|
|
18
22
|
emptyRegistry,
|
|
19
23
|
linkSessions,
|
|
20
24
|
type Registry,
|
|
21
25
|
} from "./registry/contributions.ts";
|
|
22
|
-
import { composeInteractions } from "./registry/interactions.ts";
|
|
23
26
|
import { resolveProviders } from "./registry/providers.ts";
|
|
27
|
+
import { replaceServices, ServiceRegistry } from "./registry/services.ts";
|
|
24
28
|
import { ChannelQueue } from "./routing/channel-queue.ts";
|
|
25
29
|
import { ChannelRouter } from "./routing/channel-router.ts";
|
|
26
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type ConversationTurns,
|
|
32
|
+
conversationTurns,
|
|
33
|
+
} from "./routing/conversation-turns.ts";
|
|
34
|
+
import { surfacePort } from "./routing/surface-port.ts";
|
|
35
|
+
import { AGENTS } from "./services.ts";
|
|
36
|
+
import { setTimeZone } from "./time.ts";
|
|
27
37
|
import { type ToolTierTable, toolTiers } from "./tool-tiers.ts";
|
|
28
38
|
|
|
39
|
+
/**
|
|
40
|
+
* What differs between hosts: the words and time the process speaks in, and the Pi agent
|
|
41
|
+
* directory its packages read. `run()` applies them to the process when the host starts, and
|
|
42
|
+
* plugins read the locale and zone from `PluginContext.env`.
|
|
43
|
+
*/
|
|
44
|
+
export interface HostEnvironment {
|
|
45
|
+
/** The language of the Discord text; default en. */
|
|
46
|
+
locale?: Locale;
|
|
47
|
+
/** An IANA time zone; default UTC. */
|
|
48
|
+
timeZone?: string;
|
|
49
|
+
/** The assistant's display name in the text; default Roundtable. */
|
|
50
|
+
assistant?: string;
|
|
51
|
+
/** The name of the root slash command in the text, without the slash; default roundtable. */
|
|
52
|
+
rootCommand?: string;
|
|
53
|
+
/** Exported as `PI_CODING_AGENT_DIR` for Pi packages such as pi-web-access; unset leaves the process's own. */
|
|
54
|
+
agentDir?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
export interface RoundtableOptions {
|
|
30
58
|
logger: Logger;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
* `useCommands` before any service starts, since Discord registers them as it connects.
|
|
34
|
-
*/
|
|
35
|
-
commands?: {
|
|
36
|
-
root: CommandRoot;
|
|
37
|
-
};
|
|
59
|
+
/** The host's locale, time zone and names. Nothing is applied before `run()`. */
|
|
60
|
+
environment?: HostEnvironment;
|
|
38
61
|
/** The HTTP listeners plugins attach routes to. */
|
|
39
62
|
listeners?: readonly ListenerConfig[];
|
|
40
63
|
/** How long a bare forward waits for the message it follows. */
|
|
@@ -51,9 +74,13 @@ export interface RoundtableOptions {
|
|
|
51
74
|
aborted?: (left: string[]) => Promise<void>;
|
|
52
75
|
/** The drain's limit and clock; tests shorten them. */
|
|
53
76
|
drain?: Omit<DrainOptions, "busy">;
|
|
77
|
+
/** What `listen()` calls with the shutdown's exit code; the process's own exit by default. */
|
|
54
78
|
exit?: (code: number) => void;
|
|
55
79
|
}
|
|
56
80
|
|
|
81
|
+
/** The host running in this process; the text catalog, time zone and environment are process-wide. */
|
|
82
|
+
let running: Roundtable | undefined;
|
|
83
|
+
|
|
57
84
|
type Attempt = { ok: true } | { ok: false; error: unknown };
|
|
58
85
|
|
|
59
86
|
/** Runs one step whose failure the caller reports and moves past. */
|
|
@@ -66,31 +93,48 @@ async function attempt(step: () => Promise<void> | void): Promise<Attempt> {
|
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Runs every service's background start at once and tells every plugin, as each ends, how it
|
|
98
|
+
* went; the rest of the process runs either way, and a failing start or handler never stops the
|
|
99
|
+
* others.
|
|
100
|
+
*/
|
|
101
|
+
async function startInBackground(
|
|
102
|
+
registry: Registry,
|
|
73
103
|
logger: Logger,
|
|
74
104
|
): Promise<void> {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
const { services, servicePlugins, handlers } = registry;
|
|
106
|
+
await Promise.all(
|
|
107
|
+
services.map(async (service) => {
|
|
108
|
+
if (!service.startInBackground) return;
|
|
109
|
+
const plugin = servicePlugins.get(service) ?? "unknown";
|
|
110
|
+
const started = await attempt(() => service.startInBackground?.());
|
|
111
|
+
if (started.ok) logger.info({ plugin, service: service.name }, "ready");
|
|
112
|
+
else
|
|
113
|
+
logger.error(
|
|
114
|
+
{ plugin, service: service.name, err: started.error },
|
|
115
|
+
"service did not start in the background",
|
|
116
|
+
);
|
|
117
|
+
const event: ServiceStartedEvent = {
|
|
118
|
+
plugin,
|
|
119
|
+
service: service.name,
|
|
120
|
+
outcome: started.ok ? "ready" : "failed",
|
|
121
|
+
};
|
|
122
|
+
for (const { plugin: heard, events } of handlers) {
|
|
123
|
+
const handled = await attempt(() => events.serviceStarted?.(event));
|
|
124
|
+
if (!handled.ok)
|
|
125
|
+
logger.error(
|
|
126
|
+
{ plugin: heard, err: handled.error },
|
|
127
|
+
"service started handler failed",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
88
132
|
}
|
|
89
133
|
|
|
90
134
|
/**
|
|
91
135
|
* The process around the plugins: it sets them all up, links what they add (session parts,
|
|
92
|
-
*
|
|
93
|
-
* HTTP listeners last,
|
|
136
|
+
* HTTP routes) and runs the preflight, then starts their services in order and the
|
|
137
|
+
* HTTP listeners last, then runs the services' background starts. Nothing reaches Discord or a listener unless
|
|
94
138
|
* every setup, link, and the preflight succeeded. On shutdown it waits until no work runs or
|
|
95
139
|
* waits before closing the listeners and stopping the services in reverse.
|
|
96
140
|
*/
|
|
@@ -106,12 +150,23 @@ export class Roundtable {
|
|
|
106
150
|
readonly #queue = new ChannelQueue();
|
|
107
151
|
readonly #tiers: ToolTierTable;
|
|
108
152
|
readonly #events: EventBus;
|
|
109
|
-
|
|
153
|
+
#services: ServiceRegistry | undefined;
|
|
154
|
+
/** The plugins that run: the registered ones, less those a replacement dropped, in set-up order. */
|
|
155
|
+
#active: readonly RoundtablePlugin[] = [];
|
|
156
|
+
/** The services whose start finished, in start order. */
|
|
157
|
+
#started: Service[] = [];
|
|
158
|
+
#booting: Promise<void> | undefined;
|
|
159
|
+
#bootFailed = false;
|
|
160
|
+
#stopping: Promise<number> | undefined;
|
|
110
161
|
|
|
111
162
|
constructor(
|
|
112
163
|
options: RoundtableOptions,
|
|
113
164
|
plugins: readonly RoundtablePlugin[],
|
|
114
165
|
) {
|
|
166
|
+
if ("commands" in options)
|
|
167
|
+
throw new PluginError(
|
|
168
|
+
"RoundtableOptions.commands was removed in 0.2.0; the host composes no commands. The Discord plugin composes them under the root command named by config discord.rootCommand, and a plugin adds its own with context.services.get(DISCORD).commands.add(...), with DISCORD from pi-roundtable/discord.",
|
|
169
|
+
);
|
|
115
170
|
this.#options = options;
|
|
116
171
|
this.#plugins = plugins;
|
|
117
172
|
this.#tiers = options.toolTiers ?? toolTiers();
|
|
@@ -130,6 +185,7 @@ export class Roundtable {
|
|
|
130
185
|
return {
|
|
131
186
|
handle: (message) => router().handle(message),
|
|
132
187
|
background: (turn) => router().background(turn),
|
|
188
|
+
target: (name) => router().target(name),
|
|
133
189
|
startFresh: (channel) => router().startFresh(channel),
|
|
134
190
|
deleteConversation: (channel) => router().deleteConversation(channel),
|
|
135
191
|
stop: (channel) => router().stop(channel),
|
|
@@ -137,19 +193,98 @@ export class Roundtable {
|
|
|
137
193
|
};
|
|
138
194
|
}
|
|
139
195
|
|
|
196
|
+
/** The contributed surfaces by channel prefix; every call before linking throws NotLinkedError. */
|
|
197
|
+
#surfaces(): SurfacePort {
|
|
198
|
+
return surfacePort(() => {
|
|
199
|
+
if (!this.#sessions)
|
|
200
|
+
throw new NotLinkedError(
|
|
201
|
+
"chat surfaces are linked once every plugin is set up. Use surfaces from a service's start or from a handler, not during setup.",
|
|
202
|
+
);
|
|
203
|
+
return this.#registry.surfaces;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Turns over the agent server's runtime and the surfaces; every call before linking is refused with NotLinkedError. */
|
|
208
|
+
#turns(): ConversationTurns {
|
|
209
|
+
return conversationTurns({
|
|
210
|
+
linked: () => {
|
|
211
|
+
if (!this.#sessions)
|
|
212
|
+
throw new NotLinkedError(
|
|
213
|
+
"conversation turns are linked once every plugin is set up. Use turns from a service's start or from a handler, not during setup.",
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
runtime: () => {
|
|
217
|
+
if (!this.#services)
|
|
218
|
+
throw new NotLinkedError("the host has not started yet.");
|
|
219
|
+
return this.#services.get(AGENTS).runtime;
|
|
220
|
+
},
|
|
221
|
+
surfaces: this.#surfaces(),
|
|
222
|
+
events: this.#events.sink,
|
|
223
|
+
selection: () => {
|
|
224
|
+
if (!this.#sessions)
|
|
225
|
+
throw new NotLinkedError("session parts are not linked yet.");
|
|
226
|
+
return this.#sessions.agentSelection();
|
|
227
|
+
},
|
|
228
|
+
logger: this.#options.logger,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
140
232
|
/**
|
|
141
233
|
* Sets up every plugin, links what they add and runs the preflight, refusing any clash before
|
|
142
234
|
* anything starts, then starts the services and opens the listeners; the agent server starts
|
|
143
|
-
* in the background.
|
|
235
|
+
* in the background. One host runs per process: a second `run()` is refused until the first
|
|
236
|
+
* host has stopped. A start that fails stops what it started, in reverse, closes the pool,
|
|
237
|
+
* and rethrows, so the same host may try again.
|
|
144
238
|
*/
|
|
145
239
|
async run(): Promise<void> {
|
|
146
|
-
|
|
147
|
-
|
|
240
|
+
if (running)
|
|
241
|
+
throw new PluginError(
|
|
242
|
+
"a Roundtable host is already running in this process. Stop the first host, or run this one in a separate process.",
|
|
243
|
+
);
|
|
244
|
+
running = this;
|
|
245
|
+
this.#stopping = undefined;
|
|
246
|
+
this.#bootFailed = false;
|
|
247
|
+
this.#booting = this.#boot();
|
|
248
|
+
try {
|
|
249
|
+
await this.#booting;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.#bootFailed = true;
|
|
252
|
+
await this.#teardown();
|
|
253
|
+
running = undefined;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Applies the host's environment to the process and returns what plugins read of it. */
|
|
259
|
+
#applyEnvironment(): HostEnv {
|
|
260
|
+
const {
|
|
261
|
+
locale = "en",
|
|
262
|
+
timeZone = "UTC",
|
|
263
|
+
assistant = "Roundtable",
|
|
264
|
+
rootCommand = "roundtable",
|
|
265
|
+
agentDir,
|
|
266
|
+
} = this.#options.environment ?? {};
|
|
267
|
+
setLocale(locale, { assistant, root: rootCommand });
|
|
268
|
+
setTimeZone(timeZone);
|
|
269
|
+
// Pi packages such as pi-web-access read their config from the Pi agent directory.
|
|
270
|
+
if (agentDir !== undefined) process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
271
|
+
return { locale, timeZone, now: () => new Date() };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async #boot(): Promise<void> {
|
|
275
|
+
const { logger, listeners = [] } = this.#options;
|
|
276
|
+
for (const plugin of this.#plugins) refuseRemovedFields(plugin);
|
|
277
|
+
const env = this.#applyEnvironment();
|
|
278
|
+
// A plugin that replaces a service takes the place of the one that provided it.
|
|
279
|
+
this.#active = replaceServices(this.#plugins);
|
|
280
|
+
this.#services = new ServiceRegistry(this.#active);
|
|
281
|
+
const providers = resolveProviders(this.#active, this.#options.judgeModel);
|
|
148
282
|
await this.#migrate();
|
|
149
283
|
this.#registry = await collectContributions(
|
|
150
|
-
this.#
|
|
284
|
+
this.#active,
|
|
151
285
|
{
|
|
152
286
|
logger,
|
|
287
|
+
env,
|
|
153
288
|
sessions: () => {
|
|
154
289
|
if (!this.#sessions)
|
|
155
290
|
throw new NotLinkedError(
|
|
@@ -161,12 +296,13 @@ export class Roundtable {
|
|
|
161
296
|
toolTiers: this.#tiers,
|
|
162
297
|
events: this.#events.sink,
|
|
163
298
|
conversations: this.#conversations(),
|
|
299
|
+
surfaces: this.#surfaces(),
|
|
300
|
+
turns: this.#turns(),
|
|
164
301
|
database: () => {
|
|
165
302
|
if (!this.#pool) throw new PluginError("no database is configured");
|
|
166
303
|
return this.#pool;
|
|
167
304
|
},
|
|
168
305
|
providers,
|
|
169
|
-
core: this.#core,
|
|
170
306
|
dashboard: () => {
|
|
171
307
|
if (!this.#sessions)
|
|
172
308
|
throw new NotLinkedError(
|
|
@@ -176,53 +312,37 @@ export class Roundtable {
|
|
|
176
312
|
},
|
|
177
313
|
},
|
|
178
314
|
this.#tiers,
|
|
315
|
+
this.#services,
|
|
179
316
|
);
|
|
180
|
-
const {
|
|
317
|
+
const { routes, channels } = this.#registry;
|
|
181
318
|
this.#sessions = linkSessions(this.#registry);
|
|
182
319
|
this.#events.link(this.#registry.handlers);
|
|
183
320
|
this.#router = new ChannelRouter({
|
|
184
321
|
claims: channels,
|
|
322
|
+
targets: (name) =>
|
|
323
|
+
this.#registry.backgroundTargets.find((t) => t.name === name),
|
|
185
324
|
queue: this.#queue,
|
|
186
|
-
stop: (channel) =>
|
|
187
|
-
this.#plugins.some((plugin) => plugin.stopTurn?.(channel) ?? false),
|
|
188
325
|
logger,
|
|
189
326
|
...(this.#options.conversations?.forwardJoinMs === undefined
|
|
190
327
|
? {}
|
|
191
328
|
: { forwardJoinMs: this.#options.conversations.forwardJoinMs }),
|
|
192
329
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const http = new HttpListeners(listeners, routes);
|
|
200
|
-
for (const plugin of this.#plugins) await plugin.preflight?.();
|
|
201
|
-
if (composed)
|
|
202
|
-
for (const plugin of this.#plugins) plugin.useCommands?.(composed);
|
|
203
|
-
for (const service of this.#registry.services) await service.start?.();
|
|
330
|
+
const http = new HttpListeners(listeners, routes, logger);
|
|
331
|
+
for (const plugin of this.#active) await plugin.preflight?.();
|
|
332
|
+
for (const service of this.#registry.services) {
|
|
333
|
+
await service.start?.();
|
|
334
|
+
this.#started.push(service);
|
|
335
|
+
}
|
|
204
336
|
// Requests arrive only once everything they may reach is running.
|
|
205
|
-
http.start();
|
|
206
337
|
this.#listeners = http;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** The one plugin's way to start the agent server; two plugins that start it are refused. */
|
|
213
|
-
#agentServer(): (() => Promise<void>) | undefined {
|
|
214
|
-
const starting = this.#plugins.filter((plugin) => plugin.agentServer);
|
|
215
|
-
const [first, second] = starting;
|
|
216
|
-
if (second)
|
|
217
|
-
throw new PluginError(
|
|
218
|
-
`plugins ${first?.name} and ${second.name} both start the agent server. Keep one.`,
|
|
219
|
-
);
|
|
220
|
-
return first?.agentServer?.bind(first);
|
|
338
|
+
http.start();
|
|
339
|
+
// After the listeners, so a background start may rely on everything else running.
|
|
340
|
+
void startInBackground(this.#registry, logger);
|
|
221
341
|
}
|
|
222
342
|
|
|
223
343
|
/** Opens the pool and runs every plugin's migrations; a failure closes it again and stops the boot. */
|
|
224
344
|
async #migrate(): Promise<void> {
|
|
225
|
-
const migrations = this.#
|
|
345
|
+
const migrations = this.#active.flatMap(
|
|
226
346
|
(plugin) => plugin.migrations ?? [],
|
|
227
347
|
);
|
|
228
348
|
const { database } = this.#options;
|
|
@@ -233,12 +353,22 @@ export class Roundtable {
|
|
|
233
353
|
}
|
|
234
354
|
const pool = openPool(database.url);
|
|
235
355
|
try {
|
|
236
|
-
await
|
|
356
|
+
const report = await runMigrations(pool, this.#active);
|
|
357
|
+
this.#options.logger.info(
|
|
358
|
+
{
|
|
359
|
+
applied: report.applied,
|
|
360
|
+
skipped: report.skipped.length,
|
|
361
|
+
everyBoot: report.everyBoot.length,
|
|
362
|
+
},
|
|
363
|
+
"migrations",
|
|
364
|
+
);
|
|
237
365
|
} catch (error) {
|
|
238
366
|
await pool.close();
|
|
239
367
|
if (error instanceof MigrationError) {
|
|
240
|
-
const owner = this.#
|
|
241
|
-
plugin.migrations?.some(
|
|
368
|
+
const owner = this.#active.find((plugin) =>
|
|
369
|
+
plugin.migrations?.some(
|
|
370
|
+
({ name }) => `${plugin.name}/${name}` === error.migration,
|
|
371
|
+
),
|
|
242
372
|
);
|
|
243
373
|
throw new PluginError(
|
|
244
374
|
`plugin ${owner?.name ?? "unknown"}: migration ${error.migration} failed: ${String(error.cause)}. Fix the migration or restore the database, then start again.`,
|
|
@@ -250,20 +380,41 @@ export class Roundtable {
|
|
|
250
380
|
this.#pool = pool;
|
|
251
381
|
}
|
|
252
382
|
|
|
253
|
-
/** Shuts down once idle when the process is asked to stop. */
|
|
383
|
+
/** Shuts down once idle when the process is asked to stop, then exits with the shutdown's code. */
|
|
254
384
|
listen(): void {
|
|
255
|
-
process.
|
|
256
|
-
|
|
385
|
+
const { exit = process.exit } = this.#options;
|
|
386
|
+
let signalled = false;
|
|
387
|
+
for (const signal of ["SIGTERM", "SIGINT"] as const)
|
|
388
|
+
process.once(signal, () => {
|
|
389
|
+
if (signalled) return;
|
|
390
|
+
signalled = true;
|
|
391
|
+
void this.shutdown(signal).then(exit);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Stops the host once no work runs or waits and returns the exit code: 0, or 1 when the
|
|
397
|
+
* boot had failed or a listener, service or the pool did not stop. Every call shares the one
|
|
398
|
+
* shutdown.
|
|
399
|
+
*/
|
|
400
|
+
shutdown(signal: string): Promise<number> {
|
|
401
|
+
this.#stopping ??= this.#stop(signal);
|
|
402
|
+
return this.#stopping;
|
|
257
403
|
}
|
|
258
404
|
|
|
259
|
-
async
|
|
260
|
-
const { logger, aborted, drain
|
|
261
|
-
|
|
405
|
+
async #stop(signal: string): Promise<number> {
|
|
406
|
+
const { logger, aborted, drain } = this.#options;
|
|
407
|
+
// A signal during the boot waits for it to settle; a failed boot has already stopped.
|
|
408
|
+
await this.#booting?.catch(() => undefined);
|
|
409
|
+
if (running !== this) return this.#bootFailed ? 1 : 0;
|
|
262
410
|
// Everything keeps serving until nothing runs or waits, so a deploy never cuts a turn short.
|
|
263
411
|
logger.info({ signal }, "shutting down once idle");
|
|
264
412
|
const left = await waitUntilIdle({
|
|
265
413
|
...drain,
|
|
266
|
-
busy: () =>
|
|
414
|
+
busy: () => [
|
|
415
|
+
...this.#queue.busy(),
|
|
416
|
+
...this.#started.flatMap((service) => service.busy?.() ?? []),
|
|
417
|
+
],
|
|
267
418
|
});
|
|
268
419
|
if (left.length > 0) {
|
|
269
420
|
logger.warn(
|
|
@@ -276,22 +427,44 @@ export class Roundtable {
|
|
|
276
427
|
}
|
|
277
428
|
await this.#events.deliver("shutdown", left);
|
|
278
429
|
logger.info({ signal }, "shutting down");
|
|
430
|
+
const clean = await this.#teardown();
|
|
431
|
+
running = undefined;
|
|
432
|
+
return clean ? 0 : 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Stops what a start or a run left going, in reverse of how it started: the listeners, then
|
|
437
|
+
* the started services, then the pool. Each failure is logged and the rest still stop;
|
|
438
|
+
* returns whether all of them did.
|
|
439
|
+
*/
|
|
440
|
+
async #teardown(): Promise<boolean> {
|
|
441
|
+
const { logger } = this.#options;
|
|
442
|
+
let clean = true;
|
|
279
443
|
// No request may reach a service that has stopped.
|
|
280
444
|
const closed = await attempt(() => this.#listeners?.stop());
|
|
281
|
-
if (!closed.ok)
|
|
445
|
+
if (!closed.ok) {
|
|
446
|
+
clean = false;
|
|
282
447
|
logger.error({ err: closed.error }, "http listeners did not close");
|
|
283
|
-
|
|
448
|
+
}
|
|
449
|
+
for (const service of this.#started.toReversed()) {
|
|
284
450
|
const stopped = await attempt(() => service.stop?.());
|
|
285
|
-
if (!stopped.ok)
|
|
451
|
+
if (!stopped.ok) {
|
|
452
|
+
clean = false;
|
|
286
453
|
logger.error(
|
|
287
454
|
{ service: service.name, err: stopped.error },
|
|
288
455
|
"service did not stop",
|
|
289
456
|
);
|
|
457
|
+
}
|
|
290
458
|
}
|
|
291
459
|
// Last, once nothing that queries it runs.
|
|
292
460
|
const released = await attempt(() => this.#pool?.close());
|
|
293
|
-
if (!released.ok)
|
|
461
|
+
if (!released.ok) {
|
|
462
|
+
clean = false;
|
|
294
463
|
logger.error({ err: released.error }, "database pool did not close");
|
|
295
|
-
|
|
464
|
+
}
|
|
465
|
+
this.#listeners = undefined;
|
|
466
|
+
this.#started = [];
|
|
467
|
+
this.#pool = undefined;
|
|
468
|
+
return clean;
|
|
296
469
|
}
|
|
297
470
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, rmSync } from "node:fs";
|
|
2
2
|
import type { Server } from "bun";
|
|
3
3
|
import { PluginError } from "../errors.ts";
|
|
4
|
+
import type { Logger } from "../log.ts";
|
|
4
5
|
|
|
5
6
|
/** A handler a plugin attaches to one of the host's configured listeners. */
|
|
6
7
|
export interface HttpRoute {
|
|
@@ -15,7 +16,11 @@ export interface HttpRoute {
|
|
|
15
16
|
|
|
16
17
|
/** Where a listener serves: a unix socket, reached from outside through a tunnel, or a TCP port. */
|
|
17
18
|
export type ListenerAddress =
|
|
18
|
-
| {
|
|
19
|
+
| {
|
|
20
|
+
socketPath: string;
|
|
21
|
+
/** The socket file's permission bits; default `0o660`, so only its owner and group connect. */
|
|
22
|
+
mode?: number;
|
|
23
|
+
}
|
|
19
24
|
| { port: number; hostname?: string };
|
|
20
25
|
|
|
21
26
|
/** An address the host serves HTTP on, with the id routes name it by. */
|
|
@@ -62,34 +67,52 @@ function validate(
|
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const serverError = () =>
|
|
71
|
+
new Response("Internal Server Error", { status: 500 });
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Routes one listener's request; a request no route takes is 404, and a route that throws, or
|
|
75
|
+
* whose promise rejects, is 500 with a fixed body. The failure is logged with the route's name
|
|
76
|
+
* and listener, never the URL, since paths may hold tokens.
|
|
77
|
+
*/
|
|
78
|
+
export async function routeRequest(
|
|
67
79
|
routes: readonly HttpRoute[],
|
|
68
80
|
request: Request,
|
|
69
|
-
|
|
81
|
+
listener: string,
|
|
82
|
+
logger: Logger,
|
|
83
|
+
): Promise<Response> {
|
|
70
84
|
// pi-lens-ignore: unchecked-throwing-call -- the server builds request.url, always an absolute URL
|
|
71
85
|
const path = new URL(request.url).pathname;
|
|
72
86
|
const route = routes.find(
|
|
73
87
|
(r) =>
|
|
74
88
|
matches(r, path) && (!r.methods || r.methods.includes(request.method)),
|
|
75
89
|
);
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
if (!route) return new Response("Not found", { status: 404 });
|
|
91
|
+
try {
|
|
92
|
+
return await route.handle(request);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.error({ err, route: route.name, listener }, "route failed");
|
|
95
|
+
return serverError();
|
|
96
|
+
}
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
/**
|
|
82
100
|
* Serves HTTP on a unix socket without Bun's 10-second idle timeout, which would cut long
|
|
83
101
|
* turns and quiet model streams. Bun 1.4.2 honors `idleTimeout` on unix sockets, but its
|
|
84
102
|
* types reject the option there, hence the cast. A stale socket file is removed first.
|
|
85
|
-
* (`shared/unix-server.ts` keeps its own copy for
|
|
103
|
+
* (`shared/unix-server.ts` keeps its own copy for standalone worker images.)
|
|
86
104
|
*/
|
|
87
105
|
function serveUnix(
|
|
88
106
|
socketPath: string,
|
|
89
107
|
fetch: (request: Request) => Response | Promise<Response>,
|
|
90
108
|
): Server<undefined> {
|
|
91
109
|
rmSync(socketPath, { force: true });
|
|
92
|
-
const options = {
|
|
110
|
+
const options = {
|
|
111
|
+
unix: socketPath,
|
|
112
|
+
idleTimeout: 0,
|
|
113
|
+
fetch,
|
|
114
|
+
error: serverError,
|
|
115
|
+
};
|
|
93
116
|
// SAFETY: these are Bun's unix-socket options; only `idleTimeout` is missing from its types.
|
|
94
117
|
return Bun.serve(
|
|
95
118
|
options as unknown as Parameters<typeof Bun.serve>[0],
|
|
@@ -107,6 +130,7 @@ function serveTcp(
|
|
|
107
130
|
...(hostname ? { hostname } : {}),
|
|
108
131
|
idleTimeout: 0,
|
|
109
132
|
fetch,
|
|
133
|
+
error: serverError,
|
|
110
134
|
});
|
|
111
135
|
}
|
|
112
136
|
|
|
@@ -115,31 +139,42 @@ function serveTcp(
|
|
|
115
139
|
export class HttpListeners {
|
|
116
140
|
readonly #listeners: readonly ListenerConfig[];
|
|
117
141
|
readonly #routes: readonly HttpRoute[];
|
|
142
|
+
readonly #logger: Logger;
|
|
118
143
|
#servers: Server<undefined>[] = [];
|
|
119
144
|
|
|
120
145
|
/** Validates every route before any socket opens. */
|
|
121
146
|
constructor(
|
|
122
147
|
listeners: readonly ListenerConfig[],
|
|
123
148
|
routes: readonly HttpRoute[],
|
|
149
|
+
logger: Logger,
|
|
124
150
|
) {
|
|
125
151
|
validate(listeners, routes);
|
|
152
|
+
this.#logger = logger;
|
|
126
153
|
this.#listeners = listeners;
|
|
127
154
|
this.#routes = routes;
|
|
128
155
|
}
|
|
129
156
|
|
|
157
|
+
/** Opens every listener; when one fails, the ones already open close again before the error is thrown. */
|
|
130
158
|
start(): void {
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
try {
|
|
160
|
+
for (const listener of this.#listeners) {
|
|
161
|
+
const routes = this.#routes.filter(
|
|
162
|
+
(route) => route.listener === listener.id,
|
|
163
|
+
);
|
|
164
|
+
const handle = (request: Request) =>
|
|
165
|
+
routeRequest(routes, request, listener.id, this.#logger);
|
|
166
|
+
if ("socketPath" in listener) {
|
|
167
|
+
this.#servers.push(serveUnix(listener.socketPath, handle));
|
|
168
|
+
chmodSync(listener.socketPath, listener.mode ?? 0o660);
|
|
169
|
+
} else {
|
|
170
|
+
this.#servers.push(
|
|
171
|
+
serveTcp(listener.port, listener.hostname, handle),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
142
174
|
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.stop();
|
|
177
|
+
throw error;
|
|
143
178
|
}
|
|
144
179
|
}
|
|
145
180
|
|
|
@@ -21,6 +21,8 @@ export function agentPanelEn(ctx: CatalogContext) {
|
|
|
21
21
|
}) =>
|
|
22
22
|
`### ${v.displayName}\n-# agent name \`${v.name}\`, cannot be changed\n**Model** · \`${v.model}\` · thinking \`${v.thinking}\`${v.follows}\n${v.skills}\n-# To add or remove a skill, ask any agent to use agent_skills.`,
|
|
23
23
|
agentAvatarPromptText: (prompt: string) => `**Avatar prompt**\n${prompt}`,
|
|
24
|
+
agentNoImageProvider:
|
|
25
|
+
"**Avatar** · No image provider is configured, so this avatar is generated from the agent's display name and cannot be redrawn or edited. A plugin that fills the `images` slot adds drawing.",
|
|
24
26
|
agentPromptInline: (prompt: string) =>
|
|
25
27
|
`**Prompt**\n\`\`\`\n${prompt}\n\`\`\``,
|
|
26
28
|
agentPromptFile: (length: number) =>
|
|
@@ -69,6 +71,8 @@ export function agentPanelZhTW(
|
|
|
69
71
|
}) =>
|
|
70
72
|
`### ${v.displayName}\n-# agent 名稱 \`${v.name}\`,不能更改\n**模型** \`${v.model}\` thinking \`${v.thinking}\`${v.follows}\n${v.skills}\n-# 要增減 skill,請任一個 agent 用 agent_skills。`,
|
|
71
73
|
agentAvatarPromptText: (prompt: string) => `**頭像提示詞**\n${prompt}`,
|
|
74
|
+
agentNoImageProvider:
|
|
75
|
+
"**頭像** 尚未設定圖像提供者,所以這個頭像由 agent 的顯示名自動產生,不能重畫或修改。要能畫頭像,需要一個填入 `images` 槽位的外掛。",
|
|
72
76
|
agentPromptInline: (prompt: string) =>
|
|
73
77
|
`**提示詞**\n\`\`\`\n${prompt}\n\`\`\``,
|
|
74
78
|
agentPromptFile: (length: number) =>
|