Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions examples/file-reader/file-reader.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ tasks:
prompts:
system: >
You are a helpful assistant. When asked to summarise a file, use the
read_file tool to fetch its contents first, then write a clear, concise
summary. Respond with valid JSON matching the output schema.
read_file tool to fetch its contents first, then respond with valid JSON
containing exactly two fields:
"summary" (a concise one-paragraph summary of the file) and
"key_points" (an array of short strings, one per key takeaway).
Return only the JSON object — no markdown fencing, no extra text.
user: "Please summarise the file at path: {path}"
input:
type: object
Expand Down
22 changes: 14 additions & 8 deletions oas_cli/banner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
ASCII_TITLE = r"""
_______ _______ _______ ______ _______ _______ _______ ______ _______ _______ _______ _______ _______
| _ | _ | _ | _ \ | _ | _ | _ | _ \| | | _ | _ | _ | _ |
|. | |. 1 |. 1___|. | | |. 1 |. |___|. 1___|. | |.| | | | 1___|. 1 |. 1___|. 1___|
|. | |. ____|. __)_|. | | |. _ |. | |. __)_|. | `-|. |-' |____ |. ____|. __)_|. |___
|: 1 |: | |: 1 |: | | |: | |: 1 |: 1 |: | | |: | |: 1 |: | |: 1 |: 1 |
|::.. . |::.| |::.. . |::.| | |::.|:. |::.. . |::.. . |::.| | |::.| |::.. . |::.| |::.. . |::.. . |
`-------`---' `-------`--- ---' `--- ---`-------`-------`--- ---' `---' `-------`---' `-------`-------'
"""OA CLI banner and branding assets."""

# Compact bot-face banner. Renders cleanly at 80+ columns in any modern terminal.
# The ◈ glyph is the OA "agent node" icon used consistently across CLI surfaces.

BANNER = r"""
╔═══╗
║◈ ◈║ Open Agent Spec
╚═╤═╝ Agents as code.
"""

# Keep the old name so existing callers in main.py still work until
# they are migrated to the new ui module.
ASCII_TITLE = BANNER
112 changes: 60 additions & 52 deletions oas_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
import sys
import tempfile
import time
from importlib.metadata import version as _get_version
from pathlib import Path
from typing import Any
Expand All @@ -14,12 +15,19 @@
from rich.logging import RichHandler
from rich.panel import Panel

from .banner import ASCII_TITLE
from .core import generate_files as core_generate_files
from .core import validate_spec_file
from .exceptions import AgentGenerationError
from .runner import OARunError, _choose_task, _load_spec, run_task_from_file
from .spec_test import SpecTestError, run_cases_from_file
from .ui import (
inference_spinner,
print_banner,
print_error_panel,
print_help_panel,
print_result_panel,
print_run_header,
)

app = typer.Typer(help="Open Agent (OA) CLI")
console = Console()
Expand Down Expand Up @@ -48,26 +56,13 @@ def main(
version: bool = typer.Option(False, "--version", help="Show version and exit"),
):
"""Main CLI entry point."""
cli_version = get_version_from_pyproject()
if version:
cli_version = get_version_from_pyproject()
console.print(
f"[bold cyan]Open Agent Spec CLI[/] version [green]{cli_version}[/]"
)
print_banner(console, cli_version)
raise typer.Exit()

if ctx.invoked_subcommand is None or ctx.invoked_subcommand == "help":
console.print(f"[bold cyan]{ASCII_TITLE}[/]\n")
console.print(
Panel.fit(
"Use [bold magenta]oa init aac[/] for .agents/ agent-as-code layout\n"
"Use [bold magenta]oa init --spec … --output …[/] to scaffold code\n"
"Use [bold magenta]oa update[/] to update existing agent code\n"
"Define it via Open Agent Spec YAML\n"
"Use [bold yellow]--dry-run[/] to preview without writing files.",
title="[bold green]OA CLI[/]",
subtitle="Open Agent Spec Generator",
)
)
print_help_panel(console, cli_version)


def load_and_validate_spec(
Expand Down Expand Up @@ -134,13 +129,7 @@ def _run_init_code_gen(
if verbose:
log.setLevel(logging.DEBUG)

console.print(
Panel(
ASCII_TITLE,
title="[bold cyan]OA CLI[/]",
subtitle="[green]Open Agent Spec Generator[/]",
)
)
print_banner(console, get_version_from_pyproject())

spec_path, temp_file_to_delete = resolve_spec_path(spec, template, log)
try:
Expand Down Expand Up @@ -521,13 +510,7 @@ def update(
)
raise typer.Exit(2)

console.print(
Panel(
ASCII_TITLE,
title="[bold cyan]OA CLI[/]",
subtitle="[green]Open Agent Spec Updater[/]",
)
)
print_banner(console, get_version_from_pyproject())

# Narrow types after validation above.
spec = spec
Expand Down Expand Up @@ -622,13 +605,8 @@ def run(
logging.getLogger("oas").setLevel(logging.WARNING)

if not quiet:
console.print(
Panel(
ASCII_TITLE,
title="[bold cyan]OA CLI[/]",
subtitle="[green]Open Agent Spec Runner[/]",
)
)
cli_version = get_version_from_pyproject()
print_banner(console, cli_version)

try:
input_data: dict[str, Any] | None = None
Expand Down Expand Up @@ -688,35 +666,66 @@ def run(
raise typer.BadParameter("--input JSON must be an object")
input_data = parsed

# Load spec once for the header + spinner labels (reused by the runner).
spec_data_preview = _load_spec(spec)
intel = spec_data_preview.get("intelligence") or {}
_model = intel.get("model") or "model"
# Resolve the actual task name (handles auto-selection when --task omitted)
_task_label, _ = _choose_task(spec_data_preview, task)

# Show run header (agent name, task, model) before calling the model.
if not quiet:
print_run_header(console, spec_data_preview, _task_label)

# When --quiet, redirect stdout to stderr during the run so logs from
# our code or deps (dacp, httpx) don't pollute the pipe to jq.
if quiet:
real_stdout = sys.stdout
sys.stdout = sys.stderr

t0 = time.monotonic()
try:
result = run_task_from_file(
spec,
task_name=task,
input_data=input_data,
override_system=system_prompt,
override_user=user_prompt,
)
if quiet:
result = run_task_from_file(
spec,
task_name=task,
input_data=input_data,
override_system=system_prompt,
override_user=user_prompt,
)
else:
with inference_spinner(console, _model, _task_label):
result = run_task_from_file(
spec,
task_name=task,
input_data=input_data,
override_system=system_prompt,
override_user=user_prompt,
)
finally:
if quiet:
sys.stdout = real_stdout

elapsed = time.monotonic() - t0

if quiet:
# Print only the agent's output (e.g. decision + summary) for clean piping
# Print only the agent's output for clean piping.
# dict/list → pretty JSON; plain string → written directly so no
# \n escaping or extra quotes corrupt the output.
out = result.get("output", result)
typer.echo(json.dumps(out, indent=2))
if isinstance(out, (dict, list)):
typer.echo(json.dumps(out, indent=2))
else:
typer.echo(str(out) if out is not None else "")
else:
console.print_json(data=result)
print_result_panel(console, result, elapsed_s=elapsed)

except OARunError as err:
if quiet:
# Machine-readable structured error on stderr — stdout stays clean.
typer.echo(json.dumps(err.to_dict(), indent=2), err=True)
else:
log.error(str(err))
typer.echo(str(err), err=True)
print_error_panel(console, "Run error", str(err))
raise typer.Exit(1)
except Exception as err:
if quiet:
Expand All @@ -727,8 +736,7 @@ def run(
err=True,
)
else:
log.error(str(err))
typer.echo(str(err), err=True)
print_error_panel(console, "Unexpected error", str(err))
raise typer.Exit(1)


Expand Down
Loading
Loading