diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 4ad2e459f0e2..3b5ed978545a 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -38,8 +38,6 @@ export const PlanExitTool = Tool.define( { label: "Yes", description: "Switch to build agent and start implementing the plan" }, { label: "No", description: "Stay with plan agent to continue refining the plan" }, ], - // Staying in plan mode is trivially reversible; starting to implement is not. - recommended: ["No"], }, ], tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, diff --git a/packages/opencode/src/tool/question.txt b/packages/opencode/src/tool/question.txt index 4bdfa08a360e..0a43f623734d 100644 --- a/packages/opencode/src/tool/question.txt +++ b/packages/opencode/src/tool/question.txt @@ -7,4 +7,4 @@ Use this tool when you need to ask the user questions during execution. This all Usage notes: - When `custom` is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options - Answers are returned as arrays of labels; set `multiple: true` to allow selecting more than one -- Every question needs `recommended` with at least one label: the exact label of the option you would pick (every label you would pick when `multiple` is true). Always pick, even when the choice is a matter of taste or the user's alone: recommend the option that is easiest to change later, because it is used for the user if they do not answer in time. Keep "(Recommended)" out of labels +- Put the option you would pick first: when nobody answers in time, Replo goes with the first option of each step (a free-text step is skipped). Keep "(Recommended)" out of labels diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index 1cdb15bc3190..51ff867ea44d 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -235,19 +235,11 @@ exports[`tool parameters JSON Schema (wire shape) question 1`] = ` "description": "Complete question", "type": "string", }, - "recommended": { - "description": "Exact label(s) of the option(s) you would pick yourself; at least one, even when the choice is a matter of taste", - "items": { - "type": "string", - }, - "type": "array", - }, }, "required": [ "question", "header", "options", - "recommended", ], "type": "object", }, diff --git a/packages/opencode/test/tool/parameters.test.ts b/packages/opencode/test/tool/parameters.test.ts index 17a9109154d4..9c540daad085 100644 --- a/packages/opencode/test/tool/parameters.test.ts +++ b/packages/opencode/test/tool/parameters.test.ts @@ -204,7 +204,6 @@ describe("tool parameters", () => { header: "Header", custom: false, options: [{ label: "a", description: "desc" }], - recommended: ["a"], }, ], }) @@ -213,13 +212,6 @@ describe("tool parameters", () => { test("rejects missing questions", () => { expect(accepts(Question, {})).toBe(false) }) - test("rejects a question without a recommendation", () => { - expect( - accepts(Question, { - questions: [{ question: "pick one", header: "Header", options: [{ label: "a", description: "desc" }] }], - }), - ).toBe(false) - }) }) describe("read", () => { diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 22e0887c484b..c10bf3f556a4 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" -import { Cause, Effect, Exit, Fiber, Queue } from "effect" +import { Effect, Fiber, Queue } from "effect" import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" @@ -43,19 +43,6 @@ const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Quest } }) -const colors = [ - { label: "Red", description: "The color of passion" }, - { label: "Blue", description: "The color of sky" }, -] - -const rejection = Effect.fn("QuestionToolTest.rejection")(function* (questions: unknown) { - const toolInfo = yield* QuestionTool - const tool = yield* toolInfo.init() - const exit = yield* tool.execute({ questions } as never, ctx).pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - return Exit.isFailure(exit) ? String(Cause.squash(exit.cause)) : "" -}) - describe("tool.question", () => { it.instance("should successfully execute with valid question parameters", () => Effect.gen(function* () { @@ -72,7 +59,6 @@ describe("tool.question", () => { { label: "Blue", description: "The color of sky" }, ], multiple: false, - recommended: ["Blue"], }, ] @@ -96,7 +82,6 @@ describe("tool.question", () => { question: "What is your favorite animal?", header: "This Header is Over 12", options: [{ label: "Dog", description: "Man's best friend" }], - recommended: ["Dog"], }, ] @@ -109,38 +94,6 @@ describe("tool.question", () => { }), ) - it.instance("keeps the recommendation on the asked question", () => - Effect.gen(function* () { - yield* seedSession(ctx.sessionID) - const question = yield* Question.Service - const toolInfo = yield* QuestionTool - const tool = yield* toolInfo.init() - const questions = [{ question: "Pick a color", header: "Color", options: colors, recommended: ["Blue"] }] - - const fiber = yield* tool.execute({ questions }, ctx).pipe(Effect.forkScoped) - const item = yield* pending(question) - expect(item.questions[0]?.recommended).toEqual(["Blue"]) - yield* question.reply({ requestID: item.id, answers: [["Blue"]] }) - yield* Fiber.join(fiber) - }), - ) - - it.instance("sends the model back when a recommendation is missing or wrong", () => - Effect.gen(function* () { - yield* seedSession(ctx.sessionID) - const base = { question: "Pick a color", header: "Color", options: colors } - - // Two substrings, not one: a coincidental match on "recommended" alone (e.g. from a property-access - // crash like `question.recommended.find`) would pass without the field actually being required. - const missing = yield* rejection([base]) - expect(missing).toContain("Missing key") - expect(missing).toContain("recommended") - expect(yield* rejection([{ ...base, recommended: [] }])).toContain("at least one option") - expect(yield* rejection([{ ...base, recommended: ["Green"] }])).toContain('"Green" is not an option label') - expect(yield* rejection([{ ...base, recommended: ["Red", "Blue"] }])).toContain("at most one option") - }), - ) - // intentionally removed the zod validation due to tool call errors, hoping prompting is gonna be good enough // test("should throw an Error for header exceeding 30 characters", async () => { // const tool = await QuestionTool.init() diff --git a/packages/schema/src/v1/question.ts b/packages/schema/src/v1/question.ts index f14464e1d105..295c4d9fd127 100644 --- a/packages/schema/src/v1/question.ts +++ b/packages/schema/src/v1/question.ts @@ -30,43 +30,11 @@ const base = { multiple: Schema.optional(Schema.Boolean).annotate({ description: "Allow selecting multiple choices" }), } -const recommendedDescription = - "Exact label(s) of the option(s) you would pick yourself; at least one, even when the choice is a matter of taste" - export const Info = Schema.Struct({ ...base, custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }), - // Optional here: some system-generated questions (e.g. plan_exit's) are built by hand rather than - // through the tool's validated Prompt schema, and questions asked before the tool required a - // recommendation are still stored and served. Absent does not mean "no recommendation was intended". - recommended: Schema.optional( - Schema.Array(Schema.String).annotate({ - description: "Recommended label(s) at ask time; absent when none was recorded for this question", - }), - ), }).annotate({ identifier: "QuestionInfo" }) -// The tool requires at least one recommendation, so every question carries a pick a timer can use when nobody answers. -export const Prompt = Schema.Struct({ - ...base, - recommended: Schema.Array(Schema.String).annotate({ description: recommendedDescription }), -}) - .check( - Schema.makeFilter((question) => { - if (question.recommended.length === 0) { - return "recommend at least one option" - } - const labels = question.options.map((option) => option.label) - const unknown = question.recommended.find((label) => !labels.includes(label)) - if (unknown !== undefined) { - return `recommended "${unknown}" is not an option label (options: ${labels.map((label) => `"${label}"`).join(", ")})` - } - if (!question.multiple && question.recommended.length > 1) { - return "recommend at most one option unless multiple is true" - } - return undefined - }), - ) - .annotate({ identifier: "QuestionPrompt" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" }) export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({ identifier: "QuestionTool", }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f5d1f02e936b..1c396f1bf617 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -718,10 +718,6 @@ export type QuestionInfo = { options: Array multiple?: boolean custom?: boolean - /** - * Recommended label(s) at ask time; absent when none was recorded for this question - */ - recommended?: Array } export type QuestionTool = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1ec7130d1ff8..b02912de834f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -17476,13 +17476,6 @@ }, "custom": { "type": "boolean" - }, - "recommended": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Recommended label(s) at ask time; absent when none was recorded for this question" } }, "required": ["question", "header", "options"],