Add real-time per-tool-call audit streaming (HARNESS-WP-0002-T03)
Claude Code executes its own tools internally in --print mode -- there is no way for a caller to externally dispatch individual tool calls without abandoning that self-contained agent model. What --output-format stream-json --include-hook-events does allow: observing each tool_use/tool_result/hook event in real time. - adapter.py: AgenticClaudeCodeAdapter gains an optional on_tool_event callback; streaming mode (Popen + background reader thread) is used only when set, blocking subprocess.run path is unchanged otherwise. - runner.py: run_task gains emit_tool_events/on_tool_event, collecting events onto RunResult.tool_events and posting a tool_call hub event per tool when reporting is enabled. - cli.py: --stream-tool-events flag on `run`, prints each event as a tagged JSON line ahead of the unchanged final result block. Live-verified against the real claude CLI: 5 real tool events streamed correctly (2x Bash, 1x Write) plus Stop hook lifecycle events, real commit landed, final result block unchanged. 13 new tests (test_adapter.py + 2 in test_runner.py), all passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c77a643393
commit
6c4f00e2c2
7 changed files with 384 additions and 18 deletions
|
|
@ -12,8 +12,9 @@ from __future__ import annotations
|
|||
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from rein_aharness import hub, metrics
|
||||
from rein_aharness.manifest import resolve_run_policy
|
||||
|
|
@ -53,6 +54,10 @@ class RunResult:
|
|||
budget_tokens: int | None = None
|
||||
tokens_spent: int | None = None
|
||||
execution_time_s: float = 0.0
|
||||
# Real-time per-tool-call audit events, populated only when run_task is
|
||||
# called with emit_tool_events=True. See adapter.py's module docstring
|
||||
# for why this is observation, not external tool dispatch.
|
||||
tool_events: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> str:
|
||||
|
|
@ -72,6 +77,8 @@ def run_task(
|
|||
adapter=None,
|
||||
report_to_hub: bool = True,
|
||||
write_metrics: bool = True,
|
||||
emit_tool_events: bool = False,
|
||||
on_tool_event: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> RunResult:
|
||||
try:
|
||||
profile_name, budget_tokens, lane, blueprint = resolve_run_policy(
|
||||
|
|
@ -103,12 +110,27 @@ def run_task(
|
|||
budget_tokens=None,
|
||||
)
|
||||
|
||||
collected_events: list[dict[str, Any]] = []
|
||||
|
||||
def _on_event(event: dict[str, Any]) -> None:
|
||||
collected_events.append(event)
|
||||
if report_to_hub:
|
||||
hub.post_progress_event(
|
||||
summary=f"tool event: {spec.title}",
|
||||
event_type="tool_call",
|
||||
detail={"repo": spec.target_repo.name, "agent": spec.agent, "event": event},
|
||||
task_id=spec.hub_task_id,
|
||||
)
|
||||
if on_tool_event is not None:
|
||||
on_tool_event(event)
|
||||
|
||||
if adapter is None:
|
||||
from rein_aharness.adapter import AgenticClaudeCodeAdapter
|
||||
|
||||
adapter = AgenticClaudeCodeAdapter(
|
||||
workdir=spec.target_repo,
|
||||
tool_profile=profile,
|
||||
on_tool_event=_on_event if (emit_tool_events or on_tool_event) else None,
|
||||
)
|
||||
|
||||
head_before = _git(spec.target_repo, "rev-parse", "HEAD")
|
||||
|
|
@ -162,6 +184,7 @@ def run_task(
|
|||
budget_tokens=budget_tokens,
|
||||
tokens_spent=tokens_spent,
|
||||
execution_time_s=execution_time_s,
|
||||
tool_events=collected_events,
|
||||
)
|
||||
|
||||
if write_metrics:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue