β¨ feat: CC subagent rendering via task + Thread#13928
Conversation
Claude Code marks subagent events (Agent-tool spawn) with
`parent_tool_use_id` pointing back at the outer Agent tool_use. The
adapter used to flatten these, which broke the main-agent step tracker:
each subagent turn introduced a new `message.id`, which the adapter read
as "new main-agent step" and forced a stream_end + stream_start(newStep),
producing orphan assistant bubbles and double-counted usage.
- `ToolCallPayload.parentToolCallId?: string` carries the pointer through
to downstream consumers (`packages/heterogeneous-agents/src/types.ts`).
- `claudeCode.ts` reads `raw.parent_tool_use_id` and:
* skips the main-agent newStep transition on subagent message.id changes
* skips turn_metadata usage emission for subagent events (the `result`
event has the authoritative total and would double-count otherwise)
* drops subagent text / reasoning in this adapter pass (the subagent's
final answer is packaged into the outer Agent tool_result; verified
against a real CC trace where 76 subagent assistant events carried
only tool_use content, zero text / thinking)
* stamps `parentToolCallId` onto subagent tool_use payloads
- 5 new unit tests cover: lineage propagation, no newStep for subagent
message.id changes, no turn_metadata emission, text/reasoning drop,
main-agent resuming step boundary after subagent completes, and
subagent tool_result passthrough.
Refs LOBE-7260 (Layer 1). Layer 2 (executor β task + Thread) follows in
subsequent commits on this branch.
When the CC stream emits an `Agent` tool_use (a subagent spawn), the
executor now creates a `role:'task'` placeholder in the main topic plus
an Isolation Thread with clientMode=true, seeded with a user message
carrying the subagent's prompt and an empty assistant bubble. Subagent
child tool_use events (those with `parentToolCallId`) persist as
`role:'tool'` messages inside that Thread, so they never pollute the main
assistant bubble's tools[] and the whole subagent flow renders through
the existing `ClientTaskItem` / `TaskMessages` accordion β no bespoke UI.
Mapping from CC stream β LobeHub task/thread:
Agent tool_use ββΆ task msg (metadata.taskTitle/instruction/
targetAgentId=subagent_type, tool_call_id)
+ Thread (Isolation, clientMode, Processing)
+ thread assistant bubble (hosts child tools[])
subagent child tool_use ββΆ role:'tool' msg in thread scope,
3-phase persistence mirrors main-agent path
subagent tool_result ββΆ updates the thread tool msg's content
outer Agent tool_result ββΆ thread β Completed, task.metadata.taskDetail
filled (status/duration/totalToolCalls),
thread assistant content β synthesized final
result error / Stop ββΆ in-flight threads marked Failed / Cancel
Dispatch is done inside `persistQueue` so the lookup on `subagentTasks`
sees the post-creation state β the tool_result for the Agent tool arrives
on the next raw line and would race the async task+thread setup otherwise.
4 new tests cover: task+thread creation on Agent tool_use, child tool_use
routing into thread scope (and absence from main assistant's tools[]),
finalize-on-tool_result (completed status + taskDetail population + final
text written to thread bubble), and Failed status on error.
Refs LOBE-7260.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Reportβ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## canary #13928 +/- ##
==========================================
+ Coverage 66.92% 66.94% +0.01%
==========================================
Files 2061 2061
Lines 175803 176063 +260
Branches 20735 20767 +32
==========================================
+ Hits 117660 117869 +209
- Misses 58019 58070 +51
Partials 124 124
Flags with carried forward coverage won't be shown. Click here to find out more.
π New features to boost your workflow:
|
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a548c6175
βΉοΈ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with π.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| persistQueue = persistQueue.then(async () => { | ||
| if (subagentTasks.has(spawn.id)) return; | ||
| const sub = await createSubagentTaskAndThread( | ||
| spawn, | ||
| currentAssistantMessageId, |
There was a problem hiding this comment.
Prevent subagent spawn jobs from running after cancel
This queued callback can still create a new task/thread after the operation is cancelled because it has no abort check. In that timing window (user stops right after Agent tool_use), the cancel hook only marks currently tracked subagentTasks, then this job runs later, creates a fresh thread, and no later events will finalize/cancel it because onRawLine drops post-cancel lines. The result is a task left stuck in processing.
Useful? React with πΒ / π.
|
|
||
| // 4. Best-effort: make sure the thread is marked Processing even if the | ||
| // server createThreadWithMessage didn't forward the status field. | ||
| void threadService.updateThread(threadId, { status: ThreadStatus.Processing }).catch(() => {}); |
There was a problem hiding this comment.
Avoid racing terminal status with fire-and-forget update
This updateThread(...Processing) is fired without awaiting, but later finalizeSubagentTask does an awaited updateThread(...Completed/Failed). If this earlier request resolves later, it can overwrite the terminal status back to processing (the thread model performs plain last-write-wins updates), leaving completed tasks shown as still running.
Useful? React with πΒ / π.
Instead of a standalone ChatItem, CC Agent-tool spawns now render as a compact attached block nested under the spawning assistant: header with subagent_type tag, tool count and duration, followed by the thread's TaskMessages content. Task/thread data layer stays unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* β¨ feat(heterogeneous-agents): preserve CC subagent lineage in adapter
Claude Code tags subagent events (Agent / Task tool spawns) with
parent_tool_use_id pointing back at the outer tool_use. The adapter
used to flatten these, breaking the main-agent step tracker β each
subagent turn introduces a NEW message.id, which the adapter read as
"new main-agent step" and forced stream_end + stream_start(newStep),
producing orphan assistant bubbles and double-counted usage.
- ToolCallPayload.parentToolCallId carries the pointer to downstream
consumers so they can group subagent inner tools under their parent.
- claudeCode.ts reads raw.parent_tool_use_id and:
* skips main-agent step boundary on subagent message.id changes
* skips model tracking for subagent events (the result event has
the authoritative usage, would double-count otherwise)
* drops subagent text / reasoning in this adapter pass β the
subagent's final answer is delivered via the outer tool_result;
verified against a real CC trace where 76 subagent assistant
events carried only tool_use, zero text / thinking
* stamps parentToolCallId onto subagent tool_use payloads
- 6 new unit tests cover lineage propagation, no newStep for subagent
message.id changes, no turn_metadata emission, text/reasoning drop,
main-agent resuming step boundary, and subagent tool_result
passthrough.
Refs LOBE-7319, LOBE-7260
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* β¨ feat(types): foundation types for CC Task block (LOBE-7392)
Sets up the data shape for rendering CC subagent spawns as inline
`task` blocks inside the parent assistantGroup, replacing the
role:'task' message intermediary that was previously proposed in
PR #13928. Pure data layer β no DB schema migration, no new
columns.
- TaskBlock + AssistantContentBlock.tasks?: derived view that the
MessageTransformer will populate by joining Threads onto the
parent message's tool_use entries (follow-up commit). Carries
threadId, subagentType, description, status β enough for the
folded inline header without re-fetching the thread on every
render pass.
- ThreadMetadata gains sourceToolCallId, subagentType, description.
sourceToolCallId disambiguates parallel subagents that share a
sourceMessageId (one assistant turn can spawn multiple Task
tool_uses in one batch).
- CreateThreadParams.id + zod schema field + thread router
passthrough lets clients allocate the threadId synchronously
before the create mutation resolves. The CC adapter emits
Task tool_use synchronously while the create call is async, so
having the id up-front lets us persist subagent inner messages
with the right threadId without a queue or blocking the stream.
- ClaudeCodeApiName.Task + TaskArgs match the CC tool_use shape
(description, prompt, subagent_type) so executor / renderer can
type the input safely.
Refs LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* β»οΈ refactor: extract subagent assistant handler + drop ThreadMetadata.description
Two review-feedback cleanups on the LOBE-7392 foundation:
1. **Adapter β early-return + shared helper.** The main-agent path no
longer carries `if (!isSubagentEvent)` guards; subagent events short-
circuit into a dedicated `handleSubagentAssistant` that only extracts
`tool_use` blocks, and both paths share a new `emitToolChunk` helper
for the `tools_calling` + `tool_start` emission. Adding a new
subagent suppress-rule (no model / no text / no step) now lives in
one method instead of sprinkling guards across the main handler.
2. **ThreadMetadata β drop `description`, use `Thread.title`.** Thread
already has a `title` column; storing the CC Task `description`
input there is the canonical spot and removes the redundant metadata
field. `TaskBlock.description` is collapsed into `TaskBlock.title`
(single source), and the MessageTransformer will populate it from
`thread.title` at read time. Also adds `status?: ThreadStatus` on
`TaskBlock` so the renderer gets the processing / completed / failed
state without a separate lookup.
Behavior unchanged β all 56 adapter tests still pass.
Refs LOBE-7392, LOBE-7319
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* π fix(thread-router): translate id-collision into CONFLICT error
ThreadModel.create uses onConflictDoNothing() and returns undefined
when a caller-provided id collides with an existing row. With the
new client-side id passthrough (introduced in 16d7326 to let the
CC subagent executor allocate threadId synchronously), the original
router would silently insert a follow-up message with
threadId: undefined and return { threadId: undefined } β a data-
integrity regression flagged in PR review.
Translates the model's undefined return into TRPCError(CONFLICT) at
the router boundary so callers see an explicit error and can
regenerate their id and retry. The model layer is untouched β
onConflictDoNothing remains the right primitive for server-generated
ids where collisions are unreachable; the new validation only
applies when the router is the entry point.
- ensureThreadCreated helper extracted; both createThread and
createThreadWithMessage routes funnel through it
- New thread model tests document the conflict behavior and
caller-provided id passthrough that the router relies on (16/16
pass)
Refs LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* π feat(chat-minimap): user-message peek with in-place hover preview
- Filter ticks to user messages; fall back to last user when viewport is on assistant reply
- Replace per-tick popovers with one in-place panel that crossfades from rail center
- Drop arrow nav buttons (hover panel makes them redundant)
- Smooth sqrt width curve (5β16px) so short messages cluster naturally
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* π style(claude-code-todo): chip-style detail in inspector, plain header in render
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* βͺ revert(heterogeneous-agents): pull CC adapter subagent-lineage changes
The CC subagent-lineage adapter work (parent_tool_use_id routing,
parentToolCallId on ToolCallPayload, dedicated handleSubagentAssistant /
emitToolChunk helpers, 6 subagent tests) would ship before the thread
backend changes in this PR are deployed β online flows would see the new
payload field with no server to receive it.
Holding this PR to thread-router + foundation types only. The adapter
work is preserved on feat/lobe-7392-cc-adapter-followup and will ship
as a separate PR after this one is deployed.
Refs LOBE-7392, LOBE-7319
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Agenttool spawns a subagent that runs its own intermediate tools and returns a synthesized answer via the outer tool_result. The adapter used to flatten subagent events, which broke the main-agent step tracker (each subagentmessage.idchange forced astream_end + stream_start(newStep), producing orphan bubbles and double-counted usage).role:'task'+ Thread model (the same one GTD / callAgent use) so it renders throughClientTaskItem/TaskMessagesβ no bespoke UI needed.Approach
Two layers, mirrored in the two commits on this branch:
Layer 1 β adapter preserves CC's lineage marker (
b10f85d)ToolCallPayload.parentToolCallId?: stringclaudeCode.ts:parent_tool_use_idturn_metadatausage emission for subagent events (theresultevent has the authoritative total, avoids double counting)parentToolCallIdonto subagent tool_use payloadsLayer 2 β executor routes
Agentthrough task + Thread (0a548c6)Agenttool_userole:'task'placeholder (taskTitle =input.description, instruction =input.prompt,targetAgentId=input.subagent_type,tool_call_id= toolUseId) + Thread (Isolation,clientMode: true,Processing,sourceMessageId = task msg) + thread assistant bubbletool_userole:'tool'message inside the Thread, 3-phase persistence on the thread's assistanttools[]β never touches the main assistanttool_resulttool_resultCompleted, taskmetadata.taskDetailpopulated (status / duration / totalToolCalls), thread assistant bubble content β synthesized final textresult.is_error/ user StopFailed/Cancel,taskDetailupdated accordinglyDispatch happens inside
persistQueueso the lookup onsubagentTaskssees post-creation state βtool_resultarrives on the next raw line and would race the async task+thread setup otherwise.Trace reference
Verified mapping against
.heerogeneous-tracing/cc-streaming.json(237 ndjson events, 2Γ Agent invocations, 140 subagent events). See LOBE-7319 for the stream-structure doc this PR implements.Test plan
bun run type-checkcleanFixes LOBE-7260
π€ Generated with Claude Code