ask_user console tool — pause workflow for human input (closes #31)#33
Conversation
Closes #31. Adds an `ask_user(prompt)` harness tool (in default_tools, opt-in per skill via tools:) that pauses the workflow, prompts on the console, and returns the line the human types — for confirmations, plan approval, or clarifications. Headless-safe: most runs are backgrounded/piped/CI where stdin is not a TTY and input() would hang on EOF. ask_user checks sys.stdin.isatty() first and returns a graceful `ERROR:` string (the agent reacts, the run continues) when there's no interactive console; an EOFError mid-read is handled the same way. So existing nohup / `saage remote` runs are never blocked. input/isatty are injectable for testing. tests/test_user_input.py: typed line (stripped), non-TTY ERROR without reading, EOF graceful, present in default_tools. Full suite 370 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review findings on #33: - BLOCKING tool in the default set could hang a foreground autonomous run: any no-allow-list agent (greenfield/lewm propose/implement) could call ask_user and stall on input() with no wall-clock bound. Fix: ask_user is now opt-in — kept OUT of default_tools and granted only when a skill names it in tools: (tools.opt_in_tools + AgentNode grant-on-request; the #20/#26 unknown-tool check knows the opt-in names). - KeyboardInterrupt (Ctrl+C at the prompt) is a BaseException — uncaught it escaped run_agent's `except Exception` and killed the whole run. Now caught (with EOFError) -> graceful ERROR, run continues. - sys.stdin can be None (embedded/detached) -> guard handled it (treated as non-TTY) instead of an AttributeError. tests: KeyboardInterrupt + no-stdin graceful; ask_user not in default_tools; opt_in_tools builds only requested; AgentNode grants ask_user only when listed (not for no-allow-list agents). Full suite 374 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the /code-review findings (5da804f): (1) blocking tool in the default set could hang a foreground autonomous run — |
A one-step `interview` agent uses ask_user to pause for the human's name + a topic, then writes a personalized note — a minimal human-in-the-loop flow. tests/integration/test_interactive_demo.py runs it OFFLINE: a fake-TTY stdin feeds the answers while a RoutedProvider scripts the agent's tool calls (ask_user runs for real). Asserts the two console lines were consumed + the note written; a second test confirms a non-interactive (non-TTY) run gets an ERROR from ask_user and still completes instead of hanging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new opt-in harness tool, ask_user(prompt), enabling human-in-the-loop workflows by pausing for console input when running interactively, while returning a non-blocking ERROR: result in headless/non-TTY contexts. It also adds an interactive_demo flow and expands test coverage and documentation around opt-in tool wiring.
Changes:
- Add
ask_useras an opt-in harness tool (not included indefault_tools) with non-TTY/EOF/Ctrl+C safe behavior. - Extend
AgentNodetool selection to include opt-in tools only when explicitly listed in a skill’stools:allow-list. - Add unit + integration tests, an example interactive flow, and documentation updates describing the new tool.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
saage/tools.py |
Implements ask_user, registers it as an opt-in tool, and provides opt_in_tools() plumbing. |
saage/nodes.py |
Updates AgentNode to grant opt-in tools only when listed in tools:. |
tests/test_user_input.py |
Adds focused unit tests for ask_user behavior and opt-in wiring. |
tests/test_tools.py |
Updates default-tools expectation to ensure ask_user remains opt-in. |
tests/integration/test_interactive_demo.py |
Adds an offline integration test demonstrating interactive and non-interactive behavior. |
flows/interactive_demo/interview/skill.md |
Adds a skill that uses ask_user + write_file to demonstrate human-in-the-loop use. |
flows/interactive_demo/flow.yaml |
Adds a simple demo flow wiring the interview agent step. |
docs/superpowers/specs/2026-06-24-user-input-tool-design.md |
Adds a design/spec writeup for the tool and its safety decisions. |
AGENTS.md |
Documents ask_user as an opt-in harness tool and clarifies its headless behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "TTY (this run is backgrounded / piped / non-interactive). Re-run " | ||
| "in a terminal, or seed the value via `--set` / the shared store.") | ||
| try: | ||
| return _input(f"\n{prompt}\n> ").strip() |
| "Pause the workflow and ask the human a question on the console; returns " | ||
| "the single line they type (after Enter). Use for confirmations, plan " | ||
| "approval, or clarifications. In a non-interactive run it returns an ERROR " | ||
| "instead of blocking. Note: leading/trailing whitespace is stripped.", | ||
| _obj(["prompt"], prompt=_STR), |
| `tests/test_user_input.py` — returns the typed line (stripped); non-TTY returns an | ||
| ERROR **without** calling `input()` (never blocks); EOF is graceful; `ask_user` is | ||
| in `default_tools`. `tests/test_tools.py` count updated. Full suite green | ||
| (370 passed, 7 skipped). |
| # Run it (in a real terminal, so stdin is interactive): | ||
| # OPENROUTER_API_KEY=... saage run flows/interactive_demo/flow.yaml --workspace /tmp/demo |
… path) - ask_user uses rstrip() (trailing only) instead of strip() — leading whitespace can be meaningful; tool description + spec updated to match. Test added that leading whitespace is preserved. - spec Tests section corrected: ask_user is NOT in default_tools (it's opt-in). - removed dead _spy_write/captured from the non-interactive demo test. - interactive_demo example uses a workspace-relative --workspace ./demo_run (POSIX /tmp isn't portable under Git Bash on Windows). Full suite 378 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed all 5 Copilot comments (8ac1ce4): (1) |
Closes #31. (Implemented autonomously while you were out — design calls in the spec + below; flag anything you'd change.)
Adds an
ask_user(prompt)harness tool (indefault_tools, opt-in per skill viatools:) that pauses the workflow, prompts on the console, and returns the line the human types (after Enter) — for confirmations, plan approval, or clarifications.Key design call: headless safety
Most
saage runs are backgrounded / piped / remote / CI, where stdin is not a TTY andinput()would hang on EOF. Soask_usercheckssys.stdin.isatty()first: in a non-interactive run it returns a gracefulERROR:string (the agent reacts, the run continues) instead of blocking forever.EOFErrormid-read is handled the same way. Existing nohup /saage remoteworkflows are never blocked.Details
input/isattyare injectable, so it's fully unit-tested without a real terminal.ask_userintools:can call it.Tests
tests/test_user_input.py— typed line, non-TTY ERROR (without ever callinginput()), EOF graceful, present indefault_tools. Full suite 370 passed, 7 skipped.Spec:
docs/superpowers/specs/2026-06-24-user-input-tool-design.md. Future: multi-line input; forwarding the prompt oversaage remote.🤖 Generated with Claude Code