ask_user upgrades: rich options, grouped questions, option previews (OPE-51)

Options accept {label, description, recommended, preview} objects (plain
strings unchanged — old sessions render as today's pills), and `questions`
groups up to 4 questions into one call, rendered as a stepper via the
header chips. Any option preview switches the card to a two-pane layout:
options left, monospace pane right, following hover/focus.

Grouped calls resolve with a JSON map keyed by header-or-question and
return {answers: {...}} to the agent (single stays {answer: ...});
a grouped item's first question doubles as its title/options so channel
mirrors and legacy surfaces degrade sensibly. Channel buttons use option
labels; grouped items mirror as text with the open-the-app hint.
This commit is contained in:
Devika Verma
2026-07-29 15:52:05 +05:30
parent f96ad4c8e6
commit 70cd1fa3d4
12 changed files with 930 additions and 100 deletions
+9 -6
View File
@@ -1525,15 +1525,17 @@ def create_app(manager: SessionManager) -> FastAPI:
async def question_asker(args: dict, tool_call_id=None) -> dict:
# ask_user (engine does NOT emit the event — we do, only when attended).
from ..tools.ask import answer_result, question_item_fields
fields = question_item_fields(args)
if fields is None: # engine guards too; belt-and-braces
return {"answer": "", "error": "no question"}
item = manager.inbox.add_question(
session_id,
str(args.get("question", "")),
inbox=_route(),
visibility=_visibility(),
options=list(args.get("options") or []),
allow_text=bool(args.get("allow_text", True)),
multi=bool(args.get("multi", False)),
tool_call_id=tool_call_id,
**fields,
)
if item.state == "pending":
manager.persist_session(session_id)
@@ -1548,11 +1550,12 @@ def create_app(manager: SessionManager) -> FastAPI:
"options": item.options,
"allow_text": item.allow_text,
"multi": item.multi,
"header": str(args.get("header", "")),
"header": item.header,
"questions": item.questions,
},
}
)
return {"answer": await manager.inbox.wait(item.id)}
return answer_result(item.questions, await manager.inbox.wait(item.id))
async def directory_requester(args: dict, tool_call_id=None) -> dict:
# The engine has already emitted DIRECTORY_REQUESTED. Park, await, then apply the grant.