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
|
|
@ -206,3 +206,65 @@ def test_taskspec_rejects_non_repo(tmp_path) -> None:
|
|||
)
|
||||
with pytest.raises(TaskSpecError, match="not a git repository"):
|
||||
TaskSpec.from_file(spec_file)
|
||||
|
||||
|
||||
def test_run_task_wires_on_tool_event_when_emit_tool_events(tmp_path, monkeypatch) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
spec = _spec(repo)
|
||||
captured: dict = {}
|
||||
|
||||
class FakeStreamingAdapter:
|
||||
def __init__(self, workdir, tool_profile, on_tool_event=None):
|
||||
captured["on_tool_event"] = on_tool_event
|
||||
|
||||
def execute_prompt(self, prompt, config):
|
||||
captured["on_tool_event"](
|
||||
{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Read"}]}}
|
||||
)
|
||||
(repo / "HELLO.md").write_text("hi\n")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "task"],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
from llm_connect.models import LLMResponse
|
||||
|
||||
return LLMResponse(content="done", model="m", usage={}, finish_reason="stop", metadata={})
|
||||
|
||||
monkeypatch.setattr("rein_aharness.adapter.AgenticClaudeCodeAdapter", FakeStreamingAdapter)
|
||||
|
||||
result = run_task(spec, report_to_hub=False, write_metrics=False, emit_tool_events=True)
|
||||
|
||||
assert captured["on_tool_event"] is not None
|
||||
assert len(result.tool_events) == 1
|
||||
assert result.tool_events[0]["message"]["content"][0]["name"] == "Read"
|
||||
|
||||
|
||||
def test_run_task_default_adapter_gets_no_callback_when_not_requested(tmp_path, monkeypatch) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
spec = _spec(repo)
|
||||
captured: dict = {}
|
||||
|
||||
class FakeAdapter:
|
||||
def __init__(self, workdir, tool_profile, on_tool_event=None):
|
||||
captured["on_tool_event"] = on_tool_event
|
||||
|
||||
def execute_prompt(self, prompt, config):
|
||||
(repo / "HELLO.md").write_text("hi\n")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "task"],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
from llm_connect.models import LLMResponse
|
||||
|
||||
return LLMResponse(content="done", model="m", usage={}, finish_reason="stop", metadata={})
|
||||
|
||||
monkeypatch.setattr("rein_aharness.adapter.AgenticClaudeCodeAdapter", FakeAdapter)
|
||||
|
||||
result = run_task(spec, report_to_hub=False, write_metrics=False)
|
||||
|
||||
assert captured["on_tool_event"] is None
|
||||
assert result.tool_events == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue