mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(runner): distinguish text questions from multiple choice
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
19415791d3
commit
e4a2254ef1
9 files changed
+52
-12
No files matched your search
@@ -1 +1 @@
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
@@ -24,7 +24,7 @@
|
||||
"prpVersion": 1,
|
||||
"nativeExecutionVersion": 1,
|
||||
"catalogVersion": 1,
|
||||
"catalogSha256": "sha256:d86fe304faa341cb46f92c93b6c80a6b8666770ebd915ffe61c7c9d3399e6524",
|
||||
"catalogSha256": "sha256:dbafbe104afff801d808f590f383f6e10d792e53e04e4d0d2515d5ad681f7713",
|
||||
"driverContractVersion": 1,
|
||||
"driverKind": "paperclip-deterministic",
|
||||
"driverVersion": "1.0.0"
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/native-execution-seeded.json",
|
||||
"sha256": "dcba9c50475b86260067a7e0789577fc7a5c0624fe3e3552bf3e111c8f9da0ac",
|
||||
"sha256": "fb7a1bc138af13e423830a1ea45cf1e7dd7a326fa9d658bbdbd30394787b1164",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
|
||||
@@ -277,13 +277,16 @@ describe("ACPX runtime host", () => {
|
||||
};
|
||||
let skillsHome = "";
|
||||
let assigned = true;
|
||||
let expectedReference = "ASSIGNED_SKILL_MARKER";
|
||||
const dependencies = fixture.dependencies({
|
||||
openRuntime: async (options) => {
|
||||
skillsHome = join(options.launchEnvironment.CLAUDE_CONFIG_DIR!, "skills");
|
||||
expect(await readdir(skillsHome)).toEqual(assigned ? ["assigned"] : []);
|
||||
if (assigned) {
|
||||
const reference = join(skillsHome, "assigned", "references", "answer.txt");
|
||||
expect(await readFile(reference, "utf8")).toBe("ASSIGNED_SKILL_MARKER");
|
||||
expect(await readFile(reference, "utf8")).toBe(expectedReference);
|
||||
expect(await readFile(join(skillsHome, "assigned", "SKILL.md"), "utf8"))
|
||||
.toBe(await readFile(join(skillRoot, "SKILL.md"), "utf8"));
|
||||
expect((await stat(reference)).mode & 0o222).toBe(0);
|
||||
}
|
||||
return runtimePort({
|
||||
@@ -305,6 +308,10 @@ describe("ACPX runtime host", () => {
|
||||
dependencies,
|
||||
);
|
||||
await host.close({ reason: "reopen test" });
|
||||
// A changed source must replace the prior materialized revision on resume.
|
||||
expectedReference = "UPDATED_ASSIGNED_SKILL_MARKER";
|
||||
await writeFile(join(skillRoot, "references", "answer.txt"), expectedReference);
|
||||
await writeFile(join(skillRoot, "SKILL.md"), "---\nname: assigned\ndescription: Updated instructions.\n---\nRead references/answer.txt before responding.");
|
||||
}
|
||||
// No stale assignment survives a later launch without runtime context.
|
||||
assigned = false;
|
||||
|
||||
@@ -115,7 +115,7 @@ export const requestHumanInputAction = {
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"description": "Kind-specific interaction data. For interactionKind='questions', use exactly {version:1, questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]}; option keys are id/label, not value, and question choice cardinality is selectionMode, not type. For confirmation, payload may be {}. Keep all ids stable across retries.",
|
||||
"description": "Kind-specific interaction data. For interactionKind='questions', use version:1 and questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]. Choice questions need at least two distinct meaningful options. For an open-ended text answer, ALSO include questionSet:{schema:'paperclip.question_set.v1',questions:[{id,prompt,answerMode:'text',required?}]} with no options or customAnswer in its text questions. Keep matching IDs/prompts in both arrays; the required compatibility questions entry uses selectionMode:'single' and options:[{id:'describe',label:'Your answer',freeText:true}]. Without questionSet this incorrectly renders as a one-option choice. Never use a lone Other or describe option as the presentation. Option keys are id/label, not value. For confirmation, payload may be {}. Keep IDs stable across retries.",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"targetRevisionId": {
|
||||
@@ -238,7 +238,7 @@ export const requestHumanInputAction = {
|
||||
"minLength": 1
|
||||
},
|
||||
"payload": {
|
||||
"description": "Kind-specific interaction data. Questions use {version:1, questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]}; do not use type:'single_choice' or option.value."
|
||||
"description": "Kind-specific interaction data. For interactionKind='questions', use version:1 and questions:[{id,prompt,selectionMode:'single'|'multi',required?,options:[{id,label,description?,freeText?}]}]. Choice questions need at least two distinct meaningful options. For an open-ended text answer, ALSO include questionSet:{schema:'paperclip.question_set.v1',questions:[{id,prompt,answerMode:'text',required?}]} with no options or customAnswer in its text questions. Keep matching IDs/prompts in both arrays; the required compatibility questions entry uses selectionMode:'single' and options:[{id:'describe',label:'Your answer',freeText:true}]. Without questionSet this incorrectly renders as a one-option choice. Never use a lone Other or describe option as the presentation. Option keys are id/label, not value. For confirmation, payload may be {}. Keep IDs stable across retries."
|
||||
},
|
||||
"targetRevisionId": {
|
||||
"oneOf": [
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user