1- import { has , isEqual } from 'lodash'
1+ import { isEqual } from 'lodash'
22
33import { formatToolOutput } from './codebuff-client'
44import { shouldCollapseByDefault } from './constants'
@@ -120,69 +120,79 @@ export interface SpawnAgentResultContent {
120120 hasError : boolean
121121}
122122
123+ /**
124+ * Extracts text content from a Message object's content array.
125+ * Handles assistant messages with TextPart content.
126+ */
127+ const extractTextFromMessageContent = ( content : unknown ) : string => {
128+ if ( ! Array . isArray ( content ) ) {
129+ return ''
130+ }
131+ return content
132+ . filter ( ( part : any ) => part ?. type === 'text' && typeof part ?. text === 'string' )
133+ . map ( ( part : any ) => part . text )
134+ . join ( '' )
135+ }
136+
123137/**
124138 * Extracts displayable content from a spawn_agents result value.
125139 * Handles various nested structures that can come back from agent spawns.
126140 */
127141export const extractSpawnAgentResultContent = (
128142 resultValue : unknown ,
129143) : SpawnAgentResultContent => {
144+ // Handle null/undefined
145+ if ( ! resultValue ) {
146+ return { content : '' , hasError : false }
147+ }
148+
149+ // Handle direct string
130150 if ( typeof resultValue === 'string' ) {
131151 return { content : resultValue , hasError : false }
132152 }
133153
134- if ( resultValue && typeof resultValue === 'object' ) {
135- const nestedValue = ( resultValue as any ) . value
136-
137- if ( typeof nestedValue === 'string' ) {
138- return { content : nestedValue , hasError : false }
139- }
140-
141- if ( nestedValue && typeof nestedValue === 'object' ) {
142- if ( has ( nestedValue , 'errorMessage' ) && ( nestedValue as any ) . errorMessage ) {
143- return {
144- content : String ( ( nestedValue as any ) . errorMessage ) ,
145- hasError : true ,
146- }
147- }
154+ if ( typeof resultValue !== 'object' ) {
155+ return { content : '' , hasError : false }
156+ }
148157
149- if ( has ( nestedValue , 'message' ) && ( nestedValue as any ) . message ) {
150- return { content : String ( ( nestedValue as any ) . message ) , hasError : false }
151- }
152- }
158+ const obj = resultValue as Record < string , unknown >
153159
154- if (
155- typeof resultValue === 'object' &&
156- Object . keys ( resultValue as Record < string , unknown > ) . length === 0
157- ) {
158- return { content : '' , hasError : false }
159- }
160+ // Handle empty object
161+ if ( Object . keys ( obj ) . length === 0 ) {
162+ return { content : '' , hasError : false }
163+ }
160164
161- // Handle error messages from failed agent spawns
162- if ( has ( resultValue , 'errorMessage' ) && ( resultValue as any ) . errorMessage ) {
163- return {
164- content : String ( ( resultValue as any ) . errorMessage ) ,
165- hasError : true ,
166- }
167- }
165+ // Handle error messages (check both top-level and nested)
166+ if ( obj . errorMessage ) {
167+ return { content : String ( obj . errorMessage ) , hasError : true }
168+ }
169+ if ( ( obj . value as any ) ?. errorMessage ) {
170+ return { content : String ( ( obj . value as any ) . errorMessage ) , hasError : true }
171+ }
168172
169- // Handle nested value structure like { type: "lastMessage", value: "..." }
170- if (
171- has ( resultValue , 'value' ) &&
172- ( resultValue as any ) . value &&
173- typeof ( resultValue as any ) . value === 'string'
174- ) {
175- return { content : ( resultValue as any ) . value , hasError : false }
176- }
173+ // Handle lastMessage output mode: { type: "lastMessage", value: [Message array] }
174+ // This is common for agents like researcher-web
175+ if ( obj . type === 'lastMessage' && Array . isArray ( obj . value ) ) {
176+ const messages = obj . value as Array < { role ?: string ; content ?: unknown } >
177+ const textContent = messages
178+ . filter ( ( msg ) => msg ?. role === 'assistant' )
179+ . map ( ( msg ) => extractTextFromMessageContent ( msg ?. content ) )
180+ . filter ( Boolean )
181+ . join ( '\n' )
182+ return { content : textContent , hasError : false }
183+ }
177184
178- // Handle message field
179- if ( has ( resultValue , 'message' ) && ( resultValue as any ) . message ) {
180- return { content : ( resultValue as any ) . message , hasError : false }
181- }
185+ // Handle nested string value: { value: "..." }
186+ if ( typeof obj . value === 'string' ) {
187+ return { content : obj . value , hasError : false }
182188 }
183189
184- if ( ! resultValue ) {
185- return { content : '' , hasError : false }
190+ // Handle message field (top-level or nested)
191+ if ( obj . message ) {
192+ return { content : String ( obj . message ) , hasError : false }
193+ }
194+ if ( ( obj . value as any ) ?. message ) {
195+ return { content : String ( ( obj . value as any ) . message ) , hasError : false }
186196 }
187197
188198 // Fallback to formatted output
@@ -224,6 +234,10 @@ export interface CreateAgentBlockOptions {
224234 agentType : string
225235 prompt ?: string
226236 params ?: Record < string , unknown >
237+ /** The spawn_agents tool call ID that created this block */
238+ spawnToolCallId ?: string
239+ /** The index within the spawn_agents call */
240+ spawnIndex ?: number
227241}
228242
229243/**
@@ -232,7 +246,7 @@ export interface CreateAgentBlockOptions {
232246export const createAgentBlock = (
233247 options : CreateAgentBlockOptions ,
234248) : AgentContentBlock => {
235- const { agentId, agentType, prompt, params } = options
249+ const { agentId, agentType, prompt, params, spawnToolCallId , spawnIndex } = options
236250 return {
237251 type : 'agent' ,
238252 agentId,
@@ -243,6 +257,8 @@ export const createAgentBlock = (
243257 blocks : [ ] as ContentBlock [ ] ,
244258 initialPrompt : prompt || '' ,
245259 ...( params && { params } ) ,
260+ ...( spawnToolCallId && { spawnToolCallId } ) ,
261+ ...( spawnIndex !== undefined && { spawnIndex } ) ,
246262 ...( shouldCollapseByDefault ( agentType || '' ) && { isCollapsed : true } ) ,
247263 }
248264}
0 commit comments