pi-roundtable 0.7.19 → 0.9.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 +140 -0
- package/README.md +72 -15
- package/README.zh-TW.md +65 -13
- package/docs/migrating-0.8.md +132 -0
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +853 -81
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +35 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- package/examples/prechecks.test.ts +1 -0
- package/examples/principals.test.ts +60 -0
- package/examples/principals.ts +48 -0
- package/examples/prompt.test.ts +1 -1
- package/examples/study-room.test.ts +29 -1
- package/examples/study-room.ts +16 -19
- package/examples/support-desk.test.ts +3 -1
- package/examples/tools.test.ts +6 -1
- package/package.json +7 -3
- package/src/cli/checks/database.ts +11 -0
- package/src/cli/checks/identity.ts +225 -0
- package/src/cli/checks/model.ts +21 -3
- package/src/cli/cli.ts +130 -5
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +28 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +33 -2
- package/src/cli/report.ts +12 -3
- package/src/cli/templates.ts +46 -14
- package/src/cli/upgrade-rewrite.ts +102 -0
- package/src/cli/upgrade-source.ts +430 -0
- package/src/cli/upgrade.ts +176 -0
- package/src/core/agents/agent-claim.ts +56 -34
- package/src/core/agents/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +10 -1
- package/src/core/builtin/agent-server.ts +38 -101
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +197 -43
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +120 -0
- package/src/core/config/config.ts +228 -94
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +81 -9
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +31 -11
- package/src/core/contract/surface.ts +35 -6
- package/src/core/conversations/conversation-registry.ts +56 -0
- package/src/core/conversations/conversation-store.ts +126 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +157 -57
- package/src/core/discord/agent-panel.ts +2 -2
- package/src/core/discord/card-audience.ts +148 -0
- package/src/core/discord/channel-context.ts +348 -0
- package/src/core/discord/connection.ts +15 -3
- package/src/core/discord/direct-channel.ts +54 -0
- package/src/core/discord/discord-adapter.ts +9 -0
- package/src/core/discord/discord-owners.ts +133 -0
- package/src/core/discord/discord-surface.ts +69 -14
- package/src/core/discord/fresh-marker.ts +24 -0
- package/src/core/discord/inbound-message.ts +21 -7
- package/src/core/discord/interaction-module.ts +11 -1
- package/src/core/discord/owner-cards-fixture.ts +80 -0
- package/src/core/discord/owner-cards.ts +58 -86
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +10 -0
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +33 -2
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +32 -10
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +11 -9
- package/src/core/identity/access-policy.ts +227 -0
- package/src/core/identity/actor-facts.ts +65 -0
- package/src/core/identity/caching-principal-store.ts +111 -0
- package/src/core/identity/config-owner.ts +145 -0
- package/src/core/identity/contact.ts +27 -0
- package/src/core/identity/identity-plugin.ts +56 -0
- package/src/core/identity/identity-schema.ts +282 -0
- package/src/core/identity/identity-service.ts +489 -0
- package/src/core/identity/identity-view.ts +77 -0
- package/src/core/identity/plugin-identities.ts +109 -0
- package/src/core/identity/principal-reads.ts +38 -0
- package/src/core/identity/principal-store.ts +478 -0
- package/src/core/identity/principal-tables.ts +53 -0
- package/src/core/identity/seen-throttle.ts +57 -0
- package/src/core/identity/ulid.ts +36 -0
- package/src/core/identity.ts +34 -5
- package/src/core/interactions/prompts.ts +127 -0
- package/src/core/modules/background/background-turns.ts +56 -17
- package/src/core/modules/background/personal-target.ts +50 -0
- package/src/core/modules/delegation/delegate.ts +28 -14
- package/src/core/modules/delegation/delegator.ts +23 -7
- package/src/core/modules/memory/owner-memory.ts +123 -50
- package/src/core/modules/notify/notify.ts +53 -13
- package/src/core/modules/notify/session-notify.ts +110 -0
- package/src/core/modules/schedules/prechecks.ts +8 -2
- package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
- package/src/core/modules/schedules/schedule-store.ts +51 -20
- package/src/core/modules/schedules/schedule-tools.ts +122 -12
- package/src/core/modules/schedules/scheduler.ts +43 -2
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +62 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +226 -17
- package/src/core/routing/conversation-turns.ts +85 -1
- package/src/core/routing/surface-port.ts +35 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/bridge-guard.ts +225 -0
- package/src/core/runtime/compaction-tiers.ts +5 -1
- package/src/core/runtime/conversation-sessions.ts +124 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +67 -4
- package/src/core/runtime/pi-agent-runtime.ts +143 -135
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +6 -6
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +205 -0
- package/src/core/runtime/runtime-types.ts +40 -12
- package/src/core/runtime/session-conversation.ts +143 -0
- package/src/core/runtime/session-factory.ts +115 -15
- package/src/core/runtime/session-scope.ts +78 -0
- package/src/core/runtime/steerable-run.ts +9 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +42 -2
- package/src/core/sessions.ts +30 -0
- package/src/core/shared/unix-server.ts +35 -5
- package/src/core/speakers.ts +21 -19
- package/src/core/testing/background.ts +31 -0
- package/src/core/testing/fixture-database.ts +55 -0
- package/src/core/testing/map-identity.ts +76 -0
- package/src/core/testing/module-sessions.ts +134 -0
- package/src/core/testing/modules.ts +149 -38
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +460 -0
- package/src/core/testing/test-host.ts +100 -19
- package/src/core/testing/tool-set.ts +32 -0
- package/src/core/tool-set.snapshot.json +4 -4
- package/src/core/tool-tiers.ts +57 -3
- package/src/discord/index.ts +10 -0
- package/src/index.ts +65 -8
- package/src/kit/compaction.ts +13 -0
- package/src/kit/index.ts +10 -0
- package/src/kit/support.ts +1 -1
- package/src/testing.ts +96 -21
- package/templates/.env.example +1 -1
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +22 -0
- package/templates/adapters/web/package.json.tmpl +26 -0
- package/templates/adapters/web/persona/assistant.md +2 -0
- package/templates/adapters/web/roundtable.config.ts +59 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +14 -2
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IdentityError } from "../domain/errors.ts";
|
|
2
|
+
import type { ActorFacts } from "../identity/actor-facts.ts";
|
|
3
|
+
import type { ContactAssessor } from "../identity/contact.ts";
|
|
4
|
+
import type { IdentityService } from "../identity/identity-service.ts";
|
|
5
|
+
import type { IdentityLink, Principal } from "../identity/principal-store.ts";
|
|
6
|
+
import { type SpeakerMap, speakerPolicy, tierAtLeast } from "../speakers.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Who a 0.8 speaker map serves, for a test without a database: each person their own principal,
|
|
10
|
+
* as one carried over from 0.8 is, and nothing written; a background turn runs as the person of
|
|
11
|
+
* its principal id at the map's tier for their id with no roles. It resolves as the router's assessor
|
|
12
|
+
* and as the `resolve` of `IDENTITY`; its owners are the map's, and a person has a Discord
|
|
13
|
+
* identity once the map names their user id or they have been resolved through it.
|
|
14
|
+
*/
|
|
15
|
+
export function mapIdentity(
|
|
16
|
+
map: SpeakerMap,
|
|
17
|
+
): ContactAssessor &
|
|
18
|
+
Pick<IdentityService, "resolve" | "owners" | "identities" | "speakerFor"> {
|
|
19
|
+
const policy = speakerPolicy(map);
|
|
20
|
+
const linked = new Set([
|
|
21
|
+
...map.owners,
|
|
22
|
+
...(map.admins?.users ?? []),
|
|
23
|
+
...(map.members?.users ?? []),
|
|
24
|
+
]);
|
|
25
|
+
const resolve = async (facts: ActorFacts) => {
|
|
26
|
+
const speaker = policy.resolve({
|
|
27
|
+
id: facts.subject,
|
|
28
|
+
name: facts.name,
|
|
29
|
+
roleIds: (facts.roles ?? []).map((role) =>
|
|
30
|
+
role.replace(/^discord:role:/, ""),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
if (speaker) linked.add(speaker.id);
|
|
34
|
+
return speaker;
|
|
35
|
+
};
|
|
36
|
+
const principal = (id: string): Principal => ({
|
|
37
|
+
id,
|
|
38
|
+
displayName: id,
|
|
39
|
+
disabled: false,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
resolve,
|
|
43
|
+
assess: async (facts) => {
|
|
44
|
+
const speaker = await resolve(facts);
|
|
45
|
+
return speaker && { speaker, take: async () => speaker };
|
|
46
|
+
},
|
|
47
|
+
owners: async () => map.owners.map(principal),
|
|
48
|
+
// Whom a background turn runs as: the person of the id at the map's tier for them, or lower.
|
|
49
|
+
speakerFor: async (principalId, tier) => {
|
|
50
|
+
const own = policy.resolve({
|
|
51
|
+
id: principalId,
|
|
52
|
+
name: principalId,
|
|
53
|
+
roleIds: [],
|
|
54
|
+
})?.tier;
|
|
55
|
+
if (!own) throw new IdentityError(`there is no principal ${principalId}`);
|
|
56
|
+
return {
|
|
57
|
+
id: principalId,
|
|
58
|
+
name: principalId,
|
|
59
|
+
tier: tier && tierAtLeast(own, tier) ? tier : own,
|
|
60
|
+
principalId,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
identities: async (principalId): Promise<IdentityLink[]> =>
|
|
64
|
+
linked.has(principalId)
|
|
65
|
+
? [
|
|
66
|
+
{
|
|
67
|
+
provider: "discord",
|
|
68
|
+
subject: principalId,
|
|
69
|
+
principalId,
|
|
70
|
+
source: "legacy",
|
|
71
|
+
linkedAt: new Date(0),
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionFactory,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { OwnerIdentity } from "../identity.ts";
|
|
6
|
+
import type {
|
|
7
|
+
AgentTurnScope,
|
|
8
|
+
ChannelKey,
|
|
9
|
+
SessionContext,
|
|
10
|
+
} from "../sessions.ts";
|
|
11
|
+
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
12
|
+
import { OWNER_CHANNEL, type setUpModules } from "./modules.ts";
|
|
13
|
+
import { OWNER_SPEAKER } from "./owner.ts";
|
|
14
|
+
|
|
15
|
+
/** Sessions, people, and tool calls the modules' tests share. */
|
|
16
|
+
export const HOME: ChannelKey = "discord:scout";
|
|
17
|
+
export const GROUP: ChannelKey = "discord:war-room";
|
|
18
|
+
export const OUTSIDE: ChannelKey = "other:table-1";
|
|
19
|
+
|
|
20
|
+
export const scout: AgentTurnScope = {
|
|
21
|
+
name: "scout",
|
|
22
|
+
session: HOME,
|
|
23
|
+
home: HOME,
|
|
24
|
+
};
|
|
25
|
+
export const seat: AgentTurnScope = {
|
|
26
|
+
name: "scout",
|
|
27
|
+
session: GROUP,
|
|
28
|
+
home: HOME,
|
|
29
|
+
group: "war-room",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function context(
|
|
33
|
+
agent?: AgentTurnScope,
|
|
34
|
+
home?: ChannelKey,
|
|
35
|
+
): SessionContext {
|
|
36
|
+
const session: SessionContext = {
|
|
37
|
+
kind: agent ? "agent" : "owner",
|
|
38
|
+
homeChannel: home ?? agent?.home ?? OWNER_CHANNEL,
|
|
39
|
+
turnChannel: agent?.session ?? home ?? OWNER_CHANNEL,
|
|
40
|
+
compaction: { wrap: (compactor) => compactor },
|
|
41
|
+
// Shared, as the host makes a session it has no record of; `privateTo` makes it someone's.
|
|
42
|
+
conversation: { visibility: "shared" },
|
|
43
|
+
addressee: THE_SPEAKER,
|
|
44
|
+
memory: "speaker",
|
|
45
|
+
// A turn of the owner's runs, as every tool call is part of one.
|
|
46
|
+
speaker: () => OWNER_SPEAKER,
|
|
47
|
+
runTask: async () => "report",
|
|
48
|
+
};
|
|
49
|
+
if (agent) session.agent = agent;
|
|
50
|
+
return session;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A session of a conversation of the owner's kind whose turns are someone else's. */
|
|
54
|
+
export function contextOf(speaker: Speaker, home: ChannelKey): SessionContext {
|
|
55
|
+
return { ...context(undefined, home), speaker: () => speaker };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** An admin who is not the primary owner, such as one a remote MCP token is bound to. */
|
|
59
|
+
export const ANN: Speaker = {
|
|
60
|
+
id: "p_ann",
|
|
61
|
+
name: "Ann",
|
|
62
|
+
tier: "admin",
|
|
63
|
+
principalId: "p_ann",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface Registered {
|
|
67
|
+
name: string;
|
|
68
|
+
parameters: { properties: Record<string, unknown> };
|
|
69
|
+
execute(
|
|
70
|
+
id: string,
|
|
71
|
+
params: unknown,
|
|
72
|
+
): Promise<{ content: { text: string }[]; isError?: boolean }>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The extension one of the modules' session tools gives a session; null when it gives none. */
|
|
76
|
+
export function factoryOf(
|
|
77
|
+
setup: Awaited<ReturnType<typeof setUpModules>>,
|
|
78
|
+
name: string,
|
|
79
|
+
session: SessionContext,
|
|
80
|
+
): ExtensionFactory | null | undefined {
|
|
81
|
+
return setup.contribution.sessionTools
|
|
82
|
+
?.find((tool) => tool.name === name)
|
|
83
|
+
?.snapshot()
|
|
84
|
+
.factory(session);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The tools one of the modules' extensions registers in a session. */
|
|
88
|
+
export async function registered(
|
|
89
|
+
setup: Awaited<ReturnType<typeof setUpModules>>,
|
|
90
|
+
session: SessionContext,
|
|
91
|
+
extension: string,
|
|
92
|
+
): Promise<Registered[]> {
|
|
93
|
+
const tool = setup.contribution.sessionTools?.find(
|
|
94
|
+
(candidate) => candidate.name === extension,
|
|
95
|
+
);
|
|
96
|
+
const factory: ExtensionFactory | null | undefined = tool
|
|
97
|
+
?.snapshot()
|
|
98
|
+
.factory(session);
|
|
99
|
+
if (!factory) throw new Error(`no ${extension} extension`);
|
|
100
|
+
const tools: Registered[] = [];
|
|
101
|
+
await factory({
|
|
102
|
+
registerTool: (definition: Registered) => tools.push(definition),
|
|
103
|
+
on: () => undefined,
|
|
104
|
+
} as unknown as ExtensionAPI);
|
|
105
|
+
return tools;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The session of a conversation private to `principalId`, its tools addressing them as given. */
|
|
109
|
+
export function privateTo(
|
|
110
|
+
session: SessionContext,
|
|
111
|
+
principalId: string,
|
|
112
|
+
addressee: OwnerIdentity = THE_SPEAKER,
|
|
113
|
+
): SessionContext {
|
|
114
|
+
return {
|
|
115
|
+
...session,
|
|
116
|
+
conversation: { visibility: "private", principalId },
|
|
117
|
+
addressee,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Runs notify in a session; the tool's answer and whether it was an error. */
|
|
122
|
+
export async function notifyIn(
|
|
123
|
+
setup: Awaited<ReturnType<typeof setUpModules>>,
|
|
124
|
+
session: SessionContext,
|
|
125
|
+
text = "the build is green",
|
|
126
|
+
) {
|
|
127
|
+
const [notify] = await registered(setup, session, "notify");
|
|
128
|
+
if (!notify) return undefined;
|
|
129
|
+
const answer = await notify.execute("1", { text });
|
|
130
|
+
return {
|
|
131
|
+
text: answer.content[0]?.text ?? "",
|
|
132
|
+
error: answer.isError === true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SQL } from "bun";
|
|
3
|
-
import { OWNER_TARGET } from "../agents/agent-claim.ts";
|
|
4
3
|
import { DISCORD, type DiscordServices } from "../builtin/discord.ts";
|
|
5
4
|
import { discordAdminPlugin } from "../builtin/discord-admin.ts";
|
|
6
|
-
import { modulesPlugin } from "../builtin/modules.ts";
|
|
5
|
+
import { type ModulesOptions, modulesPlugin } from "../builtin/modules.ts";
|
|
7
6
|
import { skillsPlugin } from "../builtin/skills.ts";
|
|
8
7
|
import { memoryPlugin } from "../builtin/stores.ts";
|
|
8
|
+
import type { ConversationRegistry } from "../conversations/conversation-registry.ts";
|
|
9
9
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
10
|
+
import type { IdentityService } from "../identity/identity-service.ts";
|
|
11
|
+
import type { Principal } from "../identity/principal-store.ts";
|
|
10
12
|
import { silentLogger } from "../log.ts";
|
|
13
|
+
import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
|
|
11
14
|
import type { SkillStore } from "../modules/skills/skill-store.ts";
|
|
12
15
|
import {
|
|
13
16
|
type Contribution,
|
|
@@ -15,10 +18,22 @@ import {
|
|
|
15
18
|
pluginContext,
|
|
16
19
|
type RoundtablePlugin,
|
|
17
20
|
} from "../plugin.ts";
|
|
21
|
+
import {
|
|
22
|
+
type DirectChannelProvider,
|
|
23
|
+
directChannelsPort,
|
|
24
|
+
} from "../presence/direct-channels.ts";
|
|
18
25
|
import { ServiceRegistry } from "../registry/services.ts";
|
|
19
26
|
import { surfacePort } from "../routing/surface-port.ts";
|
|
20
27
|
import type { AgentServer, MemoryStore, ScheduleStore } from "../services.ts";
|
|
21
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
AGENTS,
|
|
30
|
+
CONVERSATIONS,
|
|
31
|
+
IDENTITY,
|
|
32
|
+
MEMORY,
|
|
33
|
+
SCHEDULES,
|
|
34
|
+
} from "../services.ts";
|
|
35
|
+
import type { Tier } from "../speakers.ts";
|
|
36
|
+
import { toolTiers } from "../tool-tiers.ts";
|
|
22
37
|
|
|
23
38
|
/** What the modules' tools did, for a test to read. */
|
|
24
39
|
export interface ModuleRecord {
|
|
@@ -26,16 +41,66 @@ export interface ModuleRecord {
|
|
|
26
41
|
threadOrigins: (ChannelKey | undefined)[];
|
|
27
42
|
/** The channel each delegated report came back to. */
|
|
28
43
|
reportChannels: ChannelKey[];
|
|
29
|
-
/** How often
|
|
44
|
+
/** How often anything asked the connection for the primary owner's messages, which the modules no longer read. */
|
|
30
45
|
ownerChannelAsked: number;
|
|
46
|
+
/** Whose direct channel was asked for, in order. */
|
|
47
|
+
directAsked: string[];
|
|
48
|
+
/** The notices sent through a direct channel, by principal. */
|
|
49
|
+
notified: { principalId: string; text: string }[];
|
|
50
|
+
/** What was posted on the surface, by channel. */
|
|
51
|
+
posted: { channel: ChannelKey; text: string }[];
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
export const OWNER_CHANNEL: ChannelKey = "discord:owner-dm";
|
|
34
55
|
|
|
56
|
+
/** The primary owner the modules serve: principal "1". */
|
|
57
|
+
export const MODULES_OWNER = {
|
|
58
|
+
id: "1",
|
|
59
|
+
name: "Owner",
|
|
60
|
+
pronouns: { subject: "they", object: "them", possessive: "their" },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The direct messages the stand-in Discord provider reaches by default: the owner's, principal "1". */
|
|
64
|
+
const DIRECT: Readonly<Record<string, ChannelKey>> = { "1": OWNER_CHANNEL };
|
|
65
|
+
|
|
35
66
|
/** The modules plugin set up over stand-in stores and surface; returns its contribution and the record. */
|
|
36
67
|
export async function setUpModules(
|
|
37
|
-
options: {
|
|
68
|
+
options: {
|
|
69
|
+
agentChannelOf?: (name: string) => ChannelKey;
|
|
70
|
+
/** Whether Discord is there; default true. A host without it has no owner's messages. */
|
|
71
|
+
discord?: boolean;
|
|
72
|
+
/** Whether a plugin contributes the owner's background target, as the agent server does; default true. */
|
|
73
|
+
ownerTarget?: boolean;
|
|
74
|
+
/** Whether a claim owns a channel; by default the Discord channels are owned and no other. */
|
|
75
|
+
owns?: (channel: ChannelKey) => boolean;
|
|
76
|
+
/** Whether the claim owning a channel takes background turns; by default every owned channel's does. */
|
|
77
|
+
takesBackground?: (channel: ChannelKey) => boolean;
|
|
78
|
+
/** Whether the claim owning a channel takes the host's own turns; by default every owned channel's does. */
|
|
79
|
+
takesSystemReports?: (channel: ChannelKey) => boolean;
|
|
80
|
+
errorReporter?: ModulesOptions["errorReporter"];
|
|
81
|
+
/** The schedules the tools keep; by default a stub that answers nothing. */
|
|
82
|
+
schedules?: ScheduleStore;
|
|
83
|
+
/** The conversations the host records, which say whose a conversation is; by default none is recorded. */
|
|
84
|
+
conversations?: Pick<ConversationRegistry, "get">;
|
|
85
|
+
/**
|
|
86
|
+
* Each principal's direct messages on the stand-in Discord provider, the host's only direct
|
|
87
|
+
* channel; by default the owner's are `OWNER_CHANNEL`, and without Discord there is none.
|
|
88
|
+
*/
|
|
89
|
+
direct?: Readonly<Record<string, ChannelKey>>;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the stand-in provider reaches the network, read at each `reaches`, which throws
|
|
92
|
+
* while it does not; `knows` answers from its records either way. Default always.
|
|
93
|
+
*/
|
|
94
|
+
online?: () => boolean;
|
|
95
|
+
/** The host's owners, as `IDENTITY` lists them; by default there is no identity service. */
|
|
96
|
+
owners?: readonly Principal[];
|
|
97
|
+
/** The operator's tool tiers; by default none, so notify stays the owners'. */
|
|
98
|
+
toolTiers?: Readonly<Record<string, Tier>>;
|
|
99
|
+
/** The modules' options besides the test's own. */
|
|
100
|
+
modules?: Partial<ModulesOptions>;
|
|
101
|
+
} = {},
|
|
38
102
|
): Promise<{
|
|
103
|
+
plugin: RoundtablePlugin;
|
|
39
104
|
contribution: Contribution;
|
|
40
105
|
record: ModuleRecord;
|
|
41
106
|
services: ServiceRegistry;
|
|
@@ -44,13 +109,31 @@ export async function setUpModules(
|
|
|
44
109
|
threadOrigins: [],
|
|
45
110
|
reportChannels: [],
|
|
46
111
|
ownerChannelAsked: 0,
|
|
112
|
+
directAsked: [],
|
|
113
|
+
notified: [],
|
|
114
|
+
posted: [],
|
|
47
115
|
};
|
|
116
|
+
const direct =
|
|
117
|
+
options.direct ?? (options.discord === false ? undefined : DIRECT);
|
|
118
|
+
const providers: DirectChannelProvider[] = direct
|
|
119
|
+
? [
|
|
120
|
+
{
|
|
121
|
+
name: "discord",
|
|
122
|
+
label: "a direct message on Discord",
|
|
123
|
+
knows: async (principalId) => direct[principalId] !== undefined,
|
|
124
|
+
reaches: async (principalId) => {
|
|
125
|
+
record.directAsked.push(principalId);
|
|
126
|
+
if (options.online?.() === false)
|
|
127
|
+
throw new Error("Discord is unreachable");
|
|
128
|
+
return direct[principalId];
|
|
129
|
+
},
|
|
130
|
+
deliver: async (principalId, text) =>
|
|
131
|
+
void record.notified.push({ principalId, text }),
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
: [];
|
|
48
135
|
const plugin = modulesPlugin({
|
|
49
|
-
owner:
|
|
50
|
-
id: "1",
|
|
51
|
-
name: "Owner",
|
|
52
|
-
pronouns: { subject: "they", object: "them", possessive: "their" },
|
|
53
|
-
},
|
|
136
|
+
owner: MODULES_OWNER,
|
|
54
137
|
assistant: "Assistant",
|
|
55
138
|
// SAFETY: a worker of the test's own replaces the one that would read the runtime.
|
|
56
139
|
modelRuntime: {} as ModelRuntime,
|
|
@@ -61,58 +144,86 @@ export async function setUpModules(
|
|
|
61
144
|
thinking: "low",
|
|
62
145
|
worker: { run: async () => "found it" },
|
|
63
146
|
},
|
|
147
|
+
...(options.errorReporter ? { errorReporter: options.errorReporter } : {}),
|
|
148
|
+
...options.modules,
|
|
64
149
|
});
|
|
65
150
|
const services = new ServiceRegistry([plugin]);
|
|
66
151
|
// SAFETY: the tools under test read no store; each stub is asked for nothing else.
|
|
67
152
|
services.preset(MEMORY, {} as MemoryStore);
|
|
68
|
-
services.preset(SCHEDULES, {} as ScheduleStore);
|
|
69
|
-
|
|
153
|
+
services.preset(SCHEDULES, options.schedules ?? ({} as ScheduleStore));
|
|
154
|
+
if (options.conversations)
|
|
155
|
+
// SAFETY: the schedule tools only read a conversation's record.
|
|
156
|
+
services.preset(
|
|
157
|
+
CONVERSATIONS,
|
|
158
|
+
options.conversations as unknown as ConversationRegistry,
|
|
159
|
+
);
|
|
160
|
+
if (options.owners) {
|
|
161
|
+
const owners = options.owners;
|
|
162
|
+
// SAFETY: the modules ask the identity service for the owners and a principal by id only.
|
|
163
|
+
services.preset(IDENTITY, {
|
|
164
|
+
owners: async () => owners,
|
|
165
|
+
principal: async (id: string) => owners.find((owner) => owner.id === id),
|
|
166
|
+
} as unknown as IdentityService);
|
|
167
|
+
}
|
|
70
168
|
if (options.agentChannelOf)
|
|
169
|
+
// SAFETY: the schedule tools ask the agent team for a channel and nothing else.
|
|
71
170
|
services.preset(AGENTS, {
|
|
72
171
|
team: { channelOf: options.agentChannelOf },
|
|
73
172
|
} as unknown as AgentServer);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
173
|
+
if (options.discord !== false)
|
|
174
|
+
// SAFETY: the tools under test use the connection's owner channel and the threads' open only.
|
|
175
|
+
services.preset(DISCORD, {
|
|
176
|
+
connection: {
|
|
177
|
+
ownerChannel: async () => {
|
|
178
|
+
record.ownerChannelAsked += 1;
|
|
179
|
+
return OWNER_CHANNEL;
|
|
180
|
+
},
|
|
181
|
+
ownerOperations: () => ({}),
|
|
80
182
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return undefined;
|
|
183
|
+
threads: {
|
|
184
|
+
open: async (origin: ChannelKey | undefined) => {
|
|
185
|
+
record.threadOrigins.push(origin);
|
|
186
|
+
return undefined;
|
|
187
|
+
},
|
|
87
188
|
},
|
|
189
|
+
guard: {},
|
|
190
|
+
commands: { add: () => undefined },
|
|
191
|
+
} as unknown as DiscordServices);
|
|
192
|
+
const owns =
|
|
193
|
+
options.owns ?? ((channel: ChannelKey) => channel.startsWith("discord:"));
|
|
194
|
+
// Only Discord's channels have a surface; any other conversation has none to report in.
|
|
195
|
+
const surfaces = surfacePort(() => [
|
|
196
|
+
{
|
|
197
|
+
surface: "discord",
|
|
198
|
+
start: async () => undefined,
|
|
199
|
+
sendReply: async (channel, reply) =>
|
|
200
|
+
void record.posted.push({ channel, text: reply.chunks.join("") }),
|
|
88
201
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} as unknown as DiscordServices);
|
|
92
|
-
// SAFETY: setup reads only the logger, the conversations' background and targets, the surfaces, and the services.
|
|
202
|
+
]);
|
|
203
|
+
// SAFETY: setup and the preflight read only the logger, the conversations' background, targets and owners, the surfaces, the direct channels, and the services.
|
|
93
204
|
const context = {
|
|
94
205
|
logger: silentLogger(),
|
|
95
206
|
conversations: {
|
|
96
207
|
target: (name: string) =>
|
|
97
|
-
name ===
|
|
208
|
+
options.ownerTarget !== false && name === PERSONAL_TARGET.name
|
|
209
|
+
? PERSONAL_TARGET
|
|
210
|
+
: undefined,
|
|
211
|
+
owns,
|
|
212
|
+
takesBackground: options.takesBackground ?? owns,
|
|
213
|
+
takesSystemReports: options.takesSystemReports ?? owns,
|
|
98
214
|
background: async (turn: { channel: ChannelKey }) => {
|
|
99
215
|
record.reportChannels.push(turn.channel);
|
|
100
216
|
return { status: "ran" };
|
|
101
217
|
},
|
|
102
218
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
surface: "discord",
|
|
107
|
-
start: async () => undefined,
|
|
108
|
-
sendReply: async () => undefined,
|
|
109
|
-
},
|
|
110
|
-
]),
|
|
219
|
+
surfaces,
|
|
220
|
+
directChannels: directChannelsPort(() => providers, surfaces),
|
|
221
|
+
toolTiers: toolTiers(options.toolTiers),
|
|
111
222
|
} as unknown as Omit<PluginContext, "services">;
|
|
112
223
|
const contribution = await services.setUp(plugin, () =>
|
|
113
224
|
plugin.setup(pluginContext(plugin, context, services.forPlugin(plugin))),
|
|
114
225
|
);
|
|
115
|
-
return { contribution, record, services };
|
|
226
|
+
return { plugin, contribution, record, services };
|
|
116
227
|
}
|
|
117
228
|
|
|
118
229
|
/** What each addon plugin contributes, set up over stand-ins as far as setup reads them. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Principal } from "../identity/principal-store.ts";
|
|
1
2
|
import type { OwnerIdentity } from "../identity.ts";
|
|
2
3
|
import type { Speaker } from "../speakers.ts";
|
|
3
4
|
|
|
@@ -7,9 +8,25 @@ export const TEST_OWNER: OwnerIdentity = {
|
|
|
7
8
|
pronouns: { subject: "he", object: "him", possessive: "his" },
|
|
8
9
|
};
|
|
9
10
|
|
|
11
|
+
/** The owner as a principal of the tests, its id the owner speaker's, as for an owner carried over from 0.8. */
|
|
12
|
+
export const OWNER_PRINCIPAL: Principal = {
|
|
13
|
+
id: "1",
|
|
14
|
+
displayName: TEST_OWNER.name,
|
|
15
|
+
pronouns: "he",
|
|
16
|
+
disabled: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** A member as a principal of the tests. */
|
|
20
|
+
export const MEMBER_PRINCIPAL: Principal = {
|
|
21
|
+
id: "2",
|
|
22
|
+
displayName: "Bo",
|
|
23
|
+
disabled: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
10
26
|
/** The owner as a speaker of the tests. */
|
|
11
27
|
export const OWNER_SPEAKER: Speaker = {
|
|
12
28
|
id: "1",
|
|
13
29
|
name: TEST_OWNER.name,
|
|
14
30
|
tier: "owner",
|
|
31
|
+
principalId: OWNER_PRINCIPAL.id,
|
|
15
32
|
};
|