pi-roundtable 0.1.0 → 0.2.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 +138 -0
- package/README.md +6 -4
- 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 +9 -2
- 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
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
3
|
-
Admission,
|
|
4
|
-
ChannelClaim,
|
|
5
|
-
InboundMessage,
|
|
6
|
-
} from "../contract/channels.ts";
|
|
7
|
-
import { PluginError } from "../errors.ts";
|
|
8
|
-
import { silentLogger } from "../log.ts";
|
|
9
|
-
import type { ChannelKey } from "../sessions.ts";
|
|
10
|
-
import { ChannelQueue } from "./channel-queue.ts";
|
|
11
|
-
import { ChannelRouter, orderClaims } from "./channel-router.ts";
|
|
12
|
-
|
|
13
|
-
function message(overrides: Partial<InboundMessage> = {}): InboundMessage {
|
|
14
|
-
return {
|
|
15
|
-
channel: "discord:1",
|
|
16
|
-
messageId: "m1",
|
|
17
|
-
authorId: "owner",
|
|
18
|
-
authorName: "Riley",
|
|
19
|
-
authorIsBot: false,
|
|
20
|
-
isDirect: true,
|
|
21
|
-
mentionsBot: false,
|
|
22
|
-
repliesToBot: false,
|
|
23
|
-
text: "hi",
|
|
24
|
-
attachments: [],
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** A claim over `channels` (every channel when empty) that records what it does in `log`. */
|
|
30
|
-
function claim(
|
|
31
|
-
name: string,
|
|
32
|
-
priority: number,
|
|
33
|
-
log: string[],
|
|
34
|
-
options: {
|
|
35
|
-
channels?: string[];
|
|
36
|
-
guild?: string;
|
|
37
|
-
admit?: (message: InboundMessage) => Admission | undefined;
|
|
38
|
-
deletes?: boolean;
|
|
39
|
-
} = {},
|
|
40
|
-
): ChannelClaim {
|
|
41
|
-
const { channels = [], guild } = options;
|
|
42
|
-
return {
|
|
43
|
-
name,
|
|
44
|
-
priority,
|
|
45
|
-
owns: (channel, guildId) =>
|
|
46
|
-
channels.length === 0 ||
|
|
47
|
-
channels.includes(channel) ||
|
|
48
|
-
(guild !== undefined && guildId === guild),
|
|
49
|
-
admit:
|
|
50
|
-
options.admit ??
|
|
51
|
-
((m) => ({
|
|
52
|
-
kind: "turn",
|
|
53
|
-
run: async () => void log.push(`${name} ${m.text}`),
|
|
54
|
-
failure: `${name} failed`,
|
|
55
|
-
})),
|
|
56
|
-
background: async (turn) => {
|
|
57
|
-
log.push(`${name} background ${turn.text}`);
|
|
58
|
-
return { status: "ran" };
|
|
59
|
-
},
|
|
60
|
-
startFresh: async (channel) => {
|
|
61
|
-
log.push(`${name} fresh ${channel}`);
|
|
62
|
-
return name;
|
|
63
|
-
},
|
|
64
|
-
...(options.deletes
|
|
65
|
-
? {
|
|
66
|
-
deleteConversation: async (channel: ChannelKey) =>
|
|
67
|
-
void log.push(`${name} delete ${channel}`),
|
|
68
|
-
}
|
|
69
|
-
: {}),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function router(claims: ChannelClaim[], queue = new ChannelQueue()) {
|
|
74
|
-
return new ChannelRouter({
|
|
75
|
-
claims,
|
|
76
|
-
queue,
|
|
77
|
-
stop: () => false,
|
|
78
|
-
logger: silentLogger(),
|
|
79
|
-
forwardJoinMs: 10,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
describe("orderClaims", () => {
|
|
84
|
-
test("orders by descending priority, keeping registration order among equals", () => {
|
|
85
|
-
const log: string[] = [];
|
|
86
|
-
const ordered = orderClaims([
|
|
87
|
-
claim("owner", 0, log),
|
|
88
|
-
claim("party", 20, log),
|
|
89
|
-
claim("first", 30, log),
|
|
90
|
-
claim("second", 30, log),
|
|
91
|
-
claim("agents", 100, log),
|
|
92
|
-
]);
|
|
93
|
-
expect(ordered.map((c) => c.name)).toEqual([
|
|
94
|
-
"agents",
|
|
95
|
-
"first",
|
|
96
|
-
"second",
|
|
97
|
-
"party",
|
|
98
|
-
"owner",
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("refuses a claim name used twice", () => {
|
|
103
|
-
const log: string[] = [];
|
|
104
|
-
expect(() =>
|
|
105
|
-
orderClaims([claim("party", 20, log), claim("party", 0, log)]),
|
|
106
|
-
).toThrow(PluginError);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("ChannelRouter", () => {
|
|
111
|
-
test("the highest claim owning a channel answers there; the owner takes the rest", async () => {
|
|
112
|
-
const log: string[] = [];
|
|
113
|
-
const routing = router([
|
|
114
|
-
claim("owner", 0, log),
|
|
115
|
-
claim("party", 20, log, { channels: ["discord:party"] }),
|
|
116
|
-
claim("agents", 100, log, { channels: ["discord:agent"] }),
|
|
117
|
-
]);
|
|
118
|
-
await routing.handle(message({ channel: "discord:party", text: "a" }));
|
|
119
|
-
await routing.handle(message({ channel: "discord:agent", text: "b" }));
|
|
120
|
-
await routing.handle(message({ channel: "discord:dm", text: "c" }));
|
|
121
|
-
expect(log).toEqual(["party a", "agents b", "owner c"]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("a message the owning claim refuses is dropped, never passed to a lower claim", async () => {
|
|
125
|
-
const log: string[] = [];
|
|
126
|
-
const routing = router([
|
|
127
|
-
claim("owner", 0, log),
|
|
128
|
-
claim("agents", 100, log, {
|
|
129
|
-
channels: ["discord:agent"],
|
|
130
|
-
guild: "agent-guild",
|
|
131
|
-
admit: () => undefined,
|
|
132
|
-
}),
|
|
133
|
-
]);
|
|
134
|
-
await routing.handle(message({ guildId: "agent-guild", text: "notes" }));
|
|
135
|
-
await routing.handle(message({ guildId: "other", text: "hello" }));
|
|
136
|
-
expect(log).toEqual(["owner hello"]);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("an empty message reaches no claim", async () => {
|
|
140
|
-
const log: string[] = [];
|
|
141
|
-
await router([claim("owner", 0, log)]).handle(message({ text: " " }));
|
|
142
|
-
expect(log).toEqual([]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("a message during a turn is offered to it, else marked until its own turn starts", async () => {
|
|
146
|
-
const log: string[] = [];
|
|
147
|
-
const queue = new ChannelQueue();
|
|
148
|
-
let takes = true;
|
|
149
|
-
let release = () => {};
|
|
150
|
-
const running = new Promise<void>((resolve) => {
|
|
151
|
-
release = resolve;
|
|
152
|
-
});
|
|
153
|
-
const routing = router(
|
|
154
|
-
[
|
|
155
|
-
claim("owner", 0, log, {
|
|
156
|
-
admit: (m) => ({
|
|
157
|
-
kind: "turn",
|
|
158
|
-
busy: {
|
|
159
|
-
steer: async () => takes,
|
|
160
|
-
react: async (emoji) => void log.push(`+${emoji} ${m.text}`),
|
|
161
|
-
unreact: async (emoji) => void log.push(`-${emoji} ${m.text}`),
|
|
162
|
-
},
|
|
163
|
-
run: async () => {
|
|
164
|
-
log.push(`run ${m.text}`);
|
|
165
|
-
if (m.text === "first") await running;
|
|
166
|
-
},
|
|
167
|
-
failure: "failed",
|
|
168
|
-
}),
|
|
169
|
-
}),
|
|
170
|
-
],
|
|
171
|
-
queue,
|
|
172
|
-
);
|
|
173
|
-
const first = routing.handle(message({ text: "first" }));
|
|
174
|
-
await Bun.sleep(1);
|
|
175
|
-
await routing.handle(message({ text: "steered" }));
|
|
176
|
-
takes = false;
|
|
177
|
-
const waiting = routing.handle(message({ text: "waiting" }));
|
|
178
|
-
await Bun.sleep(1);
|
|
179
|
-
release();
|
|
180
|
-
await Promise.all([first, waiting]);
|
|
181
|
-
expect(log).toEqual([
|
|
182
|
-
"run first",
|
|
183
|
-
"+↪️ steered",
|
|
184
|
-
"+⏳ waiting",
|
|
185
|
-
"-⏳ waiting",
|
|
186
|
-
"run waiting",
|
|
187
|
-
]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("a claim without busy handling is never marked", async () => {
|
|
191
|
-
const log: string[] = [];
|
|
192
|
-
const queue = new ChannelQueue();
|
|
193
|
-
const routing = router([claim("party", 0, log)], queue);
|
|
194
|
-
const first = routing.handle(message({ text: "one" }));
|
|
195
|
-
await routing.handle(message({ text: "two" }));
|
|
196
|
-
await first;
|
|
197
|
-
expect(log).toEqual(["party one", "party two"]);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("a background admission runs as a background turn and reports when it did not run", async () => {
|
|
201
|
-
const log: string[] = [];
|
|
202
|
-
const routing = router([
|
|
203
|
-
{
|
|
204
|
-
...claim("agents", 0, log),
|
|
205
|
-
admit: (m) => ({
|
|
206
|
-
kind: "background",
|
|
207
|
-
turn: {
|
|
208
|
-
channel: m.channel,
|
|
209
|
-
mode: "owner",
|
|
210
|
-
author: { id: "ci", name: "CI" },
|
|
211
|
-
turnId: "webhook-1",
|
|
212
|
-
text: m.text,
|
|
213
|
-
report: true,
|
|
214
|
-
},
|
|
215
|
-
unanswered: (outcome) => log.push(`unanswered ${outcome.status}`),
|
|
216
|
-
}),
|
|
217
|
-
background: async () => ({ status: "skipped", reason: "busy" }),
|
|
218
|
-
},
|
|
219
|
-
]);
|
|
220
|
-
await routing.handle(message({ text: "build failed" }));
|
|
221
|
-
expect(log).toEqual(["unanswered skipped"]);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("background turns and starting over go to the channel's owner when their queued turn starts", async () => {
|
|
225
|
-
const log: string[] = [];
|
|
226
|
-
const queue = new ChannelQueue();
|
|
227
|
-
const party = new Set<string>();
|
|
228
|
-
const routing = router(
|
|
229
|
-
[
|
|
230
|
-
claim("owner", 0, log),
|
|
231
|
-
{
|
|
232
|
-
...claim("party", 20, log),
|
|
233
|
-
owns: (channel) => party.has(channel),
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
queue,
|
|
237
|
-
);
|
|
238
|
-
let release = () => {};
|
|
239
|
-
const blocker = queue.run(
|
|
240
|
-
"discord:1",
|
|
241
|
-
() =>
|
|
242
|
-
new Promise<void>((resolve) => {
|
|
243
|
-
release = resolve;
|
|
244
|
-
}),
|
|
245
|
-
);
|
|
246
|
-
await Bun.sleep(1);
|
|
247
|
-
const turn = routing.background({
|
|
248
|
-
channel: "discord:1",
|
|
249
|
-
mode: "party",
|
|
250
|
-
author: { id: "owner", name: "Riley" },
|
|
251
|
-
turnId: "schedule-1",
|
|
252
|
-
text: "reminder",
|
|
253
|
-
});
|
|
254
|
-
const fresh = routing.startFresh("discord:1");
|
|
255
|
-
// Party mode turns on while the turn waits in the queue.
|
|
256
|
-
party.add("discord:1");
|
|
257
|
-
release();
|
|
258
|
-
await blocker;
|
|
259
|
-
expect(await turn).toEqual({ status: "ran" });
|
|
260
|
-
expect(await fresh).toBe("party");
|
|
261
|
-
expect(log).toEqual(["party background reminder", "party fresh discord:1"]);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("deletes only through a claim that deletes, and never while the channel is busy", async () => {
|
|
265
|
-
const log: string[] = [];
|
|
266
|
-
const queue = new ChannelQueue();
|
|
267
|
-
const routing = router(
|
|
268
|
-
[
|
|
269
|
-
claim("owner", 0, log, { deletes: true }),
|
|
270
|
-
claim("party", 20, log, { channels: ["discord:party"] }),
|
|
271
|
-
],
|
|
272
|
-
queue,
|
|
273
|
-
);
|
|
274
|
-
await expect(routing.deleteConversation("discord:party")).rejects.toThrow(
|
|
275
|
-
"discord:party is not an owner conversation",
|
|
276
|
-
);
|
|
277
|
-
let release = () => {};
|
|
278
|
-
const blocker = queue.run(
|
|
279
|
-
"discord:1",
|
|
280
|
-
() =>
|
|
281
|
-
new Promise<void>((resolve) => {
|
|
282
|
-
release = resolve;
|
|
283
|
-
}),
|
|
284
|
-
);
|
|
285
|
-
await Bun.sleep(1);
|
|
286
|
-
expect(await routing.deleteConversation("discord:1")).toBe("busy");
|
|
287
|
-
release();
|
|
288
|
-
await blocker;
|
|
289
|
-
// The queue counts the finished task out once its tail settles.
|
|
290
|
-
await Bun.sleep(1);
|
|
291
|
-
expect(await routing.deleteConversation("discord:1")).toBe("deleted");
|
|
292
|
-
expect(log).toEqual(["owner delete discord:1"]);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("reports stay in place only where the channel's owner says so", () => {
|
|
296
|
-
const log: string[] = [];
|
|
297
|
-
const routing = router([
|
|
298
|
-
claim("owner", 0, log),
|
|
299
|
-
{
|
|
300
|
-
...claim("party", 20, log, { channels: ["discord:party"] }),
|
|
301
|
-
postsInPlace: true,
|
|
302
|
-
},
|
|
303
|
-
]);
|
|
304
|
-
expect(routing.postsInPlace("discord:party")).toBe(true);
|
|
305
|
-
expect(routing.postsInPlace("discord:dm")).toBe(false);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("a turn that throws is logged and the next still runs", async () => {
|
|
309
|
-
const log: string[] = [];
|
|
310
|
-
const routing = router([
|
|
311
|
-
claim("owner", 0, log, {
|
|
312
|
-
admit: (m) => ({
|
|
313
|
-
kind: "turn",
|
|
314
|
-
run: async () => {
|
|
315
|
-
if (m.text === "boom") throw new Error("boom");
|
|
316
|
-
log.push(`run ${m.text}`);
|
|
317
|
-
},
|
|
318
|
-
failure: "failed",
|
|
319
|
-
}),
|
|
320
|
-
}),
|
|
321
|
-
]);
|
|
322
|
-
await routing.handle(message({ text: "boom" }));
|
|
323
|
-
await routing.handle(message({ text: "next" }));
|
|
324
|
-
expect(log).toEqual(["run next"]);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** Whose conversation a channel holds, as starting it over reports it. */
|
|
2
|
-
export type ConversationKind = "owner" | "party" | "agent";
|
|
3
|
-
|
|
4
|
-
const isKind = (kind: string): kind is ConversationKind =>
|
|
5
|
-
kind === "owner" || kind === "party" || kind === "agent";
|
|
6
|
-
|
|
7
|
-
/** Narrows what a claim's startFresh said; a claim the host does not know about is a bug. */
|
|
8
|
-
export async function conversationKind(
|
|
9
|
-
whose: Promise<string>,
|
|
10
|
-
): Promise<ConversationKind> {
|
|
11
|
-
const kind = await whose;
|
|
12
|
-
if (!isKind(kind)) throw new Error(`unknown conversation kind "${kind}"`);
|
|
13
|
-
return kind;
|
|
14
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { AgentRunError } from "../domain/errors.ts";
|
|
3
|
-
import { settleTurn } from "./settle-turn.ts";
|
|
4
|
-
|
|
5
|
-
describe("settleTurn", () => {
|
|
6
|
-
test("passes a finished turn's result through", async () => {
|
|
7
|
-
expect(await settleTurn(async () => ({ ok: true }), "turn")).toEqual({
|
|
8
|
-
ok: true,
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("turns a thrown error into a failed result naming what crashed", async () => {
|
|
13
|
-
const result = await settleTurn(async () => {
|
|
14
|
-
throw new Error("socket closed");
|
|
15
|
-
}, "party turn");
|
|
16
|
-
expect(result).toEqual({
|
|
17
|
-
ok: false,
|
|
18
|
-
error: new AgentRunError("party turn crashed: Error: socket closed"),
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
type ExtensionAPI,
|
|
4
|
-
SessionManager,
|
|
5
|
-
shouldCompact,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
CompactionTiers,
|
|
9
|
-
compactionEngine,
|
|
10
|
-
HARD_COMPACT_TOKENS,
|
|
11
|
-
SOFT_COMPACT_TOKENS,
|
|
12
|
-
} from "./compaction-tiers.ts";
|
|
13
|
-
|
|
14
|
-
const WINDOWS: Record<string, number> = {
|
|
15
|
-
"anthropic/claude-opus-5-5": 1_000_000,
|
|
16
|
-
"anthropic/claude-200k-opus-5-5": 200_000,
|
|
17
|
-
};
|
|
18
|
-
const LARGE = { provider: "anthropic", id: "claude-opus-5-5" };
|
|
19
|
-
const SMALL = { provider: "anthropic", id: "claude-200k-opus-5-5" };
|
|
20
|
-
const PI_RESERVE = 16_384;
|
|
21
|
-
/** The `engine` a compaction extension records in its compactions' details. */
|
|
22
|
-
const EXTENSION_ENGINE = "acme-compaction";
|
|
23
|
-
|
|
24
|
-
/** pi estimates a message at a token per four characters. */
|
|
25
|
-
function text(tokens: number): string {
|
|
26
|
-
return "x".repeat(tokens * 4);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function say(session: SessionManager, tokens: number): string {
|
|
30
|
-
return session.appendMessage({
|
|
31
|
-
role: "user",
|
|
32
|
-
content: text(tokens),
|
|
33
|
-
timestamp: Date.now(),
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Appends a compaction that leaves about `left` tokens: its summary and one small kept message. */
|
|
38
|
-
function compacted(
|
|
39
|
-
session: SessionManager,
|
|
40
|
-
engine: "extension" | "pi",
|
|
41
|
-
left: number,
|
|
42
|
-
): void {
|
|
43
|
-
const kept = say(session, 1_000);
|
|
44
|
-
session.appendCompaction(
|
|
45
|
-
text(left - 1_000),
|
|
46
|
-
kept,
|
|
47
|
-
SOFT_COMPACT_TOKENS,
|
|
48
|
-
engine === "extension" ? { engine: EXTENSION_ENGINE } : {},
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function tiersOver(session: SessionManager): CompactionTiers {
|
|
53
|
-
return new CompactionTiers(
|
|
54
|
-
session,
|
|
55
|
-
(provider, id) => WINDOWS[`${provider}/${id}`],
|
|
56
|
-
EXTENSION_ENGINE,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The context size the model compacts past, as pi's own check sees it. */
|
|
61
|
-
function trigger(tiers: CompactionTiers, model = LARGE): number {
|
|
62
|
-
const settings = tiers.settings().getCompactionSettings(model);
|
|
63
|
-
const window = WINDOWS[`${model.provider}/${model.id}`] ?? 0;
|
|
64
|
-
const at = window - settings.reserveTokens;
|
|
65
|
-
expect(shouldCompact(at, window, settings)).toBe(false);
|
|
66
|
-
expect(shouldCompact(at + 1, window, settings)).toBe(true);
|
|
67
|
-
return at;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type Handler = (event: unknown, ctx: unknown) => unknown;
|
|
71
|
-
|
|
72
|
-
/** Loads a stand-in for a compaction extension through the tiers' wrapper. */
|
|
73
|
-
function loadExtension(tiers: CompactionTiers) {
|
|
74
|
-
const extensionCalls: number[] = [];
|
|
75
|
-
const bypasses: { reason: string; tokensBefore: number }[] = [];
|
|
76
|
-
const handlers = new Map<string, Handler>();
|
|
77
|
-
const onStart: Handler = () => undefined;
|
|
78
|
-
tiers.wrapCompactor(
|
|
79
|
-
(pi) => {
|
|
80
|
-
pi.on("session_start", onStart as never);
|
|
81
|
-
pi.on("session_before_compact", (event) => {
|
|
82
|
-
extensionCalls.push(event.preparation.tokensBefore);
|
|
83
|
-
return { compaction: { summary: "extension" } } as never;
|
|
84
|
-
});
|
|
85
|
-
},
|
|
86
|
-
(bypass) => bypasses.push(bypass),
|
|
87
|
-
)({
|
|
88
|
-
on: (event: string, handler: Handler) => handlers.set(event, handler),
|
|
89
|
-
} as unknown as ExtensionAPI);
|
|
90
|
-
const compact = (tokensBefore: number) =>
|
|
91
|
-
handlers.get("session_before_compact")?.(
|
|
92
|
-
{ type: "session_before_compact", preparation: { tokensBefore } },
|
|
93
|
-
{},
|
|
94
|
-
);
|
|
95
|
-
return { compact, extensionCalls, bypasses, onStart, handlers };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
describe("soft threshold", () => {
|
|
99
|
-
test("a 1M model compacts past 300k", () => {
|
|
100
|
-
const tiers = tiersOver(SessionManager.inMemory("/tmp"));
|
|
101
|
-
expect(trigger(tiers)).toBe(SOFT_COMPACT_TOKENS);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("a 200k model keeps pi's default reserve", () => {
|
|
105
|
-
const tiers = tiersOver(SessionManager.inMemory("/tmp"));
|
|
106
|
-
expect(tiers.settings().getCompactionSettings(SMALL).reserveTokens).toBe(
|
|
107
|
-
PI_RESERVE,
|
|
108
|
-
);
|
|
109
|
-
expect(trigger(tiers, SMALL)).toBe(200_000 - PI_RESERVE);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("an unknown model keeps pi's default reserve", () => {
|
|
113
|
-
const tiers = tiersOver(SessionManager.inMemory("/tmp"));
|
|
114
|
-
expect(
|
|
115
|
-
tiers.settings().getCompactionSettings({ provider: "x", id: "y" })
|
|
116
|
-
.reserveTokens,
|
|
117
|
-
).toBe(PI_RESERVE);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("hard ceiling", () => {
|
|
122
|
-
test("a compaction above 500k skips the extension, so pi's summary runs", async () => {
|
|
123
|
-
const extension = loadExtension(tiersOver(SessionManager.inMemory("/tmp")));
|
|
124
|
-
expect(await extension.compact(HARD_COMPACT_TOKENS + 1)).toBeUndefined();
|
|
125
|
-
expect(extension.extensionCalls).toEqual([]);
|
|
126
|
-
expect(extension.bypasses).toEqual([
|
|
127
|
-
{
|
|
128
|
-
reason: "above the hard ceiling",
|
|
129
|
-
tokensBefore: HARD_COMPACT_TOKENS + 1,
|
|
130
|
-
},
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("a compaction at or below 500k goes to the extension", async () => {
|
|
135
|
-
const extension = loadExtension(tiersOver(SessionManager.inMemory("/tmp")));
|
|
136
|
-
expect(await extension.compact(HARD_COMPACT_TOKENS)).toEqual({
|
|
137
|
-
compaction: { summary: "extension" },
|
|
138
|
-
});
|
|
139
|
-
expect(await extension.compact(SOFT_COMPACT_TOKENS + 1)).toEqual({
|
|
140
|
-
compaction: { summary: "extension" },
|
|
141
|
-
});
|
|
142
|
-
expect(extension.extensionCalls).toEqual([
|
|
143
|
-
HARD_COMPACT_TOKENS,
|
|
144
|
-
SOFT_COMPACT_TOKENS + 1,
|
|
145
|
-
]);
|
|
146
|
-
expect(extension.bypasses).toEqual([]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("after the extension left more than 500k, the next compaction is pi's", async () => {
|
|
150
|
-
const session = SessionManager.inMemory("/tmp");
|
|
151
|
-
say(session, 10_000);
|
|
152
|
-
compacted(session, "extension", 520_000);
|
|
153
|
-
const tiers = tiersOver(session);
|
|
154
|
-
expect(tiers.latest()?.contextTokens).toBeGreaterThan(HARD_COMPACT_TOKENS);
|
|
155
|
-
const extension = loadExtension(tiers);
|
|
156
|
-
expect(await extension.compact(450_000)).toBeUndefined();
|
|
157
|
-
expect(extension.extensionCalls).toEqual([]);
|
|
158
|
-
expect(extension.bypasses[0]?.reason).toContain(
|
|
159
|
-
"The extension's last compaction",
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("only session_before_compact is wrapped", () => {
|
|
164
|
-
const extension = loadExtension(tiersOver(SessionManager.inMemory("/tmp")));
|
|
165
|
-
expect(extension.handlers.get("session_start")).toBe(extension.onStart);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe("no compaction loop", () => {
|
|
170
|
-
test("the extension leaving the context near 300k moves the next compaction to 500k", () => {
|
|
171
|
-
const session = SessionManager.inMemory("/tmp");
|
|
172
|
-
say(session, 10_000);
|
|
173
|
-
compacted(session, "extension", 310_000);
|
|
174
|
-
const tiers = tiersOver(session);
|
|
175
|
-
// The same request's next check sees about what the extension left, and does not compact again.
|
|
176
|
-
const settings = tiers.settings().getCompactionSettings(LARGE);
|
|
177
|
-
expect(shouldCompact(311_000, 1_000_000, settings)).toBe(false);
|
|
178
|
-
expect(trigger(tiers)).toBe(HARD_COMPACT_TOKENS);
|
|
179
|
-
|
|
180
|
-
session.appendMessage({
|
|
181
|
-
role: "user",
|
|
182
|
-
content: "small",
|
|
183
|
-
timestamp: Date.now(),
|
|
184
|
-
});
|
|
185
|
-
compacted(session, "extension", 280_000);
|
|
186
|
-
expect(trigger(tiers)).toBe(HARD_COMPACT_TOKENS);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("a compaction well below 300k keeps the soft threshold", () => {
|
|
190
|
-
const session = SessionManager.inMemory("/tmp");
|
|
191
|
-
say(session, 10_000);
|
|
192
|
-
compacted(session, "extension", 310_000);
|
|
193
|
-
const tiers = tiersOver(session);
|
|
194
|
-
expect(trigger(tiers)).toBe(HARD_COMPACT_TOKENS);
|
|
195
|
-
// pi's summary at the ceiling brings the session back to the soft tier.
|
|
196
|
-
compacted(session, "pi", 30_000);
|
|
197
|
-
expect(trigger(tiers)).toBe(SOFT_COMPACT_TOKENS);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("pi's summary leaving the context near 500k falls back to pi's own threshold", () => {
|
|
201
|
-
const session = SessionManager.inMemory("/tmp");
|
|
202
|
-
say(session, 10_000);
|
|
203
|
-
compacted(session, "pi", 480_000);
|
|
204
|
-
expect(trigger(tiersOver(session))).toBe(1_000_000 - PI_RESERVE);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("the extension leaving the context near 500k still leaves pi the ceiling", () => {
|
|
208
|
-
const session = SessionManager.inMemory("/tmp");
|
|
209
|
-
say(session, 10_000);
|
|
210
|
-
compacted(session, "extension", 480_000);
|
|
211
|
-
expect(trigger(tiersOver(session))).toBe(HARD_COMPACT_TOKENS);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe("compaction engine", () => {
|
|
216
|
-
test("a compaction is the extension's only when it records the engine the plugin declared", () => {
|
|
217
|
-
expect(
|
|
218
|
-
compactionEngine({ engine: EXTENSION_ENGINE }, EXTENSION_ENGINE),
|
|
219
|
-
).toBe("extension");
|
|
220
|
-
expect(compactionEngine({ engine: "other" }, EXTENSION_ENGINE)).toBe("pi");
|
|
221
|
-
expect(compactionEngine({}, EXTENSION_ENGINE)).toBe("pi");
|
|
222
|
-
expect(compactionEngine(undefined, EXTENSION_ENGINE)).toBe("pi");
|
|
223
|
-
expect(compactionEngine({ engine: EXTENSION_ENGINE }, undefined)).toBe(
|
|
224
|
-
"pi",
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { OwnerAnswer, OwnerQuestion } from "../../domain/owner-prompts.ts";
|
|
4
|
-
import { messages } from "../../i18n/index.ts";
|
|
5
|
-
import { TEST_OWNER as OWNER } from "../../testing/owner.ts";
|
|
6
|
-
import { PromptSlot } from "../prompt-slot.ts";
|
|
7
|
-
import { ASK_USER_TOOL, askUserExtension } from "./ask-user.ts";
|
|
8
|
-
|
|
9
|
-
interface Tool {
|
|
10
|
-
name: string;
|
|
11
|
-
execute: (
|
|
12
|
-
id: string,
|
|
13
|
-
params: unknown,
|
|
14
|
-
signal?: AbortSignal,
|
|
15
|
-
) => Promise<{ content: { text: string }[] }>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** ask_user over a slot whose cards answer with `answer`, recording each question. */
|
|
19
|
-
function askUser(answer: OwnerAnswer | undefined, bound = true) {
|
|
20
|
-
const asked: { title: string; question: OwnerQuestion }[] = [];
|
|
21
|
-
const slot = new PromptSlot();
|
|
22
|
-
slot.bind(
|
|
23
|
-
bound
|
|
24
|
-
? {
|
|
25
|
-
confirm: async () => "expired",
|
|
26
|
-
ask: async (title, question) => {
|
|
27
|
-
asked.push({ title, question });
|
|
28
|
-
return answer;
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
: undefined,
|
|
32
|
-
"infra",
|
|
33
|
-
);
|
|
34
|
-
let tool: Tool | undefined;
|
|
35
|
-
askUserExtension(
|
|
36
|
-
slot,
|
|
37
|
-
OWNER,
|
|
38
|
-
)({
|
|
39
|
-
registerTool: (t: Tool) => {
|
|
40
|
-
tool = t;
|
|
41
|
-
},
|
|
42
|
-
} as unknown as ExtensionAPI);
|
|
43
|
-
if (tool?.name !== ASK_USER_TOOL) throw new Error("ask_user not registered");
|
|
44
|
-
const t = tool;
|
|
45
|
-
return {
|
|
46
|
-
asked,
|
|
47
|
-
call: async (params: unknown) =>
|
|
48
|
-
(await t.execute("call-1", params)).content[0]?.text ?? "",
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe("ask_user", () => {
|
|
53
|
-
test("a single choice comes back as the chosen label", async () => {
|
|
54
|
-
const { asked, call } = askUser({ choices: ["Sunday"] });
|
|
55
|
-
const text = await call({
|
|
56
|
-
question: "Which day?",
|
|
57
|
-
options: [
|
|
58
|
-
{ label: "Saturday" },
|
|
59
|
-
{ label: "Sunday", description: "afternoon" },
|
|
60
|
-
],
|
|
61
|
-
});
|
|
62
|
-
expect(text).toBe("Riley chose: Sunday");
|
|
63
|
-
expect(asked[0]).toEqual({
|
|
64
|
-
title: messages().askTitle("infra"),
|
|
65
|
-
question: {
|
|
66
|
-
question: "Which day?",
|
|
67
|
-
options: [
|
|
68
|
-
{ label: "Saturday" },
|
|
69
|
-
{ label: "Sunday", description: "afternoon" },
|
|
70
|
-
],
|
|
71
|
-
multi: false,
|
|
72
|
-
allowOther: false,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("several choices and the owner's own words", async () => {
|
|
78
|
-
const { asked, call } = askUser({
|
|
79
|
-
choices: ["Saturday", "Monday"],
|
|
80
|
-
text: "Wednesday works too",
|
|
81
|
-
});
|
|
82
|
-
const text = await call({
|
|
83
|
-
question: "Which days?",
|
|
84
|
-
options: [{ label: "Saturday" }, { label: "Monday" }],
|
|
85
|
-
multi: true,
|
|
86
|
-
allow_other: true,
|
|
87
|
-
});
|
|
88
|
-
expect(text).toBe(
|
|
89
|
-
"Riley chose: Saturday; Monday\nRiley wrote: Wednesday works too",
|
|
90
|
-
);
|
|
91
|
-
expect(asked[0]?.question).toMatchObject({ multi: true, allowOther: true });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a free-text question", async () => {
|
|
95
|
-
const { asked, call } = askUser({ choices: [], text: "call me Nova" });
|
|
96
|
-
expect(await call({ question: "What name?" })).toBe(
|
|
97
|
-
"Riley wrote: call me Nova",
|
|
98
|
-
);
|
|
99
|
-
expect(asked[0]?.question.options).toEqual([]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("no answer in time says so", async () => {
|
|
103
|
-
const { call } = askUser(undefined);
|
|
104
|
-
expect(await call({ question: "?" })).toStartWith("No answer");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a turn he did not start tells the model to ask in its reply", async () => {
|
|
108
|
-
const { asked, call } = askUser({ choices: ["x"] }, false);
|
|
109
|
-
expect(await call({ question: "?" })).toContain(
|
|
110
|
-
"Ask him in your reply instead",
|
|
111
|
-
);
|
|
112
|
-
expect(asked).toHaveLength(0);
|
|
113
|
-
});
|
|
114
|
-
});
|