pi-roundtable 0.8.0 → 0.9.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 +115 -0
- package/README.md +35 -11
- package/README.zh-TW.md +27 -10
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +618 -86
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.ts +5 -1
- 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 +5 -2
- 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 +115 -1
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +7 -0
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +5 -0
- package/src/cli/report.ts +12 -3
- 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/team-turns.ts +8 -4
- package/src/core/builtin/agent-server.ts +9 -7
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +150 -86
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +64 -1
- package/src/core/config/config.ts +87 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +82 -9
- package/src/core/contract/runtime.ts +23 -7
- package/src/core/contract/surface.ts +22 -6
- package/src/core/conversations/conversation-registry.ts +14 -0
- package/src/core/conversations/conversation-store.ts +13 -0
- package/src/core/define-roundtable.ts +42 -11
- 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-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 -97
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +7 -2
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +27 -2
- package/src/core/drain.ts +56 -5
- package/src/core/errors.ts +13 -0
- package/src/core/host-background.ts +56 -0
- package/src/core/host.ts +52 -68
- package/src/core/i18n/cards.ts +8 -4
- package/src/core/i18n/conversation.ts +2 -0
- 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 +56 -4
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/plugin.ts +48 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/routing/channel-queue.ts +18 -1
- package/src/core/routing/channel-router.ts +274 -21
- package/src/core/routing/conversation-turns.ts +45 -10
- package/src/core/routing/surface-port.ts +32 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/abort-guard.ts +58 -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 +139 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +28 -14
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/pending-confirmation-store.ts +30 -5
- package/src/core/runtime/pi-agent-runtime.ts +174 -190
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +4 -4
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +51 -5
- package/src/core/runtime/runtime-types.ts +48 -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 +21 -3
- package/src/core/runtime/thinking-level.ts +47 -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-wrapup.ts +63 -0
- package/src/core/services.ts +27 -1
- package/src/core/sessions.ts +30 -0
- 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 +108 -21
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +118 -41
- package/src/core/testing/test-host.ts +79 -16
- 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 +8 -0
- package/src/index.ts +48 -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 +55 -16
- package/templates/.env.example +1 -1
- package/templates/adapters/web/README.md +7 -0
- package/templates/adapters/web/roundtable.config.ts +14 -10
- package/templates/roundtable.config.ts +12 -1
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -2,8 +2,12 @@ import { describe, test } from "bun:test";
|
|
|
2
2
|
import type { InboundMessage } from "../contract/channels.ts";
|
|
3
3
|
import type { ChatSurface } from "../contract/surface.ts";
|
|
4
4
|
import { parseChannelKey } from "../contract/surface.ts";
|
|
5
|
-
import type { Approval } from "../domain/owner-prompts.ts";
|
|
6
5
|
import type { TurnProgress } from "../domain/progress.ts";
|
|
6
|
+
import {
|
|
7
|
+
type Approval,
|
|
8
|
+
type PromptScope,
|
|
9
|
+
promptScope,
|
|
10
|
+
} from "../interactions/prompts.ts";
|
|
7
11
|
import type { ChannelKey } from "../sessions.ts";
|
|
8
12
|
import type { Speaker } from "../speakers.ts";
|
|
9
13
|
|
|
@@ -33,23 +37,34 @@ export interface SurfaceContractSubject {
|
|
|
33
37
|
/** The person answers an open approval; without it the prompt checks are skipped. */
|
|
34
38
|
answer?(prompt: string, approved: boolean): Promise<void>;
|
|
35
39
|
/**
|
|
36
|
-
* The person as the speaker of the turns that ask them, given to `surface.prompts
|
|
37
|
-
* owner-tier speaker. Every approval the
|
|
38
|
-
* it is theirs to answer
|
|
40
|
+
* The person as the speaker of the turns that ask them, given to `surface.prompts` as the
|
|
41
|
+
* scope `promptScope(speaker, visibility)`; default an owner-tier speaker. Every approval the
|
|
42
|
+
* contract asks for needs exactly the speaker's tier, so it is theirs to answer; one above a
|
|
43
|
+
* speaker below the owner tier checks that a private conversation escalates to no one.
|
|
39
44
|
*/
|
|
40
45
|
speaker?: Speaker;
|
|
41
46
|
/**
|
|
42
|
-
* Someone else on the surface's network, not
|
|
43
|
-
* prompt and answers it; without it the responder-scope check is
|
|
47
|
+
* Someone else on the surface's network, another principal and not an owner, who knows the
|
|
48
|
+
* id of the person's open prompt and answers it; without it the responder-scope check is
|
|
49
|
+
* skipped.
|
|
44
50
|
*/
|
|
45
|
-
stranger?:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
stranger?: SurfaceContractAnswerer;
|
|
52
|
+
/**
|
|
53
|
+
* An owner on the surface's network, another principal than the person, who knows the id of
|
|
54
|
+
* the person's open prompt in their private conversation and answers it; without it that
|
|
55
|
+
* check is skipped.
|
|
56
|
+
*/
|
|
57
|
+
owner?: SurfaceContractAnswerer;
|
|
50
58
|
close?(): Promise<void>;
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
/** Someone on the network other than the person, who answers a prompt by its id. */
|
|
62
|
+
export interface SurfaceContractAnswerer {
|
|
63
|
+
/** They join the surface, once it has started. */
|
|
64
|
+
join?(): Promise<void>;
|
|
65
|
+
answer(prompt: string, approved: boolean): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/** One way the subject broke the contract. */
|
|
54
69
|
export interface SurfaceContractFailure {
|
|
55
70
|
name: string;
|
|
@@ -93,7 +108,18 @@ const seen = (running: Running, kind: SurfaceObservation["kind"]) =>
|
|
|
93
108
|
running.subject.observations().filter((o) => o.kind === kind);
|
|
94
109
|
|
|
95
110
|
const speakerOf = (subject: SurfaceContractSubject): Speaker =>
|
|
96
|
-
subject.speaker ?? {
|
|
111
|
+
subject.speaker ?? {
|
|
112
|
+
id: "contract-speaker",
|
|
113
|
+
name: "Ada",
|
|
114
|
+
tier: "owner",
|
|
115
|
+
principalId: "contract-speaker",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The person's prompt scope in a conversation of this visibility. */
|
|
119
|
+
const scopeOf = (
|
|
120
|
+
subject: SurfaceContractSubject,
|
|
121
|
+
visibility: "private" | "shared" = "shared",
|
|
122
|
+
): PromptScope => promptScope(speakerOf(subject), visibility);
|
|
97
123
|
|
|
98
124
|
/** The newest prompt the person has seen, once one more than `before` is open. */
|
|
99
125
|
async function nextPrompt(running: Running, before: number): Promise<string> {
|
|
@@ -109,7 +135,7 @@ async function nextPrompt(running: Running, before: number): Promise<string> {
|
|
|
109
135
|
/** Asks for one approval and answers it as the person does. */
|
|
110
136
|
async function approval(running: Running, approved: boolean) {
|
|
111
137
|
const { surface, channel, answer } = running.subject;
|
|
112
|
-
const prompts = surface.prompts?.(channel,
|
|
138
|
+
const prompts = surface.prompts?.(channel, scopeOf(running.subject));
|
|
113
139
|
if (!prompts || !answer)
|
|
114
140
|
throw new ContractBroken("the surface shows no prompts");
|
|
115
141
|
const before = seen(running, "prompt").length;
|
|
@@ -264,7 +290,7 @@ const CHECKS: readonly Check[] = [
|
|
|
264
290
|
subject.surface.prompts !== undefined && subject.answer !== undefined,
|
|
265
291
|
run: async (running) => {
|
|
266
292
|
const { surface, channel } = running.subject;
|
|
267
|
-
const prompts = surface.prompts?.(channel,
|
|
293
|
+
const prompts = surface.prompts?.(channel, scopeOf(running.subject));
|
|
268
294
|
if (!prompts) throw new ContractBroken("the surface shows no prompts");
|
|
269
295
|
const stop = new AbortController();
|
|
270
296
|
const before = seen(running, "prompt").length;
|
|
@@ -293,39 +319,89 @@ const CHECKS: readonly Check[] = [
|
|
|
293
319
|
subject.surface.prompts !== undefined &&
|
|
294
320
|
subject.answer !== undefined &&
|
|
295
321
|
subject.stranger !== undefined,
|
|
322
|
+
run: (running) =>
|
|
323
|
+
answeredOnlyByThem(running, running.subject.stranger, "shared"),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "another principal cannot answer for the person in their private conversation, even an owner",
|
|
327
|
+
applies: (subject) =>
|
|
328
|
+
subject.surface.prompts !== undefined &&
|
|
329
|
+
subject.answer !== undefined &&
|
|
330
|
+
subject.owner !== undefined,
|
|
331
|
+
run: (running) =>
|
|
332
|
+
answeredOnlyByThem(running, running.subject.owner, "private"),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "a private conversation's approval above the person's tier goes to no one: it expires at once",
|
|
336
|
+
applies: (subject) =>
|
|
337
|
+
subject.surface.prompts !== undefined &&
|
|
338
|
+
speakerOf(subject).tier !== "owner",
|
|
296
339
|
run: async (running) => {
|
|
297
|
-
const { surface, channel
|
|
298
|
-
const prompts = surface.prompts?.(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
340
|
+
const { surface, channel } = running.subject;
|
|
341
|
+
const prompts = surface.prompts?.(
|
|
342
|
+
channel,
|
|
343
|
+
scopeOf(running.subject, "private"),
|
|
344
|
+
);
|
|
345
|
+
if (!prompts) throw new ContractBroken("the surface shows no prompts");
|
|
302
346
|
const before = seen(running, "prompt").length;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.
|
|
306
|
-
|
|
307
|
-
"Send the report.",
|
|
308
|
-
undefined,
|
|
309
|
-
speakerOf(running.subject).tier,
|
|
310
|
-
)
|
|
311
|
-
.then((value) => {
|
|
312
|
-
settled = value;
|
|
313
|
-
return value;
|
|
314
|
-
});
|
|
315
|
-
const id = await nextPrompt(running, before);
|
|
316
|
-
await stranger.answer(id, true);
|
|
317
|
-
await Bun.sleep(50);
|
|
347
|
+
const answer = await Promise.race([
|
|
348
|
+
prompts.confirm("Approve?", "Delete the notes.", undefined, "owner"),
|
|
349
|
+
Bun.sleep(1000).then(() => "still open" as const),
|
|
350
|
+
]);
|
|
318
351
|
expectThat(
|
|
319
|
-
|
|
320
|
-
`
|
|
352
|
+
answer === "expired",
|
|
353
|
+
`an approval no one may answer came back ${answer}`,
|
|
354
|
+
);
|
|
355
|
+
expectThat(
|
|
356
|
+
seen(running, "prompt").length === before,
|
|
357
|
+
"an approval no one may answer was shown",
|
|
321
358
|
);
|
|
322
|
-
await answer(id, false);
|
|
323
|
-
const own = await result;
|
|
324
|
-
expectThat(own === "declined", `the person's answer came back ${own}`);
|
|
325
359
|
},
|
|
326
360
|
},
|
|
327
361
|
];
|
|
328
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Opens the person's approval in a conversation of `visibility`, has `other` answer it by its id,
|
|
365
|
+
* and checks that only the person's own answer settles it.
|
|
366
|
+
*/
|
|
367
|
+
async function answeredOnlyByThem(
|
|
368
|
+
running: Running,
|
|
369
|
+
other: SurfaceContractAnswerer | undefined,
|
|
370
|
+
visibility: "private" | "shared",
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
const { surface, channel, answer } = running.subject;
|
|
373
|
+
const prompts = surface.prompts?.(
|
|
374
|
+
channel,
|
|
375
|
+
scopeOf(running.subject, visibility),
|
|
376
|
+
);
|
|
377
|
+
if (!prompts || !answer || !other)
|
|
378
|
+
throw new ContractBroken("the surface shows no prompts");
|
|
379
|
+
await other.join?.();
|
|
380
|
+
const before = seen(running, "prompt").length;
|
|
381
|
+
let settled: Approval | undefined;
|
|
382
|
+
const result = prompts
|
|
383
|
+
.confirm(
|
|
384
|
+
"Approve?",
|
|
385
|
+
"Send the report.",
|
|
386
|
+
undefined,
|
|
387
|
+
speakerOf(running.subject).tier,
|
|
388
|
+
)
|
|
389
|
+
.then((value) => {
|
|
390
|
+
settled = value;
|
|
391
|
+
return value;
|
|
392
|
+
});
|
|
393
|
+
const id = await nextPrompt(running, before);
|
|
394
|
+
await other.answer(id, true);
|
|
395
|
+
await Bun.sleep(50);
|
|
396
|
+
expectThat(
|
|
397
|
+
settled === undefined,
|
|
398
|
+
`another principal's answer settled the approval as ${settled}`,
|
|
399
|
+
);
|
|
400
|
+
await answer(id, false);
|
|
401
|
+
const own = await result;
|
|
402
|
+
expectThat(own === "declined", `the person's answer came back ${own}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
329
405
|
async function runCheck(
|
|
330
406
|
check: Check,
|
|
331
407
|
make: () => Promise<SurfaceContractSubject>,
|
|
@@ -366,8 +442,9 @@ export async function checkSurfaceContract(
|
|
|
366
442
|
* Registers the chat surface contract as one test per check, under `name`: the surface names a
|
|
367
443
|
* prefix and owns its channel, delivers what its person writes, shows every chunk of a reply, and,
|
|
368
444
|
* where it offers them, delivers files, ends typing and stop controls idempotently, shows
|
|
369
|
-
* progress, resolves approvals as the person answers or as a stopped turn cancels them,
|
|
370
|
-
*
|
|
445
|
+
* progress, resolves approvals as the person answers or as a stopped turn cancels them, ignores
|
|
446
|
+
* another person's answer to the person's approval, and, in a private conversation, an owner's
|
|
447
|
+
* too, and lets an approval above the person's tier there expire at once, shown to no one. Each
|
|
371
448
|
* check gets a fresh subject from `make`; a check for an optional part the surface lacks passes.
|
|
372
449
|
*/
|
|
373
450
|
export function describeSurfaceContract(
|
|
@@ -11,27 +11,32 @@ import { type RoundtableConfig, resolveConfig } from "../config/config.ts";
|
|
|
11
11
|
import type { ConversationPort } from "../contract/channels.ts";
|
|
12
12
|
import type { AgentRuntime } from "../contract/runtime.ts";
|
|
13
13
|
import type { ChatSurface } from "../contract/surface.ts";
|
|
14
|
-
import { defineRoundtable } from "../define-roundtable.ts";
|
|
14
|
+
import { defineRoundtable, discordOwnerOf } from "../define-roundtable.ts";
|
|
15
15
|
import { CommandCollection } from "../discord/command-collection.ts";
|
|
16
16
|
import type { ComposedCommands } from "../discord/compose-commands.ts";
|
|
17
17
|
import type { DiscordConnection } from "../discord/connection.ts";
|
|
18
|
+
import { discordDirectChannel } from "../discord/direct-channel.ts";
|
|
18
19
|
import type {
|
|
19
20
|
CommandGuard,
|
|
20
21
|
InteractionContribution,
|
|
21
22
|
} from "../discord/interaction-module.ts";
|
|
22
|
-
import {
|
|
23
|
+
import { commandGuard, ownerRootCommand } from "../discord/owner-command.ts";
|
|
23
24
|
import { Roundtable } from "../host.ts";
|
|
24
25
|
import { silentLogger } from "../log.ts";
|
|
25
26
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
26
27
|
import { CompactionTiers } from "../runtime/compaction-tiers.ts";
|
|
28
|
+
import { privateCompaction } from "../runtime/extensions/private-memory.ts";
|
|
27
29
|
import { sessionExtensions } from "../runtime/runtime-types.ts";
|
|
30
|
+
import { IDENTITY } from "../services.ts";
|
|
28
31
|
import type { AgentTurnScope, SessionContext } from "../sessions.ts";
|
|
32
|
+
import { THE_SPEAKER } from "../speakers.ts";
|
|
29
33
|
import { testDatabaseUrl } from "./database.ts";
|
|
30
34
|
|
|
31
35
|
export interface TestHostOptions {
|
|
32
36
|
/**
|
|
33
|
-
* Over a test configuration:
|
|
34
|
-
* (`ROUNDTABLE_TEST_DATABASE_URL`).
|
|
37
|
+
* Over a test configuration: the owner Ada in `access`, a guild, a temporary `dataDir`, and
|
|
38
|
+
* the test database (`ROUNDTABLE_TEST_DATABASE_URL`). Giving the 0.8 `owner` or `speakers`
|
|
39
|
+
* replaces the default `access`.
|
|
35
40
|
*/
|
|
36
41
|
config?: Partial<RoundtableConfig>;
|
|
37
42
|
/** The app's plugins, after the built-in ones, as `defineRoundtable` places them. */
|
|
@@ -73,17 +78,28 @@ export interface TestHost {
|
|
|
73
78
|
stop(): Promise<void>;
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
/** The test host's owner, Ada, as `access` writes her: her principal is her Discord user id. */
|
|
82
|
+
const TEST_ACCESS: NonNullable<RoundtableConfig["access"]> = {
|
|
83
|
+
owners: [
|
|
84
|
+
{
|
|
85
|
+
name: "Ada",
|
|
86
|
+
principal: "100000000000000001",
|
|
87
|
+
identities: ["discord:100000000000000001"],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
76
92
|
function defaultConfig(withDiscord: boolean): RoundtableConfig {
|
|
77
93
|
const dataDir = mkdtempSync(join(tmpdir(), "roundtable-test-host-"));
|
|
78
94
|
if (!withDiscord)
|
|
79
95
|
return {
|
|
80
|
-
|
|
96
|
+
access: TEST_ACCESS,
|
|
81
97
|
database: { url: testDatabaseUrl },
|
|
82
98
|
dataDir,
|
|
83
99
|
model: "anthropic/claude-sonnet-5-5",
|
|
84
100
|
};
|
|
85
101
|
return {
|
|
86
|
-
|
|
102
|
+
access: TEST_ACCESS,
|
|
87
103
|
discord: {
|
|
88
104
|
token: "token",
|
|
89
105
|
guild: "900000000000000001",
|
|
@@ -122,10 +138,14 @@ function quietRuntime(): AgentRuntime {
|
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
/** A Discord that connects to nothing, recording the commands added to it, standing in for the built-in plugin that does. */
|
|
125
|
-
function standInDiscord(
|
|
141
|
+
export function standInDiscord(
|
|
126
142
|
commands: CommandCollection,
|
|
127
143
|
guard: CommandGuard,
|
|
128
144
|
given: TestHostOptions["discord"],
|
|
145
|
+
/** The primary owner's Discord user id, whose direct messages are the owner channel. */
|
|
146
|
+
ownerId: string,
|
|
147
|
+
/** What `DISCORD.channelContext` answers; by default no context, as on a host with it off. */
|
|
148
|
+
channelContext: DiscordServices["channelContext"] = async () => undefined,
|
|
129
149
|
): RoundtablePlugin {
|
|
130
150
|
const surface: ChatSurface = {
|
|
131
151
|
surface: "discord",
|
|
@@ -137,6 +157,18 @@ function standInDiscord(
|
|
|
137
157
|
provides: [DISCORD],
|
|
138
158
|
replaces: [DISCORD],
|
|
139
159
|
setup: ({ services }) => {
|
|
160
|
+
const identity = services.find(IDENTITY);
|
|
161
|
+
const ownerChannel =
|
|
162
|
+
(given ? given.ownerChannel : undefined) ??
|
|
163
|
+
(async () => "discord:owner-dm" as const);
|
|
164
|
+
// Each person's direct messages, as the Discord plugin's provider reaches them; the owner's is the owner channel.
|
|
165
|
+
const direct = discordDirectChannel({
|
|
166
|
+
directChannel: async (userId) =>
|
|
167
|
+
userId === ownerId ? ownerChannel() : `discord:dm-${userId}`,
|
|
168
|
+
sendDirect: async () => undefined,
|
|
169
|
+
...(identity ? { identity } : {}),
|
|
170
|
+
ownerId,
|
|
171
|
+
});
|
|
140
172
|
// SAFETY: the plugins after it read only these members while they set up; the rest is read when a tool runs, which a test host does not do.
|
|
141
173
|
services.provide(DISCORD, {
|
|
142
174
|
connection: {
|
|
@@ -150,8 +182,9 @@ function standInDiscord(
|
|
|
150
182
|
commands: commands.registrar,
|
|
151
183
|
guard,
|
|
152
184
|
threads: { open: async () => undefined, sweep: async () => undefined },
|
|
185
|
+
channelContext,
|
|
153
186
|
} as unknown as DiscordServices);
|
|
154
|
-
return { surfaces: [surface] };
|
|
187
|
+
return { surfaces: [surface], directChannels: [direct] };
|
|
155
188
|
},
|
|
156
189
|
};
|
|
157
190
|
}
|
|
@@ -168,8 +201,8 @@ function ignored(): Ignored {
|
|
|
168
201
|
/** The tools a factory registers, by name; every other registration (handlers, commands, events) is accepted and ignored. */
|
|
169
202
|
async function registeredBy(factory: ExtensionFactory): Promise<string[]> {
|
|
170
203
|
const names: string[] = [];
|
|
171
|
-
// SAFETY: a session factory only registers tools, handlers and commands; the fake records the tool names and ignores the rest.
|
|
172
204
|
const registerTool = (tool: { name: string }) => names.push(tool.name);
|
|
205
|
+
// SAFETY: a session factory only registers tools, handlers and commands; the fake records the tool names and ignores the rest.
|
|
173
206
|
const api = new Proxy(
|
|
174
207
|
{},
|
|
175
208
|
{
|
|
@@ -188,6 +221,7 @@ const CORE_EXTENSIONS = {
|
|
|
188
221
|
confirmationGate: noop,
|
|
189
222
|
askUser: noop,
|
|
190
223
|
selfCompactGuard: noop,
|
|
224
|
+
privateMemory: noop,
|
|
191
225
|
activeTools: noop,
|
|
192
226
|
};
|
|
193
227
|
|
|
@@ -211,17 +245,31 @@ export async function testHost(
|
|
|
211
245
|
};
|
|
212
246
|
const withDiscord = options.discord !== false;
|
|
213
247
|
const given = options.discord === false ? undefined : options.discord;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
248
|
+
const defaults = defaultConfig(withDiscord);
|
|
249
|
+
// A test that writes the 0.8 owner or speakers gets them in place of the default access.
|
|
250
|
+
if (options.config?.owner || options.config?.speakers) delete defaults.access;
|
|
251
|
+
const base: RoundtableConfig = { ...defaults, ...options.config };
|
|
218
252
|
const resolved = resolveConfig(base);
|
|
219
253
|
const rootCommand = resolved.slug;
|
|
220
|
-
|
|
254
|
+
// Like the Discord plugin's, the guard asks the host's identity service, once the host is up.
|
|
255
|
+
const identity = () => captured?.services.find(IDENTITY);
|
|
256
|
+
const guard = commandGuard({
|
|
257
|
+
ownerId: discordOwnerOf(resolved).id,
|
|
258
|
+
identity: {
|
|
259
|
+
resolve: async (facts, scope) => identity()?.resolve(facts, scope),
|
|
260
|
+
owners: async () => (await identity()?.owners()) ?? [],
|
|
261
|
+
identities: async (principalId) =>
|
|
262
|
+
(await identity()?.identities(principalId)) ?? [],
|
|
263
|
+
},
|
|
264
|
+
root: rootCommand,
|
|
265
|
+
logger: silentLogger(),
|
|
266
|
+
});
|
|
221
267
|
const config: RoundtableConfig = {
|
|
222
268
|
...base,
|
|
223
269
|
plugins: [
|
|
224
|
-
...(withDiscord
|
|
270
|
+
...(withDiscord
|
|
271
|
+
? [standInDiscord(commands, guard, given, discordOwnerOf(resolved).id)]
|
|
272
|
+
: []),
|
|
225
273
|
{
|
|
226
274
|
name: "test-runtime",
|
|
227
275
|
providers: { runtime: () => runtime },
|
|
@@ -261,9 +309,24 @@ export async function testHost(
|
|
|
261
309
|
kind: scope ? "agent" : "owner",
|
|
262
310
|
homeChannel: scope?.home ?? ownerChannel,
|
|
263
311
|
turnChannel: turnChannelOf(scope) ?? ownerChannel,
|
|
312
|
+
// Shared, as the runtime makes it, so its compactor summarizes no one's private memory.
|
|
264
313
|
compaction: {
|
|
265
|
-
wrap: (compactor) =>
|
|
314
|
+
wrap: (compactor) =>
|
|
315
|
+
tiers.wrapCompactor(
|
|
316
|
+
compactor,
|
|
317
|
+
() => undefined,
|
|
318
|
+
(event) =>
|
|
319
|
+
privateCompaction(
|
|
320
|
+
event.preparation,
|
|
321
|
+
event.branchEntries.flatMap((entry) =>
|
|
322
|
+
entry.type === "message" ? [entry.message] : [],
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
),
|
|
266
326
|
},
|
|
327
|
+
conversation: { visibility: "shared" },
|
|
328
|
+
addressee: THE_SPEAKER,
|
|
329
|
+
memory: "speaker",
|
|
267
330
|
speaker: () => undefined,
|
|
268
331
|
runTask: async () => {
|
|
269
332
|
throw new Error("a test host session cannot run tasks");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ChannelKey } from "../domain/conversation.ts";
|
|
2
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
1
3
|
import { ASK_USER_TOOL } from "../runtime/extensions/ask-user.ts";
|
|
2
4
|
import { COMPACT_TOOL } from "../runtime/extensions/self-compact-guard.ts";
|
|
3
5
|
import { AGENTS } from "../services.ts";
|
|
@@ -16,6 +18,36 @@ export const seat: AgentTurnScope = {
|
|
|
16
18
|
group: "war-room",
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The conversations the recorded sessions live in, served as a host serves them: the owner's
|
|
23
|
+
* direct messages by an app's claim, such as one that answers the owner there, and the agent's
|
|
24
|
+
* channel by the agent server; both answer background turns, so their sessions schedule and
|
|
25
|
+
* delegate. Give it to the test host the tool set is captured on.
|
|
26
|
+
*/
|
|
27
|
+
export function servedConversations(
|
|
28
|
+
ownerChannel: ChannelKey,
|
|
29
|
+
): RoundtablePlugin {
|
|
30
|
+
const served = new Set<ChannelKey>([ownerChannel, scout.home, seat.home]);
|
|
31
|
+
return {
|
|
32
|
+
name: "served-conversations",
|
|
33
|
+
setup: () => ({
|
|
34
|
+
channels: [
|
|
35
|
+
{
|
|
36
|
+
name: "served-conversations",
|
|
37
|
+
priority: 0,
|
|
38
|
+
owns: (channel) => served.has(channel),
|
|
39
|
+
admit: () => undefined,
|
|
40
|
+
background: async () => ({
|
|
41
|
+
status: "skipped",
|
|
42
|
+
reason: "the tool set's host runs no background turns",
|
|
43
|
+
}),
|
|
44
|
+
startFresh: async () => "owner",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
/** Tools the core's own extensions and pi-web-access register, which no fake session here loads. */
|
|
20
52
|
const NAMED_ELSEWHERE = [
|
|
21
53
|
ASK_USER_TOOL,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"memory_add",
|
|
38
38
|
"memory_remove",
|
|
39
39
|
"memory_search",
|
|
40
|
-
"
|
|
40
|
+
"notify",
|
|
41
41
|
"schedule_cancel",
|
|
42
42
|
"schedule_create",
|
|
43
43
|
"schedule_list",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"memory_remove",
|
|
93
93
|
"memory_search",
|
|
94
94
|
"message_agent",
|
|
95
|
-
"
|
|
95
|
+
"notify",
|
|
96
96
|
"schedule_cancel",
|
|
97
97
|
"schedule_create",
|
|
98
98
|
"schedule_list",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"memory_add",
|
|
180
180
|
"memory_remove",
|
|
181
181
|
"memory_search",
|
|
182
|
-
"
|
|
182
|
+
"notify",
|
|
183
183
|
"schedule_cancel",
|
|
184
184
|
"schedule_create",
|
|
185
185
|
"schedule_list",
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
"memory_remove": "member",
|
|
273
273
|
"memory_search": "member",
|
|
274
274
|
"message_agent": "member",
|
|
275
|
-
"
|
|
275
|
+
"notify": "owner",
|
|
276
276
|
"read": "owner",
|
|
277
277
|
"read_attachment": "member",
|
|
278
278
|
"schedule_cancel": "admin",
|
package/src/core/tool-tiers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PluginError } from "./errors.ts";
|
|
2
|
+
import type { Logger } from "./log.ts";
|
|
2
3
|
import { ASK_USER_TOOL } from "./runtime/extensions/ask-user.ts";
|
|
3
4
|
import { COMPACT_TOOL } from "./runtime/extensions/self-compact-guard.ts";
|
|
4
5
|
import { type Tier, tierAtLeast } from "./speakers.ts";
|
|
@@ -27,6 +28,40 @@ export const CORE_TOOL_TIERS: Readonly<Record<string, Tier>> = set("member", [
|
|
|
27
28
|
"read_attachment",
|
|
28
29
|
]);
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Tools renamed since 0.8, by their old name: until 1.0 the old name still names the tool in a
|
|
33
|
+
* selection, a task's `exclude`, a plugin's `requiredTools`, and the operator's and plugins'
|
|
34
|
+
* `toolTiers`, where the new name's own setting wins.
|
|
35
|
+
*/
|
|
36
|
+
export const RENAMED_TOOLS: Readonly<Record<string, string>> = {
|
|
37
|
+
notify_owner: "notify",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const warnedAliases = new WeakMap<Logger, Set<string>>();
|
|
41
|
+
|
|
42
|
+
/** Each logger warns once for an old name used in tiers or selections. */
|
|
43
|
+
function warnAliases(tools: readonly string[], logger: Logger): void {
|
|
44
|
+
const warned = warnedAliases.get(logger) ?? new Set<string>();
|
|
45
|
+
warnedAliases.set(logger, warned);
|
|
46
|
+
for (const tool of tools) {
|
|
47
|
+
const renamed = RENAMED_TOOLS[tool];
|
|
48
|
+
if (!renamed || warned.has(tool)) continue;
|
|
49
|
+
warned.add(tool);
|
|
50
|
+
logger.warn(
|
|
51
|
+
`deprecated: ${tool} is now ${renamed}; update selections and toolTiers before 1.0`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The tools named, each by its current name and once, in order. */
|
|
57
|
+
export function currentToolNames(
|
|
58
|
+
tools: readonly string[],
|
|
59
|
+
logger?: Logger,
|
|
60
|
+
): string[] {
|
|
61
|
+
if (logger) warnAliases(tools, logger);
|
|
62
|
+
return [...new Set(tools.map((tool) => RENAMED_TOOLS[tool] ?? tool))];
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
66
|
* What each tool needs: the operator's setting first, then the tier its plugin declared, then
|
|
32
67
|
* the core's default, and the owner for a tool nobody named. Plugins declare theirs when the
|
|
@@ -38,7 +73,15 @@ export class ToolTierTable implements ToolTiers {
|
|
|
38
73
|
readonly #core = new Map(Object.entries(CORE_TOOL_TIERS));
|
|
39
74
|
|
|
40
75
|
constructor(operator: Readonly<Record<string, Tier>> = {}) {
|
|
41
|
-
|
|
76
|
+
const entries = Object.entries(operator);
|
|
77
|
+
// An old name first, so the new name's own setting overrides it.
|
|
78
|
+
this.#operator = new Map([
|
|
79
|
+
...entries.flatMap(([tool, tier]) => {
|
|
80
|
+
const renamed = RENAMED_TOOLS[tool];
|
|
81
|
+
return renamed ? [[renamed, tier] as const] : [];
|
|
82
|
+
}),
|
|
83
|
+
...entries,
|
|
84
|
+
]);
|
|
42
85
|
}
|
|
43
86
|
|
|
44
87
|
/**
|
|
@@ -46,7 +89,16 @@ export class ToolTierTable implements ToolTiers {
|
|
|
46
89
|
* plugin declaring its own tool again, as when the host retries its start, replaces it.
|
|
47
90
|
*/
|
|
48
91
|
declare(plugin: string, tiers: Readonly<Record<string, Tier>>): void {
|
|
49
|
-
|
|
92
|
+
const entries = Object.entries(tiers);
|
|
93
|
+
// An old name is the new name's, declared first so the new name's own tier wins.
|
|
94
|
+
const named = [
|
|
95
|
+
...entries.flatMap(([tool, tier]) => {
|
|
96
|
+
const renamed = RENAMED_TOOLS[tool];
|
|
97
|
+
return renamed ? [[renamed, tier] as const] : [];
|
|
98
|
+
}),
|
|
99
|
+
...entries.filter(([tool]) => !RENAMED_TOOLS[tool]),
|
|
100
|
+
];
|
|
101
|
+
for (const [tool, tier] of named) {
|
|
50
102
|
const other = this.#declared.get(tool);
|
|
51
103
|
if (other && other.plugin !== plugin)
|
|
52
104
|
throw new PluginError(
|
|
@@ -73,11 +125,13 @@ export class ToolTierTable implements ToolTiers {
|
|
|
73
125
|
/** The core's tiers with the operator's on top; plugins add theirs through `declare`. */
|
|
74
126
|
export function toolTiers(
|
|
75
127
|
operator: Readonly<Record<string, Tier>> = {},
|
|
128
|
+
logger?: Logger,
|
|
76
129
|
): ToolTierTable {
|
|
130
|
+
if (logger) warnAliases(Object.keys(operator), logger);
|
|
77
131
|
return new ToolTierTable(operator);
|
|
78
132
|
}
|
|
79
133
|
|
|
80
|
-
/** The tools a tier may use, in order; a turn
|
|
134
|
+
/** The tools a tier may use, in order; a turn's tier is its speaker's, and a turn without one is refused. */
|
|
81
135
|
export function toolsForTier(
|
|
82
136
|
tools: readonly string[],
|
|
83
137
|
tier: Tier,
|
package/src/discord/index.ts
CHANGED
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
export type { DiscordServices } from "../core/builtin/discord.ts";
|
|
5
5
|
export { DISCORD } from "../core/builtin/discord.ts";
|
|
6
6
|
export type { DiscordAdapterConfig } from "../core/config/config.ts";
|
|
7
|
+
export type {
|
|
8
|
+
ChannelContext,
|
|
9
|
+
ChannelContextMessage,
|
|
10
|
+
ChannelContextOptions,
|
|
11
|
+
} from "../core/contract/channel-context.ts";
|
|
12
|
+
export { withChannelContext } from "../core/contract/channel-context.ts";
|
|
7
13
|
export type { AgentPanelMessage } from "../core/discord/agent-commands.ts";
|
|
8
14
|
export type {
|
|
9
15
|
AgentPanel,
|
|
10
16
|
AgentPanelOptions,
|
|
11
17
|
} from "../core/discord/agent-panel.ts";
|
|
12
18
|
export { agentPanel } from "../core/discord/agent-panel.ts";
|
|
19
|
+
export { CHANNEL_CONTEXT_DEFAULTS } from "../core/discord/channel-context.ts";
|
|
13
20
|
export type { ManagedChannel } from "../core/discord/channel-executor.ts";
|
|
14
21
|
export {
|
|
15
22
|
fetchManagedChannel,
|
|
@@ -35,6 +42,7 @@ export type {
|
|
|
35
42
|
DiscordConnection,
|
|
36
43
|
} from "../core/discord/connection.ts";
|
|
37
44
|
export { discord } from "../core/discord/discord-adapter.ts";
|
|
45
|
+
export type { DiscordActor } from "../core/discord/discord-owners.ts";
|
|
38
46
|
export type {
|
|
39
47
|
CommandGuard,
|
|
40
48
|
CommandRegistrar,
|