feat(missions): attribute a phase's tool calls to the agent that made them
`record_vm_tools` wrote `agent_id: None` for every call. The container tap is per-CONTAINER and every role in a phase shares one, so a phase arrived as one undifferentiated stream: every Skill-Use score was per-mission rather than per-role, and the World's per-agent view got nothing from this tier. One `claude -p` invocation is one turn is one agent, and Claude Code stamps each invocation with a `session_id` the tap was discarding. So the distinct sessions, in order of first appearance, are the phase's turns in the order they ran — and `prompt.composed` already records the agent of each turn in that same order, written by the tier as it sends each turn, so it IS the running order rather than a reconstruction of it. **It attributes nothing rather than guessing.** Only when the counts match exactly. A phase whose sessions and turns differ has something this correlation does not model — a retry, a turn that called no tool, two genuinely concurrent agents — and a plausible-looking wrong attribution is worse than none here: it puts one agent's `git push` on another agent's record, and a person later reasons from that. One call missing a session id refuses the whole batch, because a hole shifts every later session onto the wrong turn. The microVM call sites pass no turn agents and so keep today's behaviour exactly. Resolving a graph node to an agent uuid is the fix there, it cannot be tested while the fleet is offline, and guessing would put one node's actions on another node's record. Also restores the `#[cfg(test)]` gate on `repo_less_text_tests`, which my own insertion had taken — those tests would have compiled into release builds. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
This commit is contained in:
co-authored by
Claude Opus 5
parent
3f26dfeaca
commit
8591585e60
@@ -71,6 +71,14 @@ pub struct Observed {
|
||||
pub tool: String,
|
||||
/// The path the tool's **input** named, if any. From JSON, never prose.
|
||||
pub path: Option<String>,
|
||||
/// Claude Code's session id for the `claude -p` invocation this call
|
||||
/// happened inside.
|
||||
///
|
||||
/// One invocation is one turn is one agent, so this is the only thing in
|
||||
/// the payload that separates one agent's actions from another's. The tap
|
||||
/// is per-CONTAINER and every role in a phase shares one, so without this
|
||||
/// the whole phase arrives as an undifferentiated stream.
|
||||
pub session: Option<String>,
|
||||
/// The tool's arguments, bounded by [`bounded_input`].
|
||||
///
|
||||
/// Kept because the tool NAME alone answers almost nothing. A phase that
|
||||
@@ -245,6 +253,11 @@ pub fn parse(raw: &str) -> Vec<Observed> {
|
||||
Some(Observed {
|
||||
path: crate::mission_events::tool_path(&input),
|
||||
input: bounded_input(&input),
|
||||
session: v
|
||||
.get("session_id")
|
||||
.or_else(|| v.get("sessionId"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
tool,
|
||||
})
|
||||
})
|
||||
@@ -337,11 +350,13 @@ mod tests {
|
||||
tool: "Edit".into(),
|
||||
path: Some("/mission/repo/src/a.rs".into()),
|
||||
input: json!({"file_path": "/mission/repo/src/a.rs"}),
|
||||
session: None,
|
||||
},
|
||||
Observed {
|
||||
tool: "Bash".into(),
|
||||
path: None,
|
||||
input: json!({"command": "ls"}),
|
||||
session: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user