|
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import sys
|
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Annotated, Any, cast
|
|
|
|
@@ -42,6 +43,24 @@ from cleveragents.domain.models.core.session import (
|
|
|
|
|
app = typer.Typer(help="Manage interactive sessions.")
|
|
|
|
|
console = Console()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_panel_console(fmt: str) -> Console:
|
|
|
|
|
"""Get a Console instance for rendering panels.
|
|
|
|
|
|
|
|
|
|
When JSON format is requested, panels are redirected to stderr to keep
|
|
|
|
|
stdout clean for JSON output. For other formats, panels go to stdout.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
fmt: Output format string (json, yaml, plain, table, rich, etc.)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Console instance configured for the appropriate output stream.
|
|
|
|
|
"""
|
|
|
|
|
if fmt in (OutputFormat.JSON.value, "json"):
|
|
|
|
|
# Redirect Rich panels to stderr for JSON format
|
|
|
|
|
return Console(file=sys.stderr)
|
|
|
|
|
return console
|
|
|
|
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Reusable --format option description
|
|
|
|
@@ -186,6 +205,7 @@ def create(
|
|
|
|
|
agents session create --actor openai/gpt-4
|
|
|
|
|
agents session create --format json
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
try:
|
|
|
|
|
# Create session through the service, then notify the A2A
|
|
|
|
|
# facade for protocol compliance and telemetry.
|
|
|
|
@@ -215,7 +235,7 @@ def create(
|
|
|
|
|
f"[bold]Namespace:[/bold] {session.namespace}\n"
|
|
|
|
|
f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(details, title="Session", expand=False))
|
|
|
|
|
panel_console.print(Panel(details, title="Session", expand=False))
|
|
|
|
|
|
|
|
|
|
# Settings panel
|
|
|
|
|
settings_text = (
|
|
|
|
@@ -225,7 +245,7 @@ def create(
|
|
|
|
|
"[yellow]Memory:[/yellow] enabled\n"
|
|
|
|
|
"[yellow]Max History:[/yellow] 50 turns"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(settings_text, title="Settings", expand=False))
|
|
|
|
|
panel_console.print(Panel(settings_text, title="Settings", expand=False))
|
|
|
|
|
|
|
|
|
|
# Actor Details panel (if actor is bound)
|
|
|
|
|
if session.actor_name:
|
|
|
|
@@ -242,14 +262,16 @@ def create(
|
|
|
|
|
f"{getattr(actor_obj, 'temperature', 0.7)}\n"
|
|
|
|
|
"[blue]Context Window:[/blue] 200K tokens"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(actor_details, title="Actor Details", expand=False))
|
|
|
|
|
panel_console.print(
|
|
|
|
|
Panel(actor_details, title="Actor Details", expand=False)
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Actor details unavailable
|
|
|
|
|
|
|
|
|
|
console.print("[green]✓ OK[/green] Session created")
|
|
|
|
|
panel_console.print("[green]✓ OK[/green] Session created")
|
|
|
|
|
|
|
|
|
|
except SessionNotFoundError as exc:
|
|
|
|
|
console.print(f"[red]Error:[/red] {exc}")
|
|
|
|
|
panel_console.print(f"[red]Error:[/red] {exc}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session create failed", exc_info=True)
|
|
|
|
@@ -276,6 +298,7 @@ def list_sessions(
|
|
|
|
|
agents session list --format json
|
|
|
|
|
agents session list --format table
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
try:
|
|
|
|
|
service = _get_session_service()
|
|
|
|
|
sessions = service.list()
|
|
|
|
@@ -293,7 +316,7 @@ def list_sessions(
|
|
|
|
|
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
|
|
|
|
typer.echo(format_output({"sessions": [], "total": 0}, fmt))
|
|
|
|
|
return
|
|
|
|
|
console.print("[yellow]No sessions found.[/yellow]")
|
|
|
|
|
panel_console.print("[yellow]No sessions found.[/yellow]")
|
|
|
|
|
console.print("Create one with 'agents session create'")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
@@ -336,9 +359,9 @@ def list_sessions(
|
|
|
|
|
summary_table.add_row("Total Messages:", str(summary["total_messages"]))
|
|
|
|
|
summary_table.add_row("Storage:", summary["storage"])
|
|
|
|
|
|
|
|
|
|
console.print(Panel(summary_table, title="Summary", border_style="blue"))
|
|
|
|
|
panel_console.print(Panel(summary_table, title="Summary", border_style="blue"))
|
|
|
|
|
console.print()
|
|
|
|
|
console.print(f"[green]✓ OK[/green] {len(sessions)} sessions listed")
|
|
|
|
|
panel_console.print(f"[green]✓ OK[/green] {len(sessions)} sessions listed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
@@ -360,6 +383,7 @@ def show(
|
|
|
|
|
agents session show 01HXYZ...
|
|
|
|
|
agents session show 01HXYZ... --format json
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
try:
|
|
|
|
|
service = _get_session_service()
|
|
|
|
|
session = service.get(session_id)
|
|
|
|
@@ -379,7 +403,7 @@ def show(
|
|
|
|
|
f"[bold]Updated:[/bold] {session.updated_at.strftime('%Y-%m-%d %H:%M')}\n"
|
|
|
|
|
f"[bold]Automation:[/bold] {session.automation or '(none)'}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(details, title="Session Summary", expand=False))
|
|
|
|
|
panel_console.print(Panel(details, title="Session Summary", expand=False))
|
|
|
|
|
|
|
|
|
|
# Recent messages
|
|
|
|
|
if session.messages:
|
|
|
|
@@ -402,11 +426,11 @@ def show(
|
|
|
|
|
plan_table.add_column("State")
|
|
|
|
|
for lp in session.linked_plans:
|
|
|
|
|
plan_table.add_row(lp.plan_id, lp.phase, lp.state)
|
|
|
|
|
console.print(Panel(plan_table, title="Linked Plans", expand=False))
|
|
|
|
|
panel_console.print(Panel(plan_table, title="Linked Plans", expand=False))
|
|
|
|
|
elif session.linked_plan_ids:
|
|
|
|
|
# Fallback: only flat IDs available
|
|
|
|
|
plan_text = "\n".join(f" • {pid}" for pid in session.linked_plan_ids)
|
|
|
|
|
console.print(Panel(plan_text, title="Linked Plans", expand=False))
|
|
|
|
|
panel_console.print(Panel(plan_text, title="Linked Plans", expand=False))
|
|
|
|
|
|
|
|
|
|
# Token usage
|
|
|
|
|
tu = session.token_usage
|
|
|
|
@@ -415,7 +439,7 @@ def show(
|
|
|
|
|
f"[blue]Output Tokens:[/blue] {tu.output_tokens:,}\n"
|
|
|
|
|
f"[yellow]Estimated Cost:[/yellow] ${tu.estimated_cost:.4f}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(usage_text, title="Token Usage", expand=False))
|
|
|
|
|
panel_console.print(Panel(usage_text, title="Token Usage", expand=False))
|
|
|
|
|
|
|
|
|
|
# Cost budget (per-session budget cap, #584)
|
|
|
|
|
if session.cost_budget is not None:
|
|
|
|
@@ -437,12 +461,12 @@ def show(
|
|
|
|
|
f"[yellow]Utilization:[/yellow] {util_display}\n"
|
|
|
|
|
f"[green]Remaining:[/green] {remaining_display}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(budget_text, title="Cost Budget", expand=False))
|
|
|
|
|
panel_console.print(Panel(budget_text, title="Cost Budget", expand=False))
|
|
|
|
|
|
|
|
|
|
console.print("[green bold]✓ OK[/green bold] Session details loaded")
|
|
|
|
|
|
|
|
|
|
except SessionNotFoundError as exc:
|
|
|
|
|
console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
panel_console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session show failed", exc_info=True)
|
|
|
|
@@ -476,6 +500,7 @@ def delete(
|
|
|
|
|
agents session delete 01HXYZ...
|
|
|
|
|
agents session delete 01HXYZ... --yes
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt.value)
|
|
|
|
|
try:
|
|
|
|
|
service = _get_session_service()
|
|
|
|
|
|
|
|
|
@@ -486,7 +511,7 @@ def delete(
|
|
|
|
|
if not yes:
|
|
|
|
|
confirm = typer.confirm(f"Delete session {session_id}?", default=False)
|
|
|
|
|
if not confirm:
|
|
|
|
|
console.print("[yellow]Aborted.[/yellow]")
|
|
|
|
|
panel_console.print("[yellow]Aborted.[/yellow]")
|
|
|
|
|
raise typer.Abort()
|
|
|
|
|
|
|
|
|
|
service.delete(session_id)
|
|
|
|
@@ -517,15 +542,17 @@ def delete(
|
|
|
|
|
cleanup_table.add_row("Context:", "cleared")
|
|
|
|
|
cleanup_table.add_row("Checkpoints:", "none")
|
|
|
|
|
|
|
|
|
|
console.print(Panel(cleanup_table, title="Cleanup", border_style="blue"))
|
|
|
|
|
panel_console.print(
|
|
|
|
|
Panel(cleanup_table, title="Cleanup", border_style="blue")
|
|
|
|
|
)
|
|
|
|
|
console.print()
|
|
|
|
|
console.print("[green]✓ OK[/green] Session deleted")
|
|
|
|
|
panel_console.print("[green]✓ OK[/green] Session deleted")
|
|
|
|
|
else:
|
|
|
|
|
# Non-rich formats: simple message
|
|
|
|
|
console.print(f"[green]✓ OK[/green] Session {session_id} deleted")
|
|
|
|
|
panel_console.print(f"[green]✓ OK[/green] Session {session_id} deleted")
|
|
|
|
|
|
|
|
|
|
except SessionNotFoundError as exc:
|
|
|
|
|
console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
panel_console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session delete failed", exc_info=True)
|
|
|
|
@@ -575,8 +602,9 @@ def export_session(
|
|
|
|
|
agents session export 01HXYZ... -o session.json --force
|
|
|
|
|
agents session export 01HXYZ... --format md -o session.md
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
if fmt not in ("json", "md"):
|
|
|
|
|
console.print(f"[red]Invalid format:[/red] {fmt!r}. Use 'json' or 'md'.")
|
|
|
|
|
panel_console.print(f"[red]Invalid format:[/red] {fmt!r}. Use 'json' or 'md'.")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
@@ -629,10 +657,10 @@ def export_session(
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except SessionNotFoundError as exc:
|
|
|
|
|
console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
panel_console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except SessionExportError as exc:
|
|
|
|
|
console.print(f"[red]Export error:[/red] {exc}")
|
|
|
|
|
panel_console.print(f"[red]Export error:[/red] {exc}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session export failed", exc_info=True)
|
|
|
|
@@ -660,6 +688,8 @@ def _render_export_panels(
|
|
|
|
|
- "Integrity" panel: checksum, encrypted flag
|
|
|
|
|
- ``✓ OK Export completed`` success line
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
|
|
|
|
|
# Compute file size from content
|
|
|
|
|
size_bytes = len(content.encode("utf-8"))
|
|
|
|
|
if size_bytes < 1024:
|
|
|
|
@@ -693,7 +723,9 @@ def _render_export_panels(
|
|
|
|
|
export_table.add_row("Messages:", str(message_count))
|
|
|
|
|
export_table.add_row("Size:", size_str)
|
|
|
|
|
export_table.add_row("Format:", format_display)
|
|
|
|
|
console.print(Panel(export_table, title="Session Export", border_style="blue"))
|
|
|
|
|
panel_console.print(
|
|
|
|
|
Panel(export_table, title="Session Export", border_style="blue")
|
|
|
|
|
)
|
|
|
|
|
console.print()
|
|
|
|
|
|
|
|
|
|
# Contents panel
|
|
|
|
@@ -705,7 +737,7 @@ def _render_export_panels(
|
|
|
|
|
contents_table.add_row("Metadata Keys:", str(metadata_keys))
|
|
|
|
|
contents_table.add_row("Actor Config:", actor_config)
|
|
|
|
|
contents_table.add_row("Schema Version:", str(schema_version))
|
|
|
|
|
console.print(Panel(contents_table, title="Contents", border_style="blue"))
|
|
|
|
|
panel_console.print(Panel(contents_table, title="Contents", border_style="blue"))
|
|
|
|
|
console.print()
|
|
|
|
|
|
|
|
|
|
# Integrity panel
|
|
|
|
@@ -714,19 +746,23 @@ def _render_export_panels(
|
|
|
|
|
integrity_table.add_column(style="white", justify="left")
|
|
|
|
|
integrity_table.add_row("Checksum:", checksum_display)
|
|
|
|
|
integrity_table.add_row("Encrypted:", "no")
|
|
|
|
|
console.print(Panel(integrity_table, title="Integrity", border_style="blue"))
|
|
|
|
|
panel_console.print(Panel(integrity_table, title="Integrity", border_style="blue"))
|
|
|
|
|
console.print()
|
|
|
|
|
|
|
|
|
|
console.print("[green]✓ OK[/green] Export completed")
|
|
|
|
|
panel_console.print("[green]✓ OK[/green] Export completed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.command("import")
|
|
|
|
|
def import_session(
|
|
|
|
|
input_file: Annotated[
|
|
|
|
|
Path,
|
|
|
|
|
typer.Option("--input", "-i", help="Input JSON file path"),
|
|
|
|
|
],
|
|
|
|
|
) -> None:
|
|
|
|
|
input_file: Annotated[
|
|
|
|
|
Path,
|
|
|
|
|
typer.Option("--input", "-i", help="Input JSON file path"),
|
|
|
|
|
],
|
|
|
|
|
fmt: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
|
|
|
|
] = "rich",
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Import a session from a JSON file.
|
|
|
|
|
|
|
|
|
|
The file must have been produced by ``agents session export`` and must
|
|
|
|
@@ -735,15 +771,16 @@ def import_session(
|
|
|
|
|
Examples:
|
|
|
|
|
agents session import -i session.json
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
if not input_file.exists():
|
|
|
|
|
console.print(f"[red]File not found:[/red] {input_file}")
|
|
|
|
|
panel_console.print(f"[red]File not found:[/red] {input_file}")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
raw = input_file.read_text(encoding="utf-8")
|
|
|
|
|
data = json.loads(raw)
|
|
|
|
|
except json.JSONDecodeError as exc:
|
|
|
|
|
console.print(f"[red]Invalid JSON:[/red] {exc}")
|
|
|
|
|
panel_console.print(f"[red]Invalid JSON:[/red] {exc}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
@@ -759,7 +796,9 @@ def import_session(
|
|
|
|
|
f"[bold]Messages:[/bold] {session.message_count}\n"
|
|
|
|
|
f"[bold]Schema:[/bold] {schema_version}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(session_details, title="Session Import", expand=False))
|
|
|
|
|
panel_console.print(
|
|
|
|
|
Panel(session_details, title="Session Import", expand=False)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Validation panel
|
|
|
|
|
actor_ref_status = "resolved" if actor_name else "none"
|
|
|
|
@@ -768,16 +807,16 @@ def import_session(
|
|
|
|
|
f"[bold]Schema:[/bold] compatible\n"
|
|
|
|
|
f"[bold]Actor Ref:[/bold] {actor_ref_status}"
|
|
|
|
|
)
|
|
|
|
|
console.print(Panel(validation_details, title="Validation", expand=False))
|
|
|
|
|
panel_console.print(Panel(validation_details, title="Validation", expand=False))
|
|
|
|
|
|
|
|
|
|
# Merge panel
|
|
|
|
|
merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new"
|
|
|
|
|
console.print(Panel(merge_details, title="Merge", expand=False))
|
|
|
|
|
panel_console.print(Panel(merge_details, title="Merge", expand=False))
|
|
|
|
|
|
|
|
|
|
console.print("[green]✓ OK[/green] Import completed")
|
|
|
|
|
panel_console.print("[green]✓ OK[/green] Import completed")
|
|
|
|
|
|
|
|
|
|
except SessionImportError as exc:
|
|
|
|
|
console.print(f"[red]Import error:[/red] {exc}")
|
|
|
|
|
panel_console.print(f"[red]Import error:[/red] {exc}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session import failed", exc_info=True)
|
|
|
|
@@ -803,9 +842,13 @@ def tell(
|
|
|
|
|
typer.Option("--actor", help="Override actor for this message"),
|
|
|
|
|
] = None,
|
|
|
|
|
stream: Annotated[
|
|
|
|
|
bool,
|
|
|
|
|
typer.Option("--stream", help="Stream response in real-time"),
|
|
|
|
|
] = False,
|
|
|
|
|
bool,
|
|
|
|
|
typer.Option("--stream", help="Stream response in real-time"),
|
|
|
|
|
] = False,
|
|
|
|
|
fmt: Annotated[
|
|
|
|
|
str,
|
|
|
|
|
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
|
|
|
|
] = "rich",
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Send a message to a session.
|
|
|
|
|
|
|
|
|
@@ -817,6 +860,7 @@ def tell(
|
|
|
|
|
agents session tell --session 01HXYZ... --actor openai/gpt-4 "Plan a feature"
|
|
|
|
|
agents session tell --session 01HXYZ... --stream "Build tests"
|
|
|
|
|
"""
|
|
|
|
|
panel_console = _get_panel_console(fmt)
|
|
|
|
|
try:
|
|
|
|
|
service = _get_session_service()
|
|
|
|
|
|
|
|
|
@@ -850,11 +894,11 @@ def tell(
|
|
|
|
|
else:
|
|
|
|
|
from rich.markup import escape
|
|
|
|
|
|
|
|
|
|
console.print(f"[dim]user:[/dim] {escape(prompt)}")
|
|
|
|
|
console.print(f"[cyan]assistant:[/cyan] {escape(assistant_content)}")
|
|
|
|
|
panel_console.print(f"[dim]user:[/dim] {escape(prompt)}")
|
|
|
|
|
panel_console.print(f"[cyan]assistant:[/cyan] {escape(assistant_content)}")
|
|
|
|
|
|
|
|
|
|
except SessionNotFoundError as exc:
|
|
|
|
|
console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
panel_console.print(f"[red]Session not found:[/red] {session_id}")
|
|
|
|
|
raise typer.Exit(1) from exc
|
|
|
|
|
except DatabaseError as exc:
|
|
|
|
|
_log.debug("session tell failed", exc_info=True)
|
|
|
|
|