77
88import { logger } from '../../core/monitoring/logger.js' ;
99import { STRUCTURED_RESPONSE_SUFFIX } from '../../orchestrators/multimodal/constants.js' ;
10- import { exec } from 'child_process' ;
11- import { promisify } from 'util' ;
10+ import { spawn } from 'child_process' ;
1211import * as fs from 'fs' ;
1312import * as path from 'path' ;
1413import * as os from 'os' ;
@@ -26,8 +25,6 @@ import {
2625import { AnthropicBatchClient } from '../anthropic/batch-client.js' ;
2726import type { BatchRequest } from '../anthropic/batch-client.js' ;
2827
29- const execAsync = promisify ( exec ) ;
30-
3128export interface SubagentRequest {
3229 type :
3330 | 'planning'
@@ -64,8 +61,7 @@ export class ClaudeCodeSubagentClient {
6461 private activeSubagents : Map < string , AbortController > = new Map ( ) ;
6562 private mockMode : boolean ;
6663
67- constructor ( mockMode : boolean = true ) {
68- // Default to mock mode for testing
64+ constructor ( mockMode : boolean = false ) {
6965 this . mockMode = mockMode ;
7066
7167 // Create temp directory for subagent communication
@@ -236,7 +232,8 @@ export class ClaudeCodeSubagentClient {
236232 }
237233
238234 /**
239- * Original CLI-based subagent execution (unchanged behavior)
235+ * Execute subagent via Claude Code CLI (`claude -p --output-format stream-json`).
236+ * Spawns a real Claude Code process with full tool use.
240237 */
241238 private async executeSubagentViaCLI (
242239 request : SubagentRequest ,
@@ -250,34 +247,26 @@ export class ClaudeCodeSubagentClient {
250247 contextFile ,
251248 JSON . stringify ( request . context , null , 2 )
252249 ) ;
253- const resultFile = path . join ( this . tempDir , `${ subagentId } -result.json` ) ;
254- const taskCommand = this . buildTaskCommand (
255- request ,
256- prompt ,
257- contextFile ,
258- resultFile
259- ) ;
260- const result = await this . executeTaskTool ( taskCommand , request . timeout ) ;
261250
262- let subagentResult : any = { } ;
263- if ( fs . existsSync ( resultFile ) ) {
264- const resultContent = await fs . promises . readFile ( resultFile , 'utf-8' ) ;
265- try {
266- subagentResult = JSON . parse ( resultContent ) ;
267- } catch {
268- subagentResult = { rawOutput : resultContent } ;
269- }
270- }
251+ const fullPrompt = `${ prompt } \n\nContext (JSON): ${ JSON . stringify ( request . context ) } ` ;
252+ const result = await this . spawnClaude ( fullPrompt , request . timeout ) ;
271253
272254 this . cleanup ( subagentId ) ;
273255
256+ let parsed : any ;
257+ try {
258+ parsed = JSON . parse ( result . text ) ;
259+ } catch {
260+ parsed = { rawOutput : result . text } ;
261+ }
262+
274263 return {
275264 success : true ,
276- result : subagentResult ,
277- output : result . stdout ,
265+ result : parsed ,
266+ output : result . text ,
278267 duration : Date . now ( ) - startTime ,
279268 subagentType : request . type ,
280- tokens : this . estimateTokens ( prompt + JSON . stringify ( subagentResult ) ) ,
269+ tokens : this . estimateTokens ( fullPrompt + result . text ) ,
281270 } ;
282271 } catch ( error : any ) {
283272 logger . error ( `Subagent CLI execution failed: ${ request . type } ` , {
@@ -461,81 +450,115 @@ export class ClaudeCodeSubagentClient {
461450 }
462451
463452 /**
464- * Build Task tool command
465- * This creates a command that Claude Code's Task tool can execute
453+ * Spawn `claude -p --output-format stream-json` and collect the result.
454+ * Parses stream-json events to extract the final assistant text.
466455 */
467- private buildTaskCommand (
468- request : SubagentRequest ,
456+ private spawnClaude (
469457 prompt : string ,
470- contextFile : string ,
471- resultFile : string
472- ) : string {
473- // Create a script that the subagent will execute
474- const scriptContent = `
475- #!/bin/bash
476- # Subagent execution script for ${ request . type }
477-
478- # Read context
479- CONTEXT=$(cat "${ contextFile } ")
480-
481- # Execute task based on type
482- case "${ request . type } " in
483- "testing")
484- # For testing subagent, actually run tests
485- echo "Generating and running tests..."
486- # The subagent will generate test files and run them
487- ;;
488- "linting")
489- # For linting subagent, run actual linters
490- echo "Running linters..."
491- npm run lint || true
492- ;;
493- "code")
494- # For code generation, create implementation files
495- echo "Generating implementation..."
496- ;;
497- *)
498- # Default behavior
499- echo "Executing ${ request . type } task..."
500- ;;
501- esac
502-
503- # Write result
504- echo '{"status": "completed", "type": "${ request . type } "}' > "${ resultFile } "
505- ` ;
506-
507- const scriptFile = path . join ( this . tempDir , `${ request . type } -script.sh` ) ;
508- fs . writeFileSync ( scriptFile , scriptContent ) ;
509- fs . chmodSync ( scriptFile , '755' ) ;
510-
511- // Return the command that Task tool will execute
512- // In practice, this would trigger Claude Code's Task tool
513- return scriptFile ;
514- }
515-
516- /**
517- * Execute via Task tool (simulated for now)
518- * In production, this would use Claude Code's actual Task tool API
519- */
520- private async executeTaskTool (
521- command : string ,
522458 timeout ?: number
523- ) : Promise < { stdout : string ; stderr : string } > {
524- try {
525- // In production, this would call Claude Code's Task tool
526- // For now, we simulate with a subprocess
527- const result = await execAsync ( command , {
528- timeout : timeout || 300000 , // 5 minutes default
529- maxBuffer : 10 * 1024 * 1024 , // 10MB buffer
459+ ) : Promise < { text : string ; toolUseCount : number } > {
460+ return new Promise ( ( resolve , reject ) => {
461+ const args = [
462+ '-p' ,
463+ '--output-format' ,
464+ 'stream-json' ,
465+ '--dangerously-skip-permissions' ,
466+ prompt ,
467+ ] ;
468+
469+ const claude = spawn ( 'claude' , args , {
470+ cwd : process . cwd ( ) ,
471+ env : { ...process . env } ,
472+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
530473 } ) ;
531474
532- return result ;
533- } catch ( error : any ) {
534- if ( error . killed || error . signal === 'SIGTERM' ) {
535- throw new Error ( `Subagent timeout after ${ timeout } ms` ) ;
536- }
537- throw error ;
538- }
475+ const timeoutMs = timeout || 300000 ; // 5 minutes default
476+ const timer = setTimeout ( ( ) => {
477+ claude . kill ( 'SIGTERM' ) ;
478+ reject ( new Error ( `Subagent timeout after ${ timeoutMs } ms` ) ) ;
479+ } , timeoutMs ) ;
480+
481+ let lastAssistantText = '' ;
482+ let toolUseCount = 0 ;
483+ let lineBuffer = '' ;
484+ let stderr = '' ;
485+
486+ claude . stdout . on ( 'data' , ( chunk : Buffer ) => {
487+ lineBuffer += chunk . toString ( ) ;
488+ const lines = lineBuffer . split ( '\n' ) ;
489+ lineBuffer = lines . pop ( ) || '' ;
490+
491+ for ( const line of lines ) {
492+ if ( ! line . trim ( ) ) continue ;
493+ try {
494+ const event = JSON . parse ( line ) ;
495+
496+ if ( event . type === 'assistant' && event . message ) {
497+ const textBlocks = ( event . message . content || [ ] )
498+ . filter ( ( b : any ) => b . type === 'text' )
499+ . map ( ( b : any ) => b . text ) ;
500+ if ( textBlocks . length > 0 ) {
501+ lastAssistantText = textBlocks . join ( '\n' ) ;
502+ }
503+ const toolBlocks = ( event . message . content || [ ] ) . filter (
504+ ( b : any ) => b . type === 'tool_use'
505+ ) ;
506+ toolUseCount += toolBlocks . length ;
507+ }
508+
509+ if ( event . type === 'result' && event . result ) {
510+ lastAssistantText = event . result ;
511+ }
512+ } catch {
513+ // non-JSON line, ignore
514+ }
515+ }
516+ } ) ;
517+
518+ claude . stderr . on ( 'data' , ( data : Buffer ) => {
519+ stderr += data . toString ( ) ;
520+ } ) ;
521+
522+ claude . on ( 'close' , ( code : number | null ) => {
523+ clearTimeout ( timer ) ;
524+
525+ // Process remaining buffer
526+ if ( lineBuffer . trim ( ) ) {
527+ try {
528+ const event = JSON . parse ( lineBuffer ) ;
529+ if ( event . type === 'result' && event . result ) {
530+ lastAssistantText = event . result ;
531+ }
532+ } catch {
533+ // ignore
534+ }
535+ }
536+
537+ logger . info ( 'Claude subagent completed' , {
538+ code,
539+ toolUseCount,
540+ outputLength : lastAssistantText . length ,
541+ } ) ;
542+
543+ if ( code === 0 && lastAssistantText ) {
544+ resolve ( { text : lastAssistantText , toolUseCount } ) ;
545+ } else if ( code === 0 ) {
546+ resolve ( {
547+ text : '(Claude completed but produced no text output)' ,
548+ toolUseCount,
549+ } ) ;
550+ } else {
551+ reject (
552+ new Error ( `Claude exited code ${ code } : ${ stderr . slice ( 0 , 500 ) } ` )
553+ ) ;
554+ }
555+ } ) ;
556+
557+ claude . on ( 'error' , ( err : Error ) => {
558+ clearTimeout ( timer ) ;
559+ reject ( new Error ( `Failed to spawn claude: ${ err . message } ` ) ) ;
560+ } ) ;
561+ } ) ;
539562 }
540563
541564 /**
0 commit comments