Skip to content

Feat/interactive agent questions#28

Merged
clintberry merged 7 commits into
mainfrom
feat/interactive-agent-questions
Jun 9, 2026
Merged

Feat/interactive agent questions#28
clintberry merged 7 commits into
mainfrom
feat/interactive-agent-questions

Conversation

@clintberry

Copy link
Copy Markdown
Contributor

What & why

Agent questions could reach the user as a raw tool-call string in the thread, e.g. Ask_user({"question":"What file would you like me to create?"}). That's the structured question path failing — the extension_ui_request event never fires, so the model's narrated call posts as plain text. Two gaps stacked: even on the happy path the question rendered as bare text with no input affordance, and on the failure path it showed raw JSON.

This makes agent questions interactive typed prompts (text / pick-one buttons / yes-no, with a free-text "Other" fallback) and guarantees a question is never shown as raw JSON. Scoped entirely to the Pi harness — no work on the legacy claude executor (slated for removal).

Origin: requirements · plan

How it works

  • Typed prompts (U1–U4): ask_user takes an optional kind + options; the extension feature-detects ctx.ui.select/confirm and falls back to input-with-options when absent. kind+options flow through the decoder, persist on tasks (migration 012 — so a reconnect/snapshot reconstructs the prompt), ride the task_awaiting_input payload, and render as buttons / yes-no in the thread drawer. Answers route back through the existing steer → extension_ui_response path.
  • No-JSON guarantee (U5–U6):
    • A failed extension install is now loud — error-level log + a user-facing notice that agents can't ask questions (was a silent return nil). Provisioning stays non-fatal.
    • A backstop in the Pi reply-finalize path rewrites a narrated ask_user({...}) into the plain question before persist/broadcast/post, preserving surrounding prose and degrading a truncated call to a readable floor — never a JSON fragment.

Scope notes / deviations from the plan

  • Added migration 012 (pending_question_kind TEXT, pending_question_options TEXT[], both defaulted) — additive/backward-compatible. The plan assumed no persistence change, but without it a snapshot refetch would drop the buttons mid-question.
  • No Vitest runner wired — followed the repo's existing convention (Vitest-style specs excluded from tsc, type-checked via tsc --noEmit until a runner lands; see tsconfig.app.json). The reducer spec runs the moment a runner is added.
  • Rich choices are Pi-path only by design; the legacy harness only ever gets the no-JSON floor.

Testing

  • Backend: go build / go vet / go test ./... all green (full DB-backed suite). New tests: decoder options, the agent-runs reducer (incl. snapshot preservation), and the backstop (AE3/AE4 + passthrough negatives).
  • Frontend: npm run build + tsc + eslint clean.
  • Not yet verified live (no Pi runtime available locally): the real ctx.ui.select/confirm request shape and the exact value Pi expects for a confirm answer. The runtime feature-detection + "yes"/"no" text answer ships either way; worth a smoke-test in a real session before merge.

🤖 Generated with Claude Code

U1: ask_user extension accepts optional kind (input/select/confirm) and
options, feature-detecting ctx.ui.select/confirm with an input fallback.
U2: decoder extracts options; kind+options persist on tasks (migration 012)
and flow through SetAwaitingInput, the task_awaiting_input WS payload, and
the snapshot response so a reconnect reconstructs the typed prompt.
U3: carry pendingQuestionKind + pendingQuestionOptions through the AgentTask /
TaskEventPayload types and the agent-runs reducer (live event + snapshot
paths). Adds a Vitest-style reducer spec following the repo's tsc-checked
convention (no runner wired yet).
U4: the thread drawer renders choice buttons for a select question and yes/no
for a confirm, with the composer as the free-text / Other fallback; answers
route through the existing steer path.
Escalate a failed InstallPiExtension to error level, return the error so the
caller can react, and tell the user via logFn that agents in the session
cannot ask questions and may guess instead. Provisioning stays non-fatal.
… JSON

When the structured extension_ui_request never fires (extension missing, or the
model narrates the call as text), the reply arrives shaped like
ask_user({"question":"..."}). sanitizeNarratedQuestion rewrites it to the
plain question before persist/broadcast/post, preserving surrounding prose and
degrading a truncated call to a readable floor rather than a JSON fragment
(R9/R11/R12, AE3/AE4).
@clintberry clintberry merged commit 2af14a4 into main Jun 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant