@agimon-ai/po-user-feedback 0.0.0-alpha.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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +627 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +622 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +619 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/llms.txt +4 -0
- package/package.json +124 -0
- package/src/prompts/po-use-user-feedback/SKILL.md +14 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { composeServerProviderOverrides, defineServerPlugin, readPackageResource } from "@agimon-ai/po-core/serverFacet";
|
|
2
|
+
import { composeRootHooks, defineResource, defineRoot, defineServerTool, defineToolRestriction } from "@agimon-ai/po-core/extensionFile";
|
|
3
|
+
import { DOOM_HELP_WHEN } from "@agimon-ai/po-core/help";
|
|
4
|
+
import { DOOM_VOICE_AUTO_MODE_ID } from "@agimon-ai/po-core/voiceTools";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
//#region src/services/questionnaireCoordinator/index.ts
|
|
7
|
+
function cancelledResult(entry) {
|
|
8
|
+
return {
|
|
9
|
+
answers: entry.latestAnswers.map((answer) => ({ ...answer })),
|
|
10
|
+
cancelled: true
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
var QuestionnaireCoordinator = class {
|
|
14
|
+
pending = [];
|
|
15
|
+
running = /* @__PURE__ */ new Set();
|
|
16
|
+
active;
|
|
17
|
+
nextRequestId = 1;
|
|
18
|
+
accepting = true;
|
|
19
|
+
enqueue(runner, externalSignal) {
|
|
20
|
+
if (!this.accepting || externalSignal?.aborted) return Promise.resolve({
|
|
21
|
+
answers: [],
|
|
22
|
+
cancelled: true
|
|
23
|
+
});
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const entry = {
|
|
26
|
+
id: this.nextRequestId,
|
|
27
|
+
runner,
|
|
28
|
+
controller: new AbortController(),
|
|
29
|
+
...externalSignal ? { externalSignal } : {},
|
|
30
|
+
latestAnswers: [],
|
|
31
|
+
state: "queued",
|
|
32
|
+
resolve,
|
|
33
|
+
reject
|
|
34
|
+
};
|
|
35
|
+
this.nextRequestId += 1;
|
|
36
|
+
const abort = () => this.cancel(entry);
|
|
37
|
+
externalSignal?.addEventListener("abort", abort, { once: true });
|
|
38
|
+
entry.removeAbortListener = () => externalSignal?.removeEventListener("abort", abort);
|
|
39
|
+
this.pending.push(entry);
|
|
40
|
+
this.startNext();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
shutdown() {
|
|
44
|
+
if (!this.accepting && !this.active && this.pending.length === 0) return;
|
|
45
|
+
this.accepting = false;
|
|
46
|
+
if (this.active) this.cancel(this.active);
|
|
47
|
+
while (this.pending.length > 0) {
|
|
48
|
+
const entry = this.pending[0];
|
|
49
|
+
if (!entry) break;
|
|
50
|
+
this.cancel(entry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Waits for every started presenter to observe cancellation and finish. */
|
|
54
|
+
async waitForIdle() {
|
|
55
|
+
while (this.running.size > 0) await Promise.allSettled(this.running);
|
|
56
|
+
}
|
|
57
|
+
startNext() {
|
|
58
|
+
if (!this.accepting || this.active) return;
|
|
59
|
+
const entry = this.pending.shift();
|
|
60
|
+
if (!entry) return;
|
|
61
|
+
entry.state = "active";
|
|
62
|
+
this.active = entry;
|
|
63
|
+
const reportProgress = (result) => {
|
|
64
|
+
if (this.active?.id !== entry.id || entry.state !== "active") return;
|
|
65
|
+
entry.latestAnswers = result.answers.map((answer) => ({ ...answer }));
|
|
66
|
+
};
|
|
67
|
+
let operation;
|
|
68
|
+
try {
|
|
69
|
+
operation = entry.runner({
|
|
70
|
+
signal: entry.controller.signal,
|
|
71
|
+
reportProgress
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
operation = Promise.reject(error);
|
|
75
|
+
}
|
|
76
|
+
this.running.add(operation);
|
|
77
|
+
operation.then((result) => this.complete(entry, result), (error) => this.fail(entry, error)).finally(() => this.running.delete(operation));
|
|
78
|
+
}
|
|
79
|
+
complete(entry, result) {
|
|
80
|
+
if (this.active?.id !== entry.id || entry.state !== "active") return;
|
|
81
|
+
this.releaseActive(entry);
|
|
82
|
+
entry.resolve(result);
|
|
83
|
+
this.startNext();
|
|
84
|
+
}
|
|
85
|
+
fail(entry, error) {
|
|
86
|
+
if (this.active?.id !== entry.id || entry.state !== "active") return;
|
|
87
|
+
this.releaseActive(entry);
|
|
88
|
+
entry.reject(error);
|
|
89
|
+
this.startNext();
|
|
90
|
+
}
|
|
91
|
+
cancel(entry) {
|
|
92
|
+
if (entry.state === "settled") return;
|
|
93
|
+
if (entry.state === "queued") {
|
|
94
|
+
const index = this.pending.indexOf(entry);
|
|
95
|
+
this.pending.splice(index, 1);
|
|
96
|
+
entry.state = "settled";
|
|
97
|
+
entry.removeAbortListener?.();
|
|
98
|
+
entry.controller.abort();
|
|
99
|
+
entry.resolve(cancelledResult(entry));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const result = cancelledResult(entry);
|
|
103
|
+
this.releaseActive(entry);
|
|
104
|
+
entry.controller.abort();
|
|
105
|
+
entry.resolve(result);
|
|
106
|
+
this.startNext();
|
|
107
|
+
}
|
|
108
|
+
releaseActive(entry) {
|
|
109
|
+
entry.state = "settled";
|
|
110
|
+
entry.removeAbortListener?.();
|
|
111
|
+
this.active = void 0;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
|
|
116
|
+
const root = defineRoot((_context) => {
|
|
117
|
+
const coordinator = new QuestionnaireCoordinator();
|
|
118
|
+
return {
|
|
119
|
+
value: { coordinator },
|
|
120
|
+
onDispose: async () => {
|
|
121
|
+
coordinator.shutdown();
|
|
122
|
+
await coordinator.waitForIdle();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/extensions/workspaces/sessions/(backend)/root.server.ts
|
|
128
|
+
var root_server_default = defineRoot(root);
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/extensions/workspaces/sessions/(backend)/resource/poUseUserFeedback.server.ts
|
|
131
|
+
var poUseUserFeedback_server_default = defineResource({
|
|
132
|
+
name: "po-use-user-feedback",
|
|
133
|
+
when: DOOM_HELP_WHEN,
|
|
134
|
+
kind: "skill",
|
|
135
|
+
read: () => readPackageResource(import.meta.url, "src/prompts/po-use-user-feedback/SKILL.md")
|
|
136
|
+
});
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/constants/questionnaire.ts
|
|
139
|
+
const RESERVED_LABELS = [
|
|
140
|
+
"Other",
|
|
141
|
+
"Type something.",
|
|
142
|
+
"Next"
|
|
143
|
+
];
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/constants/tool.ts
|
|
146
|
+
const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool-restriction/askUserQuestion.server.ts
|
|
149
|
+
var askUserQuestion_server_default$1 = defineToolRestriction({
|
|
150
|
+
when: { state: { "minor-mode": DOOM_VOICE_AUTO_MODE_ID } },
|
|
151
|
+
excludedTools: [ASK_USER_QUESTION_TOOL_NAME]
|
|
152
|
+
});
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/schemas/questionnaire.ts
|
|
155
|
+
const OptionSchema = Type.Object({
|
|
156
|
+
label: Type.String({
|
|
157
|
+
maxLength: 60,
|
|
158
|
+
description: "MAX 60 CHARACTERS, hard limit. Concise option label shown to the user, ideally 1-5 words."
|
|
159
|
+
}),
|
|
160
|
+
description: Type.String({ description: "Explanation of what the option means and its important trade-offs." }),
|
|
161
|
+
preview: Type.Optional(Type.String({ description: "Optional markdown preview rendered while this option is focused." }))
|
|
162
|
+
}, { additionalProperties: false });
|
|
163
|
+
const QuestionSchema = Type.Object({
|
|
164
|
+
question: Type.String({ description: "The complete, clear question to ask the user." }),
|
|
165
|
+
header: Type.String({
|
|
166
|
+
maxLength: 16,
|
|
167
|
+
description: "MAX 16 CHARACTERS, short label used when navigating several questions."
|
|
168
|
+
}),
|
|
169
|
+
options: Type.Array(OptionSchema, {
|
|
170
|
+
minItems: 2,
|
|
171
|
+
maxItems: 4,
|
|
172
|
+
description: "The 2-4 choices offered for this question."
|
|
173
|
+
}),
|
|
174
|
+
multiSelect: Type.Optional(Type.Boolean({
|
|
175
|
+
description: "Allow more than one option to be selected.",
|
|
176
|
+
default: false
|
|
177
|
+
}))
|
|
178
|
+
}, { additionalProperties: false });
|
|
179
|
+
const QuestionParamsSchema = Type.Object({ questions: Type.Array(QuestionSchema, {
|
|
180
|
+
minItems: 1,
|
|
181
|
+
maxItems: 4,
|
|
182
|
+
description: "Questions to ask the user (1-4 questions)."
|
|
183
|
+
}) }, { additionalProperties: false });
|
|
184
|
+
function decodeAnswerEnvelope(value) {
|
|
185
|
+
if (value === void 0 || !value.startsWith("doom-ask-user/v1:")) return { kind: "absent" };
|
|
186
|
+
if (value.length > 65536) return { kind: "malformed" };
|
|
187
|
+
try {
|
|
188
|
+
return {
|
|
189
|
+
kind: "envelope",
|
|
190
|
+
payload: JSON.parse(value.slice(17))
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return { kind: "malformed" };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/services/answerEnvelope/index.ts
|
|
198
|
+
/**
|
|
199
|
+
* Read a whole-questionnaire answer envelope back against the questions it claims to answer.
|
|
200
|
+
*
|
|
201
|
+
* The envelope arrives on a select response, which is reader input rather than
|
|
202
|
+
* anything the tool authored, so nothing in it is taken on trust: an index has
|
|
203
|
+
* to name a question that was asked, a chosen label has to be one that
|
|
204
|
+
* question offered, and free text is capped. An envelope that fails is refused
|
|
205
|
+
* rather than repaired, because a half-read answer set reaches the model as
|
|
206
|
+
* the reader's decision and there is no way to tell from there that part of it
|
|
207
|
+
* was invented.
|
|
208
|
+
*
|
|
209
|
+
* `preview` is the clearest case: it is read from the option the tool
|
|
210
|
+
* declared, never from the wire, so the model cannot be shown a preview that
|
|
211
|
+
* was never offered.
|
|
212
|
+
*
|
|
213
|
+
* Host-neutral by construction: the questions and the payload both arrive as
|
|
214
|
+
* arguments, so the same arguments always produce the same result.
|
|
215
|
+
*/
|
|
216
|
+
const MAX_ANSWER_TEXT_LENGTH = 4e3;
|
|
217
|
+
const ANSWER_KINDS = [
|
|
218
|
+
"option",
|
|
219
|
+
"custom",
|
|
220
|
+
"multi"
|
|
221
|
+
];
|
|
222
|
+
function isRecord(value) {
|
|
223
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
224
|
+
}
|
|
225
|
+
function optionalNote(value) {
|
|
226
|
+
if (typeof value !== "string") return void 0;
|
|
227
|
+
const trimmed = value.trim();
|
|
228
|
+
return trimmed === "" ? void 0 : trimmed.slice(0, MAX_ANSWER_TEXT_LENGTH);
|
|
229
|
+
}
|
|
230
|
+
/** One answer, rebuilt from the question it names rather than copied off the wire. */
|
|
231
|
+
function readAnswer(entry, params, seen) {
|
|
232
|
+
if (!isRecord(entry)) return "an answer was not an object";
|
|
233
|
+
const questionIndex = entry.questionIndex;
|
|
234
|
+
if (typeof questionIndex !== "number" || !Number.isInteger(questionIndex)) return "an answer carried no question index";
|
|
235
|
+
const question = params.questions[questionIndex];
|
|
236
|
+
if (question === void 0) return `no question ${String(questionIndex)} was asked`;
|
|
237
|
+
if (seen.has(questionIndex)) return `question ${String(questionIndex)} was answered twice`;
|
|
238
|
+
seen.add(questionIndex);
|
|
239
|
+
const kind = entry.kind;
|
|
240
|
+
if (typeof kind !== "string" || !ANSWER_KINDS.includes(kind)) return `question ${String(questionIndex)} carried an unknown answer kind`;
|
|
241
|
+
const notes = optionalNote(entry.notes);
|
|
242
|
+
const base = {
|
|
243
|
+
questionIndex,
|
|
244
|
+
question: question.question,
|
|
245
|
+
...notes === void 0 ? {} : { notes }
|
|
246
|
+
};
|
|
247
|
+
if (kind === "multi") {
|
|
248
|
+
const selected = entry.selected;
|
|
249
|
+
if (!Array.isArray(selected)) return `question ${String(questionIndex)} selected nothing readable`;
|
|
250
|
+
const labels = selected.filter((label) => typeof label === "string");
|
|
251
|
+
if (labels.length !== selected.length) return `question ${String(questionIndex)} selected a non-label`;
|
|
252
|
+
if (labels.some((label) => !question.options.some((option) => option.label === label))) return `question ${String(questionIndex)} selected an option it was not offered`;
|
|
253
|
+
return {
|
|
254
|
+
...base,
|
|
255
|
+
kind: "multi",
|
|
256
|
+
answer: null,
|
|
257
|
+
selected: [...new Set(labels)]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (kind === "option") {
|
|
261
|
+
const label = entry.answer;
|
|
262
|
+
const chosen = typeof label === "string" ? question.options.find((option) => option.label === label) : void 0;
|
|
263
|
+
if (chosen === void 0) return `question ${String(questionIndex)} chose an option it was not offered`;
|
|
264
|
+
return {
|
|
265
|
+
...base,
|
|
266
|
+
kind: "option",
|
|
267
|
+
answer: chosen.label,
|
|
268
|
+
...chosen.preview ? { preview: chosen.preview } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const typed = entry.answer;
|
|
272
|
+
if (typeof typed !== "string") return `question ${String(questionIndex)} typed nothing readable`;
|
|
273
|
+
return {
|
|
274
|
+
...base,
|
|
275
|
+
kind: "custom",
|
|
276
|
+
answer: typed.slice(0, MAX_ANSWER_TEXT_LENGTH)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function readAnswerEnvelope(payload, params) {
|
|
280
|
+
if (!isRecord(payload) || !Array.isArray(payload.answers)) return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: "no answers were carried"
|
|
283
|
+
};
|
|
284
|
+
if (payload.answers.length === 0) return {
|
|
285
|
+
ok: false,
|
|
286
|
+
reason: "no answers were carried"
|
|
287
|
+
};
|
|
288
|
+
if (payload.answers.length > params.questions.length) return {
|
|
289
|
+
ok: false,
|
|
290
|
+
reason: "more answers than questions"
|
|
291
|
+
};
|
|
292
|
+
const seen = /* @__PURE__ */ new Set();
|
|
293
|
+
const answers = [];
|
|
294
|
+
for (const entry of payload.answers) {
|
|
295
|
+
const answer = readAnswer(entry, params, seen);
|
|
296
|
+
if (typeof answer === "string") return {
|
|
297
|
+
ok: false,
|
|
298
|
+
reason: answer
|
|
299
|
+
};
|
|
300
|
+
answers.push(answer);
|
|
301
|
+
}
|
|
302
|
+
answers.sort((left, right) => left.questionIndex - right.questionIndex);
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
answers
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/services/questionnaireService/index.ts
|
|
310
|
+
const CUSTOM_LABEL = "Type something.";
|
|
311
|
+
const DONE_LABEL = "Next";
|
|
312
|
+
function cancelled(answers) {
|
|
313
|
+
return {
|
|
314
|
+
answers,
|
|
315
|
+
cancelled: true
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** True when this response was the whole set, in which case the sink now holds the answer. */
|
|
319
|
+
function takeEnvelope(sink, selected) {
|
|
320
|
+
const read = decodeAnswerEnvelope(selected);
|
|
321
|
+
if (read.kind === "absent") return false;
|
|
322
|
+
if (read.kind === "malformed") {
|
|
323
|
+
sink.result = {
|
|
324
|
+
answers: [],
|
|
325
|
+
cancelled: true,
|
|
326
|
+
error: "malformed_answers"
|
|
327
|
+
};
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
const envelope = readAnswerEnvelope(read.payload, sink.params);
|
|
331
|
+
sink.result = envelope.ok ? {
|
|
332
|
+
answers: envelope.answers,
|
|
333
|
+
cancelled: false
|
|
334
|
+
} : {
|
|
335
|
+
answers: [],
|
|
336
|
+
cancelled: true,
|
|
337
|
+
error: "malformed_answers"
|
|
338
|
+
};
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
async function askForCustomAnswer(interaction, question, questionIndex, signal) {
|
|
342
|
+
const answer = await interaction.input(question.question, "Type your answer", signal);
|
|
343
|
+
if (answer === void 0) return void 0;
|
|
344
|
+
return {
|
|
345
|
+
questionIndex,
|
|
346
|
+
question: question.question,
|
|
347
|
+
kind: "custom",
|
|
348
|
+
answer
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async function askSingleChoice(interaction, question, questionIndex, sink, signal) {
|
|
352
|
+
const labels = [...question.options.map((option) => option.label), CUSTOM_LABEL];
|
|
353
|
+
const selected = await interaction.select(question.question, labels, signal);
|
|
354
|
+
if (selected === void 0) return void 0;
|
|
355
|
+
if (takeEnvelope(sink, selected)) return void 0;
|
|
356
|
+
if (selected === CUSTOM_LABEL) return askForCustomAnswer(interaction, question, questionIndex, signal);
|
|
357
|
+
const option = question.options.find((candidate) => candidate.label === selected);
|
|
358
|
+
if (!option) return void 0;
|
|
359
|
+
return {
|
|
360
|
+
questionIndex,
|
|
361
|
+
question: question.question,
|
|
362
|
+
kind: "option",
|
|
363
|
+
answer: option.label,
|
|
364
|
+
...option.preview ? { preview: option.preview } : {}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function multiChoiceLabels(question, selected) {
|
|
368
|
+
return [
|
|
369
|
+
...question.options.map((option) => {
|
|
370
|
+
return `${selected.has(option.label) ? "[x]" : "[ ]"} ${option.label}`;
|
|
371
|
+
}),
|
|
372
|
+
CUSTOM_LABEL,
|
|
373
|
+
DONE_LABEL
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
function stripSelectionMarker(value) {
|
|
377
|
+
return value.replace(/^\[[ x]\]\s/u, "");
|
|
378
|
+
}
|
|
379
|
+
async function askMultipleChoice(interaction, question, questionIndex, sink, signal) {
|
|
380
|
+
const selected = /* @__PURE__ */ new Set();
|
|
381
|
+
while (!signal?.aborted) {
|
|
382
|
+
const choice = await interaction.select(`${question.question} (choose options, then Next)`, multiChoiceLabels(question, selected), signal);
|
|
383
|
+
if (choice === void 0) return void 0;
|
|
384
|
+
if (takeEnvelope(sink, choice)) return void 0;
|
|
385
|
+
if (choice === CUSTOM_LABEL) return askForCustomAnswer(interaction, question, questionIndex, signal);
|
|
386
|
+
if (choice === DONE_LABEL) return {
|
|
387
|
+
questionIndex,
|
|
388
|
+
question: question.question,
|
|
389
|
+
kind: "multi",
|
|
390
|
+
answer: null,
|
|
391
|
+
selected: [...selected]
|
|
392
|
+
};
|
|
393
|
+
const label = stripSelectionMarker(choice);
|
|
394
|
+
if (!question.options.some((option) => option.label === label)) continue;
|
|
395
|
+
if (selected.has(label)) selected.delete(label);
|
|
396
|
+
else selected.add(label);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function runQuestionnaire(interaction, params, signal, reportProgress) {
|
|
400
|
+
const answers = [];
|
|
401
|
+
const sink = { params };
|
|
402
|
+
for (let questionIndex = 0; questionIndex < params.questions.length; questionIndex += 1) {
|
|
403
|
+
if (signal?.aborted) return cancelled(answers);
|
|
404
|
+
const question = params.questions[questionIndex];
|
|
405
|
+
if (!question) continue;
|
|
406
|
+
const answer = question.multiSelect ? await askMultipleChoice(interaction, question, questionIndex, sink, signal) : await askSingleChoice(interaction, question, questionIndex, sink, signal);
|
|
407
|
+
if (sink.result !== void 0) {
|
|
408
|
+
const whole = sink.result;
|
|
409
|
+
if (!whole.cancelled) reportProgress?.({
|
|
410
|
+
...whole,
|
|
411
|
+
answers: whole.answers.map((item) => ({ ...item }))
|
|
412
|
+
});
|
|
413
|
+
return whole;
|
|
414
|
+
}
|
|
415
|
+
if (!answer) return cancelled(answers);
|
|
416
|
+
answers.push(answer);
|
|
417
|
+
reportProgress?.({
|
|
418
|
+
answers: answers.map((item) => ({ ...item })),
|
|
419
|
+
cancelled: false
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
answers,
|
|
424
|
+
cancelled: false
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/services/responseService/index.ts
|
|
429
|
+
const DECLINE_MESSAGE = "User declined to answer questions";
|
|
430
|
+
const ENVELOPE_PREFIX = "User has answered your questions:";
|
|
431
|
+
const ENVELOPE_SUFFIX = "You can now continue with the user's answers in mind.";
|
|
432
|
+
const NO_INPUT_PLACEHOLDER = "(no input)";
|
|
433
|
+
function buildToolResult(text, details) {
|
|
434
|
+
return {
|
|
435
|
+
content: [{
|
|
436
|
+
type: "text",
|
|
437
|
+
text
|
|
438
|
+
}],
|
|
439
|
+
details
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function formatAnswer(answer) {
|
|
443
|
+
if (answer.kind === "multi") return answer.selected && answer.selected.length > 0 ? answer.selected.join(", ") : NO_INPUT_PLACEHOLDER;
|
|
444
|
+
return answer.answer && answer.answer.length > 0 ? answer.answer : NO_INPUT_PLACEHOLDER;
|
|
445
|
+
}
|
|
446
|
+
function buildAnswerSegment(answer) {
|
|
447
|
+
const sections = [`"${answer.question}"="${formatAnswer(answer)}"`];
|
|
448
|
+
if (answer.preview) sections.push(`selected preview: ${answer.preview}`);
|
|
449
|
+
if (answer.notes) sections.push(`user notes: ${answer.notes}`);
|
|
450
|
+
return `${sections.join(". ")}.`;
|
|
451
|
+
}
|
|
452
|
+
function buildQuestionnaireResponse(result, params) {
|
|
453
|
+
if (!result || result.cancelled) return buildToolResult(DECLINE_MESSAGE, {
|
|
454
|
+
answers: result?.answers ?? [],
|
|
455
|
+
cancelled: true
|
|
456
|
+
});
|
|
457
|
+
const segments = params.questions.flatMap((_question, index) => {
|
|
458
|
+
const answer = result.answers.find((candidate) => candidate.questionIndex === index);
|
|
459
|
+
return answer ? [buildAnswerSegment(answer)] : [];
|
|
460
|
+
});
|
|
461
|
+
if (segments.length === 0) return buildToolResult(DECLINE_MESSAGE, {
|
|
462
|
+
answers: result.answers,
|
|
463
|
+
cancelled: true
|
|
464
|
+
});
|
|
465
|
+
return buildToolResult(`${ENVELOPE_PREFIX} ${segments.join(" ")} ${ENVELOPE_SUFFIX}`, result);
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/services/validationService/index.ts
|
|
469
|
+
const ERROR_NO_QUESTIONS = "Error: At least one question is required";
|
|
470
|
+
const ERROR_TOO_MANY_QUESTIONS = `Error: At most 4 questions are allowed per invocation`;
|
|
471
|
+
const ERROR_DUPLICATE_QUESTION = "Error: Question text must be unique within an invocation";
|
|
472
|
+
const ERROR_TOO_FEW_OPTIONS = `Error: Each question requires at least 2 options`;
|
|
473
|
+
const ERROR_RESERVED_LABEL = `Error: Option label is reserved (${RESERVED_LABELS.join(", ")})`;
|
|
474
|
+
const ERROR_DUPLICATE_OPTION_LABEL = "Error: Option labels must be unique within a question";
|
|
475
|
+
const reservedLabels = new Set(RESERVED_LABELS);
|
|
476
|
+
function validateQuestionnaire(params) {
|
|
477
|
+
if (params.questions.length === 0) return {
|
|
478
|
+
ok: false,
|
|
479
|
+
error: "no_questions",
|
|
480
|
+
message: ERROR_NO_QUESTIONS
|
|
481
|
+
};
|
|
482
|
+
if (params.questions.length > 4) return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: "too_many_questions",
|
|
485
|
+
message: ERROR_TOO_MANY_QUESTIONS
|
|
486
|
+
};
|
|
487
|
+
const seenQuestions = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const question of params.questions) {
|
|
489
|
+
if (seenQuestions.has(question.question)) return {
|
|
490
|
+
ok: false,
|
|
491
|
+
error: "duplicate_question",
|
|
492
|
+
message: ERROR_DUPLICATE_QUESTION
|
|
493
|
+
};
|
|
494
|
+
seenQuestions.add(question.question);
|
|
495
|
+
}
|
|
496
|
+
for (const question of params.questions) {
|
|
497
|
+
if (question.options.length < 2) return {
|
|
498
|
+
ok: false,
|
|
499
|
+
error: "empty_options",
|
|
500
|
+
message: ERROR_TOO_FEW_OPTIONS
|
|
501
|
+
};
|
|
502
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
503
|
+
for (const option of question.options) {
|
|
504
|
+
if (reservedLabels.has(option.label)) return {
|
|
505
|
+
ok: false,
|
|
506
|
+
error: "reserved_label",
|
|
507
|
+
message: ERROR_RESERVED_LABEL
|
|
508
|
+
};
|
|
509
|
+
if (seenLabels.has(option.label)) return {
|
|
510
|
+
ok: false,
|
|
511
|
+
error: "duplicate_option_label",
|
|
512
|
+
message: ERROR_DUPLICATE_OPTION_LABEL
|
|
513
|
+
};
|
|
514
|
+
seenLabels.add(option.label);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return { ok: true };
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/extensions/workspaces/sessions/(backend)/_tools/askUserHeadless.ts
|
|
521
|
+
function output(result) {
|
|
522
|
+
return {
|
|
523
|
+
content: result.content,
|
|
524
|
+
details: result.details
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function createAskUserHeadlessTool(coordinator) {
|
|
528
|
+
return {
|
|
529
|
+
name: ASK_USER_QUESTION_TOOL_NAME,
|
|
530
|
+
label: "Ask User Question",
|
|
531
|
+
description: "Ask the user one or more structured questions during execution.",
|
|
532
|
+
parameters: QuestionParamsSchema,
|
|
533
|
+
promptSnippet: "Ask the user structured questions when requirements are ambiguous",
|
|
534
|
+
executionMode: "serial",
|
|
535
|
+
async execute(_toolCallId, parameters, signal, _onUpdate, executionContext) {
|
|
536
|
+
const params = parameters;
|
|
537
|
+
const validation = validateQuestionnaire(params);
|
|
538
|
+
if (!validation.ok) return output(buildToolResult(validation.message, {
|
|
539
|
+
answers: [],
|
|
540
|
+
cancelled: true,
|
|
541
|
+
error: validation.error
|
|
542
|
+
}));
|
|
543
|
+
const activeCoordinator = coordinator ?? new QuestionnaireCoordinator();
|
|
544
|
+
try {
|
|
545
|
+
return output(buildQuestionnaireResponse(await activeCoordinator.enqueue(async ({ signal: requestSignal, reportProgress }) => {
|
|
546
|
+
requestSignal.throwIfAborted();
|
|
547
|
+
const questionnaire = await runQuestionnaire({
|
|
548
|
+
select: async (title, options, interactionSignal) => {
|
|
549
|
+
requestSignal.throwIfAborted();
|
|
550
|
+
const response = await executionContext.client.request({
|
|
551
|
+
kind: "select",
|
|
552
|
+
title,
|
|
553
|
+
options: options.map((label) => ({
|
|
554
|
+
label,
|
|
555
|
+
value: label
|
|
556
|
+
}))
|
|
557
|
+
}, interactionSignal);
|
|
558
|
+
return typeof response === "string" ? response : void 0;
|
|
559
|
+
},
|
|
560
|
+
input: async (title, placeholder, interactionSignal) => {
|
|
561
|
+
requestSignal.throwIfAborted();
|
|
562
|
+
const response = await executionContext.client.request({
|
|
563
|
+
kind: "input",
|
|
564
|
+
title,
|
|
565
|
+
message: placeholder
|
|
566
|
+
}, interactionSignal);
|
|
567
|
+
return typeof response === "string" ? response : void 0;
|
|
568
|
+
}
|
|
569
|
+
}, params, requestSignal, reportProgress);
|
|
570
|
+
requestSignal.throwIfAborted();
|
|
571
|
+
return questionnaire;
|
|
572
|
+
}, signal), params));
|
|
573
|
+
} finally {
|
|
574
|
+
if (coordinator === void 0) {
|
|
575
|
+
activeCoordinator.shutdown();
|
|
576
|
+
await activeCoordinator.waitForIdle();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/extensions/workspaces/sessions/(backend)/tool/askUserQuestion.server.ts
|
|
584
|
+
var askUserQuestion_server_default = defineServerTool((context) => createAskUserHeadlessTool(context.root.coordinator));
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region generated/server.ts
|
|
587
|
+
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
588
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
589
|
+
...identity,
|
|
590
|
+
...value
|
|
591
|
+
};
|
|
592
|
+
const facet = defineServerPlugin({
|
|
593
|
+
name: "@agimon-ai/po-user-feedback",
|
|
594
|
+
providerOverrides: composeServerProviderOverrides(root_server_default),
|
|
595
|
+
session: async (context) => {
|
|
596
|
+
const scopeSession = await root_server_default(context);
|
|
597
|
+
const contextSession = {
|
|
598
|
+
...context,
|
|
599
|
+
root: scopeSession.value
|
|
600
|
+
};
|
|
601
|
+
return {
|
|
602
|
+
...composeRootHooks(scopeSession),
|
|
603
|
+
get resources() {
|
|
604
|
+
return [at(poUseUserFeedback_server_default, contextSession)];
|
|
605
|
+
},
|
|
606
|
+
get toolRestrictions() {
|
|
607
|
+
return [at(askUserQuestion_server_default$1, contextSession)];
|
|
608
|
+
},
|
|
609
|
+
get tools() {
|
|
610
|
+
return [via({ name: "ask_user_question" }, at(askUserQuestion_server_default, contextSession))];
|
|
611
|
+
},
|
|
612
|
+
services: [...scopeSession.services ?? []],
|
|
613
|
+
get activities() {
|
|
614
|
+
return [...scopeSession.activities ?? []];
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
//#endregion
|
|
620
|
+
export { facet as default, facet };
|
|
621
|
+
|
|
622
|
+
//# sourceMappingURL=server.mjs.map
|