Skip to content

Add a simple plan system to the react module#8

Open
silvestrid wants to merge 16 commits into
mainfrom
react-plan
Open

Add a simple plan system to the react module#8
silvestrid wants to merge 16 commits into
mainfrom
react-plan

Conversation

@silvestrid
Copy link
Copy Markdown
Collaborator

Summary

Plan System for ReAct Agent

The ReAct agent now maintains an explicit plan across iterations — a list of PlanItem entries with task, status ("todo"/"done"), and done_at_step. Each iteration the LLM outputs plan_updates alongside its usual thought/tool selection:

  • {"add": "task description"} ‚Äî add a new todo item
  • {"done": <index>} ‚Äî mark an existing item as completed

On the first turn the agent builds its plan; on subsequent turns it marks items done as tools succeed and adds new ones if the task evolves. When all items are done, the agent is force-stopped to call finish, preventing unnecessary extra iterations.

The system prompt was rewritten to reinforce this workflow: reason about what the trajectory and plan show is already done, never repeat successful tool calls, and retry failures only once before reporting.

Also included:

  • ask_to_user tool ‚Äî opt-in via enable_ask_to_user=True. The agent can request user clarification mid-execution by raising ConfirmationRequired; the user's response feeds back as an observation via aresume().
  • aresume() implementation ‚Äî fully implemented for both ask_to_user and tool-confirmation flows, completing the pending episode and continuing the loop.
  • Improved error formatting ‚Äî format_tool_exception() now shows the error message first followed by frames with max_length truncation; new format_validation_error() includes the expected schema so the LLM can self-correct.

Cancellation & Cleanup Fixes

  • CancelledError in callbacks ‚Äî with_callbacks async wrapper now catches asyncio.CancelledError (a BaseException in Python 3.9+) so end-callbacks still fire on cancellation.
  • Streaming task cleanup ‚Äî Module.astream() now explicitly cancels the background task in its finally block instead of leaving it dangling.
  • Loop-closed warnings ‚Äî run_async_with_context() replaced bare asyncio.run with a manual event loop that installs a custom exception handler suppressing harmless "Event loop is closed" errors from httpx/AsyncOpenAI client GC cleanup. Includes proper task cancellation and shutdown sequence.

Documentation Alignment

Comprehensive audit and fix of all documentation to match the actual codebase:

  • Architecture docs ‚Äî ADRs, overview, modules (base, predict, react), callbacks, streaming, LM
  • Example docs ‚Äî streaming, basic_usage, advanced, context_settings, confirmation, react, dynamic_tools
  • README & CLAUDE.md ‚Äî ConfirmationRequired naming, dependency count (5 not 2), project structure
  • CHANGELOG ‚Äî backfilled entries for versions 0.1.1 through 0.1.8

Other Changes

  • Type checker migration ‚Äî mypy ‚Üí ty across CI, justfile, and pyproject.toml
  • Type safety improvements ‚Äî getattr guards, __name__ fallbacks, signature type fixes
  • Test updates ‚Äî new test_cancellation.py, updated react and callback tests for plan system

Test plan

  • Verify ReAct agent builds a plan on first turn and tracks completion across iterations
  • Verify agent force-stops when all plan items are marked done
  • Test enable_ask_to_user=True raises ConfirmationRequired and aresume() continues correctly
  • Test streaming cancellation doesn't leave dangling tasks or produce warnings
  • Run just test ‚Äî all tests pass
  • Run just lint ‚Äî no lint errors
  • Run just typecheck ‚Äî ty passes

Fix multiple ADRs that had drifted from the code:
- ADR-001: update dependency list to all 5 deps
- ADR-004: fix ReAct context keys (plan, stream)
- ADR-005: document plan system with PlanItem
- ADR-006: fix aexecute() to return Prediction, not async generator
- ADR-008: fix callback detection to isinstance(ModuleCallback)
- ADR-009: rewrite History to match set_system_message() API
- LM layer: change from "Future Design" to current implementation
- Fix adapter methods (format_tool_schema, format_user_request, etc.)
- Fix tool/ and utils/ paths from single files to packages
- Remove settings.aclient references
- Fix nested module composition examples
- Update summary table (LM no longer "Future")
- Fix aexecute() return type from AsyncGenerator to Prediction
- Fix streaming from aexecute(stream=True) to astream()
- Fix stream chunk attribute from field to field_name
- Add ThoughtStreamChunk documentation
- Fix trajectory format to list[Episode] with typed fields
- Document plan system (PlanItem, plan_updates)
- Fix ask_to_user naming (was user_clarification)
- Fix auto_execute_tools default to True
- Fix History management in Predict docs
- Document aresume() as implemented in ReAct
- callbacks: fix settings.configure() API (no api_key/model params)
- callbacks: remove false per-module callbacks support
- streaming: add ThoughtStreamChunk to event types
- streaming: fix event flow to queue-based architecture
- lm: add acall()/\_\_call\_\_() callable interface docs
- lm: remove deprecated settings.aclient reference
- Replace StreamingPredict with Predict.astream()
- Fix settings.configure() to use lm=LM(...) pattern
- Fix user_clarification to ask_to_user
- Fix trajectory format to list[Episode]
- Fix variable name bugs in confirmation examples
- Fix enable_ask_to_user default to False
- Add missing make_signature import
- README: HumanInTheLoopRequired -> ConfirmationRequired
- README: update dependency list to all 5 deps
- CLAUDE.md: update project structure to match packages
- CLAUDE.md: fix dependency list and streaming method name
- CLAUDE.md: fix ADR-009 summary for set_system_message()
Backfill changelog from git history. Notable entries include:
- 0.1.3: LM abstraction layer, confirmation system, string signatures
- 0.1.5: auto-init LM from env vars, History in ReAct
- 0.1.6: multi-provider env var precedence
- 0.1.8: lazy openai import, callback telemetry
- Add PlanItem TypedDict and plan tracking across iterations
- Add enable_ask_to_user option with ConfirmationRequired-based flow
- Implement aresume() for continuing after confirmations
- Add plan_updates to react signature (add/done operations)
- Force-stop when all plan items are done
- Cancel streaming task on cleanup in Module.astream()
- Add plan field to ReactContext
…ings

- Catch asyncio.CancelledError in with_callbacks decorator
- Replace asyncio.run with manual event loop to suppress harmless
  "Event loop is closed" warnings from httpx/AsyncOpenAI cleanup
- Add _cancel_all_tasks helper for proper loop teardown
- Rewrite format_tool_exception to show error message first, then frames
- Add max_length truncation to prevent context bloat
- Add format_validation_error with schema hint for the LLM
- Export format_validation_error from utils package
- lm/base: use getattr for choices access to avoid AttributeError
- confirmation: handle funcs without __name__ (e.g. partials)
- exceptions: fix AdapterParseError.signature type to type[Signature]
- settings: fix USDPY_LM_MODEL typo to UDSPY_LM_MODEL, auto-init in configure()
- signature: replace mypy ignores with ty-compatible annotations
- tool: handle funcs without __name__ in Tool.__init__
- Replace mypy with ty for type checking in CI, justfile, and pyproject
- Add Python 3.14 classifier
- Update author info
- Add ruff isort config for first-party imports
- Improve example runner with env var check and glob support
- Remove .mypy_cache from .gitignore
- Add plan_updates field to mock ReAct responses
- Update callback tests for simplified input structure
- Update react tests for plan system and ask_to_user
- Add test_cancellation.py for async cancellation scenarios
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a comprehensive plan system to the ReAct agent module, along with improved error handling, cancellation support, and extensive documentation updates.

Changes:

  • Introduces a plan system for ReAct agents with PlanItem tracking, plan_updates field, and automatic force-stop when all plan items are complete
  • Adds optional ask_to_user tool for agent-initiated clarification requests with full aresume() implementation
  • Improves error formatting with format_validation_error() and enhanced format_tool_exception() with consecutive failure detection
  • Fixes asyncio cancellation handling in callbacks, streaming task cleanup, and event loop shutdown warnings
  • Migrates from mypy to ty for type checking across all CI workflows and configuration files
  • Comprehensive documentation alignment across architecture docs, examples, and API references

Reviewed changes

Copilot reviewed 41 out of 43 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/udspy/module/react.py Core plan system implementation with PlanItem TypedDict, plan_updates handling, ask_to_user tool, and complete aresume() method
src/udspy/utils/formatting.py Enhanced error formatting with max_length truncation and format_validation_error() helper
src/udspy/utils/async_support.py Improved run_async_with_context with proper task cancellation and event loop cleanup
src/udspy/module/base.py Added explicit task cancellation in astream() finally block
src/udspy/callback.py CancelledError handling in async callback wrapper
src/udspy/settings.py Fixed typo in UDSPY_LM_MODEL environment variable name and added auto-initialization in configure()
tests/test_react.py Updated test mocks to include plan_updates field and added test for force-stop behavior
tests/test_module_callbacks.py Updated all ReAct test mocks with empty plan_updates arrays
tests/test_edge_cases.py Updated malformed JSON test mocks with plan_updates
tests/test_cancellation.py New comprehensive cancellation tests for astream, aforward, and callbacks
pyproject.toml Replaced mypy with ty type checker, removed mypy config, added ty config, updated author info
justfile Changed typecheck command from mypy to ty
.github/workflows/*.yml Updated CI workflows to use ty instead of mypy
docs/**/*.md Extensive documentation updates to align with actual implementation
CHANGELOG.md Backfilled entries for versions 0.1.1 through 0.1.8
README.md, CLAUDE.md Updated dependency count and ConfirmationRequired naming

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/udspy/settings.py
Comment on lines +97 to 98
if model := os.getenv("UDSPY_LM_MODEL"):
self._lm = LM(model)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The lm property creates a new LM instance from the environment variable every time it's accessed if UDSPY_LM_MODEL is set, even if self._lm is already set. This should check if self._lm is None before creating a new instance, similar to the configure() method. Change this to: if self._lm is None and (model := os.getenv("UDSPY_LM_MODEL")): self._lm = LM(model)

Copilot uses AI. Check for mistakes.
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.

2 participants