Repository navigation
Description
Summary
When using agent.as_tool(), the sub-agent returns concatenated TextContent from all messages (filters out tool call/result), not just the final answer. Developers currently have no way to control this (e.g., no flexibility to either return last n messages, or some custom filtering useful for context engineering).
Suggestion: Consider adding an optional return_mode parameter to as_tool().
The Problem
Currently, as_tool() returns response.text which concatenates all TextContent from all messages (source).
In a typical multi-step sub-agent scenario:
| Message | Content | Returned? |
|---|---|---|
| 1 | "Let me search for that..." | ✓ YES |
| 2 | FunctionCallContent (tool call) | ✗ No |
| 3 | FunctionResultContent (2000 tokens) | ✗ No |
| 4 | "I found some info, let me dig deeper..." | ✓ YES |
| 5 | FunctionCallContent (tool call) | ✗ No |
| 6 | FunctionResultContent (3000 tokens) | ✗ No |
| 7 | "FINAL ANSWER: The solution is..." | ✓ YES |
What gets returned: "Let me search...I found some info...FINAL ANSWER: ..."
What developer likely wants (in some cases): "FINAL ANSWER: The solution is..."
In testing with 10 intermediate "thinking" messages, the coordinator receives ~10x more text than necessary.
Some developer scenarios where this is problematic:
- Context bloat: Coordinator's context fills up faster, undermining the isolation pattern's benefit for 'sub-agent' calls
- Signal vs noise: Intermediate "thinking" text adds no value to coordinator's decision-making
- Cost: More tokens processed in coordinator, increasing costs
Proposed Solution
def as_tool(
self,
*,
# ... existing params ...
return_mode: Literal["all", "last"] | Callable[[AgentRunResponse], str] = "all",
) -> AIFunction[BaseModel, str]:"all"(default): Current behavior (backward compatible)"last": Return only the last message's textCallable: Custom extraction logic with full access toAgentRunResponse
Examples:
# Return only last message
researcher.as_tool(return_mode="last")
# Return last N messages
researcher.as_tool(return_mode=lambda r: "\n".join(m.text for m in r.messages[-3:]))
# Include tool calls (for debugging/audit)
researcher.as_tool(return_mode=lambda r: format_with_tool_calls(r.messages))The callable receives the full AgentRunResponse, giving access to response.messages with raw .contents (including FunctionCallContent, FunctionResultContent) for advanced use cases.
Implementation Sketch
# In as_tool(), replace `.text` calls with `_apply_return_mode(response)`:
# - Line 464: non-streaming path
# - Line 476: streaming path
def _apply_return_mode(response: AgentRunResponse) -> str:
if callable(return_mode):
return return_mode(response)
elif return_mode == "last":
return response.messages[-1].text if response.messages else ""
return response.text # "all" - current behaviorDefault "all" preserves current behavior with no breaking changes.
Applies to Both Python and .NET
This issue affects both language implementations:
| Language | Method | Returns | Source |
|---|---|---|---|
| Python | as_tool() |
response.text |
_agents.py:464 |
| .NET | AsAIFunction() |
response.Text |
AgentExtensions.cs:77 |
Both use ConcatText() to concatenate TextContent from all messages, so both would benefit from a returnMode parameter.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
[-]Python: [.Net] Consider adding `return_mode` parameter to `as_tool()` / `AsAIFunction()`[/-][+].NET: Python: [.Net] Consider adding `return_mode` parameter to `as_tool()` / `AsAIFunction()`[/+][-].NET: Python: [.Net] Consider adding `return_mode` parameter to `as_tool()` / `AsAIFunction()`[/-][+].NET/Python: Consider adding `return_mode` parameter to `as_tool()` / `AsAIFunction()`[/+]MichaelSurgeon commentedon Dec 18, 2025
I wonder if this could be expanded for returning artifact information to? currently artifacts are lost as part of as_tool() we have a use case where we return metadata within an artifact for a file like chapter_name etc which is lost on return.