feat(world): the agent page can answer what an agent DID, not only what it is doing
deploy / test (push) Successful in 6m13s
deploy / build (push) Successful in 6m47s

The command centre's metric band reads a live feed: tokens in the last minute,
credits in the last hour, active routines, pending approvals. Every one of those
is correctly zero once a mission ends — so an operator opening an agent that ran
`JEPA Research` was shown six zeros, with nothing saying the page had understood
a different question than the one they asked.

The data was never missing. `usage_events` carries a row per turn and
`mission_events` carries every attributed tool call. Verified against production
before any of this was written:

    Tomasz     21,697 tokens   22.00 credits   96 tool calls
    Seong-min  18,125          19.00           49
    Adrian     13,855          14.00           32
    Yara        9,686          11.00           11
    Wei         7,228           8.00           18
    Osei        4,304           5.00            5

The tool counts sum to 211, which is exactly what `mission_events` holds. The
page simply never asked.

`agent.last_run` is a SEPARATE taxonomy event, not a fallback folded into
`telemetry`, and that is the whole design. `agent.task.update` already refuses to
emit for a finished mission so that "idle" stays truthful; quietly substituting
a two-day-old number into a tile the UI promises is live would undo exactly
that. The two travel apart and the card says which it is showing:

  SPEND        last-run credits, unit becomes `cr total`, tagged LAST RUN
  THROUGHPUT   last-run tokens, unit becomes `tokens · last run`, and the
               sparkline is SUPPRESSED — a flat line drawn from one repeated
               number reads as "measured and steady" when nothing was measured
  WORKING ON   idle stays idle, but names the mission, tool calls, tokens,
    NOW        status and how long ago, instead of one line of nothing
  LOOPS/DOORS  left live; zero is the correct answer there

Live always wins. History appears only where the live value is genuinely
nothing, so an agent mid-turn can never see a stale figure.

Two details that would have been silent bugs:

- `stateKey` keys the retained value per AGENT. One shared key would let the
  last agent in the roster overwrite every other agent's summary, and a late
  subscriber would paint one agent's last run onto all of them — plausible
  numbers belonging to someone else.
- `usage_events` carries no mission id, so its rows are attributed by the
  mission's time window. `mission_events` needs no such guess, which is why the
  tool count is the trustworthy half of the row and the token figure is the
  approximate one. Said so in the doc comment rather than implying both are
  equally solid.

Refreshed on the seed and then once a minute, not on the 2s poll: historical by
definition, but not seed-only either, or a mission finishing mid-session leaves
the card reading whatever it read before.

Suite: 108 binaries, 842 Rust tests, 92 frontend tests, tsc clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-08-28 20:55:32 -05:00
co-authored by Claude Opus 5
parent 2f1a870949
commit f26de3ba76
5 changed files with 462 additions and 15 deletions
+295
View File
@@ -694,6 +694,96 @@ async fn agent_telemetry(
m
}
/// What an agent did on its most recent FINISHED mission.
///
/// The metric band is a live command centre: tokens over the last minute,
/// credits over the last hour, active routines, pending approvals. Every one of
/// those is correctly zero for an agent whose mission ended, so the page an
/// operator opens to ask "what did this agent do" answers with six zeros.
///
/// This is the other half, and it is deliberately a SEPARATE event rather than
/// a fallback folded into `telemetry`. `agent.task.update` already refuses to
/// emit for a finished mission so that "idle" stays truthful, and quietly
/// substituting a two-day-old number into a live tile would undo exactly that.
/// The client decides how to label it; the protocol keeps them apart.
struct AgentLastRun {
mission_id: uuid::Uuid,
title: String,
status: String,
ended_at: Option<time::OffsetDateTime>,
tokens: i64,
credits: f64,
tool_calls: i64,
}
/// Every agent's last finished mission, in ONE query rather than N.
///
/// `usage_events` carries no mission id, so its rows are attributed by time
/// window — the mission's own span, plus a small tail because a turn's usage is
/// recorded as the turn settles rather than before the mission is marked
/// complete. `mission_events` needs no such guess: it carries `mission_id` and
/// `agent_id` directly, which is why the tool count is the trustworthy half of
/// this row and the token figure is the approximate one.
async fn agent_last_run(
pool: &PgPool,
ws: WorkspaceId,
) -> std::collections::HashMap<String, AgentLastRun> {
let mut m = std::collections::HashMap::new();
let rows = sqlx::query(
"WITH latest AS (
SELECT DISTINCT ON (tm.claw_id)
tm.claw_id AS agent_id, m.id AS mission_id, m.title, m.status,
COALESCE(m.completed_at, m.updated_at) AS ended_at,
m.created_at AS started_at
FROM team_members tm
JOIN mission_teams mt ON mt.team_id = tm.team_id
JOIN missions m ON m.id = mt.mission_id
WHERE m.workspace_id = $1
AND m.status IN ('completed', 'failed')
ORDER BY tm.claw_id, COALESCE(m.completed_at, m.updated_at) DESC
)
SELECT l.agent_id, l.mission_id, l.title, l.status, l.ended_at,
COALESCE(u.tokens, 0) AS tokens,
COALESCE(u.credits, 0) AS credits,
COALESCE(t.tool_calls, 0) AS tool_calls
FROM latest l
LEFT JOIN LATERAL (
SELECT SUM(tokens_in + tokens_out)::bigint AS tokens,
SUM(credits)::float8 AS credits
FROM usage_events ue
WHERE ue.agent_id = l.agent_id
AND ue.created_at BETWEEN l.started_at AND l.ended_at + interval '5 minutes'
) u ON TRUE
LEFT JOIN LATERAL (
SELECT count(*)::bigint AS tool_calls
FROM mission_events me
WHERE me.mission_id = l.mission_id
AND me.agent_id = l.agent_id
AND me.kind = 'tool.call'
) t ON TRUE",
)
.bind(ws.as_uuid())
.fetch_all(pool)
.await
.unwrap_or_default();
for r in rows {
let agent_id: uuid::Uuid = r.get("agent_id");
m.insert(
agent_id.to_string(),
AgentLastRun {
mission_id: r.get("mission_id"),
title: r.get("title"),
status: r.get("status"),
ended_at: r.get("ended_at"),
tokens: r.get("tokens"),
credits: r.get("credits"),
tool_calls: r.get("tool_calls"),
},
);
}
m
}
/// Query for `GET /api/world/live`.
#[derive(serde::Deserialize)]
pub struct LiveQuery {
@@ -715,6 +805,11 @@ pub async fn world_live(
let stream = async_stream::stream! {
let mut first = true;
// How many polls since the last-run summary was refreshed. Historical
// by definition, so it does not belong on the 2s cadence — but it must
// not be seed-only either, or a mission finishing mid-session leaves
// the card reading whatever it read before.
let mut polls: u32 = 0;
// Remember last status per agent so we only push deltas after the seed.
let mut last: std::collections::HashMap<String, String> = std::collections::HashMap::new();
// Per-run journal cursor so we stream only NEW run_events each poll.
@@ -1106,6 +1201,28 @@ pub async fn world_live(
}));
}
// The last finished mission, refreshed on the seed and then once a
// minute. Stateful on the client, so a late subscriber paints it
// immediately instead of waiting for the next refresh.
if first || polls % 30 == 0 {
let last_runs = agent_last_run(&pool, ws).await;
for a in &roster {
let id = a.id.to_string();
let Some(lr) = last_runs.get(&id) else { continue };
yield sse("agent.last_run", json!({
"agentId": id,
"missionId": lr.mission_id.to_string(),
"title": lr.title,
"status": lr.status,
"endedAt": lr.ended_at.map(|t| t.unix_timestamp()),
"tokens": lr.tokens,
"credits": lr.credits,
"toolCalls": lr.tool_calls,
}));
}
}
polls = polls.wrapping_add(1);
// Per-agent LIVE column: REASONING STREAM + tool lines.
//
// These two taxonomy types were declared and listened for since the
@@ -1510,3 +1627,181 @@ mod mission_feed_tests {
);
}
}
/// Does the agent command centre have anything to say about a finished mission?
///
/// Its metric cards read a LIVE feed: tokens in the last minute, credits in the
/// last hour, active routines, pending approvals. Every one is correctly zero
/// once a mission ends, so an operator opening the page to ask "what did this
/// agent do" was answered with six zeros and nothing saying the question had
/// been understood differently than they meant it.
///
/// The data was never missing — `usage_events` carries a row per turn and
/// `mission_events` every attributed tool call. Nothing queried them. That is
/// why this is a test: the failure produced no error anywhere, every query ran,
/// and the answer was honestly nothing.
#[cfg(test)]
mod last_run_tests {
use cm_domain::WorkspaceId;
use uuid::Uuid;
/// One finished mission, a crew of one, two turns of usage and four tool
/// calls — of which one belongs to nobody.
///
/// Raw SQL on purpose: this asserts on the SHAPE of the join
/// (team_members → mission_teams → missions), and going through helpers
/// that already assume that shape would be testing itself.
async fn seed(pool: &sqlx::PgPool) -> (WorkspaceId, Uuid) {
let ws = WorkspaceId::new();
let agent = Uuid::now_v7();
let team = Uuid::now_v7();
let mission = Uuid::now_v7();
let user = Uuid::now_v7();
sqlx::query("INSERT INTO workspaces (id, name, plan) VALUES ($1,'w','free')")
.bind(ws.as_uuid())
.execute(pool)
.await
.expect("workspace");
// `agents.managed_by` is NOT NULL and a real FK, so an owner has to
// exist before any agent does.
sqlx::query(
"INSERT INTO users (id, workspace_id, email, role, display_name)
VALUES ($1,$2,$3,'owner','Owner')",
)
.bind(user)
.bind(ws.as_uuid())
.bind(format!("o-{}@example.test", &user.to_string()[..8]))
.execute(pool)
.await
.expect("user");
sqlx::query(
"INSERT INTO agents
(id, workspace_id, name, job_title, system_prompt, avatar, accent,
wallpaper, managed_by, status)
VALUES ($1,$2,'Tomasz','researcher','','','#fff','',$3,'online')",
)
.bind(agent)
.bind(ws.as_uuid())
.bind(user)
.execute(pool)
.await
.expect("agent");
sqlx::query(
"INSERT INTO teams (id, workspace_id, name, kind, graph, status, lifecycle, mcp_bundles)
VALUES ($1,$2,'crew','crew','{}'::jsonb,'active','permanent','{}')",
)
.bind(team)
.bind(ws.as_uuid())
.execute(pool)
.await
.expect("team");
sqlx::query(
"INSERT INTO team_members (team_id, node_id, claw_id, role)
VALUES ($1,'n1',$2,'researcher')",
)
.bind(team)
.bind(agent)
.execute(pool)
.await
.expect("member");
sqlx::query(
"INSERT INTO missions
(id, workspace_id, title, template_kind, schedule, status, config,
runtime_kind, created_at, updated_at, completed_at)
VALUES ($1,$2,'JEPA Research','research_only','{}'::jsonb,'completed',
'{}'::jsonb,'zeroclaw',
now() - interval '2 days',
now() - interval '2 days',
now() - interval '2 days' + interval '30 minutes')",
)
.bind(mission)
.bind(ws.as_uuid())
.execute(pool)
.await
.expect("mission");
sqlx::query(
"INSERT INTO mission_teams (mission_id, team_id, purpose) VALUES ($1,$2,'crew')",
)
.bind(mission)
.bind(team)
.execute(pool)
.await
.expect("mission_team");
// Usage rows land INSIDE the mission's window, because the window is
// how they are attributed — `usage_events` carries no mission id.
for (tin, tout, credits) in [(100_i32, 900_i32, 4.0_f64), (200, 800, 5.0)] {
sqlx::query(
"INSERT INTO usage_events
(workspace_id, agent_id, kind, tokens_in, tokens_out, credits, created_at)
VALUES ($1,$2,'llm_tokens',$3,$4,$5,
now() - interval '2 days' + interval '10 minutes')",
)
.bind(ws.as_uuid())
.bind(agent)
.bind(tin)
.bind(tout)
.bind(credits)
.execute(pool)
.await
.expect("usage");
}
for owner in [Some(agent), Some(agent), Some(agent), None] {
sqlx::query(
"INSERT INTO mission_events (mission_id, agent_id, kind, target, detail)
VALUES ($1,$2,'tool.call','Bash','{}'::jsonb)",
)
.bind(mission)
.bind(owner)
.execute(pool)
.await
.expect("event");
}
(ws, agent)
}
#[tokio::test]
async fn a_finished_mission_still_answers_what_the_agent_did() {
let pool = cm_testkit::test_pool().await;
let (ws, agent) = seed(&pool).await;
let got = super::agent_last_run(&pool, ws).await;
let lr = got
.get(&agent.to_string())
.expect("the agent's last finished mission must be found");
assert_eq!(lr.title, "JEPA Research");
assert_eq!(lr.status, "completed");
assert_eq!(lr.tokens, 2000, "tokens_in + tokens_out over both turns");
assert_eq!(lr.credits, 9.0);
assert_eq!(
lr.tool_calls, 3,
"only this agent's calls — the unattributed row belongs to no one \
and must not be credited to them"
);
}
/// A running mission is the live feed's business. Reporting it here would
/// put a current number behind a card the UI labels "last run".
#[tokio::test]
async fn a_mission_still_running_is_not_reported_as_a_last_run() {
let pool = cm_testkit::test_pool().await;
let (ws, agent) = seed(&pool).await;
sqlx::query(
"UPDATE missions SET status='running', completed_at=NULL WHERE workspace_id=$1",
)
.bind(ws.as_uuid())
.execute(&pool)
.await
.expect("update");
assert!(
super::agent_last_run(&pool, ws)
.await
.get(&agent.to_string())
.is_none(),
"only completed and failed missions are history"
);
}
}