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,93 @@
|
|
|
1
|
+
import { messages } from "../i18n/index.ts";
|
|
2
|
+
import type { OwnerIdentity } from "../identity.ts";
|
|
3
|
+
import type { OwnerAnswer, Prompts } from "../interactions/prompts.ts";
|
|
4
|
+
import {
|
|
5
|
+
type Questionnaire,
|
|
6
|
+
SINGLE_QUESTION,
|
|
7
|
+
shapeFor,
|
|
8
|
+
validateQuestionnaire,
|
|
9
|
+
} from "../interactions/questionnaire.ts";
|
|
10
|
+
import {
|
|
11
|
+
answerText,
|
|
12
|
+
lateAnswerText,
|
|
13
|
+
lateQuestionnaireText,
|
|
14
|
+
questionnaireText,
|
|
15
|
+
sequentialText,
|
|
16
|
+
} from "./extensions/ask-user-text.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Asks a questionnaire on `prompts` and returns the tool's result text. A set the core refuses
|
|
20
|
+
* posts no card. One question without previews, and a surface without `questionnaire` or with
|
|
21
|
+
* room for fewer questions, go through `ask`: one question exactly as before 0.20, several one
|
|
22
|
+
* at a time, stopping at the first that does not come back answered.
|
|
23
|
+
*/
|
|
24
|
+
export async function askQuestions(
|
|
25
|
+
prompts: Prompts,
|
|
26
|
+
asker: string,
|
|
27
|
+
questionnaire: Questionnaire,
|
|
28
|
+
owner: OwnerIdentity,
|
|
29
|
+
signal: AbortSignal | undefined,
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
const refused = validateQuestionnaire(questionnaire);
|
|
32
|
+
if (refused !== undefined) return refused;
|
|
33
|
+
const title = messages().askTitle(asker);
|
|
34
|
+
const n = questionnaire.questions.length;
|
|
35
|
+
const support = prompts.asks;
|
|
36
|
+
const previews = questionnaire.questions.some((q) =>
|
|
37
|
+
q.options.some((o) => o.preview !== undefined),
|
|
38
|
+
);
|
|
39
|
+
if (
|
|
40
|
+
prompts.questionnaire &&
|
|
41
|
+
support &&
|
|
42
|
+
support.questions >= n &&
|
|
43
|
+
(n > 1 || (previews && support.previews))
|
|
44
|
+
) {
|
|
45
|
+
const answer = await prompts.questionnaire(
|
|
46
|
+
title,
|
|
47
|
+
shapeFor(questionnaire, support),
|
|
48
|
+
signal,
|
|
49
|
+
{
|
|
50
|
+
late: (late) =>
|
|
51
|
+
late.kind === "questionnaire"
|
|
52
|
+
? n === 1
|
|
53
|
+
? lateAnswerText(
|
|
54
|
+
questionnaire.questions[0]?.question ?? "",
|
|
55
|
+
late.answer.answers[0] ?? { choices: [] },
|
|
56
|
+
late.by,
|
|
57
|
+
)
|
|
58
|
+
: lateQuestionnaireText(questionnaire, late.answer, late.by)
|
|
59
|
+
: undefined,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
if (n === 1)
|
|
63
|
+
return answerText(
|
|
64
|
+
answer === "pending" || answer === undefined
|
|
65
|
+
? answer
|
|
66
|
+
: (answer.answers[0] ?? { choices: [] }),
|
|
67
|
+
owner,
|
|
68
|
+
);
|
|
69
|
+
return questionnaireText(answer, questionnaire, owner);
|
|
70
|
+
}
|
|
71
|
+
const shaped = shapeFor(questionnaire, SINGLE_QUESTION);
|
|
72
|
+
const answers: OwnerAnswer[] = [];
|
|
73
|
+
for (const [i, question] of shaped.questions.entries()) {
|
|
74
|
+
const text = questionnaire.questions[i]?.question ?? question.question;
|
|
75
|
+
const answer = await prompts.ask(title, question, signal, {
|
|
76
|
+
late: (late) =>
|
|
77
|
+
late.kind === "question"
|
|
78
|
+
? lateAnswerText(text, late.answer, late.by)
|
|
79
|
+
: undefined,
|
|
80
|
+
});
|
|
81
|
+
if (n === 1) return answerText(answer, owner);
|
|
82
|
+
if (answer === "pending" || answer === undefined)
|
|
83
|
+
return sequentialText(
|
|
84
|
+
questionnaire,
|
|
85
|
+
answers,
|
|
86
|
+
answer === "pending" ? "pending" : "unanswered",
|
|
87
|
+
owner,
|
|
88
|
+
answerText(answer, owner),
|
|
89
|
+
);
|
|
90
|
+
answers.push(answer);
|
|
91
|
+
}
|
|
92
|
+
return sequentialText(questionnaire, answers, undefined, owner, "");
|
|
93
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Parses a value a model double-encoded as a JSON string; anything else, or a bad string, comes back as it was. */
|
|
2
|
+
// pi-lens-ignore: no-unknown-returns — this reads untyped model JSON; the tool schema is where it is parsed
|
|
3
|
+
function decoded(value: unknown): unknown {
|
|
4
|
+
if (typeof value !== "string") return value;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(value) as unknown;
|
|
7
|
+
} catch {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The arguments of an `ask_user` call in the `questions` form: a legacy call's top-level
|
|
14
|
+
* `{ question, options, multi, allow_other }` becomes one question (its `allow_other` stays false
|
|
15
|
+
* unless set, as before 0.20), and a `questions` or `options` value that arrived as a JSON string
|
|
16
|
+
* is decoded. Anything else is left for the schema to refuse.
|
|
17
|
+
*/
|
|
18
|
+
// pi-lens-ignore: no-unknown-returns — pi checks the folded arguments against the tool schema right after
|
|
19
|
+
export function foldAskArguments(args: unknown): unknown {
|
|
20
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
21
|
+
return args;
|
|
22
|
+
const input = args as Record<string, unknown>;
|
|
23
|
+
if (input.questions !== undefined && input.question !== undefined)
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Use either `questions` or the old top-level `question`, not both.",
|
|
26
|
+
);
|
|
27
|
+
if (input.questions === undefined && typeof input.question === "string") {
|
|
28
|
+
const { question, options, multi, allow_other, ...rest } = input;
|
|
29
|
+
return {
|
|
30
|
+
...rest,
|
|
31
|
+
questions: [
|
|
32
|
+
{
|
|
33
|
+
question,
|
|
34
|
+
...(options !== undefined ? { options: decoded(options) } : {}),
|
|
35
|
+
...(multi !== undefined ? { multi } : {}),
|
|
36
|
+
allow_other: allow_other ?? false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const questions = decoded(input.questions);
|
|
42
|
+
if (!Array.isArray(questions)) return { ...input, questions };
|
|
43
|
+
return {
|
|
44
|
+
...input,
|
|
45
|
+
questions: questions.map((item: unknown) => {
|
|
46
|
+
const q = decoded(item);
|
|
47
|
+
return typeof q === "object" && q !== null && "options" in q
|
|
48
|
+
? { ...q, options: decoded((q as { options: unknown }).options) }
|
|
49
|
+
: q;
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type OwnerIdentity, ownerWords, PRONOUNS } from "../../identity.ts";
|
|
2
|
+
import type {
|
|
3
|
+
OwnerAnswer,
|
|
4
|
+
OwnerQuestion,
|
|
5
|
+
Questionnaire,
|
|
6
|
+
QuestionnaireAnswer,
|
|
7
|
+
} from "../../interactions/prompts.ts";
|
|
8
|
+
|
|
9
|
+
/** The longest a question's name is when it has no header, in characters. */
|
|
10
|
+
const NAME_CHARS = 120;
|
|
11
|
+
|
|
12
|
+
/** The tool's result: what the owner chose and wrote. */
|
|
13
|
+
export function answerText(
|
|
14
|
+
answer: OwnerAnswer | "pending" | undefined,
|
|
15
|
+
owner: OwnerIdentity,
|
|
16
|
+
): string {
|
|
17
|
+
const o = ownerWords(owner);
|
|
18
|
+
if (answer === "pending")
|
|
19
|
+
return `${o.name} has not answered yet. The card stays open, so do not ask again and do not wait for it. End your turn now: say briefly that you are waiting for ${o.his} answer on the card. When ${o.he} answers, a new turn brings you the answer and you continue from there.`;
|
|
20
|
+
if (!answer)
|
|
21
|
+
return `No answer: ${o.name} did not answer. Go on without it if you safely can, otherwise ask in your reply.`;
|
|
22
|
+
const lines = [
|
|
23
|
+
...(answer.choices.length > 0
|
|
24
|
+
? [`${o.name} chose: ${answer.choices.join("; ")}`]
|
|
25
|
+
: []),
|
|
26
|
+
...(answer.text !== undefined ? [`${o.name} wrote: ${answer.text}`] : []),
|
|
27
|
+
];
|
|
28
|
+
return lines.length > 0 ? lines.join("\n") : `${o.name} chose nothing.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The turn a late answer starts: the question it answers and what was chosen and written. */
|
|
32
|
+
export function lateAnswerText(
|
|
33
|
+
question: string,
|
|
34
|
+
answer: OwnerAnswer,
|
|
35
|
+
by: string,
|
|
36
|
+
): string {
|
|
37
|
+
return `(${by} answered your earlier ask_user question on its card, after your turn had ended. Continue from where you stopped, with this answer.)\nQuestion: ${question}\n${answerText(answer, { name: by, pronouns: PRONOUNS.they })}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** How a question is named in a result: its header, else its text cut short. */
|
|
41
|
+
function nameOf(question: OwnerQuestion): string {
|
|
42
|
+
if (question.header) return question.header;
|
|
43
|
+
const chars = Array.from(question.question.replace(/\s+/g, " ").trim());
|
|
44
|
+
return chars.length <= NAME_CHARS
|
|
45
|
+
? chars.join("")
|
|
46
|
+
: `${chars.slice(0, NAME_CHARS - 1).join("")}…`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function lineOf(answer: OwnerAnswer | undefined) {
|
|
50
|
+
const parts = [
|
|
51
|
+
...(answer && answer.choices.length > 0
|
|
52
|
+
? [`chose ${answer.choices.join("; ")}`]
|
|
53
|
+
: []),
|
|
54
|
+
...(answer?.text !== undefined ? [`Wrote: ${answer.text}`] : []),
|
|
55
|
+
];
|
|
56
|
+
return parts.length > 0 ? parts.join(". ") : "not answered";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One numbered line per question, then the note. */
|
|
60
|
+
function listOf(
|
|
61
|
+
questionnaire: Questionnaire,
|
|
62
|
+
answer: QuestionnaireAnswer,
|
|
63
|
+
): string[] {
|
|
64
|
+
return [
|
|
65
|
+
...questionnaire.questions.map(
|
|
66
|
+
(question, i) =>
|
|
67
|
+
`${i + 1}. ${nameOf(question)}: ${lineOf(answer.answers[i])}`,
|
|
68
|
+
),
|
|
69
|
+
...(answer.note !== undefined ? [`Note: ${answer.note}`] : []),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The tool's result for a questionnaire: each question's answer, and the note. */
|
|
74
|
+
export function questionnaireText(
|
|
75
|
+
answer: QuestionnaireAnswer | "pending" | undefined,
|
|
76
|
+
questionnaire: Questionnaire,
|
|
77
|
+
owner: OwnerIdentity,
|
|
78
|
+
): string {
|
|
79
|
+
if (answer === "pending" || !answer) return answerText(answer, owner);
|
|
80
|
+
return [
|
|
81
|
+
`${owner.name} answered your questions on the card:`,
|
|
82
|
+
...listOf(questionnaire, answer),
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The turn a late questionnaire answer starts: the same list, under a line saying when it came. */
|
|
87
|
+
export function lateQuestionnaireText(
|
|
88
|
+
questionnaire: Questionnaire,
|
|
89
|
+
answer: QuestionnaireAnswer,
|
|
90
|
+
by: string,
|
|
91
|
+
): string {
|
|
92
|
+
return [
|
|
93
|
+
`(${by} answered your earlier ask_user questions on their card, after your turn had ended. Continue from where you stopped, with these answers.)`,
|
|
94
|
+
...listOf(questionnaire, answer),
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The result of questions asked one at a time on a surface that takes one: what was answered
|
|
100
|
+
* before the first that was not (`stopped` says how it ended), and the questions not asked.
|
|
101
|
+
*/
|
|
102
|
+
export function sequentialText(
|
|
103
|
+
asked: Questionnaire,
|
|
104
|
+
answers: readonly OwnerAnswer[],
|
|
105
|
+
stopped: "pending" | "unanswered" | undefined,
|
|
106
|
+
owner: OwnerIdentity,
|
|
107
|
+
stoppedText: string,
|
|
108
|
+
): string {
|
|
109
|
+
const notAsked = asked.questions.slice(
|
|
110
|
+
answers.length + (stopped === undefined ? 0 : 1),
|
|
111
|
+
);
|
|
112
|
+
return [
|
|
113
|
+
...(answers.length > 0
|
|
114
|
+
? [
|
|
115
|
+
`${owner.name} answered your questions on the card:`,
|
|
116
|
+
...asked.questions
|
|
117
|
+
.slice(0, answers.length)
|
|
118
|
+
.map(
|
|
119
|
+
(question, i) =>
|
|
120
|
+
`${i + 1}. ${nameOf(question)}: ${lineOf(answers[i])}`,
|
|
121
|
+
),
|
|
122
|
+
]
|
|
123
|
+
: []),
|
|
124
|
+
...(stopped === undefined
|
|
125
|
+
? []
|
|
126
|
+
: [
|
|
127
|
+
`${
|
|
128
|
+
asked.questions.length > 1
|
|
129
|
+
? `${answers.length + 1}. ${nameOf(asked.questions[answers.length] as OwnerQuestion)}: `
|
|
130
|
+
: ""
|
|
131
|
+
}${stoppedText}`,
|
|
132
|
+
]),
|
|
133
|
+
...(notAsked.length > 0
|
|
134
|
+
? [
|
|
135
|
+
`Not asked: ${notAsked
|
|
136
|
+
.map(
|
|
137
|
+
(question, i) =>
|
|
138
|
+
`${answers.length + (stopped === undefined ? 0 : 1) + i + 1}. ${nameOf(question)}`,
|
|
139
|
+
)
|
|
140
|
+
.join(
|
|
141
|
+
"; ",
|
|
142
|
+
)}; this channel takes one question at a time, so ask again if you still need them.`,
|
|
143
|
+
]
|
|
144
|
+
: []),
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
@@ -1,40 +1,16 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { type OwnerIdentity, ownerWords } from "../../identity.ts";
|
|
4
|
+
import type { OwnerQuestion } from "../../interactions/prompts.ts";
|
|
5
|
+
import { QUESTIONNAIRE_LIMITS as L } from "../../interactions/questionnaire.ts";
|
|
6
6
|
import { toolText } from "../../shared/tool-result.ts";
|
|
7
|
+
import { askQuestions } from "../ask-questions.ts";
|
|
7
8
|
import type { PromptSlot } from "../prompt-slot.ts";
|
|
9
|
+
import { foldAskArguments } from "./ask-user-arguments.ts";
|
|
8
10
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
/** The tool's result: what the owner chose and wrote. */
|
|
12
|
-
export function answerText(
|
|
13
|
-
answer: OwnerAnswer | "pending" | undefined,
|
|
14
|
-
owner: OwnerIdentity,
|
|
15
|
-
): string {
|
|
16
|
-
const o = ownerWords(owner);
|
|
17
|
-
if (answer === "pending")
|
|
18
|
-
return `${o.name} has not answered yet. The card stays open, so do not ask again and do not wait for it. End your turn now: say briefly that you are waiting for ${o.his} answer on the card. When ${o.he} answers, a new turn brings you the answer and you continue from there.`;
|
|
19
|
-
if (!answer)
|
|
20
|
-
return `No answer: ${o.name} did not answer. Go on without it if you safely can, otherwise ask in your reply.`;
|
|
21
|
-
const lines = [
|
|
22
|
-
...(answer.choices.length > 0
|
|
23
|
-
? [`${o.name} chose: ${answer.choices.join("; ")}`]
|
|
24
|
-
: []),
|
|
25
|
-
...(answer.text !== undefined ? [`${o.name} wrote: ${answer.text}`] : []),
|
|
26
|
-
];
|
|
27
|
-
return lines.length > 0 ? lines.join("\n") : `${o.name} chose nothing.`;
|
|
28
|
-
}
|
|
11
|
+
export { answerText, lateAnswerText } from "./ask-user-text.ts";
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
export function lateAnswerText(
|
|
32
|
-
question: string,
|
|
33
|
-
answer: OwnerAnswer,
|
|
34
|
-
by: string,
|
|
35
|
-
): string {
|
|
36
|
-
return `(${by} answered your earlier ask_user question on its card, after your turn had ended. Continue from where you stopped, with this answer.)\nQuestion: ${question}\n${answerText(answer, { name: by, pronouns: PRONOUNS.they })}`;
|
|
37
|
-
}
|
|
13
|
+
export const ASK_USER_TOOL = "ask_user";
|
|
38
14
|
|
|
39
15
|
/** Registers ask_user, which asks the owner on a card in the turn's channel and waits. */
|
|
40
16
|
export function askUserExtension(
|
|
@@ -45,36 +21,68 @@ export function askUserExtension(
|
|
|
45
21
|
): ExtensionFactory {
|
|
46
22
|
const o = ownerWords(owner);
|
|
47
23
|
return (pi) => {
|
|
24
|
+
const parameters = Type.Object(
|
|
25
|
+
{
|
|
26
|
+
questions: Type.Array(
|
|
27
|
+
Type.Object(
|
|
28
|
+
{
|
|
29
|
+
question: Type.String({
|
|
30
|
+
minLength: 1,
|
|
31
|
+
maxLength: L.questionChars,
|
|
32
|
+
}),
|
|
33
|
+
header: Type.Optional(
|
|
34
|
+
Type.String({
|
|
35
|
+
maxLength: L.headerChars,
|
|
36
|
+
description: "A short name for the question in a set.",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
options: Type.Optional(
|
|
40
|
+
Type.Array(
|
|
41
|
+
Type.Object(
|
|
42
|
+
{
|
|
43
|
+
label: Type.String({
|
|
44
|
+
minLength: 1,
|
|
45
|
+
maxLength: L.labelChars,
|
|
46
|
+
}),
|
|
47
|
+
description: Type.Optional(
|
|
48
|
+
Type.String({ maxLength: L.descriptionChars }),
|
|
49
|
+
),
|
|
50
|
+
preview: Type.Optional(
|
|
51
|
+
Type.String({
|
|
52
|
+
maxLength: L.previewChars,
|
|
53
|
+
description:
|
|
54
|
+
"A markdown mock-up or snippet shown with the option.",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{ additionalProperties: false },
|
|
59
|
+
),
|
|
60
|
+
{ maxItems: L.options },
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
multi: Type.Optional(
|
|
64
|
+
Type.Boolean({ description: "Several options may be chosen." }),
|
|
65
|
+
),
|
|
66
|
+
allow_other: Type.Optional(
|
|
67
|
+
Type.Boolean({
|
|
68
|
+
description: `${o.He} may write an answer besides the options (default true; false for a closed choice).`,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
{ additionalProperties: false },
|
|
73
|
+
),
|
|
74
|
+
{ minItems: 1, maxItems: L.questions },
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
{ additionalProperties: false },
|
|
78
|
+
);
|
|
48
79
|
pi.registerTool({
|
|
49
80
|
name: ASK_USER_TOOL,
|
|
50
81
|
label: `Ask ${o.name}`,
|
|
51
|
-
description: `Ask ${o.name} a question on a card with buttons in this channel and wait for ${o.his} answer, within this turn. Use it when you need ${o.his} decision to continue, instead of ending your turn with a question. The card should read on its own, so put the context ${o.he} needs in the question too. Offer options when the choices are known; set multi when several may apply and allow_other to
|
|
52
|
-
parameters
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
options: Type.Optional(
|
|
56
|
-
Type.Array(
|
|
57
|
-
Type.Object(
|
|
58
|
-
{
|
|
59
|
-
label: Type.String({ minLength: 1, maxLength: 100 }),
|
|
60
|
-
description: Type.Optional(Type.String({ maxLength: 100 })),
|
|
61
|
-
},
|
|
62
|
-
{ additionalProperties: false },
|
|
63
|
-
),
|
|
64
|
-
{ maxItems: 25 },
|
|
65
|
-
),
|
|
66
|
-
),
|
|
67
|
-
multi: Type.Optional(
|
|
68
|
-
Type.Boolean({ description: "Several options may be chosen." }),
|
|
69
|
-
),
|
|
70
|
-
allow_other: Type.Optional(
|
|
71
|
-
Type.Boolean({
|
|
72
|
-
description: `${o.He} may write an answer besides the options.`,
|
|
73
|
-
}),
|
|
74
|
-
),
|
|
75
|
-
},
|
|
76
|
-
{ additionalProperties: false },
|
|
77
|
-
),
|
|
82
|
+
description: `Ask ${o.name} a question, or up to four related ones, on a card with buttons in this channel and wait for ${o.his} answer, within this turn. Use it when you need ${o.his} decision to continue, instead of ending your turn with a question. The card should read on its own, so put the context ${o.he} needs in the question too. Put every question you have into one call as questions. Offer options when the choices are known, each with a short description, and a preview (markdown) when seeing the option helps; set multi when several may apply, and allow_other to false for a closed choice (otherwise ${o.he} may write ${o.his} own answer). Give each question a short header when you ask several. If ${o.he} does not answer soon, the result says so and the card stays open: then end your turn saying you are waiting, and ${o.his} answer comes in a new turn.`,
|
|
83
|
+
parameters,
|
|
84
|
+
prepareArguments: (args) =>
|
|
85
|
+
foldAskArguments(args) as Static<typeof parameters>,
|
|
78
86
|
execute: async (_toolCallId, params, signal) => {
|
|
79
87
|
const prompts = slot.prompts;
|
|
80
88
|
if (!prompts)
|
|
@@ -82,31 +90,29 @@ export function askUserExtension(
|
|
|
82
90
|
`${o.name} cannot answer buttons in this turn, such as a schedule's. Ask ${o.him} in your reply instead.`,
|
|
83
91
|
);
|
|
84
92
|
const p = params as {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
options: p.options ?? [],
|
|
93
|
-
multi: p.multi === true,
|
|
94
|
-
allowOther: p.allow_other === true,
|
|
93
|
+
questions: {
|
|
94
|
+
question: string;
|
|
95
|
+
header?: string;
|
|
96
|
+
options?: OwnerQuestion["options"];
|
|
97
|
+
multi?: boolean;
|
|
98
|
+
allow_other?: boolean;
|
|
99
|
+
}[];
|
|
95
100
|
};
|
|
96
101
|
return toolText(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
},
|
|
108
|
-
|
|
102
|
+
await askQuestions(
|
|
103
|
+
prompts,
|
|
104
|
+
slot.asker,
|
|
105
|
+
{
|
|
106
|
+
questions: p.questions.map((q) => ({
|
|
107
|
+
question: q.question,
|
|
108
|
+
...(q.header !== undefined ? { header: q.header } : {}),
|
|
109
|
+
options: q.options ?? [],
|
|
110
|
+
multi: q.multi === true,
|
|
111
|
+
allowOther: q.allow_other !== false,
|
|
112
|
+
})),
|
|
113
|
+
},
|
|
109
114
|
addressee?.() ?? owner,
|
|
115
|
+
signal,
|
|
110
116
|
),
|
|
111
117
|
);
|
|
112
118
|
},
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { HoldRefusal } from "../domain/progress.ts";
|
|
2
2
|
import { assistantName } from "../i18n/index.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
OwnerQuestion,
|
|
5
|
+
Prompts,
|
|
6
|
+
PromptWait,
|
|
7
|
+
Questionnaire,
|
|
8
|
+
} from "../interactions/prompts.ts";
|
|
4
9
|
import { turnBudget } from "./turn-budget.ts";
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -61,7 +66,22 @@ export class PromptSlot {
|
|
|
61
66
|
get prompts(): Prompts | undefined {
|
|
62
67
|
const prompts = this.#prompts;
|
|
63
68
|
if (!prompts) return undefined;
|
|
69
|
+
const { questionnaire } = prompts;
|
|
64
70
|
return {
|
|
71
|
+
...(prompts.asks && questionnaire
|
|
72
|
+
? {
|
|
73
|
+
asks: prompts.asks,
|
|
74
|
+
questionnaire: (
|
|
75
|
+
title: string,
|
|
76
|
+
asked: Questionnaire,
|
|
77
|
+
signal?: AbortSignal,
|
|
78
|
+
wait?: PromptWait,
|
|
79
|
+
) =>
|
|
80
|
+
this.#waiting(() =>
|
|
81
|
+
questionnaire.call(prompts, title, asked, signal, wait),
|
|
82
|
+
),
|
|
83
|
+
}
|
|
84
|
+
: {}),
|
|
65
85
|
confirm: (title, message, signal, minTier, wait, details, options) =>
|
|
66
86
|
this.#waiting(() =>
|
|
67
87
|
prompts.confirm(
|