Skip to content

ask_user console tool — pause workflow for human input (closes #31)#33

Merged
cgpadwick merged 4 commits into
masterfrom
feat/user-input-tool
Jun 27, 2026
Merged

ask_user console tool — pause workflow for human input (closes #31)#33
cgpadwick merged 4 commits into
masterfrom
feat/user-input-tool

Conversation

@cgpadwick

Copy link
Copy Markdown
Owner

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 (in default_tools, opt-in per skill via tools:) 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 and input() would hang on EOF. So ask_user checks sys.stdin.isatty() first: in a non-interactive run it returns a graceful ERROR: string (the agent reacts, the run continues) instead of blocking forever. EOFError mid-read is handled the same way. Existing nohup / saage remote workflows are never blocked.

Details

  • One line, trailing whitespace stripped (multi-line is YAGNI for now).
  • input/isatty are injectable, so it's fully unit-tested without a real terminal.
  • Opt-in: only skills that list ask_user in tools: can call it.

Tests

tests/test_user_input.py — typed line, non-TTY ERROR (without ever calling input()), EOF graceful, present in default_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 over saage remote.

🤖 Generated with Claude Code

cgpadwick and others added 2 commits June 24, 2026 11:46
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>
@cgpadwick

Copy link
Copy Markdown
Owner Author

Addressed the /code-review findings (5da804f): (1) blocking tool in the default set could hang a foreground autonomous run — ask_user is now opt-in (out of default_tools, granted only when a skill names it in tools:), so a no-allow-list agent can never stall a flow on it; (2) Ctrl+C at the prompt (a KeyboardInterrupt/BaseException) is now caught (with EOFError) → graceful ERROR instead of escaping run_agent's except Exception and killing the run; (3) sys.stdin is None (embedded/detached) is handled (treated as non-TTY) instead of an AttributeError. Tests added for all three + the opt-in wiring. Full suite 374 passed.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_user as an opt-in harness tool (not included in default_tools) with non-TTY/EOF/Ctrl+C safe behavior.
  • Extend AgentNode tool selection to include opt-in tools only when explicitly listed in a skill’s tools: 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.

Comment thread saage/tools.py Outdated
"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()
Comment thread saage/tools.py
Comment on lines +246 to +250
"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),
Comment on lines +35 to +38
`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).
Comment thread tests/integration/test_interactive_demo.py Outdated
Comment thread flows/interactive_demo/flow.yaml Outdated
Comment on lines +5 to +6
# 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>
@cgpadwick

Copy link
Copy Markdown
Owner Author

Addressed all 5 Copilot comments (8ac1ce4): (1) ask_user now uses rstrip() (trailing only) so leading whitespace is preserved — tool description + spec updated to match, test added; (2) tool description fixed to 'trailing whitespace is stripped'; (3) spec Tests section corrected (ask_user is NOT in default_tools — it's opt-in); (4) removed the dead _spy_write/captured from the non-interactive demo test; (5) the demo example now uses a workspace-relative --workspace ./demo_run (portable under Git Bash on Windows, unlike /tmp). Full suite 378 passed.

@cgpadwick cgpadwick merged commit ae9a042 into master Jun 27, 2026
6 checks passed
@cgpadwick cgpadwick deleted the feat/user-input-tool branch June 27, 2026 16:39
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.

Saage should provide a user input tool

2 participants