Add a simple plan system to the react module#8
Conversation
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
There was a problem hiding this comment.
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
PlanItemtracking,plan_updatesfield, and automatic force-stop when all plan items are complete - Adds optional
ask_to_usertool for agent-initiated clarification requests with fullaresume()implementation - Improves error formatting with
format_validation_error()and enhancedformat_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.
| if model := os.getenv("UDSPY_LM_MODEL"): | ||
| self._lm = LM(model) |
There was a problem hiding this comment.
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)
Summary
Plan System for ReAct Agent
The ReAct agent now maintains an explicit plan across iterations — a list of
PlanItementries withtask,status("todo"/"done"), anddone_at_step. Each iteration the LLM outputsplan_updatesalongside its usual thought/tool selection:{"add": "task description"}— add a new todo item{"done": <index>}— mark an existing item as completedOn 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_usertool — opt-in viaenable_ask_to_user=True. The agent can request user clarification mid-execution by raisingConfirmationRequired; the user's response feeds back as an observation viaaresume().aresume()implementation — fully implemented for bothask_to_userand tool-confirmation flows, completing the pending episode and continuing the loop.format_tool_exception()now shows the error message first followed by frames withmax_lengthtruncation; newformat_validation_error()includes the expected schema so the LLM can self-correct.Cancellation & Cleanup Fixes
CancelledErrorin callbacks —with_callbacksasync wrapper now catchesasyncio.CancelledError(aBaseExceptionin Python 3.9+) so end-callbacks still fire on cancellation.Module.astream()now explicitly cancels the background task in itsfinallyblock instead of leaving it dangling.run_async_with_context()replaced bareasyncio.runwith 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:
ConfirmationRequirednaming, dependency count (5 not 2), project structureOther Changes
getattrguards,__name__fallbacks, signature type fixestest_cancellation.py, updated react and callback tests for plan systemTest plan
enable_ask_to_user=TrueraisesConfirmationRequiredandaresume()continues correctlyjust test— all tests passjust lint— no lint errorsjust typecheck— ty passes