Files
rengine/tools/runner/models.py
T
Yogesh OjhaandClaude Fable 5.1 452fecafff feat(worker): a stage that comes up short reports partial
dnsx drops 20–60% of answers under load and exits 0, so a subdomain stage
could store half its hosts unresolved and still read Success. Resolution now
runs in shuffled batches (dns_batch_size) with the thread count clamped to the
range the resolver actually keeps up with; a batch answering far below the
median of its peers is retried, and one that stops producing is abandoned after
dns_idle_timeout instead of the whole run waiting on a total ceiling. What the
resolver managed — batches, retries, stalls — is reported, never assumed.

StageResult gains warnings + partial. The stage runner records a new PARTIAL
activity status carrying the notes, logs the stage outcome as a warning and
emits it live. The run ribbon paints a partial stage amber with a triangle, the
stage dialog shows the note in a warning box, and activityRan() is the one "did
it run" test so progress and summaries count partial as done. reverse_dns
reports the same way when dnsx times out or is missing.

stream_json yields a StreamOutcome (return code, timed_out, record count,
stderr) instead of a bare iterator, kills on idle as well as on total time, and
a timed-out run keeps the records that landed instead of discarding them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YK61xuAJGMhq1uniLhHX5
2026-09-05 18:37:55 +05:30

67 lines
1.6 KiB
Python

"""Tool-agnostic models for CLI tool execution results."""
from collections.abc import Iterator
from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol, runtime_checkable
from pydantic import BaseModel, Field
class OutputFormat(Enum):
"""Output format expected from the CLI tool."""
JSONL = "jsonl"
PLAIN = "plain"
@runtime_checkable
class CommandRecorder(Protocol):
"""Optional hook the executor calls to register a command and capture its log."""
def start(self, tool: str, command: str) -> Any: ...
def finish(
self,
handle: Any,
*,
return_code: int,
output: str,
error: str | None,
duration_seconds: float,
) -> None: ...
class ToolResult(BaseModel):
"""Result of a CLI tool execution."""
success: bool
stdout: str = ""
stderr: str = ""
exit_code: int = -1
output_lines: list[str] = Field(default_factory=list)
json_records: list[dict] = Field(default_factory=list)
duration_seconds: float = 0.0
command: str = ""
error: str | None = None
timed_out: bool = False
@property
def has_output(self) -> bool:
return bool(self.json_records or self.output_lines)
@dataclass
class StreamOutcome:
"""How a stream_json run ended; populated once the caller's block exits."""
records: Iterator[dict]
return_code: int = -1
timed_out: bool = False
record_count: int = 0
stderr: str = ""
@property
def ok(self) -> bool:
return self.return_code == 0 and not self.timed_out