11import { spawn } from "node:child_process" ;
2- import { existsSync } from "node:fs" ;
3- import { mkdir , readFile , writeFile } from "node:fs/promises" ;
2+ import { mkdir , readFile , stat , writeFile } from "node:fs/promises" ;
43import { join } from "node:path" ;
54import { randomUUID } from "node:crypto" ;
65import type { ChildProcess } from "node:child_process" ;
7- import type { ExecutorAdapter , RunHandle } from "@devagent-runner/core" ;
8- import { RunnerError } from "@devagent-runner/core" ;
6+ import type { ExecutorAdapter , RunHandle , RunStatus } from "@devagent-runner/core" ;
7+ import { RunnerError , TrackedRunHandle } from "@devagent-runner/core" ;
8+ import { validateTaskExecutionEvent , validateTaskExecutionResult } from "@devagent-sdk/validation" ;
99import type {
1010 ArtifactKind ,
1111 ArtifactRef ,
@@ -19,12 +19,21 @@ import { PROTOCOL_VERSION } from "@devagent-sdk/types";
1919async function waitForFile ( path : string , timeoutMs = 500 ) : Promise < boolean > {
2020 const deadline = Date . now ( ) + timeoutMs ;
2121 while ( Date . now ( ) <= deadline ) {
22- if ( existsSync ( path ) ) {
22+ if ( await fileExists ( path ) ) {
2323 return true ;
2424 }
2525 await new Promise ( ( resolve ) => setTimeout ( resolve , 25 ) ) ;
2626 }
27- return existsSync ( path ) ;
27+ return fileExists ( path ) ;
28+ }
29+
30+ async function fileExists ( path : string ) : Promise < boolean > {
31+ try {
32+ await stat ( path ) ;
33+ return true ;
34+ } catch {
35+ return false ;
36+ }
2837}
2938
3039function artifactFileName ( kind : ArtifactKind ) : string {
@@ -67,33 +76,17 @@ function artifactTitle(taskType: TaskExecutionRequest["taskType"]): string {
6776 return taskType [ 0 ] ! . toUpperCase ( ) + taskType . slice ( 1 ) ;
6877}
6978
70- class ProcessRunHandle implements RunHandle {
71- private currentStatus : "running" | "success" | "failed" | "cancelled" = "running" ;
72- readonly pid : number | undefined ;
73-
79+ class ProcessRunHandle extends TrackedRunHandle {
7480 constructor (
7581 readonly id : string ,
7682 private readonly child : ChildProcess ,
77- private readonly resultPromise : Promise < TaskExecutionResult > ,
83+ resultPromise : Promise < TaskExecutionResult > ,
7884 ) {
79- this . pid = child . pid ?? undefined ;
80- void this . resultPromise . then ( ( result ) => {
81- this . currentStatus = result . status ;
82- } ) . catch ( ( ) => {
83- this . currentStatus = "failed" ;
84- } ) ;
85- }
86-
87- status ( ) : "running" | "success" | "failed" | "cancelled" {
88- return this . currentStatus ;
89- }
90-
91- wait ( ) : Promise < TaskExecutionResult > {
92- return this . resultPromise ;
85+ super ( id , child . pid ?? undefined , resultPromise ) ;
9386 }
9487
9588 async cancel ( ) : Promise < void > {
96- this . currentStatus = "cancelled" ;
89+ this . markCancelled ( ) ;
9790 this . child . kill ( "SIGTERM" ) ;
9891 }
9992}
@@ -155,7 +148,7 @@ async function createFallbackResult(
155148}
156149
157150function errorForStatus (
158- status : TaskExecutionResult [ "status" ] ,
151+ status : RunStatus ,
159152 message : string ,
160153) : TaskExecutionResult [ "error" ] | undefined {
161154 if ( status === "success" ) {
@@ -335,7 +328,7 @@ export class DevAgentAdapter implements ExecutorAdapter {
335328 const lines = chunk . toString ( ) . split ( "\n" ) . filter ( ( line : string ) => line . trim ( ) ) ;
336329 for ( const line of lines ) {
337330 try {
338- onEvent ( JSON . parse ( line ) as TaskExecutionEvent ) ;
331+ onEvent ( validateTaskExecutionEvent ( JSON . parse ( line ) ) ) ;
339332 } catch {
340333 onEvent ( {
341334 protocolVersion : PROTOCOL_VERSION ,
@@ -364,19 +357,17 @@ export class DevAgentAdapter implements ExecutorAdapter {
364357
365358 const resultPromise = new Promise < TaskExecutionResult > ( ( resolve , reject ) => {
366359 child . once ( "error" , ( error : Error ) => reject ( new RunnerError ( "PROCESS_LAUNCH_FAILED" , error . message ) ) ) ;
367- let exitCode : number | null = null ;
368360 let exitSignal : NodeJS . Signals | null = null ;
369361
370- child . once ( "exit" , ( code : number | null , signal : NodeJS . Signals | null ) => {
371- exitCode = code ;
362+ child . once ( "exit" , ( _code : number | null , signal : NodeJS . Signals | null ) => {
372363 exitSignal = signal ;
373364 } ) ;
374365
375366 child . once ( "close" , async ( ) => {
376367 try {
377368 const resultPath = join ( artifactDir , "result.json" ) ;
378369 if ( ! await waitForFile ( resultPath ) ) {
379- const status = exitSignal === "SIGTERM" ? "cancelled" : "failed" ;
370+ const status : RunStatus = exitSignal === "SIGTERM" ? "cancelled" : "failed" ;
380371 const fallback = await createFallbackResult (
381372 request ,
382373 artifactDir ,
@@ -388,10 +379,34 @@ export class DevAgentAdapter implements ExecutorAdapter {
388379 exitSignal === "SIGTERM" ? "Cancelled by operator" : ( stderr || "Missing result.json" ) ,
389380 ) ,
390381 ) ;
382+ if ( status === "failed" ) {
383+ onEvent ( {
384+ protocolVersion : PROTOCOL_VERSION ,
385+ type : "log" ,
386+ at : new Date ( ) . toISOString ( ) ,
387+ taskId : request . taskId ,
388+ stream : "stderr" ,
389+ message : stderr || "DevAgent did not emit result.json" ,
390+ } as TaskExecutionEvent ) ;
391+ onEvent ( {
392+ protocolVersion : PROTOCOL_VERSION ,
393+ type : "artifact" ,
394+ at : new Date ( ) . toISOString ( ) ,
395+ taskId : request . taskId ,
396+ artifact : fallback . artifacts [ 0 ] ! ,
397+ } as TaskExecutionEvent ) ;
398+ onEvent ( {
399+ protocolVersion : PROTOCOL_VERSION ,
400+ type : "completed" ,
401+ at : new Date ( ) . toISOString ( ) ,
402+ taskId : request . taskId ,
403+ status,
404+ } as TaskExecutionEvent ) ;
405+ }
391406 resolve ( fallback ) ;
392407 return ;
393408 }
394- const parsed = JSON . parse ( await readFile ( resultPath , "utf-8" ) ) as TaskExecutionResult ;
409+ const parsed = validateTaskExecutionResult ( JSON . parse ( await readFile ( resultPath , "utf-8" ) ) ) ;
395410 resolve ( parsed ) ;
396411 } catch ( error ) {
397412 reject ( error ) ;
@@ -421,7 +436,7 @@ export class CodexAdapter extends CliPromptAdapter {
421436 ] ,
422437 parseOutput : async ( stdout , artifactDir ) => {
423438 const lastMessagePath = join ( artifactDir , "last-message.txt" ) ;
424- if ( existsSync ( lastMessagePath ) ) {
439+ if ( await fileExists ( lastMessagePath ) ) {
425440 return readFile ( lastMessagePath , "utf-8" ) ;
426441 }
427442 return stdout ;
0 commit comments