chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:45:58 +08:00
commit 5defe661fa
560 changed files with 113225 additions and 0 deletions
@@ -0,0 +1,6 @@
"""aisuite coding agent CLI."""
from .app import CodeCli
from .config import CliConfig, DEFAULT_ALLOWED_COMMANDS
__all__ = ["CliConfig", "CodeCli", "DEFAULT_ALLOWED_COMMANDS"]
@@ -0,0 +1,92 @@
from __future__ import annotations
import aisuite as ai
from .config import CliConfig
def build_agent(config: CliConfig) -> ai.Agent:
tools = _coding_tools(config)
if config.enable_reviewer:
tools.append(
ai.agent_tool(
build_reviewer_agent(config),
name="review_changes",
description=(
"Ask the reviewer subagent to inspect the current work for "
"bugs, regressions, missing tests, and maintainability risks."
),
)
)
return ai.Agent(
name="aisuite_code",
model=config.model,
instructions=_main_agent_instructions(config),
tools=tools,
tags=["cli", "code"],
metadata={"app": "aisuite_code_cli"},
)
def build_reviewer_agent(config: CliConfig) -> ai.Agent:
return ai.Agent(
name="reviewer",
model=config.model,
instructions=(
"You are a code reviewer subagent for aisuite-code. Review the "
"user-described work and available project files for correctness, "
"regressions, missing tests, security risks, and maintainability "
"issues. Do not edit files. Do not run commands. Prefer specific "
"findings with file paths and line references when available. If "
"you find no material issues, say that clearly and mention any "
"residual test gaps. Keep the response concise."
),
tools=ai.toolkits.files(root=config.cwd, allow_write=False),
tags=["cli", "code", "reviewer"],
metadata={"app": "aisuite_code_cli", "role": "reviewer"},
)
def _coding_tools(config: CliConfig) -> list:
return [
*ai.toolkits.files(
root=config.cwd,
allow_write=config.allow_write,
),
*ai.toolkits.git(root=config.cwd),
*ai.toolkits.shell(
cwd=config.cwd,
allowed_commands=config.allowed_commands,
allow_all=config.allow_shell_all,
),
]
def _main_agent_instructions(config: CliConfig) -> str:
reviewer_instruction = (
" When the user asks for a review, or after substantial edits when a "
"second opinion would help, call review_changes with a concise summary "
"of the work, changed files, and verification results."
if config.enable_reviewer
else ""
)
return (
"You are aisuite-code, a concise local coding agent. Work inside the "
"configured project directory. Inspect files before editing. Prefer "
"small, focused changes. Use replace_in_file for exact, focused text "
"replacements, apply_patch for multi-line targeted edits, and "
"write_file for new or full-file replacements. apply_patch accepts "
"only this Codex-style envelope: *** Begin Patch, then operations "
"like *** Update File: path, @@, context lines prefixed with a space, "
"removed lines prefixed with -, added lines prefixed with +, and "
"*** End Patch. Example: *** Begin Patch\\n*** Update File: app.py\\n"
"@@\\n-print('old')\\n+print('new')\\n*** End Patch. "
"apply_unified_diff is different: it accepts standard unified diffs "
"with --- a/path, +++ b/path, and numbered @@ -x,y +x,y @@ hunks; do "
"not send apply_patch envelopes to apply_unified_diff. Use shell "
"commands to create projects, run builds, and verify behavior when "
"useful. Do not use shell heredocs or redirection for file edits. "
"Explain command results and summarize changed files. Do not attempt "
"destructive actions unless the user explicitly asks and the approval prompt "
f"allows it.{reviewer_instruction}"
)
@@ -0,0 +1,280 @@
from __future__ import annotations
import json
import sys
from typing import Optional, TextIO
import aisuite as ai
from .agent import build_agent
from .approval import ApprovalController
from .config import CliConfig
from .rendering import print_steps
class CodeCli:
def __init__(
self,
config: CliConfig,
*,
input_stream: TextIO = sys.stdin,
output_stream: TextIO = sys.stdout,
):
self.config = config
self.input_stream = input_stream
self.output_stream = output_stream
self.approvals = ApprovalController(
input_stream=input_stream,
output_stream=output_stream,
)
self.trace_sinks = [ai.tracing.LocalTraceSink(config.trace_file)]
self.artifact_store = ai.FileArtifactStore(config.artifact_root)
if config.trace_http:
self.trace_sinks.append(ai.tracing.HttpTraceSink(config.trace_http))
self.agent = build_agent(config)
self.result: Optional[ai.RunResult] = None
self.viewer: Optional[ai.tracing.ViewerServer] = None
def run(self) -> None:
self._print_header()
if self.config.start_viewer:
self._start_viewer()
try:
while True:
print("\nYou > ", end="", file=self.output_stream, flush=True)
user_input = self.input_stream.readline()
if not user_input:
print("", file=self.output_stream)
break
user_input = user_input.strip()
if not user_input:
continue
if user_input.startswith("/"):
if self._handle_command(user_input):
break
continue
self._run_agent(user_input)
finally:
if self.viewer is not None:
self.viewer.stop()
def _run_agent(self, user_input: str) -> None:
print("\nWorking...", file=self.output_stream)
try:
if self.result is None:
self.result = ai.Runner.run_sync(
self.agent,
user_input,
max_turns=self.config.max_turns,
run_name="code_cli_turn",
group_id="aisuite-code-cli",
trace_sinks=self.trace_sinks,
tool_policy=self.approvals,
artifact_store=self.artifact_store,
)
else:
self.result = ai.Runner.continue_sync(
self.result,
user_input,
max_turns=self.config.max_turns,
trace_sinks=self.trace_sinks,
tool_policy=self.approvals,
artifact_store=self.artifact_store,
)
except Exception as exc:
self._print_error(exc)
return
print_steps(self.result, self.output_stream)
print("\nAssistant", file=self.output_stream)
final_output = str(self.result.final_output or "").strip()
if final_output:
for line in final_output.splitlines():
print(f" {line}", file=self.output_stream)
else:
print(" (no final output)", file=self.output_stream)
self._print_trace_hint()
def _handle_command(self, command: str) -> bool:
if command in {"/exit", "/quit"}:
return True
if command == "/help":
self._print_help()
return False
if command == "/viewer":
print("\nViewer", file=self.output_stream)
if self.viewer is not None:
print(f" open: {self.viewer.url}", file=self.output_stream)
print(" start here: /viewer start", file=self.output_stream)
print(
" run separately: python -m aisuite.tracing.viewer "
f"--trace-file {self.config.trace_file} "
f"--artifact-root {self.config.artifact_root}",
file=self.output_stream,
)
print(
" live endpoint: "
f"{self.config.trace_http or '(pass --trace-http to stream events)'}",
file=self.output_stream,
)
return False
if command == "/viewer start":
self._start_viewer()
return False
if command == "/status":
self._print_status()
return False
if command == "/examples":
self._print_examples()
return False
if command == "/last":
self._print_last_turn()
return False
if command == "/clear":
self.result = None
print("\nConversation state cleared.", file=self.output_stream)
return False
print(f"\nUnknown command: {command}", file=self.output_stream)
self._print_help()
return False
def _start_viewer(self) -> None:
if self.viewer is None:
self.viewer = ai.tracing.start_viewer(
self.config.trace_file,
port=0,
artifact_root=self.config.artifact_root,
)
print(f"\nViewer: {self.viewer.url}", file=self.output_stream)
def _print_status(self) -> None:
print(f"\nmodel: {self.config.model}", file=self.output_stream)
print(f"cwd: {self.config.cwd}", file=self.output_stream)
print(f"trace_file: {self.config.trace_file}", file=self.output_stream)
print(f"artifact_root: {self.config.artifact_root}", file=self.output_stream)
print(f"trace_http: {self.config.trace_http or '-'}", file=self.output_stream)
print(f"write_tools: {self.config.allow_write}", file=self.output_stream)
print(f"reviewer: {self.config.enable_reviewer}", file=self.output_stream)
print(
f"allowed_commands: {', '.join(self.config.allowed_commands) or 'all'}",
file=self.output_stream,
)
def _print_header(self) -> None:
print("aisuite-code", file=self.output_stream)
print("-" * 56, file=self.output_stream)
print("Session", file=self.output_stream)
print(f" model: {self.config.model}", file=self.output_stream)
print(f" cwd: {self.config.cwd}", file=self.output_stream)
print(
f" tools: writes {'on' if self.config.allow_write else 'off'} · "
f"shell {'all' if self.config.allow_shell_all else 'limited'} · "
f"reviewer {'on' if self.config.enable_reviewer else 'off'}",
file=self.output_stream,
)
print(f" traces: {self.config.trace_file}", file=self.output_stream)
print(f" artifacts:{self.config.artifact_root}", file=self.output_stream)
if self.config.trace_http:
print(f" stream: {self.config.trace_http}", file=self.output_stream)
print(
"\nType /help for commands. Use /viewer start for local traces.",
file=self.output_stream,
)
self._print_starter_prompts()
def _print_help(self) -> None:
print("\nCommands", file=self.output_stream)
print(
" /viewer show viewer status and command", file=self.output_stream
)
print(" /viewer start start local viewer server", file=self.output_stream)
print(" /status show current configuration", file=self.output_stream)
print(" /examples show good starter prompts", file=self.output_stream)
print(" /last show last turn details", file=self.output_stream)
print(" /clear clear conversation state", file=self.output_stream)
print(" /exit quit", file=self.output_stream)
self._print_examples()
def _print_trace_hint(self) -> None:
if self.result is None:
return
print(f"\nTrace: {self.result.trace_id}", file=self.output_stream)
if self.viewer is not None:
focused_url = f"{self.viewer.url}?embed=1&trace_id={self.result.trace_id}"
print(f" focused viewer: {focused_url}", file=self.output_stream)
else:
print(" start viewer: /viewer start", file=self.output_stream)
def _print_starter_prompts(self) -> None:
print("\nTry", file=self.output_stream)
for prompt in (
"List files in this directory and tell me what you see.",
"Read README.md and summarize the project in 5 bullets.",
"Create app.py with add(a, b), then run it.",
):
print(f" {prompt}", file=self.output_stream)
def _print_examples(self) -> None:
self._print_starter_prompts()
print(
" Run the focused tests for the files you changed.",
file=self.output_stream,
)
print(
" Ask the reviewer subagent to review the current changes.",
file=self.output_stream,
)
def _print_last_turn(self) -> None:
if self.result is None:
print("\nNo turns yet.", file=self.output_stream)
return
print("\nLast turn", file=self.output_stream)
print(f" trace: {self.result.trace_id}", file=self.output_stream)
print(f" status: {self.result.status}", file=self.output_stream)
print(
f" input: {self._compact(self.result.input, 260)}", file=self.output_stream
)
print(" output:", file=self.output_stream)
output = str(self.result.final_output or "").strip() or "(no final output)"
for line in output.splitlines():
print(f" {line}", file=self.output_stream)
if not self.result.steps:
return
print(" steps:", file=self.output_stream)
for step in self.result.steps:
print(
f" - {step.type}: {step.name or '-'} ({step.id})",
file=self.output_stream,
)
if step.data:
print(f" {self._compact(step.data, 700)}", file=self.output_stream)
def _compact(self, value: object, max_chars: int = 500) -> str:
if isinstance(value, str):
rendered = value
else:
rendered = json.dumps(value, sort_keys=True, default=str)
rendered = rendered.replace("\n", "\\n")
if len(rendered) <= max_chars:
return rendered
return rendered[: max_chars - 3] + "..."
def _print_error(self, exc: Exception) -> None:
message = str(exc)
print(f"\nError: {message}", file=self.output_stream)
if "No module named 'openai'" in message:
print(
" Install CLI dependencies from cli/py/aisuite-code-cli with "
"python3 -m poetry install.",
file=self.output_stream,
)
if "OPENAI_API_KEY" in message or "API key" in message:
print(
" Set OPENAI_API_KEY in your shell, or source the repo .env file.",
file=self.output_stream,
)
print(
" Use /status to inspect the active model, cwd, and trace files.",
file=self.output_stream,
)
@@ -0,0 +1,212 @@
from __future__ import annotations
import json
import sys
from typing import Any, TextIO
import aisuite as ai
class ApprovalController:
def __init__(
self,
*,
input_stream: TextIO = sys.stdin,
output_stream: TextIO = sys.stdout,
):
self.input_stream = input_stream
self.output_stream = output_stream
self.always_allow_tools: set[str] = set()
self.always_allow_commands: set[str] = set()
def evaluate(self, context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
metadata = context.tool_metadata
command = _exact_command(context)
if command and command in self.always_allow_commands:
return ai.ToolPolicyDecision(
allowed=True,
reason="command allowed for session",
)
if context.tool_name in self.always_allow_tools:
return ai.ToolPolicyDecision(
allowed=True,
reason="tool allowed for session",
)
if metadata is None or not metadata.requires_approval:
return ai.ToolPolicyDecision(allowed=True, reason="low risk")
self._print_approval_request(context)
choice = self._read_choice()
if choice == "c" and command:
self.always_allow_commands.add(command)
return ai.ToolPolicyDecision(
allowed=True,
reason="command allowed for session",
)
if choice == "a":
self.always_allow_tools.add(context.tool_name)
return ai.ToolPolicyDecision(
allowed=True,
reason="tool allowed for session",
)
if choice == "y":
return ai.ToolPolicyDecision(allowed=True, reason="approved by user")
return ai.ToolPolicyDecision(allowed=False, reason="denied by user")
def _print_approval_request(self, context: ai.ToolPolicyContext) -> None:
metadata = context.tool_metadata
action = _approval_action(context)
effect = _approval_effect(context)
preview = _approval_preview(context)
risk = metadata.risk_level if metadata else "unknown"
category = metadata.category if metadata and metadata.category else "-"
print("\nPermission required", file=self.output_stream)
print(" Action", file=self.output_stream)
print(f" {action}", file=self.output_stream)
print(" Risk", file=self.output_stream)
print(f" {risk} · {category}", file=self.output_stream)
print(" Effect", file=self.output_stream)
print(f" {effect}", file=self.output_stream)
print(" Preview", file=self.output_stream)
for line in preview:
print(f" {line}", file=self.output_stream)
prompt = " Allow? [y] once [n] deny [a] always this tool"
if _exact_command(context):
prompt += " [c] always this command"
print(prompt, file=self.output_stream)
def _read_choice(self) -> str:
print("> ", end="", file=self.output_stream, flush=True)
choice = self.input_stream.readline().strip().lower()
if choice in {"y", "yes"}:
return "y"
if choice in {"a", "always"}:
return "a"
if choice in {"c", "command"}:
return "c"
return "n"
def _approval_action(context: ai.ToolPolicyContext) -> str:
args = context.arguments or {}
if context.tool_name == "run_shell":
command = args.get("command") or "-"
return f"run shell command: {_single_line(str(command), 140)}"
if context.tool_name == "write_file":
return f"write file: {args.get('path', '-')}"
if context.tool_name == "replace_in_file":
return f"edit file: {args.get('path', '-')}"
if context.tool_name == "apply_patch":
return "apply Codex-style patch"
if context.tool_name == "apply_unified_diff":
return "apply unified diff"
if context.tool_name == "review_changes":
return "invoke reviewer subagent"
return f"run tool: {context.tool_name}"
def _approval_effect(context: ai.ToolPolicyContext) -> str:
args = context.arguments or {}
if context.tool_name == "run_shell":
return "Executes a command in the configured workspace."
if context.tool_name == "write_file":
overwrite = args.get("overwrite", True)
mode = "create or overwrite" if overwrite else "create if missing"
return f"May {mode} a file under the configured workspace."
if context.tool_name == "replace_in_file":
expected = args.get("expected_replacements", 1)
return f"Replaces exact text in one file; expected replacements: {expected}."
if context.tool_name == "apply_patch":
return "Applies one or more targeted file edits from a Codex-style patch."
if context.tool_name == "apply_unified_diff":
return "Applies one or more file edits from a unified diff."
if context.tool_name == "review_changes":
return "Runs a read-only reviewer subagent and records a child trace."
return "Runs the tool with the summarized arguments below."
def _approval_preview(context: ai.ToolPolicyContext) -> list[str]:
args = context.arguments or {}
if context.tool_name == "run_shell":
return [f"command: {_summarize_argument(args.get('command'), max_chars=260)}"]
if context.tool_name == "write_file":
return [
f"path: {args.get('path', '-')}",
f"content: {_text_stats(args.get('content'))}",
]
if context.tool_name == "replace_in_file":
return [
f"path: {args.get('path', '-')}",
f"old: {_text_stats(args.get('old'))}",
f"new: {_text_stats(args.get('new'))}",
]
if context.tool_name == "apply_patch":
return [f"patch: {_text_stats(args.get('patch'))}"]
if context.tool_name == "apply_unified_diff":
return [f"diff: {_text_stats(args.get('diff'))}"]
if context.tool_name == "review_changes":
return [f"input: {_summarize_argument(args.get('input'), max_chars=260)}"]
if not args:
return ["no arguments"]
return [f"{key}: {_summarize_argument(value)}" for key, value in args.items()]
def _summarize_argument(value: Any, max_chars: int = 700) -> str:
if _is_artifact_ref(value):
ref = value.get("artifact_ref") or {}
preview = value.get("preview")
pieces = [
f"artifact {ref.get('artifact_id')}",
(
f"{ref.get('size_bytes')} bytes"
if ref.get("size_bytes") is not None
else None
),
]
if preview:
pieces.append(f"preview={_single_line(str(preview), 180)}")
return " · ".join(piece for piece in pieces if piece)
if isinstance(value, str):
lines = len(value.splitlines())
if len(value) <= max_chars:
return value
return (
f"{len(value)} chars · {lines} lines · "
f"preview={_single_line(value, 360)}"
)
try:
rendered = json.dumps(value, sort_keys=True)
except TypeError:
rendered = str(value)
if len(rendered) <= max_chars:
return rendered
return f"{len(rendered)} chars · preview={_single_line(rendered, 360)}"
def _is_artifact_ref(value: Any) -> bool:
return (
isinstance(value, dict)
and value.get("type") == "artifact_ref"
and isinstance(value.get("artifact_ref"), dict)
)
def _single_line(value: str, max_chars: int) -> str:
rendered = value.replace("\n", "\\n")
if len(rendered) <= max_chars:
return rendered
return rendered[: max_chars - 3] + "..."
def _exact_command(context: ai.ToolPolicyContext) -> str | None:
if context.tool_name != "run_shell":
return None
command = context.arguments.get("command") if context.arguments else None
return command if isinstance(command, str) and command else None
def _text_stats(value: Any) -> str:
if not isinstance(value, str):
return "-"
return f"{len(value)} chars · {len(value.splitlines())} lines"
@@ -0,0 +1,101 @@
from __future__ import annotations
import argparse
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
DEFAULT_ALLOWED_COMMANDS = [
"npm",
"npx",
"node",
"python",
"python3",
"pytest",
"git status",
"git diff",
"git log",
"mkdir",
"ls",
"pwd",
]
@dataclass
class CliConfig:
model: str
cwd: Path
trace_file: Path
artifact_root: Path = field(default_factory=lambda: Path(".aisuite/artifacts"))
trace_http: Optional[str] = None
allow_write: bool = True
allow_shell_all: bool = False
allowed_commands: list[str] = field(default_factory=list)
max_turns: int = 8
start_viewer: bool = False
enable_reviewer: bool = True
def __post_init__(self) -> None:
self.cwd = self.cwd.expanduser().resolve()
if not self.trace_file.is_absolute():
self.trace_file = self.cwd / self.trace_file
if not self.artifact_root.is_absolute():
self.artifact_root = self.cwd / self.artifact_root
def parse_args(argv: Optional[list[str]] = None) -> CliConfig:
parser = argparse.ArgumentParser(description="Run the aisuite coding agent CLI.")
parser.add_argument("--model", default="openai:gpt-4o-mini")
parser.add_argument("--cwd", default=".")
parser.add_argument("--trace-file", default=".aisuite/code.jsonl")
parser.add_argument("--artifact-root", default=".aisuite/artifacts")
parser.add_argument(
"--trace-http",
default=None,
help="Optional viewer /api/events endpoint for live trace streaming.",
)
parser.add_argument(
"--read-only",
action="store_true",
help="Disable file write tools.",
)
parser.add_argument("--allow-shell-all", action="store_true")
parser.add_argument(
"--allow-command",
action="append",
dest="allowed_commands",
default=[],
help="Command prefix to allow. Can be supplied multiple times.",
)
parser.add_argument("--max-turns", type=int, default=8)
parser.add_argument(
"--viewer",
action="store_true",
help="Start the local viewer when the CLI launches.",
)
parser.add_argument(
"--no-reviewer",
action="store_true",
help="Disable the default read-only reviewer subagent tool.",
)
args = parser.parse_args(argv)
cwd = Path(args.cwd).expanduser().resolve()
trace_file = Path(args.trace_file)
if not trace_file.is_absolute():
trace_file = cwd / trace_file
artifact_root = Path(args.artifact_root)
if not artifact_root.is_absolute():
artifact_root = cwd / artifact_root
return CliConfig(
model=args.model,
cwd=cwd,
trace_file=trace_file,
artifact_root=artifact_root,
trace_http=args.trace_http,
allow_write=not args.read_only,
allow_shell_all=args.allow_shell_all,
allowed_commands=args.allowed_commands or list(DEFAULT_ALLOWED_COMMANDS),
max_turns=args.max_turns,
start_viewer=args.viewer,
enable_reviewer=not args.no_reviewer,
)
@@ -0,0 +1,15 @@
from __future__ import annotations
from typing import Optional
from .app import CodeCli
from .config import parse_args
def main(argv: Optional[list[str]] = None) -> None:
config = parse_args(argv)
CodeCli(config).run()
if __name__ == "__main__":
main()
@@ -0,0 +1,146 @@
from __future__ import annotations
import json
from typing import TextIO
import aisuite as ai
WRITE_TOOL_NAMES = {
"write_file",
"replace_in_file",
"apply_patch",
"apply_unified_diff",
}
def print_steps(result: ai.RunResult, output_stream: TextIO) -> None:
tool_steps = [
step for step in result.steps if step.type in {"tool_call", "tool_result"}
]
if not tool_steps:
return
print("\nActivity", file=output_stream)
for step in tool_steps[-10:]:
data = step.data
if step.type == "tool_call":
status = "allowed" if data.get("allowed") else "denied"
summary = summarize_tool_arguments(step.name, data.get("arguments"))
print(
f" tool request: {step.name} · {status}{summary}",
file=output_stream,
)
if data.get("reason"):
print(f" reason: {data['reason']}", file=output_stream)
elif step.type == "tool_result":
result_summary = summarize_result_preview(
step.name, data.get("result_preview")
)
status = data.get("status") or "completed"
print(
f" tool result: {step.name} · {status}{result_summary}",
file=output_stream,
)
def summarize_tool_arguments(tool_name: str, value: object) -> str:
if not isinstance(value, dict) or not value:
return ""
if tool_name == "run_shell":
command = value.get("command")
return f" · {compact_value(command, 180)}" if command else ""
if tool_name in WRITE_TOOL_NAMES:
path = value.get("path")
if path:
if tool_name == "write_file" and "content" in value:
return f" · {path} · {text_stats(value.get('content'))}"
if tool_name == "replace_in_file":
return f" · {path} · replace {text_stats(value.get('old'))}"
return f" · {path}"
if "patch" in value:
return f" · patch · {text_stats(value.get('patch'))}"
if "diff" in value:
return f" · diff · {text_stats(value.get('diff'))}"
if tool_name == "review_changes":
return " · reviewer subagent"
return " · " + compact_value(value, 220)
def summarize_result_preview(tool_name: str, preview: object) -> str:
if not preview:
return ""
try:
value = json.loads(preview) if isinstance(preview, str) else preview
except json.JSONDecodeError:
return " · " + compact_value(preview, 220)
if isinstance(value, dict) and {"stdout", "stderr", "exit_code"} & set(value):
exit_code = value.get("exit_code")
stdout = value.get("stdout") or ""
stderr = value.get("stderr") or ""
parts = [f"exit {exit_code}" if exit_code is not None else None]
if stdout:
parts.append(f"stdout {text_stats(stdout)}")
if stderr:
parts.append(f"stderr {text_stats(stderr)}")
return " · " + " · ".join(part for part in parts if part)
if isinstance(value, dict):
if "path" in value and "content" in value:
return f" · {value['path']} · {text_stats(value.get('content'))}"
changed = value.get("changed_files") or []
if changed:
files = ", ".join(changed[:3])
more = "..." if len(changed) > 3 else ""
return f" · {len(changed)} file(s): {files}{more}"
return " · " + compact_value(value, 220)
def print_mapping(label: str, value: object, output_stream: TextIO) -> None:
if not value:
return
print(f" {label}:", file=output_stream)
if isinstance(value, dict):
for key, item in value.items():
print(f" {key}: {compact_value(item)}", file=output_stream)
return
print(f" {compact_value(value)}", file=output_stream)
def print_result_preview(preview: str, output_stream: TextIO) -> None:
try:
value = json.loads(preview)
except json.JSONDecodeError:
print(f" result: {compact_value(preview)}", file=output_stream)
return
if isinstance(value, dict) and {"stdout", "stderr", "exit_code"} & set(value):
for key in ("exit_code", "timed_out"):
if key in value:
print(f" {key}: {value[key]}", file=output_stream)
for key in ("stdout", "stderr"):
if value.get(key):
print(
f" {key}: {compact_value(value[key])}",
file=output_stream,
)
return
print(f" result: {compact_value(value)}", file=output_stream)
def text_stats(value: object) -> str:
if not isinstance(value, str):
return "-"
line_count = len(value.splitlines())
return f"{len(value)} chars, {line_count} lines"
def compact_value(value: object, max_chars: int = 500) -> str:
if value is None:
return ""
if isinstance(value, str):
rendered = value
else:
rendered = json.dumps(value, sort_keys=True)
rendered = rendered.replace("\n", "\\n")
if len(rendered) <= max_chars:
return rendered
return rendered[: max_chars - 3] + "..."