{"version":3,"file":"ask-options.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/ask-options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAe,YAAY,EAAuC,MAAM,gCAAgC,CAAC;AAgCrH,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAkHvD","sourcesContent":["/**\n * Options pane — the ask_options tool.\n *\n * The model calls this tool when it needs the user to make a decision before\n * continuing. Each question is shown in an inline options pane where the user\n * moves with up/down, advances with right, and may type a custom answer.\n */\n\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport type { AskQuestion, ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { LOOP_AUTO_CHANGED, LOOP_HALT } from \"./loop.js\";\n\nconst askOptionsSchema = Type.Object({\n\tquestions: Type.Array(\n\t\tType.Object({\n\t\t\tquestion: Type.String({ description: \"The question to ask the user.\" }),\n\t\t\tdetail: Type.Optional(Type.String({ description: \"Optional clarifying sub-text shown under the question.\" })),\n\t\t\toptions: Type.Array(\n\t\t\t\tType.Object({\n\t\t\t\t\tlabel: Type.String({ description: \"The option text; returned verbatim when chosen.\" }),\n\t\t\t\t\tdescription: Type.Optional(\n\t\t\t\t\t\tType.String({ description: \"Optional short description shown next to the option.\" }),\n\t\t\t\t\t),\n\t\t\t\t\trecommended: Type.Optional(\n\t\t\t\t\t\tType.Boolean({\n\t\t\t\t\t\t\tdescription: \"When true, the option is marked '(recommended)' to help the user choose.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t\t{ description: \"The options the user can choose from.\" },\n\t\t\t),\n\t\t\tallow_custom: Type.Optional(\n\t\t\t\tType.Boolean({\n\t\t\t\t\tdescription: \"When true, the user can type a free-form answer instead of choosing an option.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\t{ description: \"One or more decisions to ask the user, in order.\" },\n\t),\n});\n\nexport function setupAskOptions(hoo: ExtensionAPI): void {\n\t// Capture the latest context so the tool can reach the interactive UI.\n\tlet activeCtx: ExtensionContext | undefined;\n\thoo.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t});\n\n\t// Track whether an autonomous /loop is running so we never block on a human\n\t// who isn't there. The loop extension broadcasts this on the shared bus.\n\tlet autoLoopActive = false;\n\thoo.events.on(LOOP_AUTO_CHANGED, (data) => {\n\t\tautoLoopActive = !!(data as { active?: boolean })?.active;\n\t});\n\n\thoo.registerTool({\n\t\tname: \"ask_options\",\n\t\tlabel: \"Ask the user\",\n\t\tdescription:\n\t\t\t\"Ask the user to make one or more decisions before continuing. Each question is presented \" +\n\t\t\t\"in an interactive options pane where the user selects an option (or types a custom answer). \" +\n\t\t\t\"Use this when you genuinely need input to proceed and cannot reasonably decide yourself. \" +\n\t\t\t\"Returns the user's answer for each question; if the user skips, no answers are returned.\",\n\t\tpromptSnippet: \"Put a decision to the user as selectable options\",\n\t\tparameters: askOptionsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof askOptionsSchema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\tif (!activeCtx || !activeCtx.hasUI) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Cannot ask the user: no interactive UI is available in this session. Proceed using your best judgement.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!params.questions.length) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No questions were provided.\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Autonomous loop: no human is watching, so never block on the pane.\n\t\t\t// Decide any question that carries a recommended default and proceed;\n\t\t\t// if any question lacks one, there is no safe default — halt the loop\n\t\t\t// and let the model report the blocker instead of guessing.\n\t\t\tif (autoLoopActive) {\n\t\t\t\tconst blockers = params.questions.filter((q) => !q.options.some((o) => o.recommended));\n\t\t\t\tif (blockers.length) {\n\t\t\t\t\tconst list = blockers.map((q) => `  • ${q.question}`).join(\"\\n\");\n\t\t\t\t\thoo.events.emit(LOOP_HALT, {\n\t\t\t\t\t\treason: `ask_options had ${blockers.length} question(s) with no recommended default.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext:\n\t\t\t\t\t\t\t\t\t`Autonomous loop: no user is available to answer, and ${blockers.length} question(s) have ` +\n\t\t\t\t\t\t\t\t\t`no recommended default to fall back on:\\n${list}\\n\\n` +\n\t\t\t\t\t\t\t\t\t`The loop has been stopped. Do not guess — stop and report this blocker to the user, ` +\n\t\t\t\t\t\t\t\t\t`explaining what decision is needed and the options you were weighing.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst text = params.questions\n\t\t\t\t\t.map((q) => {\n\t\t\t\t\t\tconst rec = q.options.find((o) => o.recommended);\n\t\t\t\t\t\treturn `${q.question}\\n  → ${rec?.label} (auto-selected recommended default; autonomous loop, no user present)`;\n\t\t\t\t\t})\n\t\t\t\t\t.join(\"\\n\\n\");\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst questions: AskQuestion[] = params.questions.map((q) => ({\n\t\t\t\tquestion: q.question,\n\t\t\t\tdetail: q.detail,\n\t\t\t\toptions: q.options.map((o) => ({ label: o.label, description: o.description, recommended: o.recommended })),\n\t\t\t\tallowCustom: q.allow_custom,\n\t\t\t}));\n\n\t\t\tconst answers = await activeCtx.ui.askOptions(questions, { signal });\n\n\t\t\tif (!answers) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"The user skipped the question(s) without answering. Ask how they would like to proceed.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = questions.map((q, i) => `${q.question}\\n  → ${answers[i] ?? \"(no answer)\"}`).join(\"\\n\\n\");\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t});\n}\n"]}