Workers tab: full transcript viewer, live SSE streaming, introspection tool#192
Workers tab: full transcript viewer, live SSE streaming, introspection tool#192
Conversation
… worker list and detail views, transcript handling, and integration with live context
…ipt and improved error handling for missing data
…components for displaying tool call counts
… worker completion message format
…ripts and recent runs
…users to cancel running workers and update documentation to include cancellation instructions
…badge rendering; update cancellation logging in channel
… and improve message rendering logic
WalkthroughThis PR extends worker management with transcript persistence, live status tracking, and new API endpoints. It introduces database migrations for storing worker transcripts and tool call counts, enriches events with metadata fields, adds frontend components for browsing and inspecting workers, implements a new worker_inspect tool, and wires transcript (de)serialization throughout the backend. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Frontend as AgentWorkers UI
participant API as Workers API
participant DB as Database
participant ToolServer as Tool Server
participant WorkerInspectTool as worker_inspect Tool
User->>Frontend: Request worker list
Frontend->>API: GET /agents/workers?agentId=...
API->>DB: list_worker_runs(agentId, status_filter)
DB-->>API: WorkerRunRow[]
API->>API: Merge live status from channel StatusBlocks
API-->>Frontend: WorkerListResponse
Frontend-->>User: Display worker list with live indicators
User->>Frontend: Click worker for detail
Frontend->>API: GET /agents/workers/detail?agentId=...&workerId=...
API->>DB: get_worker_detail(workerId)
DB-->>API: WorkerDetailRow (with compressed transcript)
API->>API: Decompress transcript blob
API-->>Frontend: WorkerDetailResponse
Frontend->>Frontend: Render transcript steps + tool calls
Frontend-->>User: Display worker details & transcript
User->>ToolServer: Invoke worker_inspect tool
ToolServer->>WorkerInspectTool: call({worker_id: "...", limit: 10})
WorkerInspectTool->>DB: list_worker_runs or get_worker_detail
DB-->>WorkerInspectTool: Worker data
WorkerInspectTool->>WorkerInspectTool: Format transcript summary
WorkerInspectTool-->>ToolServer: WorkerInspectOutput (summary text)
ToolServer-->>User: Display transcript summary in chat
sequenceDiagram
participant Worker as Worker Process
participant RunLogger as ProcessRunLogger
participant DB as SQLite Database
participant EventBus as Event Bus / SSE
rect rgba(100, 150, 200, 0.5)
Note over Worker,EventBus: Worker Lifecycle Logging
end
Worker->>RunLogger: log_worker_started(worker_id, task, worker_type, agent_id)
RunLogger->>DB: INSERT worker_runs (worker_id, agent_id, worker_type, task, ...)
Worker->>EventBus: Emit ProcessEvent::WorkerStarted {worker_type, ...}
loop Worker executing
Worker->>RunLogger: log_tool_started(tool_name, args)
RunLogger->>EventBus: Emit ProcessEvent::ToolStarted {args (truncated), ...}
Worker->>RunLogger: log_tool_completed(tool_name, result)
RunLogger->>EventBus: Emit ProcessEvent::ToolCompleted {result, ...}
end
Worker->>RunLogger: persist_transcript(history)
RunLogger->>RunLogger: serialize_transcript() → gzip compress
RunLogger->>DB: UPDATE worker_runs SET transcript=blob, tool_calls=count
Worker->>RunLogger: log_worker_completed(worker_id, result, success)
RunLogger->>DB: UPDATE worker_runs SET status=(...), result=..., completed_at=...
RunLogger->>EventBus: Emit ProcessEvent::WorkerComplete {success, ...}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes This PR introduces substantial new functionality across frontend and backend: a 622-line AgentWorkers component, 221-line worker_inspect tool, 184-line workers API module, and extensive event system enrichment. The changes span multiple concerns (transcripts, live status, new endpoints, tool integration) with significant logic density, particularly around transcript serialization, live status merging, and component state management. The heterogeneous nature of frontend UI composition, backend database queries, event propagation, and tool integration demands careful reasoning across each subsystem. Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
# Conflicts: # src/api/agents.rs # src/tools.rs
…dump tests - Introduced ProcessRunLogger to both dump_branch_context and dump_all_contexts functions. - Updated branch tool server creation to include the new logger for improved process tracking.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
interface/src/routes/ChannelDetail.tsx (1)
80-101:⚠️ Potential issue | 🟡 MinorAvoid nesting the Cancel button inside the worker link.
Placing a
<button>inside a<Link>(anchor) is invalid HTML and can cause accidental navigation or keyboard accessibility issues when cancelling. Move the cancel control outside the link (or use a non-interactive container for navigation).💡 One way to separate the link and the cancel action
- <Link - to="/agents/$agentId/workers" - params={{ agentId }} - search={{ worker: item.id }} - className="block rounded-md bg-amber-500/10 px-3 py-2 transition-colors hover:bg-amber-500/15" - > - <div className="flex min-w-0 items-center gap-2 overflow-hidden"> - <div className="h-2 w-2 animate-pulse rounded-full bg-amber-400" /> - <span className="text-sm font-medium text-amber-300">Worker</span> - <span className="min-w-0 flex-1 truncate text-sm text-ink-dull">{item.task}</span> - <CancelButton onClick={() => { api.cancelProcess(channelId, "worker", item.id).catch(console.warn); }} /> - </div> - <div className="mt-1 flex min-w-0 items-center gap-3 overflow-hidden pl-4 text-tiny text-ink-faint"> - <span className="truncate">{live.status}</span> - {live.currentTool && ( - <span className="truncate text-amber-400/70">{live.currentTool}</span> - )} - {live.toolCalls > 0 && ( - <span>{live.toolCalls} tool calls</span> - )} - </div> - </Link> + <div className="rounded-md bg-amber-500/10 px-3 py-2 transition-colors hover:bg-amber-500/15"> + <Link + to="/agents/$agentId/workers" + params={{ agentId }} + search={{ worker: item.id }} + className="block" + > + <div className="flex min-w-0 items-center gap-2 overflow-hidden"> + <div className="h-2 w-2 animate-pulse rounded-full bg-amber-400" /> + <span className="text-sm font-medium text-amber-300">Worker</span> + <span className="min-w-0 flex-1 truncate text-sm text-ink-dull">{item.task}</span> + </div> + <div className="mt-1 flex min-w-0 items-center gap-3 overflow-hidden pl-4 text-tiny text-ink-faint"> + <span className="truncate">{live.status}</span> + {live.currentTool && ( + <span className="truncate text-amber-400/70">{live.currentTool}</span> + )} + {live.toolCalls > 0 && ( + <span>{live.toolCalls} tool calls</span> + )} + </div> + </Link> + <div className="mt-2 flex justify-end"> + <CancelButton onClick={() => { api.cancelProcess(channelId, "worker", item.id).catch(console.warn); }} /> + </div> + </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@interface/src/routes/ChannelDetail.tsx` around lines 80 - 101, The CancelButton is currently nested inside the Link (the <Link ...> block rendering the worker) which is invalid and can cause accidental navigation; move the <CancelButton ... /> out of the Link so the Link only wraps the navigational content (the divs showing worker info), render the CancelButton as a sibling (e.g. immediately after the Link) and keep its onClick using api.cancelProcess(channelId, "worker", item.id).catch(console.warn); to preserve behavior, and add an onClick handler that calls event.stopPropagation() (and ensure it is a real <button> with an accessible aria-label) so clicking cancel won’t trigger the surrounding navigation.
🧹 Nitpick comments (2)
src/agent/channel.rs (1)
1448-1486: Duplication with theskipped && is_retriggerfallback block above (lines 1406–1437).Both branches perform identical logic: trim →
extract_reply_from_tool_syntax→ derive source →normalize_discord_mention_tokens→ log bot message → send. Consider extracting asend_retrigger_fallback(&self, response: &str)helper to DRY this up.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/channel.rs` around lines 1448 - 1486, The retrigger fallback logic (trim → extract_reply_from_tool_syntax → derive source → normalize_discord_mention_tokens → log bot message → send via response_tx) is duplicated; refactor by adding a private helper method send_retrigger_fallback(&self, response: &str) that encapsulates the flow (use extract_reply_from_tool_syntax, crate::tools::reply::normalize_discord_mention_tokens, self.state.conversation_logger.log_bot_message, and self.response_tx.send) and call it from both the skipped && is_retrigger block and the else if is_retrigger block to remove duplication while preserving the existing logging and error handling behavior.src/tools/worker_inspect.rs (1)
34-34:JsonSchemaderive onWorkerInspectArgsis unused.The
definition()method manually constructs the JSON schema (lines 65-80), so theJsonSchemaderive onWorkerInspectArgsis dead code. Either useschemars::schema_for!(WorkerInspectArgs)insidedefinition()to stay DRY, or remove the derive.Also applies to: 61-82
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/worker_inspect.rs` at line 34, The JsonSchema derive on WorkerInspectArgs is redundant because definition() builds the schema manually; fix by either removing the JsonSchema derive and any unused schemars imports, or (preferred) use schemars::schema_for!(WorkerInspectArgs) inside WorkerInspectArgs::definition() to return the generated schema and remove the handcrafted schema block—ensure you update imports to include schemars::schema_for and remove now-unused manual schema construction (references: WorkerInspectArgs, definition(), JsonSchema).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@interface/src/components/WebChatPanel.tsx`:
- Around line 177-218: The SSE-sourced messages in sseMessages are not
deduplicated against the current messages array, causing duplicate IDs in
allMessages; fix this by filtering out any sseMessages whose id already exists
in messages before composing allMessages (e.g. compute a Set of message ids from
messages and either call
setSseMessages(prev=>prev.filter(m=>!messageIdSet.has(m.id))) inside the
messages effect or derive const filteredSse =
sseMessages.filter(m=>!messageIdSet.has(m.id)); then use const allMessages =
[...messages, ...filteredSse]; reference sseMessages, setSseMessages, messages,
and allMessages to locate the change.
In `@interface/src/routes/AgentWorkers.tsx`:
- Around line 103-175: The merge logic for mergedWorkers and mergedDetail uses
global activeWorkers and can surface SSE-only workers from other agents;
restrict activeWorkers to the current agent before overlay/synthesis by checking
agent affiliation (e.g., live.agent_id === currentAgentId or
channelAgentMap[live.channelId] === currentAgentId). Update the mergedWorkers
mapping and synthetic creation (symbols: mergedWorkers, activeWorkers, workers)
to filter Object.values(activeWorkers) and the overlay lookup
(activeWorkers[worker.id]) by agent, and likewise update mergedDetail (symbols:
mergedDetail, detailData, selectedWorkerId, activeWorkers) to only consider live
when the live entry matches the currentAgentId so synthesized detail views
cannot show cross-agent workers.
In `@src/agent/worker.rs`:
- Around line 479-511: persist_transcript currently serializes only the
in-memory history so transcripts become partial after compact_history drains
older messages; update the logic so drained messages are preserved and included
in what persist_transcript serializes—either (A) add a persistent/owned
transcript buffer (e.g., a Vec or String in the worker state) that
compact_history appends drained messages to before removing them, and then have
persist_transcript call
crate::conversation::worker_transcript::serialize_transcript on that combined
buffer + current history, or (B) call persist_transcript (or a new
persist_drained_messages helper that uses serialize_transcript) immediately
before compact_history removes messages so the drained messages are persisted to
worker_runs; modify persist_transcript, compact_history, and where
serialize_transcript is invoked to use this preserved transcript source instead
of only the history slice.
In `@src/api/workers.rs`:
- Around line 81-87: Clamp and validate pagination inputs before calling
logger.list_worker_runs: ensure limit is constrained to the range [0, 200] (e.g.
take query.limit, clamp to 0.min..200.max) and ensure offset is non‑negative
(clamp query.offset to >= 0) then convert to the expected numeric type for
list_worker_runs; replace the current let limit = query.limit.min(200) with
these validated values and pass the validated limit and offset into
list_worker_runs.
In `@src/conversation/history.rs`:
- Around line 453-487: The count query uses a WHERE template that expects the
status at parameter ?4 while count_q only binds two params, causing status
filtering to fail; update the code to use separate WHERE clauses or parameter
ordering for count and list (e.g., introduce where_clause_count = "WHERE
w.agent_id = ?1 AND w.status = ?2" and keep where_clause_list = "WHERE
w.agent_id = ?1 AND w.status = ?4"), then build count_query from
where_clause_count and bind agent_id then filter to count_q (while leaving
list_query/ list_q binding unchanged), or alternatively switch to named
parameters so count_q and list_q bind the same logical parameters correctly
(modify count_query/count_q and list_query/list_q accordingly to ensure the
status_filter value is bound to the positional placeholder used in that query).
In `@src/conversation/worker_transcript.rs`:
- Around line 45-53: The serialize_transcript function silently swallows
serialization and compression errors; change its signature from pub fn
serialize_transcript(history: &[rig::message::Message]) -> Vec<u8> to return a
Result (e.g. -> Result<Vec<u8>, Box<dyn std::error::Error>> or
anyhow::Result<Vec<u8>>), replace serde_json::to_vec(...).unwrap_or_default()
with let json = serde_json::to_vec(&steps)?;, replace
encoder.write_all(&json).ok() with encoder.write_all(&json)?;, and replace
encoder.finish().unwrap_or_default() with let out = encoder.finish()?; then
return Ok(out); update any callers of serialize_transcript accordingly so errors
are propagated or logged.
In `@src/hooks/spacebot.rs`:
- Around line 195-204: The send on the broadcast channel is silently discarding
the Result via "let _ = self.event_tx.send(event);"; update the send to
explicitly call .ok() on the Result to follow the guideline for channel sends.
Locate the block that constructs ProcessEvent::ToolStarted (uses capped_args
from crate::tools::truncate_output and fields
agent_id/process_id/channel_id/tool_name) and replace the "let _ =
self.event_tx.send(event);" pattern with "self.event_tx.send(event).ok();" so
the Result is explicitly ignored in the approved way.
In `@src/tools/worker_inspect.rs`:
- Around line 137-142: The code truncates with a byte slice (&text[..500]) which
panics on multi-byte UTF-8 boundaries; replace that byte-slice truncation with a
char-safe truncation (e.g., build the preview with
text.chars().take(500).collect::<String>() or compute a char-aware byte index
via char_indices) and use that result when formatting the display variable while
still keeping text.len() (or text.as_bytes().len()) for the total byte count if
desired; update the block that constructs display (the variable named display
using text) to avoid any direct byte-indexing of text.
---
Outside diff comments:
In `@interface/src/routes/ChannelDetail.tsx`:
- Around line 80-101: The CancelButton is currently nested inside the Link (the
<Link ...> block rendering the worker) which is invalid and can cause accidental
navigation; move the <CancelButton ... /> out of the Link so the Link only wraps
the navigational content (the divs showing worker info), render the CancelButton
as a sibling (e.g. immediately after the Link) and keep its onClick using
api.cancelProcess(channelId, "worker", item.id).catch(console.warn); to preserve
behavior, and add an onClick handler that calls event.stopPropagation() (and
ensure it is a real <button> with an accessible aria-label) so clicking cancel
won’t trigger the surrounding navigation.
---
Nitpick comments:
In `@src/agent/channel.rs`:
- Around line 1448-1486: The retrigger fallback logic (trim →
extract_reply_from_tool_syntax → derive source →
normalize_discord_mention_tokens → log bot message → send via response_tx) is
duplicated; refactor by adding a private helper method
send_retrigger_fallback(&self, response: &str) that encapsulates the flow (use
extract_reply_from_tool_syntax,
crate::tools::reply::normalize_discord_mention_tokens,
self.state.conversation_logger.log_bot_message, and self.response_tx.send) and
call it from both the skipped && is_retrigger block and the else if is_retrigger
block to remove duplication while preserving the existing logging and error
handling behavior.
In `@src/tools/worker_inspect.rs`:
- Line 34: The JsonSchema derive on WorkerInspectArgs is redundant because
definition() builds the schema manually; fix by either removing the JsonSchema
derive and any unused schemars imports, or (preferred) use
schemars::schema_for!(WorkerInspectArgs) inside WorkerInspectArgs::definition()
to return the generated schema and remove the handcrafted schema block—ensure
you update imports to include schemars::schema_for and remove now-unused manual
schema construction (references: WorkerInspectArgs, definition(), JsonSchema).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (28)
interface/src/api/client.tsinterface/src/components/WebChatPanel.tsxinterface/src/hooks/useLiveContext.tsxinterface/src/router.tsxinterface/src/routes/AgentWorkers.tsxinterface/src/routes/ChannelDetail.tsxmigrations/20260223000001_worker_transcript.sqlmigrations/20260224000001_worker_tool_calls.sqlprompts/en/channel.md.j2prompts/en/tools/worker_inspect_description.md.j2src/agent/channel.rssrc/agent/ingestion.rssrc/agent/worker.rssrc/api.rssrc/api/agents.rssrc/api/server.rssrc/api/state.rssrc/api/workers.rssrc/conversation.rssrc/conversation/history.rssrc/conversation/worker_transcript.rssrc/hooks/spacebot.rssrc/lib.rssrc/main.rssrc/prompts/text.rssrc/tools.rssrc/tools/worker_inspect.rstests/context_dump.rs
…ndling and deduplication - Updated WebChatPanel to use useMemo for deduplicating messages from SSE and API. - Enhanced LiveContextProvider to include agentId in activeWorkers and optimized the retrieval of active workers based on agentId. - Adjusted AgentWorkers to utilize scopedActiveWorkers for better performance and clarity. - Modified worker history management in the Rust backend to improve context handling and transcript persistence. - Improved API endpoints for listing and retrieving worker details to ensure proper agent context is maintained.
Summary
/agents/$agentId/workersreplaces the "coming soon" placeholder. Left panel lists all worker runs across channels, right panel shows full detail with transcript.worker_runsat completion. Typical 30-message transcript compresses from ~15-50KB to ~3-8KB.worker_inspecttool on branches lets the LLM retrieve a worker's full transcript to verify what it actually did. Useful for debugging thin results or answering "what did you check?"POST /channels/cancelAPI.[Worker {uuid} completed]: {result}) so the channel LLM can reference specific workers.cancelandworker_inspecttools.Backend
New files:
migrations/20260223000001_worker_transcript.sql—worker_type,agent_id,transcriptcolumnsmigrations/20260224000001_worker_tool_calls.sql—tool_callscolumnsrc/conversation/worker_transcript.rs— serialize/deserialize Rig history to gzipped transcript blobsrc/api/workers.rs— list + detail endpoints with StatusBlock live mergesrc/tools/worker_inspect.rs— branch tool for transcript introspectionKey changes:
ProcessEvent::WorkerStartednow carriesworker_type,WorkerCompletecarriessuccessProcessEvent::ToolStartednow carriesargs,ApiEvent::ToolCompletednow forwardsresult— enables live transcript on the frontendWorker::run()persists transcript blob on all exit paths (success, failure, cancellation)log_worker_completedwrites'done'vs'failed'status based on the newsuccessfield (was always'done'before)[System:compaction markersFrontend
New files:
interface/src/routes/AgentWorkers.tsx— two-column worker viewerKey changes:
useLiveContextnow exposesactiveWorkers(flat map),workerEventVersion(SSE counter),liveTranscripts(per-worker transcript accumulator from SSE tool events)MarkdowncomponentNote
This PR adds a complete workers visibility layer with full transcript persistence and real-time streaming. Users can now inspect worker execution in detail from a dedicated tab, and branches can introspect worker transcripts programmatically for debugging and verification. The 29-file diff spans frontend/backend event streaming, database schema, API endpoints, and new introspection tooling.
Written by Tembo for commit 859f643.