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
54 changes: 54 additions & 0 deletions src/iac_code/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def main(
model: str = typer.Option("", "--model", "-m", help=_("LLM model to use")),
prompt: str = typer.Option("", "--prompt", "-p", help=_("Non-interactive mode: run a single prompt and exit")),
output_format: str = typer.Option("text", "--output-format", help=_("Output format: text, json, stream-json")),
input_format: str = typer.Option("text", "--input-format", help=_("Input format: text, stream-json")),
max_turns: int = typer.Option(100, "--max-turns", help=_("Maximum agent turns in headless mode")),
debug: bool = typer.Option(False, "--debug", "-d", help=_("Enable debug logging")),
verbose: bool = typer.Option(False, "--verbose", help=_("Show headless progress on stderr")),
Expand Down Expand Up @@ -177,6 +178,59 @@ def main(
typer.echo(_("Error: --resume and --continue cannot be used together."), err=True)
raise typer.Exit(1)

normalized_input_format = (input_format or "text").strip().lower()
if normalized_input_format not in {"text", "stream-json"}:
typer.echo(_("Invalid --input-format '{}'. Valid values: text, stream-json").format(input_format), err=True)
raise typer.Exit(1)

if normalized_input_format == "stream-json":
normalized_output_format = (output_format or "text").strip().lower()
if prompt:
typer.echo(_("--prompt cannot be used with --input-format stream-json."), err=True)
raise typer.Exit(1)
if normalized_output_format != "stream-json":
typer.echo(_("--input-format stream-json requires --output-format stream-json."), err=True)
raise typer.Exit(1)

from iac_code.pipeline.config import get_run_mode

run_mode = get_run_mode()

if not model:
try:
model = load_saved_model() or DEFAULT_MODEL
except ValueError as exc:
typer.echo(str(exc), err=True)
raise typer.Exit(1)

if permission_mode:
from iac_code.services.permissions.loader import parse_cli_permission_mode

try:
parse_cli_permission_mode(permission_mode)
except ValueError as exc:
typer.echo(str(exc), err=True)
raise typer.Exit(1) from exc

setup_logging(session_id="process-mode", debug=debug)

from iac_code.cli.process_mode import ProcessModeOptions, ProcessModeRunner

cli_allowed = [s.strip() for s in allowed_tools.split(",") if s.strip()] if allowed_tools else None
cli_disallowed = [s.strip() for s in disallowed_tools.split(",") if s.strip()] if disallowed_tools else None
runner = ProcessModeRunner(
ProcessModeOptions(
model=model,
cwd=os.getcwd(),
run_mode=run_mode.value,
max_turns=max_turns,
cli_allowed_tools=cli_allowed,
cli_disallowed_tools=cli_disallowed,
cli_permission_mode=permission_mode or None,
)
)
raise SystemExit(asyncio.run(runner.run()))

fmt = None
if prompt:
from iac_code.cli.output_formats import OutputFormat
Expand Down
11 changes: 8 additions & 3 deletions src/iac_code/cli/output_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ def _sanitize_public_value(value: Any, *, public_path_roots: list[dict[str, str]
return value


def _stream_json_event_data(event: StreamEvent) -> dict[str, Any]:
def stream_json_event_data(event: StreamEvent) -> dict[str, Any]:
"""Return the public stream-json representation for a stream event."""
if isinstance(event, ToolUseStartEvent):
return {
"tool_use_id": event.tool_use_id,
Expand Down Expand Up @@ -120,7 +121,7 @@ def _stream_json_event_data(event: StreamEvent) -> dict[str, Any]:
if isinstance(event, SubPipelineStreamEvent):
return {
"candidate_index": event.candidate_index,
"inner": _stream_json_event_data(event.inner),
"inner": stream_json_event_data(event.inner),
"sub_pipeline_id": event.sub_pipeline_id,
"type": event.type,
}
Expand All @@ -140,6 +141,10 @@ def _stream_json_event_data(event: StreamEvent) -> dict[str, Any]:
return data


def _stream_json_event_data(event: StreamEvent) -> dict[str, Any]:
return stream_json_event_data(event)


class TextWriter:
"""Writes only assistant text content to the output stream.

Expand Down Expand Up @@ -231,7 +236,7 @@ def __init__(self, stream: IO[str] | None = None) -> None:
self._stream = stream or sys.stdout

def handle(self, event: StreamEvent) -> None:
data = _stream_json_event_data(event)
data = stream_json_event_data(event)
self._stream.write(json.dumps(data, ensure_ascii=False, default=str))
self._stream.write("\n")
self._stream.flush()
Expand Down
56 changes: 56 additions & 0 deletions src/iac_code/cli/process_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Event and error mapping for CLI process mode."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import Any

from iac_code.cli.output_formats import stream_json_event_data
from iac_code.cli.process_protocol import ProcessFrameValidationError, SDKErrorPayload, SDKProcessRuntimeError
from iac_code.providers.manager import ProviderNotConfiguredError
from iac_code.types.stream_events import ErrorEvent
from iac_code.utils.public_errors import sanitize_public_text


@dataclass(frozen=True)
class ProcessSerializedEvent:
"""Already-public event payload to embed in a process-mode stream_event frame."""

payload: dict[str, Any]


class ProcessEventSerializer:
"""Serialize internal stream events to the public stream-json event shape."""

def serialize(self, event: Any) -> dict:
if isinstance(event, ProcessSerializedEvent):
return event.payload
return stream_json_event_data(event)


class ProcessErrorMapper:
"""Map runtime exceptions and error events to stable process-mode error payloads."""

def from_event(self, event: ErrorEvent) -> SDKErrorPayload:
return SDKErrorPayload(
code="stream_error",
message=sanitize_public_text(event.error),
retryable=event.is_retryable,
error_id=event.error_id,
)

def from_exception(self, exc: BaseException) -> SDKErrorPayload:
if isinstance(exc, SDKProcessRuntimeError):
return exc.payload
if isinstance(exc, ProcessFrameValidationError):
return SDKErrorPayload(code=exc.code, message=exc.message, retryable=exc.retryable)
if isinstance(exc, ProviderNotConfiguredError):
return SDKErrorPayload(
code="provider_not_configured",
message=sanitize_public_text(str(exc)),
retryable=False,
)
if isinstance(exc, asyncio.CancelledError):
return SDKErrorPayload(code="turn_canceled", message="Turn canceled.", retryable=False)
return SDKErrorPayload(code="internal_error", message=sanitize_public_text(str(exc)), retryable=False)
Loading
Loading