Skip to content

✨ feat: CC subagent rendering via task + Thread#13928

Closed
arvinxx wants to merge 3 commits into
canaryfrom
feat/lobe-7260-cc-subagent-lineage
Closed

✨ feat: CC subagent rendering via task + Thread#13928
arvinxx wants to merge 3 commits into
canaryfrom
feat/lobe-7260-cc-subagent-lineage

Conversation

@arvinxx

@arvinxx arvinxx commented Apr 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Claude Code's Agent tool 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 subagent message.id change forced a stream_end + stream_start(newStep), producing orphan bubbles and double-counted usage).
  • We now map the whole subagent interaction onto LobeHub's existing role:'task' + Thread model (the same one GTD / callAgent use) so it renders through ClientTaskItem / 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?: string
  • claudeCode.ts:
    • skips main-agent newStep transitions when the event carries parent_tool_use_id
    • skips turn_metadata usage emission for subagent events (the result event has the authoritative total, avoids double counting)
    • stamps parentToolCallId onto subagent tool_use payloads
    • drops subagent text/reasoning at the adapter (verified against a real CC trace: 76 subagent assistant events all carried only tool_use; the subagent's final answer lives in the outer Agent tool_result)
  • 5 new unit tests

Layer 2 β€” executor routes Agent through task + Thread (0a548c6)

CC stream LobeHub mapping
Agent tool_use role:'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 bubble
subagent child tool_use role:'tool' message inside the Thread, 3-phase persistence on the thread's assistant tools[] β€” never touches the main assistant
subagent tool_result updates the thread tool message content
outer Agent tool_result thread β†’ Completed, task metadata.taskDetail populated (status / duration / totalToolCalls), thread assistant bubble content ← synthesized final text
result.is_error / user Stop in-flight threads marked Failed / Cancel, taskDetail updated accordingly

Dispatch happens inside persistQueue so the lookup on subagentTasks sees post-creation state β€” tool_result arrives 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

  • Adapter unit tests: 29 pass (24 existing + 5 subagent lineage)
  • Executor unit tests: 20 pass (16 existing + 4 subagent routing β€” task/thread creation, child tool routing, finalize on Agent tool_result, failed status on error)
  • bun run type-check clean
  • Manual: CC agent spawns subagent via Agent tool β†’ task accordion appears in main bubble β†’ child tool calls collected in thread β†’ final synthesized text appears as the task's "final block"

Fixes LOBE-7260

πŸ€– Generated with Claude Code

arvinxx added 2 commits April 17, 2026 20:16
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.
@vercel

vercel Bot commented Apr 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lobehub Ready Ready Preview, Comment Apr 17, 2026 0:50am

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

Comment thread src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts Fixed
@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.82707% with 51 lines in your changes missing coverage. Please review.
βœ… Project coverage is 66.94%. Comparing base (2298ad8) to head (1970b7f).
⚠️ Report is 38 commits behind head on canary.

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              
Flag Coverage Ξ”
app 59.39% <80.82%> (+0.04%) ⬆️
database 92.42% <ΓΈ> (ΓΈ)
packages/agent-runtime 79.72% <ΓΈ> (ΓΈ)
packages/context-engine 83.22% <ΓΈ> (ΓΈ)
packages/conversation-flow 92.36% <ΓΈ> (ΓΈ)
packages/file-loaders 87.02% <ΓΈ> (ΓΈ)
packages/memory-user-memory 74.74% <ΓΈ> (ΓΈ)
packages/model-bank 99.86% <ΓΈ> (ΓΈ)
packages/model-runtime 84.21% <ΓΈ> (ΓΈ)
packages/prompts 69.08% <ΓΈ> (ΓΈ)
packages/python-interpreter 92.90% <ΓΈ> (ΓΈ)
packages/ssrf-safe-fetch 0.00% <ΓΈ> (ΓΈ)
packages/utils 90.34% <ΓΈ> (ΓΈ)
packages/web-crawler 88.66% <ΓΈ> (ΓΈ)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Ξ”
Store 66.43% <80.82%> (+0.11%) ⬆️
Services 51.78% <ΓΈ> (ΓΈ)
Server 66.93% <ΓΈ> (ΓΈ)
Libs 51.35% <ΓΈ> (ΓΈ)
Utils 91.12% <ΓΈ> (ΓΈ)
πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ 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".

Comment on lines +838 to +842
persistQueue = persistQueue.then(async () => {
if (subagentTasks.has(spawn.id)) return;
const sub = await createSubagentTaskAndThread(
spawn,
currentAssistantMessageId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
* Normalize an Agent tool_result content blob into plain text. CC's
* tool_result content is usually an array of `{ type: 'text', text }` blocks.
*/
const extractSubagentFinalText = (raw: unknown): string => {
@arvinxx arvinxx closed this Apr 20, 2026
arvinxx added a commit that referenced this pull request Apr 20, 2026
* ✨ 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant