fix(runner): distinguish text questions from multiple choice

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-16 08:03:30 -05:00
1 parent 19415791d3
commit e4a2254ef1
9 files changed
+52 -12

No files matched your search

@@ -110,6 +110,9 @@ describe("native execution input external-chat framing", () => {
);
expect(input.task.prompt).toContain("request_human_input");
expect(input.task.prompt).toContain('interactionKind="questions"');
expect(input.task.prompt).toContain("at least two distinct meaningful options");
expect(input.task.prompt).toContain("payload.questionSet");
expect(input.task.prompt).toContain('answerMode="text"');
expect(input.task.prompt).toContain("one question at a time");
expect(input.task.prompt).toContain("Never fabricate answer URLs");
expect(input.task.prompt.indexOf("Choose a color: Amber")).toBeLessThan(
@@ -468,6 +471,9 @@ describe("native execution input external-chat framing", () => {
runtimeContext: nativeRuntimeContextFixture(),
});
expect(input.task.prompt).toContain('interactionKind="questions"');
expect(input.task.prompt).toContain("at least two distinct meaningful options");
expect(input.task.prompt).toContain("payload.questionSet");
expect(input.task.prompt).toContain('answerMode="text"');
expect(input.task.prompt).toContain('continuationPolicy="wake_assignee"');
expect(input.task.prompt).toContain("Create the actual question before yielding");
expect(input.task.prompt).toContain("Wait for its real answer");
@@ -22,7 +22,7 @@ import {
const NATIVE_QUESTION_GUIDANCE = [
"## Questions that need a user response",
"A request for clickable choices, buttons, or a decision needed before continuing is not a self-contained text answer. The zero-API-call shortcut does not prohibit the structured question tool.",
'Use the available request_human_input tool with interactionKind="questions", continuationPolicy="wake_assignee", a title, prompt, and a stable idempotencyKey. Put the actual requested choices in payload.questions: each question needs an id, prompt, selectionMode="single", and options with stable id and label fields. Reuse the same key if that creation call must be retried.',
'Use the available request_human_input tool with interactionKind="questions", continuationPolicy="wake_assignee", a title, prompt, and a stable idempotencyKey. For choices, use payload.questions with stable id and prompt, selectionMode="single" or "multi", and at least two distinct meaningful options with stable id and label fields. For open-ended answers, include payload.questionSet with schema="paperclip.question_set.v1" and questions containing id, prompt, answerMode="text", and no options or customAnswer. Include matching payload.questions entries with the same IDs/prompts and a freeText option only for storage compatibility; questionSet supplies the text-field presentation. Never present a lone "Other" or "I will describe it" choice. Reuse the same key if that creation call must be retried.',
"Paperclip renders the supported question controls and authenticates the answer. Never fabricate answer URLs, query-string choice links, callback tokens, or fake Markdown buttons. Do not manually post a duplicate question card or use call_api as a substitute.",
"For one question at a time, read the current request and authoritative prior answers, then create only the next unanswered question. Wait for its real answer before asking another; do not infer a selection or answer your own interaction. Keep completion and disposition truthful while waiting, and preserve existing review or approval gates.",
"If the tool is unavailable or creation fails, report that actual limitation plainly; do not pretend interactive controls were created.",
@@ -268,7 +268,7 @@ describe("PaperclipRunnerToolAuthority", () => {
},
);
it("executes the advertised payload.questions shape once on the bound reviewed task", async () => {
it.each(["choice", "text"] as const)("executes the advertised %s question once on the bound reviewed task", async (answerMode) => {
const binding = {
companyId: randomUUID(),
agentId: randomUUID(),
@@ -278,7 +278,7 @@ describe("PaperclipRunnerToolAuthority", () => {
await db.insert(companies).values({
id: binding.companyId,
name: "Question invocation",
issuePrefix: "RQA",
issuePrefix: answerMode === "choice" ? "RQA" : "RQT",
});
await db.insert(agents).values({
id: binding.agentId,
@@ -328,6 +328,33 @@ describe("PaperclipRunnerToolAuthority", () => {
],
},
];
const payloadDescription = (advertised.inputSchema as {
properties: { payload: { description: string } };
}).properties.payload.description;
expect(payloadDescription).toContain("at least two distinct meaningful options");
expect(payloadDescription).toContain("questionSet");
expect(payloadDescription).not.toContain("use exactly");
const payload = answerMode === "choice"
? { version: 1, questions }
: {
version: 1,
questions: [{
id: "goal",
prompt: "What should we accomplish?",
selectionMode: "single",
required: true,
options: [{ id: "describe", label: "Your answer", freeText: true }],
}],
questionSet: {
schema: "paperclip.question_set.v1",
questions: [{
id: "goal",
prompt: "What should we accomplish?",
answerMode: "text",
required: true,
}],
},
};
const call = {
tool: "request_human_input",
callId: "advertised-question",
@@ -337,7 +364,7 @@ describe("PaperclipRunnerToolAuthority", () => {
title: "Choose one color",
prompt: "Choose one color",
continuationPolicy: "wake_assignee",
payload: { version: 1, questions },
payload,
},
};
const first = await authority.execute(call);
@@ -350,7 +377,7 @@ describe("PaperclipRunnerToolAuthority", () => {
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
payload: { version: 1, questions },
payload,
},
});
await expect(
@@ -154,7 +154,7 @@ export class PaperclipRunnerToolAuthority {
descriptor.operationId === "register_deliverable"
? "Prepare one verified workspace file for Paperclip's final task or external-chat response. This records the attachment, work product, and explicit same-run selection; it does not confirm provider delivery."
: descriptor.operationId === "request_human_input"
? "Create a typed, durable human-input interaction on the current Paperclip task bound to this run. For structured questions and choices, use interactionKind 'questions' with payload.questions as described by the payload schema. Paperclip renders the interaction in its UI and, for connected chats, uses supported provider question controls or a safe fallback. Normal task permissions and review gates still apply."
? "Create a typed, durable human-input interaction on the current Paperclip task bound to this run. For structured questions and choices, use interactionKind 'questions' with payload.questions for choices (at least two distinct meaningful options), or include payload.questionSet for open-ended text fields as described by the payload schema. Paperclip renders the interaction in its UI and, for connected chats, uses supported provider question controls or a safe fallback. Normal task permissions and review gates still apply."
: descriptor.description,
inputSchema:
descriptor.operationId === "register_deliverable"