-
Notifications
You must be signed in to change notification settings - Fork 371
Feat: hybrid Agents-as-Tools/MCP-as-tools experimental agent #515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
iqdoctor
wants to merge
48
commits into
evalstate:main
Choose a base branch
from
strato-space:feat/agents-as-tools
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,569
−41
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…ldren as tools with parallel execution - add AgentsAsToolsAgent (ToolAgent subclass) that lists child agents as tools and runs tool calls in parallel - factory: BASIC with child_agents -> AgentsAsToolsAgent; otherwise keep McpAgent - validation: include BASIC.child_agents in dependency graph for proper creation order
docs: rename plantype references to plan_type (evalstate#456)
… Agents-as-Tools - pass RequestParams(show_chat=False, show_tools=False) to child agents when invoked as tools - always use aggregated display regardless of single/parallel tool count - single agent: 'Calling agent: X' with full content blocks in result - multiple agents: summary list with previews - removes duplicate stacked tool call/result blocks
- RequestParams doesn't support show_chat/show_tools (those are Settings.logger fields) - temporarily modify child.display.config before calling generate() - restore original config in finally block - fixes 'AsyncCompletions.create() got unexpected keyword argument' error
- display individual tool call blocks with full arguments for each agent - display individual tool result blocks with full content for each agent - removes minimal aggregated view in favor of detailed per-agent display - fixes missing chat logs for agent arguments and responses
- show 'instances N' in status when multiple agents called in parallel - metadata['instance_info'] passed to tool_call display - _instance_count attribute added to tool_result for display - parallel execution already working via asyncio.gather - displays in right_info: 'tool request - name | instances 2'
Optimizations: - Move json and copy imports to module level (avoid repeated imports) - Remove unused _tool_names variable - Simplify child agent lookup with chained or operator - Streamline input_text serialization logic (remove nested try/except) - Remove redundant iteration in _show_parallel_tool_results - Remove unnecessary descriptor_by_id.get() checks (key always exists) - Simplify inline conditionals for readability No behavior changes, purely code cleanup and performance improvement.
Changes: - Add instance IDs (: 1, : 2, etc.) to child agent names when instances > 1 - Modified before task creation so progress events use numbered names - Restored after execution completes - Shows as 'PM-1-DayStatusSummarizer: 1' and 'PM-1-DayStatusSummarizer: 2' in progress panel - Restore child agent tool call logs (show_tools) - Only suppress show_chat (child's assistant messages) - Keep show_tools=True to see child's internal tool activity - Fixes 'lost logs from child agents' issue Result: Separate progress lines for parallel instances + full visibility into child tool calls
- name is a read-only @Property that returns self._name - setting child.name had no effect - now properly modifies child._name to show instance numbers in progress panel - fixes missing :1 :2 labels in progress display
- Use modern type hints: dict/list instead of Dict/List (PEP 585) - Use pipe union syntax: Any | None instead of Optional[Any] (PEP 604) - Add comprehensive docstrings to all public methods - Remove unnecessary imports (Dict, List, Optional) - Improve inline comments clarity - Match formatting style used in tool_agent.py and parallel_agent.py No functional changes, pure style alignment.
- Cleaner display format for parallel agent instances - Shows as 'PM-1-DayStatusSummarizer#1' and 'PM-1-DayStatusSummarizer#2' - Appears in both progress panel and chat headers
Changes: - Agent names: 'PM-1-DayStatusSummarizer[1]' instead of 'PM-1-DayStatusSummarizer#1' - Tool headers: '[tool request - agent__PM-1-DayStatusSummarizer[2]]' instead of '[... | instances 2]' - Tool results: '[tool result - agent__PM-1-DayStatusSummarizer[2]]' - Removed metadata-based instance display from tool_display.py Cleaner display: instance count embedded directly in tool name for both requests and results.
Fixes: 1. Tool headers now show individual instance numbers [1], [2] instead of total count [2] - Tool request: 'agent__PM-1-DayStatusSummarizer[1]' for first call - Tool request: 'agent__PM-1-DayStatusSummarizer[2]' for second call 2. Bottom items show unique labels: 'agent__PM-1[1] · running', 'agent__PM-1[2] · running' 3. Store original names before ANY modifications to prevent [1][2] bug 4. Wrapper coroutine sets agent name at execution time for progress tracking Note: Separate progress panel lines require architecture changes (same agent object issue).
Implements user's suggested UX: 1. Parent agent line shows 'Ready' status while instances run 2. New lines appear: PM-1-DayStatusSummarizer[1], PM-1-DayStatusSummarizer[2] 3. Each instance line shows real-time progress (Chatting, turn N, tool calls) 4. After completion, instance lines are hidden from progress panel 5. Parent agent name restored Flow: - Emit READY event for parent agent (sets to idle state) - Create unique agent_name for each instance - Emit CHATTING event to create separate progress line - Child agent emits normal progress events with instance name - After gather() completes, hide instance task lines Result: Clean visual separation of parallel executions in left status panel.
Problem: When child agents called tools, progress events (CALLING_TOOL) were emitted with parent agent name instead of instance name, causing tool status to appear in wrong line. Root cause: MCPAggregator caches agent_name in __init__, so changing child._name didn't update the aggregator's agent_name. When aggregator emits progress for tool calls, it used the old cached name. Solution: - Update child._aggregator.agent_name when setting instance name - Restore child._aggregator.agent_name when restoring original name - Now tool call progress (Calling tool, tg-ro, etc.) appears in correct instance line Result: Each instance line shows its own 'Calling tool' status independently.
Ensures child agent tool calls remain visible in chat log by explicitly setting show_tools = True when creating temporary config.
Changes: - Parent agent line now hidden when child instances start (not 'Ready') - Only child instance lines visible during parallel execution - Each instance shows independent status - After completion: parent line restored, instance lines hidden Result: Clean progress panel with no 'stuck' parent status. Only active instance lines show during execution.
Added module-level documentation covering: 1. Overview - Pattern inspired by OpenAI Agents SDK - Hierarchical composition without orchestrator complexity 2. Rationale - Benefits over traditional orchestrator/iterative_planner - Simpler codebase, better LLM utilization - Natural composition with parallel by default 3. Algorithm - 4-step process: init → discovery → execution → parallel - Detailed explanation of each phase 4. Progress Panel Behavior - Before/during/after parallel execution states - Parent line shows 'Ready' during child execution - Instance lines with [1], [2] numbering - Visibility management for clean UX 5. Implementation Notes - Name modification timing (runtime vs creation time) - Original name caching to prevent [1][2] bugs - Progress event routing via aggregator.agent_name - Display suppression strategy 6. Usage Example - Simple code snippet showing pattern in action 7. References - OpenAI Agents SDK link - GitHub issue placeholder
Problem: Instance lines stayed visible showing 'stuck' status even after completing their work. Instance[1] would show 'Chatting' even though it finished and returned results. Root cause: Instance lines were only hidden after ALL tasks completed via asyncio.gather(). If one instance finished quickly and another took longer, the first instance's line remained visible with stale status. Solution: - Add finally block to task wrapper coroutine - Hide each instance line immediately when its task completes - Remove duplicate hiding logic from cleanup section - Now each instance disappears as soon as it's done Result: Clean, dynamic progress panel where instance lines appear when tasks start and disappear as each individual task finishes.
Problem: Instance lines remained visible ('stuck') even after tasks completed.
Root cause: progress_display was being re-imported in multiple scopes,
potentially creating different singleton instances or scope issues.
Solution:
- Import progress_display once at outer scope as 'outer_progress_display'
- Use same instance in wrapper coroutine's finally block
- Use same instance for parent Ready status update
- Added debug logging to track visibility changes
Note: The 'duplicate records' in chat log are actually separate results from
parallel instances [1] and [2], not true duplicates. Each instance gets its
own tool request/result header for clarity.
Problem: Only seeing logs from instance evalstate#4 when multiple instances of the same child agent run in parallel. Root cause: Multiple parallel instances share the same child agent object. When instance 1 finishes, it restores display config (show_chat=True), which immediately affects instances 2, 3, 4 that are still running. The last instance (evalstate#4) ends up with restored config and shows all its chat logs. Race condition flow: 1. Instance 1 starts → sets show_chat=False on shared object 2. Instances 2,3,4 start → see show_chat=False 3. Instance 1 finishes → restores show_chat=True 4. Instances 2,3,4 still running → now have show_chat=True (see logs!) Solution: Reference counting - Track active instance count per child agent ID - Only modify display config when first instance starts - Only restore display config when last instance completes - Store original config per child_id for safe restoration Data structures: - _display_suppression_count[child_id] → count of active instances - _original_display_configs[child_id] → stored original config Now all instances respect show_chat=False until ALL complete.
Updated comprehensive documentation to reflect: Algorithm section: - Reference counting for display config suppression - Parallel execution improvements (name+aggregator updates, immediate hiding) Progress Panel Behavior: - As each instance completes (not after all complete) - No stuck status lines - After all complete (restoration of configs) Implementation Notes: - Display suppression with reference counting explanation - _display_suppression_count and _original_display_configs dictionaries - Race condition prevention details (only modify on first, restore on last) - Instance line visibility using consistent progress_display singleton - Chat log separation with instance numbers for traceability All documentation now accurately reflects the production implementation.
Fixed three issues:
1. Duplicate labels in bottom status bar
- Before: Each tool call showed ALL instance labels
- After: Each tool call shows only its OWN label
- Changed from passing shared bottom_items array to passing single-item array per call
2. Final logs showing without instance index
- Before: Display config restored in call_tool finally block, causing final logs
to use original name (no [N])
- After: Display config restoration moved to run_tools, AFTER all tool results
are displayed
- Now all logs (including final) keep instance numbers: PM-1[1], PM-1[2], etc.
3. Display config restoration timing
- Removed restoration from call_tool finally block
- Added restoration in run_tools after _show_parallel_tool_results
- Cleanup of _display_suppression_count and _original_display_configs dictionaries
Result:
- Bottom bar: | PM-1[1] · running | (no duplicates)
- Final logs: ▎◀ PM-1-DayStatusSummarizer[4] [tool result] (keeps index)
- Clean separation of instance logs throughout execution
Fixed three issues:
1. Label truncation in bottom status bar
- Increased max_item_length from 28 to 50 characters
- Prevents '...' truncation of long agent/tool names
- Now shows: agent__PM-1-DayStatusSummarizer[1] (full name)
2. Display config reference counting improvements
- Separate initialization of _display_suppression_count and _original_display_configs
- Increment count BEFORE checking if first instance
- Only modify config if count==1 AND not already stored
- Added debug logging to track suppression lifecycle
3. Config restoration timing and cleanup
- Added logging to track decrements in finally block
- Check existence before accessing/deleting dictionary keys
- Restore config for both multi-instance and single-instance cases
- Clean up suppression count only when it reaches 0
The reference counting now ensures:
- First instance (count 0→1): Suppress chat, store original config
- Additional instances (count 1→2,3,4): Use existing suppressed config
- Instances complete (count 4→3,2,1): Keep suppressed config
- Last instance completes (count 1→0): Restore original config
Debug logs added:
- 'Suppressed chat for {name} (first instance)'
- 'Decremented count for {name}: N instances remaining'
- 'Restored display config for {name}'
Problem: Only instance evalstate#4 was showing chat logs. The issue was that call_tool was trying to suppress display config inside each parallel task, creating a race condition where configs would get overwritten. Solution: 1. Move display suppression to run_tools BEFORE parallel execution starts 2. Iterate through all child agents that will be called and suppress once 3. Store original configs in _original_display_configs dictionary 4. Remove all suppression logic from call_tool - it just executes now 5. After results displayed, restore all configs that were suppressed This ensures: - All instances use the same suppressed config (no race conditions) - Config is suppressed ONCE before parallel tasks start - All parallel instances respect show_chat=False - Config restored after all results are displayed The key insight: Don't try to suppress config inside parallel tasks - do it before they start so they all inherit the same suppressed state.
…ying config Problem: Even with pre-suppression, instances were still showing chat logs because they all share the same display object and config modifications weren't taking effect properly. Solution: 1. Create completely new ConsoleDisplay objects with suppressed config 2. Replace child.display with the new suppressed display object 3. Store both the original display object and config for restoration 4. After results shown, restore the original display object (not just config) This ensures complete isolation - each parallel execution uses a display object that has show_chat=False baked in from creation, eliminating any timing issues or race conditions with config modifications. The key insight: Don't just modify config on shared objects - create new objects with the desired behavior to ensure complete isolation.
Problem: All 4 parallel tasks were modifying the same child agent's _name simultaneously, causing a race condition where the last task to set it (usually instance [4]) would dominate the logs. Events from instances [1], [2], [3] were showing up under the main instance name or instance [4]. Root Cause: - Tasks ran concurrently: asyncio.gather(*tasks) - Each task did: child._name = instance_name (MUTATING SHARED STATE\!) - Race condition: Last writer wins, all tasks use that name - Result: All logs showed instance [4] name Solution - Sequential Name Ownership: 1. Build instance_map BEFORE tasks start - Maps correlation_id -> (child, instance_name, instance_num) - No shared state mutation yet 2. Each task owns the name during its execution: - On entry: Save old_name, set instance_name - Execute: All logs use this instance's name - On exit (finally): Restore old_name immediately 3. This creates sequential ownership windows: - Task 1: Sets [1], executes, restores - Task 2: Sets [2], executes, restores - Each task's logs correctly show its instance number Additional Changes: - Removed display suppression to see all logs for debugging - Keep main instance visible in progress panel (don't hide/suppress) - Each task restores names in finally block (no global cleanup needed) - Pass correlation_id to wrapper so it can lookup pre-assigned instance info This ensures each instance's logs are correctly attributed to that instance, making event routing visible for debugging.
Problem: Multiple concurrent tasks were mutating the same child agent's _name,
causing:
1. Race condition - tool calls from different instances got mixed up
2. Duplicate progress panel rows - each rename triggered new events
3. Logs showing wrong instance numbers
Root Cause: Even with try/finally, execution overlaps:
- Task 1: Sets name to [1], starts executing
- Task 2: Sets name to [2] (overwrites\!), Task 1 still running
- Task 1's logs now show [2] instead of [1]
Solution: Don't rename agents AT ALL
- Instance numbers already shown in display headers via _show_parallel_tool_calls
- Display code already does: display_tool_name = f'{tool_name}[{i}]'
- No need to mutate shared agent state
- Each task just calls the tool directly
- Parallel execution works without interference
Benefits:
- True parallel execution (no locks/serialization)
- No race conditions (no shared state mutation)
- No duplicate panel rows (child emits events with original name)
- Instance numbers still visible in tool call/result headers
The instance_map is now only used for logging context, not for renaming.
Problem: Duplicate progress panel rows showing 4+ entries for PM-1-DayStatusSummarizer Root Cause: Each child agent execution emits its own progress events, creating a new panel row each time. With 4 parallel instances, we got 4+ duplicate rows. Solution: Suppress child display output during parallel execution 1. BEFORE parallel tasks start: Suppress child.display.config - Set show_chat = False - Set show_tools = False - This prevents child from emitting ANY display events 2. Execute parallel tasks: Child runs silently, no panel rows created 3. AFTER results shown: Restore original child.display.config Benefits: - Only orchestrator's display headers show (with instance numbers [1], [2], etc.) - No duplicate progress panel rows - Clean consolidated view of parallel execution - Instance numbers still visible in tool call/result headers The key insight: Child agents should be 'silent' during parallel execution, letting the orchestrator handle all display output.
…lel execution Problem: Still seeing duplicate progress panel rows despite display config suppression Root Cause: Progress events are NOT controlled by display.config.logger settings. They come from a separate progress system that gets called regardless of config. Solution: Replace child.display with NullDisplay during parallel execution NullDisplay class: - Has config = None - Returns no-op lambda for ANY method call via __getattr__ - Completely suppresses ALL output: chat, tools, progress events, everything Flow: 1. BEFORE parallel: child.display = NullDisplay() 2. DURING parallel: All child output suppressed (no panel rows) 3. AFTER parallel: child.display = original_display (restored) Benefits: - Zero duplicate panel rows (child can't emit ANY events) - Zero race conditions (no shared state mutations) - Clean orchestrator-only display with instance numbers [1], [2], [3], [4] - True parallel execution maintained
Progress events are emitted by logger.info() calls, not just display. Need to suppress BOTH display AND logger to eliminate duplicate panel rows. Added NullLogger class that suppresses all logging calls. Store and restore both display and logger during parallel execution.
MCP tools emit progress events via aggregator.logger, not child.logger. Need to suppress aggregator's logger too. Now suppressing: - child.display - child.logger - child._aggregator.logger (NEW - this was the missing piece\!) This should finally eliminate all duplicate progress panel rows.
Reverted from NullDisplay/NullLogger approach back to simpler config modification. Suppression approach: - Store original child.display.config - Create temp config with show_chat=False, show_tools=False - Apply temp config during parallel execution - Restore original config after results shown Benefits: - Simpler implementation (no complex null object classes) - Less intrusive (just config changes, not object replacement) - Easier to debug and maintain - Still prevents duplicate progress panel rows This approach relies on display.config.logger settings to control output, which should be sufficient for most cases.
Added detailed inline documentation explaining: 1. PARALLEL EXECUTION SETUP section: - Instance numbering strategy (displayed in headers only) - Display suppression approach (config modification) - Why we avoid agent renaming (prevents race conditions) 2. _show_parallel_tool_calls docstring: - Example output showing instance numbers [1], [2], [3], [4] - Explains orchestrator displays tool call headers 3. _show_parallel_tool_results docstring: - Example output showing matching instance numbers in results - Shows how instance numbers correspond to calls Key design principles documented: - NO agent renaming during execution (true parallelism) - Instance numbers ONLY in display headers (no shared state) - Display suppression via config (prevents duplicate panel rows) - Orchestrator-only display (child agents silent during parallel execution) This documentation makes the parallel execution strategy clear for future maintenance and debugging.
Architectural improvement suggested by user: - First instance executes without index or suppression (natural behavior) - Only when 2nd+ instances appear, they get indexed [2], [3], [4] and suppressed Benefits: 1. Simpler logic - first instance untouched, runs as designed 2. Less config manipulation - only suppress when truly needed 3. More intuitive - single execution looks normal, parallel adds indexes 4. Cleaner code - fewer edge cases and state changes New numbering: - Instance 1: PM-1-DayStatusSummarizer (no index, full display) - Instance 2: PM-1-DayStatusSummarizer[2] (indexed, suppressed) - Instance 3: PM-1-DayStatusSummarizer[3] (indexed, suppressed) - Instance 4: PM-1-DayStatusSummarizer[4] (indexed, suppressed) Progress panel shows single entry from first instance. Instances 2+ are silent (suppressed) to avoid duplicates. Updated documentation and examples to reflect new approach.
Major architectural improvements based on user feedback: 1. PANEL VISIBILITY: - First instance: PM-1-DayStatusSummarizer (full display + streaming) - Instances 2+: PM-1-DayStatusSummarizer[2], [3], [4] (visible in panel) - ALL instances shown in progress panel (no hiding) 2. STREAMING SUPPRESSION: - First instance: streaming_display=True (typing effect visible) - Instances 2+: streaming_display=False (no typing clutter) - Instances 2+: show_chat=True, show_tools=True (panel entries visible) - Only the typing effect is suppressed, not the entire display 3. THREAD SAFETY: - Added self._instance_lock (asyncio.Lock) in __init__ - Protected instance creation with async with self._instance_lock - Prevents race conditions on concurrent run_tools calls - Sequential modification of instance_map and suppressed_configs Benefits: - User sees all parallel instances progressing in panel - No visual clutter from multiple streaming outputs - First instance behaves naturally (untouched) - Thread-safe instance creation for concurrent calls This approach provides full visibility into parallel execution while avoiding the distraction of multiple simultaneous typing effects.
all from upstream
- Add detached per-call cloning in LlmDecorator so child agents can be spawned via spawn_detached_instance and later merged with merge_usage_from. - Rework AgentsAsToolsAgent.run_tools to execute child agents in parallel using detached clones, with clearer per-instance progress lines and tool-call/result panels. - Track ownership of MCPConnectionManager in MCPAggregator and only shut it down from the owning aggregator, fixing “Task group is not active” errors when short‑lived clones exit. - Improve MCPAggregator tool refresh to rebuild namespaced tool maps per server and log UPDATED progress events with tool counts. - Extend log→ProgressEvent conversion to treat THINKING like STREAMING for token counts and to use the typed ProgressAction field. - Add RichProgressDisplay.hide_task API for future UI behaviors and wire small fastagent/listener changes around the updated progress pipeline.
- Add detached per-call cloning in LlmDecorator so child agents can be spawned via spawn_detached_instance and later merged with merge_usage_from. - Rework AgentsAsToolsAgent.run_tools to execute child agents in parallel using detached clones, with clearer per-instance progress lines and tool-call/result panels. - Track ownership of MCPConnectionManager in MCPAggregator and only shut it down from the owning aggregator, fixing “Task group is not active” errors when short‑lived clones exit. - Improve MCPAggregator tool refresh to rebuild namespaced tool maps per server and log UPDATED progress events with tool counts. - Extend log→ProgressEvent conversion to treat THINKING like STREAMING for token counts and to use the typed ProgressAction field. - Add RichProgressDisplay.hide_task API for future UI behaviors and wire small fastagent/listener changes around the updated progress pipeline.
- Remove temporary FAST_AGENT_DEBUG flag and prints from FastAgent.__init__ - Drop file-based progress debug logging from core.logging.listeners.convert_log_event - Remove RichProgressDisplay.hide_task and update design docs to FINISHED-based instance lines - Fix _invoke_child_agent indentation and guard display suppression with suppress_display flag
- Restore convert_log_event in core/logging/listeners.py to upstream-style ProgressAction handling (no extra debug logging) - Keep RichProgressDisplay FINISHED/FATAL_ERROR behavior simple: mark the current task completed without hiding other tasks - Align Agents-as-Tools design docs with detached per-call clones and FINISHED-based progress lines (no hide_task API) - Clarify AgentsAsToolsAgent module docstring and helper behavior to match current implementation (_invoke_child_agent, detached clones, usage merge)
- Make AgentsAsToolsAgent subclass McpAgent instead of ToolAgent - Merge MCP tools and agent-tools into a single list_tools() surface - Route call_tool() to child agents first, then fall back to MCP/local tools - Update run_tools() to split mixed batches into child vs MCP calls and execute child calls via detached clones while delegating remaining tools to McpAgent.run_tools(), merging all results and errors - Keep existing detached per-call clone behavior and progress panel semantics - Update agents-as-tools design doc and module docstrings to describe the hybrid MCP-aware behavior and mark merged MCP + agent-tools view as implemented
- Add simple PMO Agents-as-Tools example (agents_as_tools_simple.py) with NY-Project-Manager and London-Project-Manager using the local `time` MCP server. - Add extended PMO example (agents_as_tools_extended.py) that uses `time` + `fetch`, retries alternative sources on 403/robots.txt, and includes Fast-Agent / BBC / FT hints. - Update README Agents-as-Tools section with the PMO minimal example and a link to the extended workflow file. - Run black and minor style cleanups on AgentsAsToolsAgent without changing behavior.
- Expand module docstring with Agents-as-Tools rationale, algorithm, and progress/usage semantics. - Add minimal decorator-based usage example showing agents=[...] pattern. - Add GitHub-style links to design doc, docs repo, OpenAI Agents SDK, and issue evalstate#458 for future readers. - Keep runtime behavior unchanged apart from clearer structure and black formatting (no logic changes).
- Add simple and extended PMO Agents-as-Tools workflows using local time/fetch MCP servers. - Document AgentsAsToolsAgent behavior and architecture in README and module docstring. - Wire detached clone support via LlmDecorator.spawn_detached_instance and merge_usage_from. - Fix import ordering and type-checking-only imports so scripts/lint.py passes cleanly.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Agents-as-Tools: hybrid agent + examples
Closes #458.
This adds a hybrid Agents-as-Tools/MCP-as-tools experimental agent and two PMO-style examples that show a parent agent calling child agents as tools (with parallel execution), plus a brief design doc and README section.
What’s included
Core hybrid agent
src/fast_agent/agents/workflow/agents_as_tools_agent.py
New AgentsAsToolsAgent(McpAgent):
Inherits MCP behavior and tools from
McpAgent.Exposes each child agent as an additional tool (
agent__ChildName).list_tools() returns MCP tools + agent-tools as one surface.
call_tool() routes to child agents first, then falls back to
McpAgent.call_tool.run_tools():
spawn_detached_instance), with suffixed names likeChild[1].ProgressEventupdates so each clone appears as its own line in the progress panel.McpAgent.run_toolsand merges all results + error text.Simple PMO example (minimal)
examples/workflows/agents_as_tools_simple.py
NY-Project-ManagerandLondon-Project-Manageruseservers=["time"].PMO-orchestratorhasagents=[...]and calls children as tools:{OpenAI, Fast-Agent, Anthropic}.Entry point:
await agent("Get PMO report").Extended PMO + news example
examples/workflows/agents_as_tools_extended.py
Same agents, but with
servers=["time", "fetch"].NY PM:
London PM:
PMO-orchestrator:evalstate/fast-agent, OpenAI).Docs & design
README.md
New Agents-as-Tools section:
Explains the pattern in a few bullets.
Shows the minimal PMO example (simple file).
Brief “Architecture (brief)” list:
Links to the extended workflow file.
agetns_as_tools_plan_scratch.md
From-scratch design plan:
How to run
From repo root: