feat(telemetry): the reasoning stream actually streams
`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:
co-authored by
Claude Opus 5
parent
8be7b3c9b2
commit
bf40d10064
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user