Skip to content

Agent-as-tool: streaming propagation and structured I/O #100

Description

@weeco

Problem Statement

AgentTool wraps an agent as a tool for hierarchical composition, but the current implementation has significant limitations:

No streaming propagation

AgentTool.Execute() runs the child agent to completion and blocks until done. All streaming events — token deltas, tool execution progress, intermediate reasoning — are consumed silently. The parent agent (and ultimately the user) sees nothing until the child finishes.

In practice: invoke a sub-agent for a research task, wait 5 minutes with zero progress indication, then get a text blob back. This is the opposite of the real-time streaming experience the SDK otherwise provides.

Lossy I/O schema

  • Input: Fixed to {message: string}. The child agent receives raw JSON args as a text message. There's no way to pass structured data, files, or context that the child could use programmatically.
  • Output: Fixed to {result: string} containing only the last assistant message's text. Multi-part responses (tool results, reasoning traces, structured data), token usage from the child, and any artifacts produced are all lost.

No usage tracking propagation

Token usage from the child agent is not reported back to the parent. A parent agent can't make cost-aware decisions about sub-agent delegation.

Fresh session every time

Each invocation creates a completely isolated session. There's no way to share relevant context from the parent's conversation, and no way for the child to persist learning across multiple invocations.

Proposed Solution

Streaming tool interface

Add an optional streaming variant for tools that produce incremental output:

// StreamingTool extends Tool with streaming execution support.
type StreamingTool interface {
    Tool
    // ExecuteStream returns an iterator of progress events.
    ExecuteStream(ctx context.Context, args json.RawMessage) iter.Seq2[ToolEvent, error]
}

// ToolEvent represents progress from a streaming tool.
type ToolEvent interface {
    isToolEvent()
}

type ToolProgressEvent struct {
    Message string          // Human-readable progress update
    Data    json.RawMessage // Optional structured progress data
}

type ToolResultEvent struct {
    Result json.RawMessage // Final result
}

AgentTool would implement StreamingTool, forwarding child agent events as ToolProgressEvent entries. The agent loop would emit these as they arrive, giving the parent (and consumer) real-time visibility.

Richer I/O for agent-as-tool

Input: Allow structured input that maps to the child agent's capabilities:

type AgentToolInput struct {
    Message string            `json:"message"`           // Required: task description
    Context json.RawMessage   `json:"context,omitempty"` // Optional: structured context from parent
}

Output: Return richer results including usage and structured data:

type AgentToolResult struct {
    Result    string          `json:"result"`              // Text response (backward compatible)
    Usage     *llm.TokenUsage `json:"usage,omitempty"`     // Token usage from child
    Turns     int             `json:"turns"`               // How many turns the child took
    Artifacts json.RawMessage `json:"artifacts,omitempty"` // Any artifacts produced
}

Usage propagation

When AgentTool completes, it should report the child's cumulative token usage via the ToolCallInfo metadata, allowing the parent's interceptors (OTel, cost tracking) to account for sub-agent costs.

Optional context sharing

Allow configuring whether the child gets relevant context from the parent:

agenttool.New(childAgent,
    agenttool.WithContextSharing(true),   // Pass parent's recent messages as context
    agenttool.WithSharedMessages(5),      // Last 5 messages from parent session
)

Use Case Example

Research agent with streaming progress:

researcher := llmagent.New("researcher", "Research the given topic thoroughly", model,
    llmagent.WithTools(webSearchRegistry),
)

// Parent agent delegates research
mainTools := tool.NewRegistry(tool.RegistryConfig{})
mainTools.Register(agenttool.New(researcher))

// During execution, the user sees:
// [sub-agent: researcher] Searching for "distributed consensus algorithms"...
// [sub-agent: researcher] Found 5 relevant papers, analyzing...
// [sub-agent: researcher] Querying second source for comparison...
// [sub-agent: researcher] Complete. Used 12,450 tokens across 4 turns.
// [main agent] Based on the research, here's a summary...

Why This Matters

  • User experience: Waiting minutes with no progress indication is unacceptable for interactive applications. Users need to see that the agent is working and making progress, even when work is delegated to sub-agents.
  • Observability: In production multi-agent systems, losing visibility into sub-agent execution makes debugging nearly impossible. Streaming events from children through to the consumer enables end-to-end tracing.
  • Cost awareness: Without usage propagation, the parent agent can't make informed decisions about delegation cost. A parent that delegates to an expensive sub-agent 50 times has no way to track or limit the spend.
  • Structured delegation: Complex agent systems need to pass structured context between agents, not just text messages. The current {message: string} input means the parent must serialize everything to prose, and the child must parse it back — lossy and unreliable.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentAgent frameworkbreaking-changeRequires breaking API changesenhancementNew feature or requesttoolTool system

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions