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.
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 profiles are configured in the subagent plugin configuration. Each profile defines a specialized agent with specific capabilities, model settings, and visual representation.
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:
auto_discover_profiles (boolean) - Enable/disable auto-discovery (default: true)profiles_dir (string) - Directory to scan for profiles (default: .jaato/profiles)Merge behavior: Discovered profiles are merged with explicitly configured profiles. Explicit profiles take precedence on name conflicts.
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": [
" ┌─┐ ",
" │★│ ",
" └─┘ "
]
}
}
}
description (string, required) - Human-readable description of the agent's purposeplugins (array) - List of plugin names to enable (e.g., ["cli", "file_edit", "web_search"])plugin_configs (object) - Per-plugin configuration overridessystem_instructions (string) - Additional system instructions for the agentmodel (string) - Model override (uses parent's model if not specified)max_turns (integer) - Maximum conversation turns before returning (default: 10)auto_approved (boolean) - Whether this agent can spawn without permission (default: false)icon (array of 3 strings) - Custom ASCII art icon (exactly 3 lines)icon_name (string) - Name of predefined icon to useIcons are displayed in the Agents panel (right side, 20% width) to visually identify agents. Icons are exactly 3 lines of ASCII art.
The system resolves icons in the following order:
icon field in profile (highest priority)icon_name field matches a default icon"main" or "subagent")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": ⚙ ⚙ ▀▄▀ ║║
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)
]
}
}
}
Called when a new agent is created (main or subagent).
"main" for main agent"subagent_1", "subagent_2" for top-level subagents"parent.child" for nested subagents (e.g., "code-assist.analyzer")"main", "code-assist", "code-assist.analyzer").
"main" or "subagent".
"code_assistant"), None for main agent.
None for main or top-level subagents.
None to use default.
Called whenever an agent produces output (model response, tool output, etc.).
"model" for model responses, "user" for user input,
or plugin name for tool output (e.g., "cli", "mcp").
"write" for new output block, "append" to continue previous block.
Called when an agent's status changes.
"active", "done", or "error".
"error", None otherwise.
Called when an agent completes execution.
True if agent succeeded, False if errored.
"prompt_tokens", "output_tokens", "total_tokens".
None if not available.
None if not available.
Called after each conversation turn completes. Enables per-agent, per-turn token accounting.
prompt_tokens + output_tokens.
'name' and 'duration_seconds' keys.
Called when agent's context usage changes. Enables per-agent context tracking.
Called when agent's conversation history changes (after each turn). Enables per-agent history isolation.
Message objects).
Called when a tool starts executing. Enables real-time tool call visualization in the UI (e.g., showing active tools below the spinner).
"cli_execute", "web_search").
Called when a tool finishes executing. Removes the tool from the active tools display.
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.
# 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)
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", ...)
agent_id uses dotted notation: "parent.child".history and context commands show the currently selected agent's data.shared/plugins/subagent/ui_hooks.py - Protocol definition (includes tool hooks)shared/jaato_client.py - Main agent hook integrationshared/jaato_session.py - Tool call hooks emission (set_ui_hooks, _run_chat_loop)shared/plugins/subagent/plugin.py - Subagent hook integrationjaato-tui/agent_registry.py - Agent state managementjaato-tui/output_buffer.py - Active tool tracking and spinner renderingjaato-tui/agent_panel.py - Agent visualizationjaato-tui/rich_client.py - Hook implementation example