Files
AIPA/orchestration/ui.py
T
2026-04-03 09:01:01 -07:00

935 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ui.py — AIPA Terminal Interface
Authored by: Iris, Director of Interface & Experience
Visual language:
Miranda — cyan primary orchestrator, delivers to Principal
Vera — yellow auditor, independent track
Atlas — green research
Cole — blue operations
Clio — magenta analysis
Evelyn — white personnel & systems
Iris — bright_cyan interface
System — dim white infrastructure messages
Error — red failures and blockers
Warning — orange flags and cautions
Panels carry the agent name as their title. The Principal never has to read
a header to know who is speaking — color and panel style make it immediate.
"""
from contextlib import contextmanager
from datetime import datetime
import json as _json
import config as _config
from rich.console import Console, Group
from rich.columns import Columns
from rich.live import Live
from rich.markdown import Markdown
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TaskProgressColumn,
TextColumn,
TimeElapsedColumn,
)
from rich.rule import Rule
from rich.table import Table
from rich.text import Text
from rich.theme import Theme
from rich import box
# ---------------------------------------------------------------------------
# Console setup
# ---------------------------------------------------------------------------
def _build_theme() -> Theme:
"""Build the Rich theme dynamically from agents.yaml color definitions."""
theme_dict: dict[str, str] = {
# System styles — not agent-specific
"system": "dim white",
"error": "bold red",
"warning": "bold yellow",
"task_id": "dim cyan",
"label": "bold white",
"muted": "dim",
"pass": "bold green",
"flag": "bold yellow",
"reject": "bold red",
}
# Agent name aliases — one entry per agent in agents.yaml
for name in _config.AGENT_CONFIGS:
theme_dict[name] = f"bold {_config.agent_color(name)}"
# Role aliases — stable names regardless of which agent holds the role
for role, agent in (
("orchestrator", _config.ORCHESTRATOR_AGENT),
("auditor", _config.AUDITOR_AGENT),
("recruiter", _config.RECRUITER_AGENT),
):
if agent:
theme_dict[role] = f"bold {_config.agent_color(agent)}"
return Theme(theme_dict)
console = Console(theme=_build_theme(), highlight=False)
_last_thinking: str = "" # Stored from most recent streamed response
_last_stream_rendered: bool = False # True when stream_agent_output rendered the final panel
# ---------------------------------------------------------------------------
# Agent visual config
# ---------------------------------------------------------------------------
def _agent_color(name: str) -> str:
return _config.agent_color(name.lower())
def _agent_display_title(name: str) -> str:
"""Return 'Name — Title' using the title from agents.yaml."""
title = _config.agent_title(name)
return f"{name.capitalize()}{title}" if title else name.capitalize()
# ---------------------------------------------------------------------------
# Session chrome
# ---------------------------------------------------------------------------
def print_session_header():
"""Opening banner for a new Principal session."""
cos_name = _config.ORCHESTRATOR_AGENT.capitalize()
cos_title = _config.agent_title(_config.ORCHESTRATOR_AGENT)
aud_name = _config.AUDITOR_AGENT.capitalize()
app_name = _config.APP_NAME
console.print()
console.rule(f"[bold cyan]{app_name}[/bold cyan] [dim]Autonomous Intelligence Principal Architecture[/dim]", style="cyan")
console.print()
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
table.add_column(style="dim")
table.add_column(style="white")
table.add_row("Session opened", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
table.add_row("Orchestrator", f"{cos_name}{cos_title}")
table.add_row("Auditor", f"{aud_name} (independent track)")
table.add_row("Commands", "/help for available commands")
console.print(table)
console.print()
def print_session_footer(task_count: int):
"""Closing rule when the session ends."""
console.print()
console.rule(
f"[dim]Session closed — {task_count} directive{'s' if task_count != 1 else ''} processed[/dim]",
style="dim"
)
console.print()
# ---------------------------------------------------------------------------
# Input prompt
# ---------------------------------------------------------------------------
def get_directive() -> str:
"""Styled input prompt for the Principal."""
console.print()
return console.input("[bold white]Principal [/bold white] ").strip()
# ---------------------------------------------------------------------------
# System / status messages
# ---------------------------------------------------------------------------
def print_system(message: str):
console.print(f" [system]⬡ {message}[/system]")
def print_error(message: str):
console.print(f"\n [error]✗ {message}[/error]\n")
def print_warning(message: str):
console.print(f" [warning]⚠ {message}[/warning]")
def print_task_received(task_id: str, directive_preview: str):
console.print()
console.print(
f" [task_id]{task_id}[/task_id] [dim]Directive received:[/dim] "
f"[white]{directive_preview[:100]}{'' if len(directive_preview) > 100 else ''}[/white]"
)
# ---------------------------------------------------------------------------
# Lead dispatch progress
# ---------------------------------------------------------------------------
def _brief_preview(brief: str, max_len: int = 72) -> str:
"""Return a short preview of a task brief, skipping the header lines."""
lines = [l.strip() for l in brief.splitlines() if l.strip()]
content = [
l for l in lines
if not l.upper().startswith("TASK BRIEF") and not l.upper().startswith("TO:")
]
text = " ".join(content)
return (text[:max_len] + "") if len(text) > max_len else text
def print_task_dispatch_plan(tasks: list):
"""
Show the task assignments before leads are dispatched.
Each row: task_id | lead | brief preview.
"""
console.print()
console.rule("[dim]Lead dispatch[/dim]", style="dim")
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2), expand=False)
table.add_column(style="task_id", no_wrap=True)
table.add_column(no_wrap=True)
table.add_column(style="dim")
for t in tasks:
color = _agent_color(t.assigned_to)
table.add_row(
t.task_id,
f"[{color}]{_agent_display_title(t.assigned_to)}[/{color}]",
_brief_preview(t.brief),
)
console.print(table)
@contextmanager
def lead_dispatch_progress(tasks: list):
"""
Per-task live progress display while leads are working.
Yields (progress, rows) where rows is keyed by task_id.
"""
def _row(task_id: str, lead_name: str, status: str) -> str:
color = _agent_color(lead_name)
return (
f"[task_id]{task_id}[/task_id] "
f"[{color}]{lead_name.capitalize()}[/{color}] "
f"{status}"
)
with Progress(
SpinnerColumn(style="dim"),
TextColumn("{task.description}"),
TimeElapsedColumn(),
console=console,
transient=True,
) as progress:
rows = {
t.task_id: progress.add_task(
_row(t.task_id, t.assigned_to, "[dim]queued[/dim]"),
total=1,
)
for t in tasks
}
yield progress, rows
def mark_task_running(progress, rows: dict, task_id: str, lead_name: str):
"""Update a task row to show it is actively being worked."""
if task_id not in rows:
return
color = _agent_color(lead_name)
progress.update(
rows[task_id],
description=(
f"[task_id]{task_id}[/task_id] "
f"[{color}]{lead_name.capitalize()}[/{color}] "
f"[yellow]working…[/yellow]"
),
)
def mark_task_done(progress, rows: dict, task_id: str, lead_name: str, error: bool = False):
"""Update a task row to show it is finished (complete or error)."""
if task_id not in rows:
return
color = _agent_color(lead_name)
status = "[red]error[/red]" if error else "[green]done[/green]"
progress.update(
rows[task_id],
completed=1,
description=(
f"[task_id]{task_id}[/task_id] "
f"[{color}]{lead_name.capitalize()}[/{color}] "
f"{status}"
),
)
def print_direct_response_notice():
"""Shown when Miranda handles a directive without dispatching leads."""
console.print(
" [dim]Miranda responded directly — no lead dispatch required.[/dim]"
)
# ---------------------------------------------------------------------------
# Agent output panels
# ---------------------------------------------------------------------------
def print_agent_panel(agent_name: str, content: str, task_id: str = ""):
"""
Render an agent's output in a styled panel.
Content is treated as Markdown.
Skips printing if stream_agent_output already rendered this response.
"""
global _last_stream_rendered
if _last_stream_rendered:
_last_stream_rendered = False
return
color = _agent_color(agent_name)
title_text = _agent_display_title(agent_name)
subtitle = f"[dim]{task_id}[/dim]" if task_id else ""
panel = Panel(
Markdown(content),
title=f"[{color}] {title_text} [/{color}]",
subtitle=subtitle,
border_style=color,
padding=(1, 2),
box=box.ROUNDED,
)
console.print()
console.print(panel)
def print_deliverable(content: str, task_id: str = ""):
"""
Orchestrator's synthesized deliverable.
Skips printing if stream_agent_output already rendered this response.
"""
global _last_stream_rendered
if _last_stream_rendered:
_last_stream_rendered = False
return
_print_deliverable_panel(content, task_id)
def _print_deliverable_panel(content: str, task_id: str = ""):
"""Internal: always render the deliverable panel unconditionally."""
agent_name = _config.ORCHESTRATOR_AGENT
color = _agent_color(agent_name)
title_text = _agent_display_title(agent_name)
subtitle = f"[dim]{task_id}[/dim]" if task_id else ""
panel = Panel(
Markdown(content),
title=f"[{color}] {title_text} [/{color}]",
subtitle=subtitle,
border_style=color,
padding=(1, 2),
box=box.ROUNDED,
)
console.print()
console.print(panel)
def print_audit_memo(content: str, task_id: str = ""):
"""
Auditor's memo. Verdict line drives the border colour.
PASS = green, PASS WITH NOTES = yellow, FLAG = yellow, REJECT = red.
Skips printing if stream_agent_output already rendered this response.
"""
global _last_stream_rendered
if _last_stream_rendered:
_last_stream_rendered = False
return
_print_audit_memo_panel(content, task_id)
def _print_audit_memo_panel(content: str, task_id: str = ""):
"""Internal: always render the audit memo panel with verdict-based border."""
aud_name = _config.AUDITOR_AGENT
aud_title = _config.agent_title(aud_name)
aud_color = _agent_color(aud_name)
verdict_style = "dim"
for line in content.splitlines():
upper = line.upper()
if "VERDICT:" in upper:
if "REJECT" in upper:
verdict_style = "reject"
elif "FLAG" in upper:
verdict_style = "flag"
elif "PASS WITH NOTES" in upper:
verdict_style = "warning"
elif "PASS" in upper:
verdict_style = "pass"
break
panel = Panel(
Markdown(content),
title=f"[{aud_color}] {aud_name.capitalize()}{aud_title} [/{aud_color}]",
subtitle=f"[dim]{task_id}[/dim]" if task_id else "",
border_style=verdict_style if verdict_style != "dim" else aud_color,
padding=(1, 2),
box=box.ROUNDED,
)
console.print()
console.print(panel)
def print_lead_output(agent_name: str, content: str, task_id: str = ""):
"""A lead's raw output (shown only in DEBUG mode)."""
print_agent_panel(agent_name, content, task_id)
# ---------------------------------------------------------------------------
# Standing Brief renderer
# ---------------------------------------------------------------------------
def print_standing_brief(raw_md: str):
"""
Render the standing brief as a navigable terminal layout.
Sections are separated by rules; tables are preserved where present.
"""
console.print()
console.rule("[bold cyan]Standing Brief[/bold cyan]", style="cyan")
console.print()
console.print(Markdown(raw_md))
console.print()
console.rule(style="dim")
console.print()
# ---------------------------------------------------------------------------
# Synthesis progress (Miranda)
# ---------------------------------------------------------------------------
@contextmanager
def synthesis_progress():
"""Spinner shown while Miranda synthesizes lead outputs."""
with Progress(
SpinnerColumn(style="cyan"),
TextColumn("[cyan]Miranda synthesizing…[/cyan]"),
TimeElapsedColumn(),
console=console,
transient=True,
) as progress:
progress.add_task("", total=None)
yield
@contextmanager
def audit_progress():
"""Spinner shown while Vera audits."""
with Progress(
SpinnerColumn(style="yellow"),
TextColumn("[yellow]Vera auditing…[/yellow]"),
TimeElapsedColumn(),
console=console,
transient=True,
) as progress:
progress.add_task("", total=None)
yield
@contextmanager
def brief_update_progress():
"""Spinner shown while Miranda updates the standing brief."""
with Progress(
SpinnerColumn(style="dim"),
TextColumn("[dim]Updating standing brief…[/dim]"),
console=console,
transient=True,
) as progress:
progress.add_task("", total=None)
yield
# ---------------------------------------------------------------------------
# Streaming output with thinking support
# ---------------------------------------------------------------------------
def _thinking_panel_title(agent_name: str, mode: str) -> str:
"""Return the thinking panel title, tinted by agent identity."""
if mode == "audit":
color = _agent_color(agent_name)
return f"[{color}]Vera thinking…[/{color}]"
color = _agent_color(agent_name)
return f"[{color} dim]Thinking…[/{color} dim]"
def _build_thinking_renderable(
thinking_text: str,
active: bool,
agent_name: str,
mode: str,
window: int = 300,
) -> object:
"""
Build the thinking panel (active) or collapsed summary line (done).
Active: dim panel with a rolling window of the last `window` chars,
plus an overflow indicator when earlier content has scrolled off.
Collapsed: single dim line — ' ▸ Thinking (N chars, L lines)'.
"""
if not thinking_text:
return None
if active:
overflow = len(thinking_text) - window
if overflow > 0:
preview = thinking_text[-window:]
header = Text(f" +{overflow} chars earlier\n", style="dim italic")
body = Text(preview, style="dim")
content = Group(header, body)
else:
content = Text(thinking_text, style="dim")
return Panel(
content,
title=_thinking_panel_title(agent_name, mode),
border_style="dim",
padding=(0, 1),
box=box.SIMPLE,
)
else:
lines = thinking_text.count("\n") + 1
return Text(
f" ▸ Thinking [{len(thinking_text):,} chars · {lines} lines]",
style="dim",
)
async def stream_agent_output(
agent_name: str,
stream_gen,
mode: str = "deliverable",
) -> str:
"""
Consume an async generator of ('thinking'|'content', text) chunks and
render them live in the terminal.
mode values
-----------
"deliverable" Normal agent response — thinking panel + full styled panel.
The Live display is NOT transient: the final Markdown panel
is rendered inside the Live block, so there is no erase/
reprint flash. Sets _last_stream_rendered = True so that
the caller's print_deliverable / print_agent_panel becomes
a no-op.
"audit" Like "deliverable" but uses Vera's yellow identity cues
during the thinking phase. Sets _last_stream_rendered so
print_audit_memo is a no-op (the verdict-border panel is
rendered here instead).
"background" Housekeeping call (e.g. standing-brief update). A compact
transient progress line is shown; no full panel is printed.
_last_stream_rendered is set so the caller's print_* is a
no-op.
In all modes the full content string is returned and _last_thinking is
updated so /thinking works.
Thinking tokens stream inside a dim panel. When the first content token
arrives the panel collapses to a single dim summary line and content begins
streaming in the agent's normal styled panel.
The full thinking text is stored in _last_thinking and can be retrieved
with get_last_thinking() (exposed to the /thinking command).
Returns the full content string.
"""
global _last_thinking, _last_stream_rendered
color = _agent_color(agent_name)
title_text = _agent_display_title(agent_name)
# ── Background mode: compact transient progress line ────────────────────
if mode == "background":
thinking_parts: list[str] = []
content_parts: list[str] = []
with Progress(
SpinnerColumn(style="dim"),
TextColumn("[dim]Updating standing brief…[/dim]"),
TimeElapsedColumn(),
console=console,
transient=True,
) as prog:
prog.add_task("", total=None)
async for chunk_type, text in stream_gen:
if chunk_type == "thinking":
thinking_parts.append(text)
elif chunk_type == "content":
content_parts.append(text)
_last_thinking = "".join(thinking_parts)
_last_stream_rendered = True
return "".join(content_parts)
# ── Deliverable / audit modes ────────────────────────────────────────────
thinking_parts: list[str] = []
content_parts: list[str] = []
phase = "start" # "start" | "thinking" | "content"
def _build_final_panel(content_text: str) -> Panel:
"""Render the agent's finished output as a polished Markdown panel."""
if mode == "audit":
# Re-derive verdict border from completed content
aud_name = _config.AUDITOR_AGENT
aud_title = _config.agent_title(aud_name)
aud_color = _agent_color(aud_name)
verdict_style = "dim"
for line in content_text.splitlines():
upper = line.upper()
if "VERDICT:" in upper:
if "REJECT" in upper:
verdict_style = "reject"
elif "FLAG" in upper:
verdict_style = "flag"
elif "PASS WITH NOTES" in upper:
verdict_style = "warning"
elif "PASS" in upper:
verdict_style = "pass"
break
return Panel(
Markdown(content_text),
title=f"[{aud_color}] {aud_name.capitalize()}{aud_title} [/{aud_color}]",
border_style=verdict_style if verdict_style != "dim" else aud_color,
padding=(1, 2),
box=box.ROUNDED,
)
else:
return Panel(
Markdown(content_text),
title=f"[{color}] {title_text} [/{color}]",
border_style=color,
padding=(1, 2),
box=box.ROUNDED,
)
def _renderable(final: bool = False):
"""
Build the live renderable for the current stream state.
final=True: replace plain Text content with Markdown and apply the
verdict-aware border so the Live display ends in its final polished
form. This avoids a transient erase + reprint flash.
"""
thinking_text = "".join(thinking_parts)
content_text = "".join(content_parts)
items: list = []
# ── Agent identity line (shown while only thinking, not yet content) ──
if phase in ("start", "thinking") and not content_text:
items.append(Text(
f" {title_text}",
style=f"dim {color}",
))
# ── Thinking panel / collapsed summary ──────────────────────────────
thinking_active = (phase == "thinking")
think_renderable = _build_thinking_renderable(
thinking_text, thinking_active, agent_name, mode
)
if think_renderable is not None:
items.append(think_renderable)
# ── Content panel ────────────────────────────────────────────────────
if content_text or phase == "content":
if final and content_text:
items.append(_build_final_panel(content_text))
else:
items.append(Panel(
Text(content_text),
title=f"[{color}] {title_text} [/{color}]",
border_style=color,
padding=(1, 2),
box=box.ROUNDED,
))
if not items:
# Nothing yet — show a faint connecting indicator
return Text(
f" [{_agent_color(agent_name)} dim]{title_text} …[/{_agent_color(agent_name)} dim]",
style="dim",
)
return Group(*items) if len(items) > 1 else items[0]
console.print()
# transient=False: the Live display persists — we render the final panel
# inside it before stopping, so there is no erase/reprint cycle.
with Live(
_renderable(),
console=console,
refresh_per_second=15,
transient=False,
vertical_overflow="visible",
) as live:
async for chunk_type, text in stream_gen:
if chunk_type == "thinking":
if phase == "start":
phase = "thinking"
thinking_parts.append(text)
elif chunk_type == "content":
if phase in ("start", "thinking"):
phase = "content"
content_parts.append(text)
live.update(_renderable())
# Final update: replace plain Text with formatted Markdown panel
# (and apply verdict border for audit mode) before the Live context
# exits and the display freezes in place.
live.update(_renderable(final=True))
_last_thinking = "".join(thinking_parts)
_last_stream_rendered = True
return "".join(content_parts)
def get_last_thinking() -> str:
"""Return the thinking text captured from the most recent streamed response."""
return _last_thinking
def print_thinking_expansion(thinking_text: str):
"""Render the stored thinking output in a dim panel."""
panel = Panel(
Text(thinking_text, style="dim"),
title="[dim] Thinking — last response [/dim]",
border_style="dim",
padding=(1, 2),
box=box.ROUNDED,
)
console.print()
console.print(panel)
# ---------------------------------------------------------------------------
# Dry-run callout
# ---------------------------------------------------------------------------
def print_dry_run_banner():
panel = Panel(
"[bold yellow]DRY RUN MODE[/bold yellow]\n"
"[dim]Routing and pipeline logic will execute normally.\n"
"No provider API calls will be made.[/dim]",
border_style="yellow",
box=box.ROUNDED,
padding=(0, 2),
)
console.print()
console.print(panel)
console.print()
# ---------------------------------------------------------------------------
# Slash command displays
# ---------------------------------------------------------------------------
def print_help(commands: list[tuple[str, str]]):
"""Render the slash command reference table."""
console.print()
console.rule("[bold cyan]Commands[/bold cyan]", style="cyan")
console.print()
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2), expand=False)
table.add_column(style="bold cyan", no_wrap=True)
table.add_column(style="white")
for cmd, description in commands:
table.add_row(cmd, description)
console.print(table)
console.print()
def print_status(
session_start: datetime,
directive_count: int,
session_log: list[str],
auto_audit: bool,
debug: bool,
dry_run: bool,
):
"""Render current session status."""
console.print()
console.rule("[bold cyan]Session Status[/bold cyan]", style="cyan")
console.print()
elapsed = datetime.now() - session_start
hours, remainder = divmod(int(elapsed.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
elapsed_str = f"{hours}h {minutes}m {seconds}s" if hours else f"{minutes}m {seconds}s"
global_provider = _config.ACTIVE_PROVIDER
provider_display = (
global_provider if global_provider != "none"
else "[dim]none — set default_provider in config/agents.yaml[/dim]"
)
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
table.add_column(style="dim", no_wrap=True)
table.add_column(style="white")
table.add_row("Session started", session_start.strftime("%Y-%m-%d %H:%M:%S"))
table.add_row("Elapsed", elapsed_str)
table.add_row("Directives", str(directive_count))
table.add_row("Global provider", provider_display)
table.add_row("Auto-audit", "[green]on[/green]" if auto_audit else "[dim]off[/dim]")
table.add_row("Debug output", "[yellow]on[/yellow]" if debug else "[dim]off[/dim]")
table.add_row("Dry run", "[yellow]on[/yellow]" if dry_run else "[dim]off[/dim]")
console.print(table)
if session_log:
console.print()
console.rule("[dim]Session log[/dim]", style="dim")
console.print()
for entry in session_log:
console.print(f" [dim]·[/dim] [muted]{entry}[/muted]")
console.print()
def print_agents(agent_configs: dict, active_provider: str):
"""Render the named agent roster with per-agent provider resolution."""
console.print()
console.rule("[bold cyan]Agent Roster[/bold cyan]", style="cyan")
console.print()
table = Table(box=box.SIMPLE, show_header=True, padding=(0, 2), expand=False)
table.add_column("Agent", style="bold white", no_wrap=True)
table.add_column("Role", style="dim", no_wrap=True)
table.add_column("Provider", style="white", no_wrap=True)
table.add_column("Model", style="cyan")
table.add_column("Temp", style="dim", justify="right")
table.add_column("Tokens", style="dim", justify="right")
for name, cfg in agent_configs.items():
color = _agent_color(name)
label = _config.agent_title(name)
provider = _config.agent_provider_name(name)
model = _config.agent_model(name) or "[dim]unset[/dim]"
provider_display = (
f"[dim]{provider}[/dim]" if provider == "none"
else provider
)
table.add_row(
f"[{color}]{name.capitalize()}[/{color}]",
label,
provider_display,
model,
str(cfg["temperature"]),
str(cfg["max_tokens"]),
)
console.print(table)
if active_provider == "none":
console.print(" [dim]Global fallback: none — set default_provider in config/agents.yaml[/dim]")
else:
console.print(f" [dim]Global fallback provider:[/dim] [white]{active_provider}[/white]")
console.print()
def print_history(session_log: list[str]):
"""Render the session log for this session."""
console.print()
console.rule("[bold cyan]Session History[/bold cyan]", style="cyan")
console.print()
if not session_log:
console.print(" [dim]No directives processed yet.[/dim]")
else:
for i, entry in enumerate(session_log, 1):
console.print(f" [dim]{i:>2}.[/dim] [muted]{entry}[/muted]")
console.print()
def print_tasks(tasks: list, session_id: str = ""):
"""Render the task roster for the current session."""
console.print()
title = f"[bold cyan]Tasks[/bold cyan]"
if session_id:
title += f" [dim]{session_id}[/dim]"
console.rule(title, style="cyan")
console.print()
if not tasks:
console.print(" [dim]No tasks dispatched this session.[/dim]")
console.print()
return
STATUS_STYLE = {
"pending": "[dim]pending[/dim]",
"in_progress": "[yellow]in progress[/yellow]",
"complete": "[green]complete[/green]",
"error": "[red]error[/red]",
}
table = Table(box=box.SIMPLE, show_header=True, padding=(0, 2), expand=False)
table.add_column("Task", style="task_id", no_wrap=True)
table.add_column("Lead", no_wrap=True)
table.add_column("Status", no_wrap=True)
table.add_column("Brief", style="dim")
for t in tasks:
color = _agent_color(t.assigned_to)
status = STATUS_STYLE.get(t.status, t.status)
table.add_row(
t.task_id,
f"[{color}]{t.assigned_to.capitalize()}[/{color}]",
status,
_brief_preview(t.brief, max_len=60),
)
console.print(table)
console.print()
def print_debug_toggle(enabled: bool):
state = "[yellow]ON[/yellow]" if enabled else "[dim]OFF[/dim]"
console.print(f" [dim]Debug output:[/dim] {state}")
def print_audit_toggle(enabled: bool):
state = "[green]ON[/green]" if enabled else "[dim]OFF[/dim]"
console.print(f" [dim]Auto-audit (Vera):[/dim] {state}")
# ---------------------------------------------------------------------------
# Tool-call display
# ---------------------------------------------------------------------------
def print_tool_call(agent_name: str, tool_name: str, arguments_json: str, result_json: str):
"""
Render a single tool invocation inline during an agent's tool-calling loop.
Layout:
⚙ AgentName tool_name
arguments (pretty-printed JSON, dim)
← result (pretty-printed JSON; red on parse error)
"""
color = _agent_color(agent_name)
# Header line
console.print(
f" [dim]⚙[/dim] [{color}]{agent_name.capitalize()}[/{color}]"
f" [dim][/dim] [bold]{tool_name}[/bold]"
)
# Arguments
try:
args_pretty = _json.dumps(_json.loads(arguments_json), indent=2, ensure_ascii=False)
for line in args_pretty.splitlines():
console.print(f" [dim]{line}[/dim]")
except Exception:
console.print(f" [dim]{arguments_json}[/dim]")
# Result
try:
result_obj = _json.loads(result_json)
result_pretty = _json.dumps(result_obj, indent=2, ensure_ascii=False)
has_error = isinstance(result_obj, dict) and "error" in result_obj
style = "red" if has_error else "dim"
console.print(f" [dim]←[/dim]")
for line in result_pretty.splitlines():
console.print(f" [{style}]{line}[/{style}]")
except Exception:
console.print(f" [dim]← {result_json}[/dim]")