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>
This commit is contained in:
Arvin Xu 2026-04-21 00:17:33 +08:00
parent 8ef2620644
commit 16d73261f9
4 changed files with 92 additions and 0 deletions

View file

@ -17,6 +17,16 @@ export enum ClaudeCodeApiName {
Grep = 'Grep',
Read = 'Read',
Skill = 'Skill',
/**
* Spawns a subagent. CC emits this as a regular `tool_use`; downstream
* events for the subagent's internal turns are tagged with
* `parent_tool_use_id` pointing back at this tool_use's id, and the
* subagent's final answer arrives as the `tool_result` for this id.
* The executor turns this into a Thread (linked via
* `metadata.sourceToolCallId = tool_use.id`) instead of a separate
* `role: 'task'` message.
*/
Task = 'Task',
TodoWrite = 'TodoWrite',
ToolSearch = 'ToolSearch',
Write = 'Write',
@ -60,3 +70,15 @@ export interface ToolSearchArgs {
max_results?: number;
query?: string;
}
/**
* Arguments for CC's built-in `Task` tool. The model fills these in when it
* decides to delegate work to a subagent: the description shows up in the
* folded header, the prompt becomes the subagent's initial user message, and
* `subagent_type` selects which subagent template handles it.
*/
export interface TaskArgs {
description?: string;
prompt?: string;
subagent_type?: string;
}

View file

@ -41,6 +41,43 @@ export interface ChatFileItem {
url: string;
}
/**
* A subagent execution embedded inline in the parent assistant block.
*
* Used for Claude Code's `Task` tool (and equivalent subagent-spawning tools):
* the LLM emits a Task tool_use, the executor creates a Thread to run the
* subagent, and the rendered block shows a folded header + (on expand) the
* Thread's child messages — instead of producing a separate `role: 'task'`
* ChatItem bubble.
*
* Derived view, not persisted: the MessageTransformer reconstructs
* `block.tasks[]` by joining Threads (`threads.sourceMessageId = msg.id`,
* matched by `metadata.sourceToolCallId === tool_use.id`) onto the parent
* message's tool_use entries.
*/
export interface TaskBlock {
/** Description from the spawning tool_use input (e.g. CC Task `description`) */
description?: string;
/** Execution duration in milliseconds (Thread.metadata.duration) */
duration?: number;
/** Error details when subagent failed */
error?: any;
/** Equals the parent tool_use id that spawned this subagent */
id: string;
/** Subagent type, e.g. CC's `subagent_type` input (Explore, Plan, ...) */
subagentType?: string;
/** Status of the underlying Thread */
threadId: string;
/** Title pulled from the thread (defaults to description) */
title?: string;
/** Total cost in dollars */
totalCost?: number;
/** Total tokens consumed */
totalTokens?: number;
/** Total tool calls made by the subagent */
totalToolCalls?: number;
}
export interface AssistantContentBlock {
content: string;
error?: ChatMessageError | null;
@ -50,6 +87,13 @@ export interface AssistantContentBlock {
metadata?: Record<string, any>;
performance?: ModelPerformance;
reasoning?: ModelReasoning;
/**
* Subagent executions embedded inline. Disambiguated from regular tools
* because each task carries a Thread reference and renders as a folded
* panel (showing the Thread's child messages on expand) instead of a
* standalone tool result.
*/
tasks?: TaskBlock[];
tools?: ChatToolPayloadWithResult[];
usage?: ModelUsage;
}

View file

@ -34,14 +34,26 @@ export interface ThreadMetadata {
clientMode?: boolean;
/** Task completion time */
completedAt?: string;
/** Description of the task (e.g. CC Task tool's `description` input) */
description?: string;
/** Execution duration in milliseconds */
duration?: number;
/** Error details when task failed */
error?: any;
/** Operation ID for tracking */
operationId?: string;
/**
* The specific tool_use id within `sourceMessageId` that spawned this thread.
* Used to position the thread inline as a `task` block within the parent
* message's content stream — e.g. CC's `Task` tool_use spawning a subagent.
* Multiple threads can share the same `sourceMessageId` (parallel subagents),
* disambiguated by this field.
*/
sourceToolCallId?: string;
/** Task start time, used to calculate duration */
startedAt?: string;
/** Subagent type identifier, e.g. CC's `subagent_type` input (Explore, Plan, ...) */
subagentType?: string;
/** Total cost in dollars */
totalCost?: number;
/** Total messages created during execution */
@ -77,6 +89,14 @@ export interface CreateThreadParams {
agentId?: string;
/** Group ID for group chat context */
groupId?: string;
/**
* Optional client-provided id. Lets the caller derive the thread id
* synchronously (e.g. when wiring CC subagent threads from the stream
* adapter, where the id needs to be known before the create call returns
* so subagent inner messages can be persisted with the right `threadId`).
* Falls back to the schema's `idGenerator` when omitted.
*/
id?: string;
/** Initial metadata for the thread */
metadata?: ThreadMetadata;
parentThreadId?: string;
@ -91,10 +111,13 @@ export interface CreateThreadParams {
export const threadMetadataSchema = z.object({
clientMode: z.boolean().optional(),
completedAt: z.string().optional(),
description: z.string().optional(),
duration: z.number().optional(),
error: z.any().optional(),
operationId: z.string().optional(),
sourceToolCallId: z.string().optional(),
startedAt: z.string().optional(),
subagentType: z.string().optional(),
totalCost: z.number().optional(),
totalMessages: z.number().optional(),
totalTokens: z.number().optional(),
@ -104,6 +127,7 @@ export const threadMetadataSchema = z.object({
export const createThreadSchema = z.object({
agentId: z.string().optional(),
groupId: z.string().optional(),
id: z.string().optional(),
metadata: threadMetadataSchema.optional(),
parentThreadId: z.string().optional(),
sourceMessageId: z.string().optional(),

View file

@ -22,6 +22,7 @@ const threadProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
export const threadRouter = router({
createThread: threadProcedure.input(createThreadSchema).mutation(async ({ input, ctx }) => {
const thread = await ctx.threadModel.create({
id: input.id,
metadata: input.metadata,
parentThreadId: input.parentThreadId,
sourceMessageId: input.sourceMessageId,
@ -40,6 +41,7 @@ export const threadRouter = router({
)
.mutation(async ({ input, ctx }) => {
const thread = await ctx.threadModel.create({
id: input.id,
metadata: input.metadata,
parentThreadId: input.parentThreadId,
sourceMessageId: input.sourceMessageId,