Agent Sandbox Registry

agent-sandbox

Decision: Separate CLI result output from progress and log channels

无中文版

.agents/notes/implemented/architecture/2026-09-29-cli-output-channel-separation.md

Decision: Separate CLI result output from progress and log channels

Status: implemented Implemented: 2026-09-29 Related: 2026-09-29-init-create-semantics.md, 2026-09-29-nl-create-qwen-code-integration.md

Problem

Executing abx create (both the --template path and the Qwen Code NL path) and the shared deploy pipeline produced output that could not be consumed reliably:

  1. Duplicate warning. The capability-resolver fallback warning rendered twice: once by the SDK's standalone handler (agent_sandbox.utils.logging, timestamped format, agent_sandbox package logger) and once by the CLI's root handler (WARNING: ...). Two handlers rendered the same record.
  2. Garbled live output. The Rich spinner renders on stderr; warnings and log lines written directly to stderr interleaved with the live display into unreadable text such as ⠋ Waiting...Warning: ... and ⠋ Waiting...DEBUG: https://....
  3. Machine-readable pollution. In --json mode every message (info / progress / warning / debug / error) was emitted as JSON on stdout, so abx create --json | jq received a sequence of documents instead of one. Raw click.echo calls bypassed --quiet / --ci entirely.
  4. Bypassed manager. _do_deploy warned with a bare click.echo(..., err=True) that ignored quiet/CI suppression and the spinner guard; on_output=click.echo in verbose mode wrote docker build logs to stdout, the result channel.
  5. Inconsistent result printing. abx create printed its result through the legacy OutputFormatter (fmt.print_dict / fmt.print_success) while the rest of its progress went through OutputManager.

Decision

Channel policy

Channel Content Consumers
stdout final results: data, table, success humans and scripts (abx ... --json \| jq)
stderr progress state and diagnostics: info, progress, warning, error, debug, plus every stdlib logging record humans following a long-running command

One logging bridge

Spinner pause / resume

_spinner_guard wraps every write (click.echo, JSON, Rich table): the live Rich Status objects pushed by spinner() / live_spinner() are stopped before a write and restarted afterwards. Spinners already render on stderr and stay suppressed in verbose/quiet/JSON/non-TTY sessions.

Log-level precedence during a CLI run

The CLI flags (--verbose / --quiet / --log-level plus CI auto-detection) own the log level: the manager sets the root level and the bridge level. It deliberately does not touch the agent_sandbox package logger level, which keeps honouring SANDBOX_LOG_LEVEL for standalone SDK use.

Deploy/create paths go through the manager

Task 160 invariants are untouched: the top-level abx init stays deleted; the abx config init / abx template init / abx create boundaries are unchanged; the DESCRIPTION + --template conflict is still rejected before any config read, AI generation, or network call, and the new test pins it (stdout == "", codegen and Sandbox.create never called).

API Design

No public library API changes; the CLI output surface changes:

# Before (--json):  stdout = {"level":"info"...}
#                            {"level":"progress"...}
#                            {"ID":...}
#                            {"status":"success"...}
# After  (--json):  stdout = {"ID":...}                       # single document
#                   stderr = {"level":"info"...} {"level":"progress"...} {"level":"warning"...}

# Before: capability fallback warning rendered twice
#         2026-09-29 10:00:00 [WARNING] agent_sandbox.api.capability: Could not resolve ...
#         Warning: Could not resolve ...
# After:  exactly one "WARNING: Could not resolve capabilities ..." line on stderr

# Before: out.warning(...) in quiet mode  -> suppressed (unchanged)
#         raw click.echo(..., err=True)   -> always printed, bypassing quiet/CI
# After:  raw call replaced by out.warning(...) on the shared path

OutputManager method → channel mapping (unchanged signatures):

Method Channel quiet JSON
info / progress / warning / debug stderr suppressed (debug: verbose only) JSON object → stderr
error stderr shown {"status":"error",...} → stderr
success stdout suppressed {"status":"success",...} → stdout
data stdout values only single JSON document
table stdout tab-separated list of dicts

Alternatives considered

Dependencies

Test Strategy

Acceptance criteria

Files changed

Consequences