Subagent UI Hooks API

The AgentUIHooks protocol enables rich terminal UIs (like jaato-tui) to integrate with the agent system, providing visibility into main agent and subagent execution, output, and accounting.

Overview

Location: shared/plugins/subagent/ui_hooks.py

The hooks allow UIs to:

Both main agent and subagents use the same hook interface, ensuring consistent tracking across all agent types.

Subagent Profile Configuration

Subagent profiles are configured in the subagent plugin configuration. Each profile defines a specialized agent with specific capabilities, model settings, and visual representation.

Profile Auto-Discovery

Profiles can be automatically discovered from a directory (default: .jaato/profiles/). Each .json or .yaml file in this directory is parsed as a profile definition.

# Directory structure
.jaato/
└── profiles/
    ├── code_assistant.json
    ├── research_agent.yaml
    └── custom_agent.json

Configuration options:

Merge behavior: Discovered profiles are merged with explicitly configured profiles. Explicit profiles take precedence on name conflicts.

Configuration Schema

A subagent profile supports the following fields:

{
  "profiles": {
    "code_assistant": {
      "description": "Analyzes and refactors code",
      "plugins": ["cli", "file_edit", "grep"],
      "plugin_configs": {
        "cli": {
          "allowed_commands": ["tree", "cat", "grep"]
        }
      },
      "system_instructions": "You are a code analysis specialist...",
      "model": "claude-sonnet-4-20250514",
      "max_turns": 10,
      "auto_approved": false,
      "icon_name": "code_assistant"
    },
    "custom_agent": {
      "description": "Custom agent with unique icon",
      "plugins": ["cli"],
      "icon": [
        " ┌─┐  ",
        " │★│  ",
        " └─┘  "
      ]
    }
  }
}

Profile Fields

Agent Icons

Icons are displayed in the Agents panel (right side, 20% width) to visually identify agents. Icons are exactly 3 lines of ASCII art.

Icon Resolution Priority

The system resolves icons in the following order:

  1. Custom icon - icon field in profile (highest priority)
  2. Profile-registered icon - Custom icons registered programmatically
  3. Predefined icon by name - icon_name field matches a default icon
  4. Agent type default - Icon based on agent type ("main" or "subagent")
  5. Fallback - Generic subagent icon (lowest priority)

Available Predefined Icons

The following predefined icons are available via the icon_name field:

# Main agent (default for agent_type="main")
"main":
  ╭─┐
  │█│
  └┬┘

# Code assistant - use icon_name: "code_assistant"
"code_assistant":
 </>
  ▼
 ╚═╝

# Research agent - use icon_name: "research"
"research":
 [🔍]
  ║║║
  ╚╩╝

# File editor - use icon_name: "file_editor"
"file_editor":
 ┌─┐
 │≡│
 └─┘

# Data analyst - use icon_name: "data_analyst"
"data_analyst":
 ▄▄▄
 ║█║
 ╚═╝

# Test runner - use icon_name: "test_runner"
"test_runner":
 ▶║
 ▶║
 ▶║

# Web scraper - use icon_name: "web_scraper"
"web_scraper":
 ╔╦╗
 ╠╬╣
 ╚╩╝

# Generic task agent - use icon_name: "task_agent"
"task_agent":
 ┌▶┐
 │░│
 └─┘

# Default subagent (fallback)
"default_subagent":
  ⚙ ⚙
   ▀▄▀
   ║║

Custom Icon Example

To define a custom icon, use the icon field with exactly 3 strings:

{
  "profiles": {
    "my_agent": {
      "description": "My custom agent",
      "plugins": ["cli"],
      "icon": [
        " ╔═╗  ",  // Line 1 (top)
        " ║★║  ",  // Line 2 (middle)
        " ╚═╝  "   // Line 3 (bottom)
      ]
    }
  }
}
Note: Icons should be approximately 5-7 characters wide for best display in the agent panel. Each line must be the same length to avoid rendering issues.

Protocol: AgentUIHooks

on_agent_created(agent_id, agent_name, agent_type, profile_name, parent_agent_id, icon_lines, created_at)

Called when a new agent is created (main or subagent).

agent_id str
Unique identifier. Format:
  • "main" for main agent
  • "subagent_1", "subagent_2" for top-level subagents
  • "parent.child" for nested subagents (e.g., "code-assist.analyzer")
agent_name str
Human-readable display name (e.g., "main", "code-assist", "code-assist.analyzer").
agent_type str
Either "main" or "subagent".
profile_name Optional[str]
Profile name if subagent (e.g., "code_assistant"), None for main agent.
parent_agent_id Optional[str]
Parent agent's ID if nested subagent, None for main or top-level subagents.
icon_lines Optional[List[str]]
Custom ASCII art icon (3 lines) defined in profile, or None to use default.
created_at datetime
Timestamp when agent was created.
on_agent_output(agent_id, source, text, mode)

Called whenever an agent produces output (model response, tool output, etc.).

agent_id str
Which agent produced this output.
source str
Output source: "model" for model responses, "user" for user input, or plugin name for tool output (e.g., "cli", "mcp").
text str
The output text content.
mode str
"write" for new output block, "append" to continue previous block.
on_agent_status_changed(agent_id, status, error=None)

Called when an agent's status changes.

agent_id str
Which agent's status changed.
status str
New status: "active", "done", or "error".
error Optional[str]
Error message if status is "error", None otherwise.
on_agent_completed(agent_id, completed_at, success, token_usage=None, turns_used=None)

Called when an agent completes execution.

agent_id str
Which agent completed.
completed_at datetime
Timestamp when agent completed.
success bool
True if agent succeeded, False if errored.
token_usage Optional[Dict[str, int]]
Dictionary with keys: "prompt_tokens", "output_tokens", "total_tokens". None if not available.
turns_used Optional[int]
Number of conversation turns used. None if not available.
on_agent_turn_completed(agent_id, turn_number, prompt_tokens, output_tokens, total_tokens, duration_seconds, function_calls)

Called after each conversation turn completes. Enables per-agent, per-turn token accounting.

agent_id str
Which agent completed the turn.
turn_number int
Turn index (0-based).
prompt_tokens int
Tokens consumed by the prompt.
output_tokens int
Tokens generated in the response.
total_tokens int
Sum of prompt_tokens + output_tokens.
duration_seconds float
Time taken for the turn.
function_calls List[Dict[str, Any]]
List of function calls made during the turn, each with 'name' and 'duration_seconds' keys.
on_agent_context_updated(agent_id, total_tokens, prompt_tokens, output_tokens, turns, percent_used)

Called when agent's context usage changes. Enables per-agent context tracking.

agent_id str
Which agent's context updated.
total_tokens int
Total tokens used.
prompt_tokens int
Cumulative prompt tokens.
output_tokens int
Cumulative output tokens.
turns int
Number of turns.
percent_used float
Percentage of context window used.
on_agent_history_updated(agent_id, history)

Called when agent's conversation history changes (after each turn). Enables per-agent history isolation.

agent_id str
Which agent's history updated.
history List[Message]
Complete conversation history snapshot (list of Message objects).
on_tool_call_start(agent_id, tool_name, tool_args)

Called when a tool starts executing. Enables real-time tool call visualization in the UI (e.g., showing active tools below the spinner).

agent_id str
Which agent initiated the tool call.
tool_name str
Name of the tool being called (e.g., "cli_execute", "web_search").
tool_args Dict[str, Any]
Arguments passed to the tool. Can be displayed truncated in the UI.
on_tool_call_end(agent_id, tool_name, success, duration_seconds)

Called when a tool finishes executing. Removes the tool from the active tools display.

agent_id str
Which agent's tool call completed.
tool_name str
Name of the tool that finished.
success bool
Whether the tool executed successfully.
duration_seconds float
How long the tool took to execute.

Tool Call Visualization

The on_tool_call_start and on_tool_call_end hooks enable real-time visualization of active tool calls. When implemented, the UI can show active tools below the spinner:

Model> ⠋ thinking...
       ├─ cli_execute({'cmd': 'ls -la'})
       └─ web_search({'query': 'python docs'})

These hooks are emitted automatically for all plugins from JaatoSession._run_chat_loop(). No plugin-specific changes are required - any tool that executes through the session will trigger these hooks.

Integration Example

# Rich client integration
from shared.plugins.subagent.ui_hooks import AgentUIHooks
from rich_client.agent_registry import AgentRegistry

class RichClientHooks:
    """UI hooks implementation for rich client."""

    def __init__(self, agent_registry: AgentRegistry):
        self._registry = agent_registry

    def on_agent_created(self, agent_id, agent_name, agent_type,
                         profile_name, parent_agent_id, icon_lines, created_at):
        self._registry.create_agent(
            agent_id=agent_id,
            name=agent_name,
            agent_type=agent_type,
            profile_name=profile_name,
            icon_lines=icon_lines,
            created_at=created_at
        )

    def on_agent_output(self, agent_id, source, text, mode):
        buffer = self._registry.get_buffer(agent_id)
        if buffer:
            buffer.append(source, text, mode)

    def on_agent_status_changed(self, agent_id, status, error=None):
        self._registry.update_status(agent_id, status)

    def on_agent_completed(self, agent_id, completed_at,
                          success, token_usage, turns_used):
        self._registry.mark_completed(agent_id, completed_at)

    def on_agent_turn_completed(self, agent_id, turn_number, prompt_tokens,
                               output_tokens, total_tokens, duration_seconds,
                               function_calls):
        self._registry.update_turn_accounting(
            agent_id, turn_number, prompt_tokens, output_tokens,
            total_tokens, duration_seconds, function_calls
        )

    def on_agent_context_updated(self, agent_id, total_tokens, prompt_tokens,
                                output_tokens, turns, percent_used):
        self._registry.update_context_usage(
            agent_id, total_tokens, prompt_tokens,
            output_tokens, turns, percent_used
        )

    def on_agent_history_updated(self, agent_id, history):
        self._registry.update_history(agent_id, history)

    def on_tool_call_start(self, agent_id, tool_name, tool_args):
        buffer = self._registry.get_buffer(agent_id)
        if buffer:
            buffer.add_active_tool(tool_name, tool_args)

    def on_tool_call_end(self, agent_id, tool_name, success, duration_seconds):
        buffer = self._registry.get_buffer(agent_id)
        if buffer:
            buffer.remove_active_tool(tool_name)

# Register hooks
hooks = RichClientHooks(agent_registry)
jaato_client.set_ui_hooks(hooks)
subagent_plugin.set_ui_hooks(hooks)

Callback Sequence

Typical callback sequence when main agent spawns a subagent:

1. Main agent created (at startup):
   on_agent_created(agent_id="main", agent_type="main", ...)
   on_agent_status_changed(agent_id="main", status="active")

2. Main agent receives user input:
   on_agent_output(agent_id="main", source="user", text="Analyze code", mode="write")

3. Main agent responds:
   on_agent_output(agent_id="main", source="model", text="I'll use subagent...", mode="write")

4. Main agent spawns subagent:
   on_agent_created(agent_id="subagent_1", agent_name="code-assist",
                    agent_type="subagent", profile_name="code_assistant", ...)
   on_agent_status_changed(agent_id="subagent_1", status="active")

5. Subagent executes (with tool calls):
   on_agent_output(agent_id="subagent_1", source="model", text="Analyzing...", mode="write")
   on_tool_call_start(agent_id="subagent_1", tool_name="cli_execute", tool_args={"cmd": "tree"})
   on_agent_output(agent_id="subagent_1", source="cli", text="tree output", mode="write")
   on_tool_call_end(agent_id="subagent_1", tool_name="cli_execute", success=True, duration_seconds=0.5)
   on_agent_turn_completed(agent_id="subagent_1", turn_number=0, ...)
   on_agent_context_updated(agent_id="subagent_1", ...)
   on_agent_history_updated(agent_id="subagent_1", ...)

6. Subagent completes:
   on_agent_status_changed(agent_id="subagent_1", status="done")
   on_agent_completed(agent_id="subagent_1", success=True, token_usage={...}, ...)

7. Main agent continues:
   on_agent_output(agent_id="main", source="model", text="Based on analysis...", mode="write")
   on_agent_turn_completed(agent_id="main", turn_number=0, ...)
   on_agent_context_updated(agent_id="main", ...)
   on_agent_history_updated(agent_id="main", ...)

Thread Safety

Important: All hooks may be called from background threads (especially for subagents). Implementations must be thread-safe. Use thread-safe queues, locks, or other synchronization primitives as appropriate.

Notes

Related Files