@@ -9,6 +9,86 @@ import type { StepText, ToolCall } from '../types/agent-definition'
99
1010type FilePickerMode = 'default' | 'max'
1111
12+ /**
13+ * Type guard to check if value is a non-null object.
14+ * DUPLICATE: Keep in sync with copies inside handleStepsDefault and handleStepsMax (required for serialization).
15+ */
16+ function isObject ( value : unknown ) : value is Record < string , unknown > {
17+ return value !== null && typeof value === 'object'
18+ }
19+
20+ /**
21+ * Extracts spawn results from tool result array, returning agent values.
22+ * DUPLICATE: Keep in sync with copies inside handleStepsDefault and handleStepsMax (required for serialization).
23+ */
24+ function extractSpawnResults ( results : unknown [ ] | undefined ) : unknown [ ] {
25+ if ( ! results || results . length === 0 ) return [ ]
26+ const jsonResult = results . find (
27+ ( r ) : r is { type : 'json' ; value : unknown } =>
28+ isObject ( r ) && r . type === 'json' ,
29+ )
30+ if ( ! jsonResult ?. value ) return [ ]
31+ const spawnedResults = Array . isArray ( jsonResult . value )
32+ ? jsonResult . value
33+ : [ jsonResult . value ]
34+ // Each spawned result may be an object with a .value property (spawn wrapper)
35+ // or the agent output directly (type: 'lastMessage' or type: 'error')
36+ return spawnedResults
37+ . map ( ( result : unknown ) => {
38+ if ( ! isObject ( result ) ) return undefined
39+ // If it's a spawn wrapper with .value, extract the value
40+ if ( 'value' in result && result . type !== 'lastMessage' && result . type !== 'error' ) {
41+ return result . value
42+ }
43+ // Otherwise it's the agent output directly
44+ return result
45+ } )
46+ . filter ( Boolean )
47+ }
48+
49+ /**
50+ * Extracts the most recent assistant text from an agent's output.
51+ * DUPLICATE: Keep in sync with copies inside handleStepsDefault and handleStepsMax (required for serialization).
52+ */
53+ function extractLastMessageText ( agentOutput : unknown ) : string | null {
54+ if ( ! isObject ( agentOutput ) ) return null
55+ if ( agentOutput . type !== 'lastMessage' || ! Array . isArray ( agentOutput . value ) ) {
56+ return null
57+ }
58+ for ( let i = agentOutput . value . length - 1 ; i >= 0 ; i -- ) {
59+ const message = agentOutput . value [ i ]
60+ if (
61+ isObject ( message ) &&
62+ message . role === 'assistant' &&
63+ Array . isArray ( message . content )
64+ ) {
65+ for ( const part of message . content ) {
66+ if (
67+ isObject ( part ) &&
68+ part . type === 'text' &&
69+ typeof part . text === 'string'
70+ ) {
71+ return part . text
72+ }
73+ }
74+ }
75+ }
76+ return null
77+ }
78+
79+ /**
80+ * Extracts error message from agent output if present.
81+ * DUPLICATE: Keep in sync with copies inside handleStepsDefault and handleStepsMax (required for serialization).
82+ */
83+ function extractErrorMessage ( agentOutput : unknown ) : string | null {
84+ if ( ! isObject ( agentOutput ) ) return null
85+ if ( agentOutput . type === 'error' ) {
86+ if ( typeof agentOutput . message === 'string' ) return agentOutput . message
87+ if ( typeof agentOutput . value === 'string' ) return agentOutput . value
88+ }
89+ return null
90+ }
91+
1292export const createFilePicker = (
1393 mode : FilePickerMode ,
1494) : Omit < SecretAgentDefinition , 'id' > => {
@@ -67,6 +147,71 @@ const handleStepsDefault: SecretAgentDefinition['handleSteps'] = function* ({
67147 prompt,
68148 params,
69149} ) {
150+ // ============================================================================
151+ // Helper functions duplicated inside generator for sandbox serialization.
152+ // DUPLICATE: Keep in sync with module-level versions.
153+ // ============================================================================
154+ function isObject ( value : unknown ) : value is Record < string , unknown > {
155+ return value !== null && typeof value === 'object'
156+ }
157+
158+ function extractSpawnResults ( results : unknown [ ] | undefined ) : unknown [ ] {
159+ if ( ! results || results . length === 0 ) return [ ]
160+ const jsonResult = results . find (
161+ ( r ) : r is { type : 'json' ; value : unknown } =>
162+ isObject ( r ) && r . type === 'json' ,
163+ )
164+ if ( ! jsonResult ?. value ) return [ ]
165+ const spawnedResults = Array . isArray ( jsonResult . value )
166+ ? jsonResult . value
167+ : [ jsonResult . value ]
168+ return spawnedResults
169+ . map ( ( result : unknown ) => {
170+ if ( ! isObject ( result ) ) return undefined
171+ if ( 'value' in result && result . type !== 'lastMessage' && result . type !== 'error' ) {
172+ return result . value
173+ }
174+ return result
175+ } )
176+ . filter ( Boolean )
177+ }
178+
179+ function extractLastMessageText ( agentOutput : unknown ) : string | null {
180+ if ( ! isObject ( agentOutput ) ) return null
181+ if ( agentOutput . type !== 'lastMessage' || ! Array . isArray ( agentOutput . value ) ) {
182+ return null
183+ }
184+ for ( let i = agentOutput . value . length - 1 ; i >= 0 ; i -- ) {
185+ const message = agentOutput . value [ i ]
186+ if (
187+ isObject ( message ) &&
188+ message . role === 'assistant' &&
189+ Array . isArray ( message . content )
190+ ) {
191+ for ( const part of message . content ) {
192+ if (
193+ isObject ( part ) &&
194+ part . type === 'text' &&
195+ typeof part . text === 'string'
196+ ) {
197+ return part . text
198+ }
199+ }
200+ }
201+ }
202+ return null
203+ }
204+
205+ function extractErrorMessage ( agentOutput : unknown ) : string | null {
206+ if ( ! isObject ( agentOutput ) ) return null
207+ if ( agentOutput . type === 'error' ) {
208+ if ( typeof agentOutput . message === 'string' ) return agentOutput . message
209+ if ( typeof agentOutput . value === 'string' ) return agentOutput . value
210+ }
211+ return null
212+ }
213+ // ============================================================================
214+
70215 const { toolResult : fileListerResults } = yield {
71216 toolName : 'spawn_agents' ,
72217 input : {
@@ -120,50 +265,78 @@ const handleStepsDefault: SecretAgentDefinition['handleSteps'] = function* ({
120265
121266 yield 'STEP'
122267
123- function extractSpawnResults ( results : any [ ] | undefined ) : any [ ] {
268+ }
269+
270+ // handleSteps for max mode - spawns 2 file-listers in parallel
271+ const handleStepsMax : SecretAgentDefinition [ 'handleSteps' ] = function * ( {
272+ prompt,
273+ params,
274+ } ) {
275+ // ============================================================================
276+ // Helper functions duplicated inside generator for sandbox serialization.
277+ // DUPLICATE: Keep in sync with module-level versions.
278+ // ============================================================================
279+ function isObject ( value : unknown ) : value is Record < string , unknown > {
280+ return value !== null && typeof value === 'object'
281+ }
282+
283+ function extractSpawnResults ( results : unknown [ ] | undefined ) : unknown [ ] {
124284 if ( ! results || results . length === 0 ) return [ ]
125- const jsonResult = results . find ( ( r ) => r . type === 'json' )
285+ const jsonResult = results . find (
286+ ( r ) : r is { type : 'json' ; value : unknown } =>
287+ isObject ( r ) && r . type === 'json' ,
288+ )
126289 if ( ! jsonResult ?. value ) return [ ]
127290 const spawnedResults = Array . isArray ( jsonResult . value )
128291 ? jsonResult . value
129292 : [ jsonResult . value ]
130- return spawnedResults . map ( ( result : any ) => result ?. value ) . filter ( Boolean )
293+ return spawnedResults
294+ . map ( ( result : unknown ) => {
295+ if ( ! isObject ( result ) ) return undefined
296+ if ( 'value' in result && result . type !== 'lastMessage' && result . type !== 'error' ) {
297+ return result . value
298+ }
299+ return result
300+ } )
301+ . filter ( Boolean )
131302 }
132303
133- function extractLastMessageText ( agentOutput : any ) : string | null {
134- if ( ! agentOutput ) return null
135- if (
136- agentOutput . type === 'lastMessage' &&
137- Array . isArray ( agentOutput . value )
138- ) {
139- for ( let i = agentOutput . value . length - 1 ; i >= 0 ; i -- ) {
140- const message = agentOutput . value [ i ]
141- if ( message . role === 'assistant' && Array . isArray ( message . content ) ) {
142- for ( const part of message . content ) {
143- if ( part . type === 'text' && typeof part . text === 'string' ) {
144- return part . text
145- }
304+ function extractLastMessageText ( agentOutput : unknown ) : string | null {
305+ if ( ! isObject ( agentOutput ) ) return null
306+ if ( agentOutput . type !== 'lastMessage' || ! Array . isArray ( agentOutput . value ) ) {
307+ return null
308+ }
309+ for ( let i = agentOutput . value . length - 1 ; i >= 0 ; i -- ) {
310+ const message = agentOutput . value [ i ]
311+ if (
312+ isObject ( message ) &&
313+ message . role === 'assistant' &&
314+ Array . isArray ( message . content )
315+ ) {
316+ for ( const part of message . content ) {
317+ if (
318+ isObject ( part ) &&
319+ part . type === 'text' &&
320+ typeof part . text === 'string'
321+ ) {
322+ return part . text
146323 }
147324 }
148325 }
149326 }
150327 return null
151328 }
152329
153- function extractErrorMessage ( agentOutput : any ) : string | null {
154- if ( ! agentOutput ) return null
330+ function extractErrorMessage ( agentOutput : unknown ) : string | null {
331+ if ( ! isObject ( agentOutput ) ) return null
155332 if ( agentOutput . type === 'error' ) {
156- return agentOutput . message ?? agentOutput . value ?? null
333+ if ( typeof agentOutput . message === 'string' ) return agentOutput . message
334+ if ( typeof agentOutput . value === 'string' ) return agentOutput . value
157335 }
158336 return null
159337 }
160- }
338+ // ============================================================================
161339
162- // handleSteps for max mode - spawns 2 file-listers in parallel
163- const handleStepsMax : SecretAgentDefinition [ 'handleSteps' ] = function * ( {
164- prompt,
165- params,
166- } ) {
167340 const { toolResult : fileListerResults } = yield {
168341 toolName : 'spawn_agents' ,
169342 input : {
@@ -221,44 +394,6 @@ const handleStepsMax: SecretAgentDefinition['handleSteps'] = function* ({
221394 }
222395
223396 yield 'STEP'
224-
225- function extractSpawnResults ( results : any [ ] | undefined ) : any [ ] {
226- if ( ! results || results . length === 0 ) return [ ]
227- const jsonResult = results . find ( ( r ) => r . type === 'json' )
228- if ( ! jsonResult ?. value ) return [ ]
229- const spawnedResults = Array . isArray ( jsonResult . value )
230- ? jsonResult . value
231- : [ jsonResult . value ]
232- return spawnedResults . map ( ( result : any ) => result ?. value ) . filter ( Boolean )
233- }
234-
235- function extractLastMessageText ( agentOutput : any ) : string | null {
236- if ( ! agentOutput ) return null
237- if (
238- agentOutput . type === 'lastMessage' &&
239- Array . isArray ( agentOutput . value )
240- ) {
241- for ( let i = agentOutput . value . length - 1 ; i >= 0 ; i -- ) {
242- const message = agentOutput . value [ i ]
243- if ( message . role === 'assistant' && Array . isArray ( message . content ) ) {
244- for ( const part of message . content ) {
245- if ( part . type === 'text' && typeof part . text === 'string' ) {
246- return part . text
247- }
248- }
249- }
250- }
251- }
252- return null
253- }
254-
255- function extractErrorMessage ( agentOutput : any ) : string | null {
256- if ( ! agentOutput ) return null
257- if ( agentOutput . type === 'error' ) {
258- return agentOutput . message ?? agentOutput . value ?? null
259- }
260- return null
261- }
262397}
263398
264399const definition : SecretAgentDefinition = {
0 commit comments