pi-roundtable 0.19.2 → 0.20.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 +20 -0
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/docs/plugins.md +31 -2
- package/package.json +2 -1
- package/src/core/discord/owner-card-answers.ts +180 -0
- package/src/core/discord/owner-card-asks.ts +149 -0
- package/src/core/discord/owner-card-questionnaire.ts +281 -0
- package/src/core/discord/owner-card-rows.ts +40 -18
- package/src/core/discord/owner-card-types.ts +54 -5
- package/src/core/discord/owner-cards-fixture.ts +37 -3
- package/src/core/discord/owner-cards.ts +61 -133
- package/src/core/discord/utf16.ts +11 -0
- package/src/core/i18n/cards.ts +32 -2
- package/src/core/interactions/prompts.ts +33 -22
- package/src/core/interactions/questionnaire.ts +213 -0
- package/src/core/runtime/ask-questions.ts +93 -0
- package/src/core/runtime/extensions/ask-user-arguments.ts +52 -0
- package/src/core/runtime/extensions/ask-user-text.ts +146 -0
- package/src/core/runtime/extensions/ask-user.ts +87 -81
- package/src/core/runtime/prompt-slot.ts +21 -1
- package/src/core/testing/surface-contract-questionnaire.ts +192 -0
- package/src/core/testing/surface-contract-support.ts +22 -0
- package/src/core/testing/surface-contract.ts +21 -19
- package/src/index.ts +11 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type AskSupport,
|
|
4
|
+
type Questionnaire,
|
|
5
|
+
type QuestionnaireAnswer,
|
|
6
|
+
shapeFor,
|
|
7
|
+
} from "../interactions/questionnaire.ts";
|
|
8
|
+
import type { SurfaceContractSubject } from "./surface-contract.ts";
|
|
9
|
+
import {
|
|
10
|
+
ContractBroken,
|
|
11
|
+
expectThat,
|
|
12
|
+
nextPrompt,
|
|
13
|
+
type Running,
|
|
14
|
+
runCheck,
|
|
15
|
+
scopeOf,
|
|
16
|
+
seen,
|
|
17
|
+
until,
|
|
18
|
+
} from "./surface-contract-support.ts";
|
|
19
|
+
|
|
20
|
+
/** One check of the questionnaire part of the surface contract. */
|
|
21
|
+
export interface QuestionnaireCheck {
|
|
22
|
+
name: string;
|
|
23
|
+
applies(subject: SurfaceContractSubject): boolean;
|
|
24
|
+
run(running: Running): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The set the checks ask: two questions, the first with previews where the surface shows them. */
|
|
28
|
+
function contractSet(asks: AskSupport): Questionnaire {
|
|
29
|
+
const preview = asks.previews ? { preview: "```\nlayout\n```" } : {};
|
|
30
|
+
const day = {
|
|
31
|
+
question: "Which day?",
|
|
32
|
+
header: "Day",
|
|
33
|
+
options: [
|
|
34
|
+
{ label: "Saturday", ...preview },
|
|
35
|
+
{ label: "Sunday", ...preview },
|
|
36
|
+
],
|
|
37
|
+
multi: false,
|
|
38
|
+
allowOther: true,
|
|
39
|
+
};
|
|
40
|
+
const food = {
|
|
41
|
+
question: "Which food?",
|
|
42
|
+
options: [{ label: "Rice" }, { label: "Noodles" }],
|
|
43
|
+
multi: true,
|
|
44
|
+
allowOther: false,
|
|
45
|
+
};
|
|
46
|
+
return shapeFor(
|
|
47
|
+
{ questions: asks.questions >= 2 ? [day, food] : [day] },
|
|
48
|
+
asks,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The person's prompts, once the surface has started; undefined where it declares no `asks`. */
|
|
53
|
+
function questionnairePrompts(running: Running) {
|
|
54
|
+
const { surface, channel } = running.subject;
|
|
55
|
+
const prompts = surface.prompts?.(channel, scopeOf(running.subject));
|
|
56
|
+
if (!prompts?.asks || !prompts.questionnaire)
|
|
57
|
+
throw new ContractBroken(
|
|
58
|
+
"the subject answers questionnaires, but its prompts declare no asks",
|
|
59
|
+
);
|
|
60
|
+
return { prompts, asks: prompts.asks, ask: prompts.questionnaire };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const answerOf = (
|
|
64
|
+
set: Questionnaire,
|
|
65
|
+
asks: AskSupport,
|
|
66
|
+
): QuestionnaireAnswer => ({
|
|
67
|
+
answers: set.questions.map((_, i) =>
|
|
68
|
+
i === 0 ? { choices: ["Sunday"] } : undefined,
|
|
69
|
+
),
|
|
70
|
+
...(asks.note ? { note: "thanks" } : {}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const applies = (subject: SurfaceContractSubject) =>
|
|
74
|
+
subject.surface.prompts !== undefined &&
|
|
75
|
+
subject.answerQuestionnaire !== undefined;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The questionnaire checks. They apply to a subject that gives `answerQuestionnaire`, which must declare `asks`;
|
|
79
|
+
* a surface that takes one question at a time gives none and is skipped.
|
|
80
|
+
*/
|
|
81
|
+
export const QUESTIONNAIRE_CHECKS: readonly QuestionnaireCheck[] = [
|
|
82
|
+
{
|
|
83
|
+
name: "a questionnaire resolves as the person answers it, keeps a blank question blank, and closes",
|
|
84
|
+
applies,
|
|
85
|
+
run: async (running) => {
|
|
86
|
+
const found = questionnairePrompts(running);
|
|
87
|
+
const answer = running.subject.answerQuestionnaire;
|
|
88
|
+
if (!answer) return;
|
|
89
|
+
const set = contractSet(found.asks);
|
|
90
|
+
const before = seen(running, "prompt").length;
|
|
91
|
+
const result = found.ask("Questions", set);
|
|
92
|
+
const id = await nextPrompt(running, before);
|
|
93
|
+
const given = answerOf(set, found.asks);
|
|
94
|
+
await answer(id, given, set);
|
|
95
|
+
const got = await result;
|
|
96
|
+
if (got === "pending" || got === undefined)
|
|
97
|
+
throw new ContractBroken(`the person's answers came back ${got}`);
|
|
98
|
+
expectThat(
|
|
99
|
+
JSON.stringify(got.answers.map((a) => a ?? null)) ===
|
|
100
|
+
JSON.stringify(given.answers.map((a) => a ?? null)),
|
|
101
|
+
`the answers came back as ${JSON.stringify(got.answers)}`,
|
|
102
|
+
);
|
|
103
|
+
expectThat(
|
|
104
|
+
got.note === given.note,
|
|
105
|
+
`the note came back as ${JSON.stringify(got.note)}`,
|
|
106
|
+
);
|
|
107
|
+
await until(
|
|
108
|
+
() =>
|
|
109
|
+
seen(running, "prompt_closed").some(
|
|
110
|
+
(o) => o.kind === "prompt_closed" && o.id === id,
|
|
111
|
+
),
|
|
112
|
+
"the answered questionnaire closes",
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "a stopped turn's questionnaire resolves with no answer and closes",
|
|
118
|
+
applies,
|
|
119
|
+
run: async (running) => {
|
|
120
|
+
const found = questionnairePrompts(running);
|
|
121
|
+
const stop = new AbortController();
|
|
122
|
+
const before = seen(running, "prompt").length;
|
|
123
|
+
const result = found.ask(
|
|
124
|
+
"Questions",
|
|
125
|
+
contractSet(found.asks),
|
|
126
|
+
stop.signal,
|
|
127
|
+
);
|
|
128
|
+
const id = await nextPrompt(running, before);
|
|
129
|
+
stop.abort();
|
|
130
|
+
const got = await result;
|
|
131
|
+
expectThat(
|
|
132
|
+
got === undefined,
|
|
133
|
+
`a stopped turn's questionnaire got ${got}`,
|
|
134
|
+
);
|
|
135
|
+
await until(
|
|
136
|
+
() =>
|
|
137
|
+
seen(running, "prompt_closed").some(
|
|
138
|
+
(o) => o.kind === "prompt_closed" && o.id === id,
|
|
139
|
+
),
|
|
140
|
+
"the stopped questionnaire closes",
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "another person cannot answer the person's questionnaire",
|
|
146
|
+
applies: (subject) =>
|
|
147
|
+
applies(subject) && subject.stranger?.answerQuestionnaire !== undefined,
|
|
148
|
+
run: async (running) => {
|
|
149
|
+
const found = questionnairePrompts(running);
|
|
150
|
+
const stranger = running.subject.stranger;
|
|
151
|
+
const answer = running.subject.answerQuestionnaire;
|
|
152
|
+
if (!answer || !stranger?.answerQuestionnaire) return;
|
|
153
|
+
await stranger.join?.();
|
|
154
|
+
const set = contractSet(found.asks);
|
|
155
|
+
const before = seen(running, "prompt").length;
|
|
156
|
+
let settled: unknown;
|
|
157
|
+
const result = found.ask("Questions", set).then((value) => {
|
|
158
|
+
settled = value;
|
|
159
|
+
return value;
|
|
160
|
+
});
|
|
161
|
+
const id = await nextPrompt(running, before);
|
|
162
|
+
await stranger.answerQuestionnaire(id, answerOf(set, found.asks), set);
|
|
163
|
+
await Bun.sleep(50);
|
|
164
|
+
expectThat(
|
|
165
|
+
settled === undefined,
|
|
166
|
+
"another person's answer settled the questionnaire",
|
|
167
|
+
);
|
|
168
|
+
await answer(id, answerOf(set, found.asks), set);
|
|
169
|
+
const own = await result;
|
|
170
|
+
expectThat(
|
|
171
|
+
own !== "pending" && own !== undefined,
|
|
172
|
+
`the person's answer came back ${own}`,
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Registers only the questionnaire part of the contract, for a card layer without a chat around
|
|
180
|
+
* it. Internal: the published contract is `describeSurfaceContract`.
|
|
181
|
+
*/
|
|
182
|
+
export function describeQuestionnaireContract(
|
|
183
|
+
name: string,
|
|
184
|
+
make: () => Promise<SurfaceContractSubject>,
|
|
185
|
+
): void {
|
|
186
|
+
describe(`${name} keeps the questionnaire contract`, () => {
|
|
187
|
+
for (const check of QUESTIONNAIRE_CHECKS)
|
|
188
|
+
test(check.name, async () => {
|
|
189
|
+
await runCheck(check, make);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -192,3 +192,25 @@ export async function namedApproverAnswers(running: Running): Promise<void> {
|
|
|
192
192
|
`the approval reported ${reported?.principalId} as its approver, not ${approver.principalId}`,
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
/** Runs one check on a fresh subject from `make`, starting and stopping its surface around it. */
|
|
197
|
+
export async function runCheck(
|
|
198
|
+
check: {
|
|
199
|
+
applies?(subject: SurfaceContractSubject): boolean;
|
|
200
|
+
run(running: Running): Promise<void>;
|
|
201
|
+
},
|
|
202
|
+
make: () => Promise<SurfaceContractSubject>,
|
|
203
|
+
): Promise<"passed" | "skipped"> {
|
|
204
|
+
const subject = await make();
|
|
205
|
+
const running: Running = { subject, delivered: [] };
|
|
206
|
+
try {
|
|
207
|
+
if (check.applies && !check.applies(subject)) return "skipped";
|
|
208
|
+
await subject.surface.start((message) => running.delivered.push(message));
|
|
209
|
+
await subject.join?.();
|
|
210
|
+
await check.run(running);
|
|
211
|
+
return "passed";
|
|
212
|
+
} finally {
|
|
213
|
+
await subject.close?.();
|
|
214
|
+
await subject.surface.stop?.();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -3,8 +3,13 @@ import type { ChatSurface } from "../contract/surface.ts";
|
|
|
3
3
|
import { parseChannelKey } from "../contract/surface.ts";
|
|
4
4
|
import type { TurnProgress } from "../domain/progress.ts";
|
|
5
5
|
import type { Approval } from "../interactions/prompts.ts";
|
|
6
|
+
import type {
|
|
7
|
+
Questionnaire,
|
|
8
|
+
QuestionnaireAnswer,
|
|
9
|
+
} from "../interactions/questionnaire.ts";
|
|
6
10
|
import type { ChannelKey } from "../sessions.ts";
|
|
7
11
|
import type { Speaker } from "../speakers.ts";
|
|
12
|
+
import { QUESTIONNAIRE_CHECKS } from "./surface-contract-questionnaire.ts";
|
|
8
13
|
import {
|
|
9
14
|
answeredOnlyByThem,
|
|
10
15
|
approval,
|
|
@@ -13,6 +18,7 @@ import {
|
|
|
13
18
|
namedApproverAnswers,
|
|
14
19
|
nextPrompt,
|
|
15
20
|
type Running,
|
|
21
|
+
runCheck,
|
|
16
22
|
scopeOf,
|
|
17
23
|
seen,
|
|
18
24
|
speakerOf,
|
|
@@ -44,6 +50,12 @@ export interface SurfaceContractSubject {
|
|
|
44
50
|
observations(): readonly SurfaceObservation[];
|
|
45
51
|
/** The person answers an open approval; without it the prompt checks are skipped. */
|
|
46
52
|
answer?(prompt: string, approved: boolean): Promise<void>;
|
|
53
|
+
/** The person answers an open questionnaire, as shaped for `asks`; without it those checks are skipped. */
|
|
54
|
+
answerQuestionnaire?(
|
|
55
|
+
prompt: string,
|
|
56
|
+
answer: QuestionnaireAnswer,
|
|
57
|
+
questionnaire: Questionnaire,
|
|
58
|
+
): Promise<void>;
|
|
47
59
|
/**
|
|
48
60
|
* The person as the speaker of the turns that ask them, given to `surface.prompts` as the
|
|
49
61
|
* scope `promptScope(speaker, visibility)`; default an owner-tier speaker. Every approval the
|
|
@@ -85,6 +97,12 @@ export interface SurfaceContractAnswerer {
|
|
|
85
97
|
/** They join the surface, once it has started. */
|
|
86
98
|
join?(): Promise<void>;
|
|
87
99
|
answer(prompt: string, approved: boolean): Promise<void>;
|
|
100
|
+
/** They answer an open questionnaire; without it its stranger check is skipped. */
|
|
101
|
+
answerQuestionnaire?(
|
|
102
|
+
prompt: string,
|
|
103
|
+
answer: QuestionnaireAnswer,
|
|
104
|
+
questionnaire: Questionnaire,
|
|
105
|
+
): Promise<void>;
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
/** One way the subject broke the contract. */
|
|
@@ -401,26 +419,9 @@ const CHECKS: readonly Check[] = [
|
|
|
401
419
|
);
|
|
402
420
|
},
|
|
403
421
|
},
|
|
422
|
+
...QUESTIONNAIRE_CHECKS,
|
|
404
423
|
];
|
|
405
424
|
|
|
406
|
-
async function runCheck(
|
|
407
|
-
check: Check,
|
|
408
|
-
make: () => Promise<SurfaceContractSubject>,
|
|
409
|
-
): Promise<"passed" | "skipped"> {
|
|
410
|
-
const subject = await make();
|
|
411
|
-
const running: Running = { subject, delivered: [] };
|
|
412
|
-
try {
|
|
413
|
-
if (check.applies && !check.applies(subject)) return "skipped";
|
|
414
|
-
await subject.surface.start((message) => running.delivered.push(message));
|
|
415
|
-
await subject.join?.();
|
|
416
|
-
await check.run(running);
|
|
417
|
-
return "passed";
|
|
418
|
-
} finally {
|
|
419
|
-
await subject.close?.();
|
|
420
|
-
await subject.surface.stop?.();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
425
|
/**
|
|
425
426
|
* Runs the chat surface contract on fresh subjects from `make`, one per check, and returns the
|
|
426
427
|
* checks the surface broke. `describeSurfaceContract` registers the same checks as tests.
|
|
@@ -449,7 +450,8 @@ export async function checkSurfaceContract(
|
|
|
449
450
|
* too, and lets an approval above the person's tier there come back at once, shown to no one.
|
|
450
451
|
* Where it can name approvers, an approval that does is answered by them, reported as the
|
|
451
452
|
* approver, and not by the person or another person, and a private conversation's is answered by
|
|
452
|
-
* its person alone, whoever the call names besides them.
|
|
453
|
+
* its person alone, whoever the call names besides them. A subject that gives
|
|
454
|
+
* `answerQuestionnaire` must declare `asks`, and then questionnaires keep the same rules. Each
|
|
453
455
|
* check gets a fresh subject from `make`; a check for an optional part the surface lacks passes.
|
|
454
456
|
*/
|
|
455
457
|
export function describeSurfaceContract(
|
package/src/index.ts
CHANGED
|
@@ -195,6 +195,7 @@ export type {
|
|
|
195
195
|
ApprovalOptions,
|
|
196
196
|
ApprovedBy,
|
|
197
197
|
AskOption,
|
|
198
|
+
AskSupport,
|
|
198
199
|
LateAnswer,
|
|
199
200
|
OwnerAnswer,
|
|
200
201
|
OwnerPrompts,
|
|
@@ -202,8 +203,18 @@ export type {
|
|
|
202
203
|
PromptScope,
|
|
203
204
|
Prompts,
|
|
204
205
|
PromptWait,
|
|
206
|
+
Questionnaire,
|
|
207
|
+
QuestionnaireAnswer,
|
|
205
208
|
} from "./core/interactions/prompts.ts";
|
|
206
209
|
export { promptScope, withdrawalNote } from "./core/interactions/prompts.ts";
|
|
210
|
+
export {
|
|
211
|
+
checkAnswer,
|
|
212
|
+
checkQuestionnaireAnswer,
|
|
213
|
+
QUESTIONNAIRE_LIMITS,
|
|
214
|
+
SINGLE_QUESTION,
|
|
215
|
+
shapeFor,
|
|
216
|
+
validateQuestionnaire,
|
|
217
|
+
} from "./core/interactions/questionnaire.ts";
|
|
207
218
|
export type { JudgeModel } from "./core/judging/model-judge.ts";
|
|
208
219
|
export type { LogEntry, LogFn, Logger } from "./core/log.ts";
|
|
209
220
|
export type { ThinkingLevel, ThinkingSetting } from "./core/models.ts";
|