feat(telemetry): the reasoning stream actually streams
deploy / test (push) Successful in 4m18s
deploy / build (push) Successful in 5m17s

`agent.reasoning.delta` and `agent.tool.call` have been declared in the taxonomy
and listened for by the command centre since it shipped — and NOTHING ever
emitted them. The world feed emitted five types; neither was among them, so
REASONING STREAM could not populate no matter what an agent did.

The feed is a database poll, not a push bus, so a live card can only show what
was persisted. The worker already holds each step's output text and the claw
that produced it, so it records a `reasoning` mission_event (truncated — the
card renders a tail, not a transcript, and mission_events is capped per phase),
and the feed emits it forward from a cursor that starts at the current max so a
page load streams rather than replaying history.

`tool.call` is emitted from the same place. On the container tier it will stay
empty, and that is correct rather than broken: those agents are tool-free behind
the §15 door. Tool lines appear where agents actually hold tools.

Verified on a live mission: agent.reasoning.delta observed on /api/world/live
carrying the agent's own text, keyed by agentId.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-15 15:58:56 -07:00
co-authored by Claude Opus 5
parent 8be7b3c9b2
commit bf40d10064
2 changed files with 103 additions and 11 deletions
+31
View File
@@ -494,6 +494,37 @@ async fn drive<E: TurnExecutor>(
// mission turn costs what it costs. It clamps at the available
// balance and still records the full obligation, so an empty
// wallet cannot fail a turn.
// The agent's own words, for the REASONING STREAM card. The
// world feed is a DB poll, not a push bus, so a live card can
// only show what was persisted — this is the step output the
// worker already has in hand, attributed to the claw that
// produced it. Truncated because the card renders a tail, not a
// transcript, and mission_events is capped per phase.
if let Some(agent_id) = agent_of.get(&last.node_id).copied() {
let text: String = last.output.chars().take(600).collect();
if !text.trim().is_empty() {
if let Some(mission_id) = sqlx::query_scalar::<_, Option<Uuid>>(
"SELECT mission_id FROM topology_runs WHERE id = $1",
)
.bind(id)
.fetch_optional(&pool)
.await
.ok()
.flatten()
.flatten()
{
let mut ev = crate::mission_events::MissionEvent::new(
mission_id,
"reasoning",
);
ev.agent_id = Some(agent_id);
ev.run_id = Some(id);
ev.target = Some(last.role.clone());
ev.detail = serde_json::json!({ "text": text });
crate::mission_events::record(&pool, ev).await;
}
}
}
if let Some(agent_id) = agent_of.get(&last.node_id).copied() {
if last.tokens > 0 {
// The executor reports ONE total, not an in/out split.