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/testing.ts
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import type { SQL } from "bun";
|
|
3
|
-
import
|
|
6
|
+
import { agentClaim, OWNER_TARGET } from "./core/agents/agent-claim.ts";
|
|
7
|
+
import { DISCORD } from "./core/builtin/discord.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ConversationPort,
|
|
10
|
+
InboundMessage,
|
|
11
|
+
} from "./core/contract/channels.ts";
|
|
4
12
|
import type { Providers } from "./core/contract/providers.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AgentRuntime,
|
|
15
|
+
HeldActionStore,
|
|
16
|
+
RuntimeDeps,
|
|
17
|
+
} from "./core/contract/runtime.ts";
|
|
18
|
+
import type { ServiceKey } from "./core/contract/services.ts";
|
|
19
|
+
import type { ChatSurface, SurfacePort } from "./core/contract/surface.ts";
|
|
20
|
+
import { CommandCollection } from "./core/discord/command-collection.ts";
|
|
21
|
+
import type { ComposedCommands } from "./core/discord/compose-commands.ts";
|
|
22
|
+
import type {
|
|
23
|
+
CommandGuard,
|
|
24
|
+
CommandRegistrar,
|
|
25
|
+
InteractionContribution,
|
|
26
|
+
} from "./core/discord/interaction-module.ts";
|
|
27
|
+
import { OwnerGuard, ownerRootCommand } from "./core/discord/owner-command.ts";
|
|
28
|
+
import type { PendingConfirmation } from "./core/domain/conversation.ts";
|
|
5
29
|
import { NotLinkedError, PluginError } from "./core/errors.ts";
|
|
6
30
|
import { EventBus } from "./core/events.ts";
|
|
31
|
+
import type { HoldCheck } from "./core/holds.ts";
|
|
32
|
+
import type { OwnerIdentity } from "./core/identity.ts";
|
|
33
|
+
import { ConfirmationJudge } from "./core/judging/confirmation-judge.ts";
|
|
7
34
|
import { silentLogger } from "./core/log.ts";
|
|
35
|
+
import { ConversationBackgroundTurns } from "./core/modules/background/background-turns.ts";
|
|
8
36
|
import type {
|
|
9
37
|
Contribution,
|
|
38
|
+
HostEnv,
|
|
10
39
|
LinkedSessions,
|
|
11
40
|
PluginContext,
|
|
12
41
|
RoundtablePlugin,
|
|
@@ -18,15 +47,131 @@ import {
|
|
|
18
47
|
linkSessions,
|
|
19
48
|
} from "./core/registry/contributions.ts";
|
|
20
49
|
import { resolveProviders } from "./core/registry/providers.ts";
|
|
50
|
+
import { ServiceRegistry } from "./core/registry/services.ts";
|
|
21
51
|
import { ChannelQueue } from "./core/routing/channel-queue.ts";
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
24
|
-
|
|
52
|
+
import { ChannelRouter } from "./core/routing/channel-router.ts";
|
|
53
|
+
import {
|
|
54
|
+
type ConversationTurns,
|
|
55
|
+
conversationTurns,
|
|
56
|
+
} from "./core/routing/conversation-turns.ts";
|
|
57
|
+
import { surfacePort } from "./core/routing/surface-port.ts";
|
|
58
|
+
import { AGENTS, type AgentServer, BACKGROUND_TURNS } from "./core/services.ts";
|
|
59
|
+
import type { ChannelKey, SessionContext } from "./core/sessions.ts";
|
|
60
|
+
import { type Speaker, speakerPolicy } from "./core/speakers.ts";
|
|
25
61
|
import { type ToolTierTable, toolTiers } from "./core/tool-tiers.ts";
|
|
26
62
|
|
|
63
|
+
// Fixtures for plugin tests.
|
|
64
|
+
|
|
65
|
+
export { silentLogger } from "./core/log.ts";
|
|
66
|
+
|
|
67
|
+
export type { TestStore } from "./core/testing/database.ts";
|
|
68
|
+
export {
|
|
69
|
+
describeDb,
|
|
70
|
+
openTestStore,
|
|
71
|
+
TEST_GUILD,
|
|
72
|
+
testDatabaseUrl,
|
|
73
|
+
} from "./core/testing/database.ts";
|
|
74
|
+
export { eagerText, useEagerCatalog } from "./core/testing/eager-catalog.ts";
|
|
75
|
+
export type { TestLocale } from "./core/testing/locale.ts";
|
|
76
|
+
export { useTestLocale } from "./core/testing/locale.ts";
|
|
77
|
+
|
|
78
|
+
export { OWNER_SPEAKER } from "./core/testing/owner.ts";
|
|
79
|
+
export type { TestHost, TestHostOptions } from "./core/testing/test-host.ts";
|
|
80
|
+
export { testHost } from "./core/testing/test-host.ts";
|
|
81
|
+
export type { FakeThreadHost } from "./core/testing/thread-host.ts";
|
|
82
|
+
export { fakeThreads } from "./core/testing/thread-host.ts";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A service the plugin under test reads, with the members the test gives it, made by
|
|
86
|
+
* `servicePair(KEY, { ... })`. Reading a member the test did not give throws a PluginError that
|
|
87
|
+
* names it.
|
|
88
|
+
*/
|
|
89
|
+
export interface ServicePair {
|
|
90
|
+
readonly key: ServiceKey<unknown>;
|
|
91
|
+
readonly given: object;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pairs a service key with the members the test gives it, for the `services` option of `testPlugin`. */
|
|
95
|
+
export function servicePair<T extends object>(
|
|
96
|
+
key: ServiceKey<T>,
|
|
97
|
+
given: Partial<T>,
|
|
98
|
+
): ServicePair {
|
|
99
|
+
return { key, given };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A stand-in for the Discord plugin's `DISCORD` service, made by `fakeDiscord()`. */
|
|
103
|
+
export interface FakeDiscord {
|
|
104
|
+
/** Give this to `testPlugin`'s `services` option: `{ services: [discord.service] }`. */
|
|
105
|
+
readonly service: ServicePair;
|
|
106
|
+
/** What `DISCORD.commands` is to the plugin: the real registrar, which refuses what the Discord plugin refuses. */
|
|
107
|
+
readonly commands: CommandRegistrar;
|
|
108
|
+
/** What `DISCORD.guard` is: the owner, `owner` unless the test gave another id, serving the root command. */
|
|
109
|
+
readonly guard: CommandGuard;
|
|
110
|
+
/** Every contribution the plugin added, in the order it added them. */
|
|
111
|
+
added(): readonly InteractionContribution[];
|
|
112
|
+
/** The tree the Discord plugin would register, composed under the root command the fake was made with. */
|
|
113
|
+
compose(): ComposedCommands;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A `DISCORD` service for a plugin that adds slash commands, without a Discord connection: the
|
|
118
|
+
* plugin's `commands.add` calls are recorded, and `compose()` shows the tree Discord would get.
|
|
119
|
+
* `rootCommand` is the root command's name, `roundtable` by default. Only `commands` and `guard` are given; reading another member throws.
|
|
120
|
+
*/
|
|
121
|
+
export function fakeDiscord(
|
|
122
|
+
options: { ownerId?: string; rootCommand?: string } = {},
|
|
123
|
+
): FakeDiscord {
|
|
124
|
+
const collection = new CommandCollection();
|
|
125
|
+
const { rootCommand = "roundtable" } = options;
|
|
126
|
+
const guard = new OwnerGuard(
|
|
127
|
+
options.ownerId ?? "owner",
|
|
128
|
+
silentLogger(),
|
|
129
|
+
rootCommand,
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
service: servicePair(DISCORD, { commands: collection.registrar, guard }),
|
|
133
|
+
commands: collection.registrar,
|
|
134
|
+
guard,
|
|
135
|
+
added: () => collection.added(),
|
|
136
|
+
compose: () => collection.compose(ownerRootCommand(rootCommand)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
27
140
|
export interface TestPluginOptions {
|
|
141
|
+
/** The host environment the plugin sees; default en and UTC. */
|
|
142
|
+
env?: Partial<HostEnv>;
|
|
143
|
+
/**
|
|
144
|
+
* The owner the runtime's dependencies and the agent server's claim know; default `owner`,
|
|
145
|
+
* named Owner, who is addressed as they.
|
|
146
|
+
*/
|
|
147
|
+
owner?: { id: string; name: string; pronouns?: OwnerIdentity["pronouns"] };
|
|
28
148
|
database?: SQL;
|
|
29
149
|
providers?: Partial<Providers>;
|
|
150
|
+
/**
|
|
151
|
+
* Chat surfaces besides the plugin's own, such as a fake one: they are in `context.surfaces`,
|
|
152
|
+
* started with the plugin's services (their messages reach `conversations`), and stopped last.
|
|
153
|
+
*/
|
|
154
|
+
surfaces?: readonly ChatSurface[];
|
|
155
|
+
/**
|
|
156
|
+
* What the plugin reads from `context.services`, one `servicePair(KEY, { ... })` per service.
|
|
157
|
+
* `servicePair(AGENTS, { runtime })` is the runtime that `context.turns` runs turns on; without
|
|
158
|
+
* it, a plugin that fills the `runtime` slot gets the runtime its provider builds. A service
|
|
159
|
+
* not given here reads as absent to `find`, and `get` names this option, except for what the
|
|
160
|
+
* host supplies anyway: `BACKGROUND_TURNS`, the real background turns over the test's router,
|
|
161
|
+
* and, once `AGENTS` is given, its `approvals` (the real confirmation judge over
|
|
162
|
+
* `providers.judge`, when the test gives a judge). Giving `AGENTS` a `team` adds the agent
|
|
163
|
+
* server's own claim to the router, answering as the host's would for the `owner`.
|
|
164
|
+
*/
|
|
165
|
+
services?: readonly ServicePair[];
|
|
166
|
+
/**
|
|
167
|
+
* The plugin's conversations: by default a router over the plugin's own `channels` claims, so a
|
|
168
|
+
* message a surface delivers reaches them. Methods given here replace the router's.
|
|
169
|
+
*/
|
|
170
|
+
conversations?: Partial<ConversationPort>;
|
|
171
|
+
/** Replaces `context.turns`, which by default runs turns over the runtime and the surfaces. */
|
|
172
|
+
turns?: ConversationTurns;
|
|
173
|
+
/** How long the router holds a bare forward for the message that follows it; the host option `conversations.forwardJoinMs`. */
|
|
174
|
+
forwardJoinMs?: number;
|
|
30
175
|
}
|
|
31
176
|
|
|
32
177
|
export interface RecordedEvent {
|
|
@@ -36,13 +181,23 @@ export interface RecordedEvent {
|
|
|
36
181
|
|
|
37
182
|
export interface TestPluginResult {
|
|
38
183
|
contribution: Contribution;
|
|
184
|
+
/** The hold rules the plugin contributed, chained as the host links them: the description of a call that must be approved first, or undefined. */
|
|
185
|
+
holds: HoldCheck;
|
|
186
|
+
/** What the plugin sees as `context.conversations`, for a test to drive its claims. */
|
|
187
|
+
conversations: ConversationPort;
|
|
188
|
+
/** What the plugin sees as `context.turns`. */
|
|
189
|
+
turns: ConversationTurns;
|
|
190
|
+
/** What the plugin sees as `context.surfaces`: its own surfaces and the injected ones. */
|
|
191
|
+
surfaces: SurfacePort;
|
|
192
|
+
/** The runtime the plugin's `runtime` provider built, given stand-in dependencies; undefined when it fills no such slot. */
|
|
193
|
+
runtime: AgentRuntime | undefined;
|
|
39
194
|
tools: readonly string[];
|
|
40
195
|
tiers: ToolTierTable;
|
|
41
196
|
events: RecordedEvent[];
|
|
42
197
|
runTool(
|
|
43
198
|
name: string,
|
|
44
199
|
args: Record<string, unknown>,
|
|
45
|
-
options?: { speaker?: Speaker },
|
|
200
|
+
options?: { speaker?: Speaker; channel?: ChannelKey },
|
|
46
201
|
): Promise<string>;
|
|
47
202
|
stop(): Promise<void>;
|
|
48
203
|
}
|
|
@@ -53,6 +208,10 @@ const NOT_LINKED = {
|
|
|
53
208
|
"conversations are linked once every plugin is set up. Use them from a service's start or from a handler, not during setup.",
|
|
54
209
|
sessions:
|
|
55
210
|
"session parts are linked once every plugin is set up. Call sessions() from a service's start or from a handler, not during setup.",
|
|
211
|
+
surfaces:
|
|
212
|
+
"chat surfaces are linked once every plugin is set up. Use surfaces from a service's start or from a handler, not during setup.",
|
|
213
|
+
turns:
|
|
214
|
+
"conversation turns are linked once every plugin is set up. Use turns from a service's start or from a handler, not during setup.",
|
|
56
215
|
dashboard:
|
|
57
216
|
"dashboard lines are linked once every plugin is set up. Call dashboard() from a service's start or from a handler, not during setup.",
|
|
58
217
|
};
|
|
@@ -61,77 +220,274 @@ function unlinked(part: keyof typeof NOT_LINKED): never {
|
|
|
61
220
|
throw new NotLinkedError(NOT_LINKED[part]);
|
|
62
221
|
}
|
|
63
222
|
|
|
223
|
+
/** Members a probe may ask of any object: a test's partial service is not refused for them. */
|
|
224
|
+
const PROBED = new Set(["then", "toJSON", "asymmetricMatch"]);
|
|
225
|
+
|
|
226
|
+
/** A service the test gave only some members of: reading another names the option that adds it. */
|
|
227
|
+
function partialService(key: ServiceKey<unknown>, given: object): object {
|
|
228
|
+
return new Proxy(given, {
|
|
229
|
+
get(target, member, receiver) {
|
|
230
|
+
if (member in target || typeof member === "symbol" || PROBED.has(member))
|
|
231
|
+
return Reflect.get(target, member, receiver);
|
|
232
|
+
throw new PluginError(
|
|
233
|
+
`testPlugin gave service ${key.id} no "${member}". Give it in the services option: testPlugin(plugin, { services: [servicePair(KEY, { ${member}: ... })] }).`,
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Held actions kept in memory, for the runtime a plugin's provider builds under test. */
|
|
240
|
+
function memoryHeldActions(): HeldActionStore {
|
|
241
|
+
const held = new Map<ChannelKey, PendingConfirmation>();
|
|
242
|
+
return {
|
|
243
|
+
load: async (conversation) => held.get(conversation),
|
|
244
|
+
save: async (conversation, actions) => {
|
|
245
|
+
if (actions) held.set(conversation, actions);
|
|
246
|
+
else held.delete(conversation);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
64
251
|
/** Build one plugin without Discord or PostgreSQL, retaining the host's validation and tier logic. */
|
|
252
|
+
// pi-lens-ignore: high-complexity, large-function — one host's context and lifecycle, mirrored end to end
|
|
65
253
|
export async function testPlugin(
|
|
66
254
|
plugin: RoundtablePlugin,
|
|
67
255
|
options: TestPluginOptions = {},
|
|
68
256
|
): Promise<TestPluginResult> {
|
|
69
257
|
const logger = silentLogger();
|
|
258
|
+
const owner = {
|
|
259
|
+
id: "owner",
|
|
260
|
+
name: "Owner",
|
|
261
|
+
pronouns: { subject: "they", object: "them", possessive: "their" },
|
|
262
|
+
...options.owner,
|
|
263
|
+
};
|
|
70
264
|
const tiers = toolTiers();
|
|
71
265
|
const bus = new EventBus(logger);
|
|
72
266
|
const events: RecordedEvent[] = [];
|
|
73
|
-
|
|
267
|
+
// The plugin under test is the only one registered, so a service it reads comes from the test.
|
|
268
|
+
const services = new ServiceRegistry(
|
|
269
|
+
[plugin],
|
|
270
|
+
"testPlugin has no built-in plugins: give it in the services option, testPlugin(plugin, { services: [servicePair(KEY, { ... })] }).",
|
|
271
|
+
);
|
|
272
|
+
const given = new Map(
|
|
273
|
+
(options.services ?? []).map((pair) => [pair.key.id, pair] as const),
|
|
274
|
+
);
|
|
275
|
+
const agents = given.get(AGENTS.id);
|
|
276
|
+
const resolved = resolveProviders([plugin]);
|
|
74
277
|
let linked: LinkedSessions | undefined;
|
|
278
|
+
let router: ChannelRouter | undefined;
|
|
279
|
+
const queue = new ChannelQueue();
|
|
75
280
|
const conversations: ConversationPort = {
|
|
76
|
-
handle: async () =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
281
|
+
handle: async (message) =>
|
|
282
|
+
(router ?? unlinked("conversations")).handle(message),
|
|
283
|
+
background: async (turn) =>
|
|
284
|
+
(router ?? unlinked("conversations")).background(turn),
|
|
285
|
+
target: (name) => (router ?? unlinked("conversations")).target(name),
|
|
286
|
+
startFresh: async (channel) =>
|
|
287
|
+
(router ?? unlinked("conversations")).startFresh(channel),
|
|
288
|
+
deleteConversation: async (channel) =>
|
|
289
|
+
(router ?? unlinked("conversations")).deleteConversation(channel),
|
|
290
|
+
stop: (channel) => (router ?? unlinked("conversations")).stop(channel),
|
|
291
|
+
postsInPlace: (channel) =>
|
|
292
|
+
(router ?? unlinked("conversations")).postsInPlace(channel),
|
|
293
|
+
...options.conversations,
|
|
82
294
|
};
|
|
83
|
-
const
|
|
295
|
+
const surfaces = surfacePort(() =>
|
|
296
|
+
linked
|
|
297
|
+
? [...registry.surfaces, ...(options.surfaces ?? [])]
|
|
298
|
+
: unlinked("surfaces"),
|
|
299
|
+
);
|
|
300
|
+
const sink = {
|
|
301
|
+
turnStarted: (turn: TurnEvent) => {
|
|
302
|
+
events.push({ name: "turnStarted", turn });
|
|
303
|
+
bus.sink.turnStarted(turn);
|
|
304
|
+
},
|
|
305
|
+
turnEnded: (turn: TurnEndEvent) => {
|
|
306
|
+
events.push({ name: "turnEnded", turn });
|
|
307
|
+
bus.sink.turnEnded(turn);
|
|
308
|
+
},
|
|
309
|
+
changed: () => {
|
|
310
|
+
events.push({ name: "changed" });
|
|
311
|
+
bus.sink.changed();
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
const turns: ConversationTurns =
|
|
315
|
+
options.turns ??
|
|
316
|
+
conversationTurns({
|
|
317
|
+
linked: () => {
|
|
318
|
+
if (!linked) unlinked("turns");
|
|
319
|
+
},
|
|
320
|
+
runtime: () => services.get(AGENTS).runtime,
|
|
321
|
+
surfaces,
|
|
322
|
+
events: sink,
|
|
323
|
+
selection: () => (linked ?? unlinked("sessions")).agentSelection(),
|
|
324
|
+
logger,
|
|
325
|
+
});
|
|
326
|
+
const providers = {
|
|
327
|
+
...resolved,
|
|
328
|
+
...options.providers,
|
|
329
|
+
filled: new Set([
|
|
330
|
+
...resolved.filled,
|
|
331
|
+
...(Object.keys(options.providers ?? {}) as (keyof Providers)[]),
|
|
332
|
+
]),
|
|
333
|
+
};
|
|
334
|
+
const env: HostEnv = {
|
|
335
|
+
locale: "en",
|
|
336
|
+
timeZone: "UTC",
|
|
337
|
+
now: () => new Date(),
|
|
338
|
+
...options.env,
|
|
339
|
+
};
|
|
340
|
+
// What the agent server does with the slot: the provider's runtime serves `context.turns`.
|
|
341
|
+
let runtime: AgentRuntime | undefined;
|
|
342
|
+
if (providers.filled.has("runtime")) {
|
|
343
|
+
const deps: RuntimeDeps = {
|
|
344
|
+
logger,
|
|
345
|
+
env,
|
|
346
|
+
owner: { id: owner.id, name: owner.name },
|
|
347
|
+
sessions: () => linked ?? unlinked("sessions"),
|
|
348
|
+
toolTiers: tiers,
|
|
349
|
+
prompts: (channel, speaker) => surfaces.prompts(channel, speaker),
|
|
350
|
+
agents: {
|
|
351
|
+
workDir: tmpdir(),
|
|
352
|
+
skills: () => [],
|
|
353
|
+
modelOf: () => ({ model: "test/model", thinking: "off" }),
|
|
354
|
+
turnChannel: (scope) => scope.home,
|
|
355
|
+
},
|
|
356
|
+
confirmations: memoryHeldActions(),
|
|
357
|
+
judge: providers.judge,
|
|
358
|
+
};
|
|
359
|
+
runtime = providers.runtime(deps);
|
|
360
|
+
// The provider's runtime serves `context.turns` unless the test gave another.
|
|
361
|
+
if (!agents || !("runtime" in agents.given))
|
|
362
|
+
given.set(AGENTS.id, {
|
|
363
|
+
key: AGENTS,
|
|
364
|
+
given: { ...agents?.given, runtime } satisfies Partial<AgentServer>,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
// Once the agent server is there, it judges the owner's replies to held actions as the host's does.
|
|
368
|
+
const server = given.get(AGENTS.id);
|
|
369
|
+
if (server && providers.filled.has("judge") && !("approvals" in server.given))
|
|
370
|
+
given.set(AGENTS.id, {
|
|
371
|
+
key: AGENTS,
|
|
372
|
+
given: {
|
|
373
|
+
...server.given,
|
|
374
|
+
approvals: new ConfirmationJudge({
|
|
375
|
+
judge: providers.judge,
|
|
376
|
+
threshold: 0.6,
|
|
377
|
+
logger,
|
|
378
|
+
}),
|
|
379
|
+
} satisfies Partial<AgentServer>,
|
|
380
|
+
});
|
|
381
|
+
for (const { key, given: members } of given.values())
|
|
382
|
+
services.preset(key, partialService(key, members));
|
|
383
|
+
// The turns nobody wrote run through the same router, unless the test gave others or the plugin provides them.
|
|
384
|
+
if (
|
|
385
|
+
!given.has(BACKGROUND_TURNS.id) &&
|
|
386
|
+
!plugin.provides?.some((key) => key.id === BACKGROUND_TURNS.id)
|
|
387
|
+
)
|
|
388
|
+
services.preset(
|
|
389
|
+
BACKGROUND_TURNS,
|
|
390
|
+
new ConversationBackgroundTurns({
|
|
391
|
+
conversations,
|
|
392
|
+
system: { id: "assistant", name: "Assistant" },
|
|
393
|
+
logger,
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
const context: Omit<PluginContext, "services"> = {
|
|
84
397
|
logger,
|
|
398
|
+
env,
|
|
85
399
|
sessions: () => linked ?? unlinked("sessions"),
|
|
86
|
-
queue
|
|
400
|
+
queue,
|
|
87
401
|
toolTiers: tiers,
|
|
88
|
-
events:
|
|
89
|
-
turnStarted: (turn) => {
|
|
90
|
-
events.push({ name: "turnStarted", turn });
|
|
91
|
-
bus.sink.turnStarted(turn);
|
|
92
|
-
},
|
|
93
|
-
turnEnded: (turn) => {
|
|
94
|
-
events.push({ name: "turnEnded", turn });
|
|
95
|
-
bus.sink.turnEnded(turn);
|
|
96
|
-
},
|
|
97
|
-
changed: () => {
|
|
98
|
-
events.push({ name: "changed" });
|
|
99
|
-
bus.sink.changed();
|
|
100
|
-
},
|
|
101
|
-
},
|
|
402
|
+
events: sink,
|
|
102
403
|
conversations,
|
|
404
|
+
surfaces,
|
|
405
|
+
turns,
|
|
103
406
|
database: () => {
|
|
104
407
|
if (!options.database) throw new PluginError("no database is configured");
|
|
105
408
|
return options.database;
|
|
106
409
|
},
|
|
107
|
-
|
|
108
|
-
providers: { ...resolveProviders([plugin]), ...options.providers },
|
|
410
|
+
providers,
|
|
109
411
|
dashboard: () => (linked ? registry.dashboard : unlinked("dashboard")),
|
|
110
412
|
};
|
|
111
|
-
const registry = await collectContributions(
|
|
413
|
+
const registry = await collectContributions(
|
|
414
|
+
[plugin],
|
|
415
|
+
context,
|
|
416
|
+
tiers,
|
|
417
|
+
services,
|
|
418
|
+
);
|
|
112
419
|
linked = linkSessions(registry);
|
|
113
420
|
bus.link(registry.handlers);
|
|
421
|
+
// With a team given, the agent server's claim and its target are in the router, as on a host.
|
|
422
|
+
const team = given.get(AGENTS.id)?.given;
|
|
423
|
+
const agentServer = team && "team" in team ? services.get(AGENTS) : undefined;
|
|
424
|
+
const attachmentDir = mkdtempSync(join(tmpdir(), "roundtable-test-plugin-"));
|
|
425
|
+
router = new ChannelRouter({
|
|
426
|
+
claims: [
|
|
427
|
+
...(agentServer
|
|
428
|
+
? [
|
|
429
|
+
agentClaim({
|
|
430
|
+
owner,
|
|
431
|
+
speakers: speakerPolicy({ owners: [owner.id] }),
|
|
432
|
+
// SAFETY: the claim reads the concrete team's members (guildId, owns, answerOwner, answerGroup, answerBackground, startFresh), which a test that gives a team for it supplies.
|
|
433
|
+
team: agentServer.team as unknown as Parameters<
|
|
434
|
+
typeof agentClaim
|
|
435
|
+
>[0]["team"],
|
|
436
|
+
runtime: {
|
|
437
|
+
steer: (...args) => services.get(AGENTS).runtime.steer(...args),
|
|
438
|
+
stop: (channel) => services.get(AGENTS).runtime.stop(channel),
|
|
439
|
+
},
|
|
440
|
+
surface: surfaces,
|
|
441
|
+
attachmentDir: () => attachmentDir,
|
|
442
|
+
logger,
|
|
443
|
+
}),
|
|
444
|
+
]
|
|
445
|
+
: []),
|
|
446
|
+
...registry.channels,
|
|
447
|
+
],
|
|
448
|
+
targets: (name) =>
|
|
449
|
+
registry.backgroundTargets.find((t) => t.name === name) ??
|
|
450
|
+
(agentServer && name === OWNER_TARGET.name ? OWNER_TARGET : undefined),
|
|
451
|
+
queue,
|
|
452
|
+
logger,
|
|
453
|
+
...(options.forwardJoinMs === undefined
|
|
454
|
+
? {}
|
|
455
|
+
: { forwardJoinMs: options.forwardJoinMs }),
|
|
456
|
+
});
|
|
114
457
|
const contribution: Contribution = {
|
|
115
458
|
services: registry.services,
|
|
116
459
|
events: registry.handlers[0]?.events,
|
|
117
|
-
interactions: registry.interactions,
|
|
118
460
|
http: registry.routes,
|
|
119
461
|
holdRules: registry.holdRules,
|
|
120
462
|
piPackages: registry.piPackages,
|
|
121
463
|
sessionTools: registry.sessionTools,
|
|
122
464
|
channels: registry.channels,
|
|
465
|
+
surfaces: registry.surfaces,
|
|
466
|
+
personas: registry.personas,
|
|
467
|
+
backgroundTargets: registry.backgroundTargets,
|
|
123
468
|
dashboard: registry.dashboard,
|
|
124
469
|
tools: registry.tools,
|
|
125
470
|
seeds: registry.seeds,
|
|
126
471
|
prompt: registry.prompt,
|
|
472
|
+
requiredTools: registry.requiredTools,
|
|
127
473
|
...(registry.agentSelections.length > 0
|
|
128
474
|
? { agentSelection: linked.agentSelection }
|
|
129
475
|
: {}),
|
|
130
476
|
};
|
|
477
|
+
// The plugin's own surfaces start as services; the injected ones start beside them, after.
|
|
478
|
+
const deliver = (message: InboundMessage) =>
|
|
479
|
+
void conversations.handle(message);
|
|
131
480
|
for (const service of registry.services) await service.start?.();
|
|
481
|
+
for (const surface of options.surfaces ?? []) await surface.start(deliver);
|
|
482
|
+
await runtime?.preflight?.();
|
|
132
483
|
let stopped = false;
|
|
133
484
|
return {
|
|
134
485
|
contribution,
|
|
486
|
+
holds: linked.holds,
|
|
487
|
+
conversations,
|
|
488
|
+
turns,
|
|
489
|
+
surfaces,
|
|
490
|
+
runtime,
|
|
135
491
|
tools: registry.tools.map((tool) => tool.name),
|
|
136
492
|
tiers,
|
|
137
493
|
events,
|
|
@@ -148,10 +504,11 @@ export async function testPlugin(
|
|
|
148
504
|
isError?: boolean;
|
|
149
505
|
}>;
|
|
150
506
|
}[] = [];
|
|
507
|
+
const channel = runOptions?.channel ?? "test:1";
|
|
151
508
|
const session = {
|
|
152
509
|
kind: "agent",
|
|
153
|
-
homeChannel:
|
|
154
|
-
turnChannel:
|
|
510
|
+
homeChannel: channel,
|
|
511
|
+
turnChannel: channel,
|
|
155
512
|
compaction: { wrap: (factory) => factory },
|
|
156
513
|
speaker: () => runOptions?.speaker,
|
|
157
514
|
runTask: async () => {
|
|
@@ -175,8 +532,11 @@ export async function testPlugin(
|
|
|
175
532
|
if (stopped) return;
|
|
176
533
|
stopped = true;
|
|
177
534
|
await bus.deliver("shutdown", []);
|
|
535
|
+
for (const surface of (options.surfaces ?? []).toReversed())
|
|
536
|
+
await surface.stop?.();
|
|
178
537
|
for (const service of registry.services.toReversed())
|
|
179
538
|
await service.stop?.();
|
|
539
|
+
await runtime?.dispose?.();
|
|
180
540
|
},
|
|
181
541
|
};
|
|
182
542
|
}
|
package/examples/guide.test.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
const root = join(import.meta.dir, "..");
|
|
6
|
-
const guide = readFileSync(join(root, "docs/plugins.md"), "utf8");
|
|
7
|
-
const files = readdirSync(import.meta.dir).filter((name) =>
|
|
8
|
-
name.endsWith(".ts"),
|
|
9
|
-
);
|
|
10
|
-
const examples = files.filter(
|
|
11
|
-
(name) => !name.endsWith(".test.ts") && name !== "guide.test.ts",
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
/** The guide embeds a file as a `ts` block between these two markers. */
|
|
15
|
-
const MARKER = /<!-- example: (\S+) -->/g;
|
|
16
|
-
const BLOCK =
|
|
17
|
-
/<!-- example: (\S+) -->\n```ts\n([\s\S]*?)\n```\n<!-- \/example -->/g;
|
|
18
|
-
|
|
19
|
-
test("every marker in the guide wraps one ts block", () => {
|
|
20
|
-
expect([...guide.matchAll(BLOCK)].length).toBe(
|
|
21
|
-
[...guide.matchAll(MARKER)].length,
|
|
22
|
-
);
|
|
23
|
-
expect([...guide.matchAll(MARKER)].length).toBeGreaterThan(0);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("every block in the guide is exactly the file it names", () => {
|
|
27
|
-
for (const [, path, embedded] of guide.matchAll(BLOCK)) {
|
|
28
|
-
const file = readFileSync(join(root, path as string), "utf8");
|
|
29
|
-
expect({ path, code: embedded }).toEqual({ path, code: file.trimEnd() });
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("every example is embedded in the guide and has a test beside it", () => {
|
|
34
|
-
const embedded = [...guide.matchAll(MARKER)].map((match) => match[1]);
|
|
35
|
-
for (const name of examples) {
|
|
36
|
-
expect(embedded).toContain(`examples/${name}`);
|
|
37
|
-
expect(files).toContain(name.replace(/\.ts$/, ".test.ts"));
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("examples import only the public entries, typebox, bun, and their neighbors", () => {
|
|
42
|
-
for (const name of files.filter((file) => file !== "guide.test.ts")) {
|
|
43
|
-
const source = readFileSync(join(import.meta.dir, name), "utf8");
|
|
44
|
-
for (const [, specifier] of source.matchAll(/from "([^"]+)"/g))
|
|
45
|
-
expect({ name, specifier }).toEqual({
|
|
46
|
-
name,
|
|
47
|
-
specifier: expect.stringMatching(
|
|
48
|
-
/^(pi-roundtable|pi-roundtable\/testing|typebox|bun|bun:test|\.\/[a-z-]+\.ts)$/,
|
|
49
|
-
),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
});
|