fix(viz): read the gateway's real tool_call keys, and correct the record
The frame is `{"type":"tool_call","id","name","args"}` — zeroclaw-gateway
/src/ws.rs. The tap read `tool` then `name`, and `arguments` then `input`.
`name` happened to be in the fallback chain; `args` was not in it at all,
so a container-tier tool call would have been recorded with its name and
NO path — a tool that reads as having touched nothing. `tool` and
`arguments` belong to `approval_request`, which is where they came from.
Also corrects what the histogram was read as saying. A mission turn on
gw-04 carried only chunk/done/session_start, and the first reading was
"there is no tool_call frame". Wrong: `grep -c tool_call` on the deployed
0.8.3 binary returns 46. Container-tier agents are provisioned tool-free
behind the MCP door (§15), so they call nothing — there is nothing to
observe on that tier, and nothing is broken.
That distinction is exactly what the histogram was shipped to make
possible: a tap matching no frame is otherwise indistinguishable from a
mission that used no tools.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
31158467f4
commit
d810fc0a86
@@ -87,12 +87,19 @@ pub struct ToolTrace {
|
||||
pub calls: Vec<ToolCall>,
|
||||
/// Frame `type` values this drain did not recognise, counted.
|
||||
///
|
||||
/// Shipped in the same change as the tap on purpose: the frame name
|
||||
/// `tool_call` is taken from a comment in this file, not from a captured
|
||||
/// frame. If the runtime calls it something else, the tap records nothing
|
||||
/// and nothing anywhere errors — the World simply stays as sparse as it was
|
||||
/// before. This histogram is how one gw-04 run names the real frame instead
|
||||
/// of a bisect.
|
||||
/// Shipped in the same change as the tap on purpose: the frame name was
|
||||
/// taken from a comment in this file rather than from a captured frame. If
|
||||
/// the runtime called it something else, the tap would record nothing and
|
||||
/// nothing anywhere would error — the World would simply stay as sparse as
|
||||
/// it was before.
|
||||
///
|
||||
/// MEASURED on gw-04 (v0.8.3, 2026-08-11): a mission turn's stream carried
|
||||
/// `chunk`, `done` and `session_start` and no tool frames at all. That is
|
||||
/// not a protocol mismatch — `tool_call` is in the deployed binary
|
||||
/// (`zeroclaw-gateway/src/ws.rs` emits `{"type":"tool_call","id","name",
|
||||
/// "args"}`) — it is §15: these agents are provisioned tool-free behind the
|
||||
/// MCP door, so they call nothing. The histogram is what let us tell those
|
||||
/// two apart, which was its whole purpose.
|
||||
pub unmatched: std::collections::BTreeMap<String, u32>,
|
||||
}
|
||||
|
||||
@@ -511,16 +518,27 @@ impl ZeroClawDriveExecutor {
|
||||
// often enough to be believed and wrong often enough to
|
||||
// put files on the map that nobody edited.
|
||||
"tool_call" => {
|
||||
// `name` is what the gateway sends; `tool` is
|
||||
// what `approval_request` uses, kept as a fallback.
|
||||
let tool = v
|
||||
.get("tool")
|
||||
.or_else(|| v.get("name"))
|
||||
.get("name")
|
||||
.or_else(|| v.get("tool"))
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !tool.is_empty() {
|
||||
// `args` FIRST: that is what the gateway
|
||||
// actually sends (`{"type":"tool_call","id",
|
||||
// "name","args"}` — zeroclaw-gateway/src/ws.rs).
|
||||
// The others were guesses, and a guess that
|
||||
// never matches costs the file path silently:
|
||||
// the tool call is still recorded, with no
|
||||
// target, and reads as a tool that touched
|
||||
// nothing.
|
||||
let args = v
|
||||
.get("arguments")
|
||||
.get("args")
|
||||
.or_else(|| v.get("arguments"))
|
||||
.or_else(|| v.get("input"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
@@ -670,15 +688,17 @@ mod tests {
|
||||
let _ = socket.recv().await;
|
||||
for f in [
|
||||
json!({"type": "session_start"}),
|
||||
json!({"type": "tool_call", "tool": "Read",
|
||||
"arguments": {"file_path": "/mission/repo/src/a.rs"}}),
|
||||
// The REAL frame shape, copied from the gateway:
|
||||
// {"type":"tool_call","id","name","args"}.
|
||||
json!({"type": "tool_call", "id": "t1", "name": "Read",
|
||||
"args": {"file_path": "/mission/repo/src/a.rs"}}),
|
||||
// A tool whose arguments name no path at all.
|
||||
json!({"type": "tool_call", "tool": "Bash",
|
||||
"arguments": {"command": "cargo test"}}),
|
||||
json!({"type": "tool_call", "id": "t2", "name": "Bash",
|
||||
"args": {"command": "cargo test"}}),
|
||||
// Prose that MENTIONS a path. It must not become a file touch.
|
||||
json!({"type": "tool_call", "tool": "Grep",
|
||||
json!({"type": "tool_call", "id": "t3", "name": "Grep",
|
||||
"arguments_summary": "searching src/main.rs",
|
||||
"arguments": {"pattern": "fn main"}}),
|
||||
"args": {"pattern": "fn main"}}),
|
||||
json!({"type": "a_frame_we_have_never_seen"}),
|
||||
json!({"type": "a_frame_we_have_never_seen"}),
|
||||
json!({"type": "done", "input_tokens": 1, "output_tokens": 1}),
|
||||
|
||||
Reference in New Issue
Block a user