UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

152 lines (140 loc) • 4.74 kB
import { splitSubagentWire, subagentWireText, wireSubagentRunId, } from '../../../utilities/subagent-wire' import type { SubagentWireGroup } from '../../../utilities/subagent-wire' import type { ModelMessage, RunAgentResumeItem, SubagentPart, SubagentStatus, UIMessage, } from '../../../types' type AnyMessage = UIMessage | ModelMessage export interface SubagentTurnChild { subagentRunId: string name: string status: SubagentStatus parentToolCallId?: string /** The child's own messages from the earlier run. */ messages: Array<AnyMessage> /** Text the child wrote. */ text: string /** Resume entries for this child and its nested children. */ resume: Array<RunAgentResumeItem> } /** The interrupted turn that a resume continues. */ export interface SubagentTurn { /** * Messages up to and including the last top-level user message. Child wire * messages do not count. */ before: Array<AnyMessage> /** Direct children of the parent in that turn. */ children: Array<SubagentTurnChild> /** Resume entries that no child owns. They belong to the parent run. */ rest: Array<RunAgentResumeItem> /** The router plan of the earlier run, from the children's metadata. */ plan?: unknown } /** Key of the router plan in `SUBAGENT_STARTED` metadata (`metadata.tanstack`). */ export const SUBAGENT_PLAN_KEY = 'subagentPlan' function planOf(metadata: Record<string, unknown> | undefined): unknown { const tanstack = metadata?.tanstack if (typeof tanstack !== 'object' || tanstack === null) return undefined return (tanstack as Record<string, unknown>)[SUBAGENT_PLAN_KEY] } function uiText(messages: ReadonlyArray<UIMessage>) { return messages .flatMap((message) => message.role === 'assistant' ? message.parts.flatMap((part) => part.type === 'text' && part.content.trim() !== '' ? [part.content.trim()] : [], ) : [], ) .join('\n\n') } function partInterruptIds(part: SubagentPart): Array<string> { return [ ...(part.subagent.interruptIds ?? []), ...part.subagent.messages.flatMap((message) => message.parts.flatMap((nested) => nested.type === 'subagent' ? partInterruptIds(nested) : [], ), ), ] } function groupInterruptIds(group: SubagentWireGroup<unknown>): Array<string> { return [ ...(group.info.interruptIds ?? []), ...splitSubagentWire(group.messages).groups.flatMap(groupInterruptIds), ] } /** * Find the children of the trailing assistant turn that `resume` answers. * Returns undefined when no child owns a resume entry. */ export function readSubagentTurn( messages: ReadonlyArray<AnyMessage>, resume: ReadonlyArray<RunAgentResumeItem> | undefined, ): SubagentTurn | undefined { if (!resume || resume.length === 0) return undefined const lastUser = messages.findLastIndex( (message) => message.role === 'user' && wireSubagentRunId(message) === undefined, ) const before = messages.slice(0, lastUser + 1) const turn = messages.slice(lastUser + 1) const found: Array<Omit<SubagentTurnChild, 'resume'> & { ids: Set<string> }> = [] let plan: unknown for (const message of turn) { if (!('parts' in message)) continue for (const part of message.parts) { if (part.type !== 'subagent') continue plan ??= planOf(part.subagent.metadata) found.push({ subagentRunId: part.subagent.id, name: part.subagent.name, status: part.subagent.status, ...(part.subagent.parentToolCallId !== undefined && { parentToolCallId: part.subagent.parentToolCallId, }), messages: part.subagent.messages, text: uiText(part.subagent.messages), ids: new Set(partInterruptIds(part)), }) } } for (const group of splitSubagentWire(turn).groups) { plan ??= planOf(group.info.metadata) found.push({ subagentRunId: group.id, name: group.info.name, status: group.info.status, ...(group.info.parentToolCallId !== undefined && { parentToolCallId: group.info.parentToolCallId, }), messages: group.messages, text: subagentWireText(group.messages), ids: new Set(groupInterruptIds(group)), }) } const owned = new Set<string>() const children = found.map(({ ids, ...child }) => { const entries = resume.filter((entry) => ids.has(entry.interruptId)) for (const entry of entries) owned.add(entry.interruptId) return { ...child, resume: entries } }) if (owned.size === 0) return undefined return { before, children, rest: resume.filter((entry) => !owned.has(entry.interruptId)), ...(plan !== undefined && { plan }), } }