microsoft / microsoft/agent-framework
.NET/Python: Consider adding `return_mode` parameter to `as_tool()` / `AsAIFunction()`
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
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](https://github.com/microsoft/agent-framework/blob/06c7209000b3283c92a6bd6d72230ff38387092e/python/packages/core/agent_framework/_agents.py#L464)).
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:
1. **Context bloat**: Coordinator's context fills up faster, undermining the isolation pattern's benefit for 'sub-agent' calls
2. **Signal vs noise**: Intermediate "thinking" text adds no value to coordinator's decision-making
3. **Cost**: More tokens processed in coordinator, increasing costs
## Proposed Solution
```python
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 text
- `Callable`: Custom extraction logic with full access to `AgentRunResponse`
**Examples:**
```python
# 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
```python
# 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 behavior
```
Default `"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](https://github.com/microsoft/agent-framework/blob/06c7209000b3283c92a6bd6d72230ff38387092e/python/packages/core/agent_framework/_agents.py#L464) |
| .NET | `AsAIFunction()` | `response.Text` | [AgentExtensions.cs:77](https://github.com/microsoft/agent-framework/blob/06c7209000b3283c92a6bd6d72230ff38387092e/dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs#L77) |
Both use `ConcatText()` to concatenate `TextContent` from all messages, so both would benefit from a `returnMode` parameter.
Contributor guide
Assessment
This issue has not been assessed yet.