@@ -29,6 +29,7 @@ import {
2929 WORKFLOW_LIMITS ,
3030 resolveWorkflowConcurrency ,
3131 type WorkflowEventRecord ,
32+ type WorkflowAgentCallRecord ,
3233 type WorkflowRunRecord ,
3334 type WorkflowRunSource ,
3435} from "./workflow-types.js" ;
@@ -62,6 +63,12 @@ export async function runWorkflowCommand(
6263 case "list" :
6364 await runWorkflowList ( config ) ;
6465 return ;
66+ case "calls" :
67+ await runWorkflowCalls ( rest , config ) ;
68+ return ;
69+ case "call" :
70+ await runWorkflowCall ( rest , config ) ;
71+ return ;
6572 case "__worker" :
6673 await runWorkflowWorker ( rest , config ) ;
6774 return ;
@@ -82,30 +89,38 @@ export function printWorkflowHelp(): void {
8289 "DevSpace workflows" ,
8390 "" ,
8491 "Usage:" ,
85- " devspace workflow run ( --file <path> | --name <name> | --resume <runId>) " ,
92+ " devspace workflow run [ --file|--script-path <path> | --name <name>] [ --resume <runId>] " ,
8693 " [--arg key=value]... [--follow]" ,
8794 " devspace workflow status <runId> [--follow]" ,
8895 " devspace workflow cancel <runId>" ,
8996 " devspace workflow ls" ,
97+ " devspace workflow calls <runId>" ,
98+ " devspace workflow call <runId> <callIndex>" ,
9099 ] . join ( "\n" ) ,
91100 ) ;
92101}
93102
94103async function runWorkflowRun ( args : string [ ] , config : ServerConfig ) : Promise < void > {
95104 const { flags } = splitFlags ( args ) ;
96105 const follow = flags . has ( "follow" ) ;
97- const file = flagValue ( flags , "file" ) ;
106+ const file = flagValue ( flags , "script-path" ) ?? flagValue ( flags , " file") ;
98107 const name = flagValue ( flags , "name" ) ;
99108 const resumeFrom = flagValue ( flags , "resume" ) ;
100109 const parsedArgs = parseWorkflowArgFlagsResult ( collectArgTokens ( args ) ) ;
101110 if ( parsedArgs . isErr ( ) ) throw parsedArgs . error ;
102111 const workflowArgs = parsedArgs . value . args ;
103112
113+ if ( file && name ) {
114+ throw new InvalidWorkflowInputError ( {
115+ code : "ambiguous_source" ,
116+ message : "Provide only one of --file/--script-path or --name" ,
117+ } ) ;
118+ }
104119 if ( ! file && ! name && ! resumeFrom ) {
105120 throw new InvalidWorkflowInputError ( {
106121 code : "missing_source" ,
107122 message :
108- "Usage: devspace workflow run ( --file <path> | --name <name> | --resume <runId>) " ,
123+ "Usage: devspace workflow run [ --file|--script-path <path> | --name <name>] [ --resume <runId>] " ,
109124 } ) ;
110125 }
111126
@@ -125,13 +140,20 @@ async function runWorkflowRun(args: string[], config: ServerConfig): Promise<voi
125140 const prior = priorResult . value ;
126141 if ( ! prior ) throw new WorkflowNotFoundError ( resumeFrom ) ;
127142 priorRunId = prior . id ;
128- priorScriptPath = prior . scriptPath ;
129- const resolvedResult = await readWorkflowScriptFileResult ( prior . scriptPath ) ;
130- if ( resolvedResult . isErr ( ) ) throw resolvedResult . error ;
131- const resolved = resolvedResult . value ;
143+ const overrideResult = file || name
144+ ? await resolveWorkflowScriptFromPathOrNameResult ( {
145+ file,
146+ name,
147+ workspaceRoot,
148+ stateDir : config . stateDir ,
149+ } )
150+ : await readWorkflowScriptFileResult ( prior . scriptPath ) ;
151+ if ( overrideResult . isErr ( ) ) throw overrideResult . error ;
152+ const resolved = overrideResult . value ;
132153 source = resolved . source ;
133- scriptHash = prior . scriptHash ;
134- nameHint = prior . name ;
154+ scriptHash = resolved . scriptHash ;
155+ nameHint = "nameHint" in resolved ? resolved . nameHint : prior . name ;
156+ priorScriptPath = file ?? prior . scriptPath ;
135157 runSource = "resume" ;
136158 if ( ! Object . keys ( workflowArgs ) . length && prior . argsJson && prior . argsJson !== "null" ) {
137159 try {
@@ -157,14 +179,14 @@ async function runWorkflowRun(args: string[], config: ServerConfig): Promise<voi
157179 }
158180
159181 const parsed = parseWorkflowScript ( source , {
160- filename : priorScriptPath ?? file ?? name ?? "workflow:inline" ,
182+ filename : file ?? priorScriptPath ?? name ?? "workflow:inline" ,
161183 } ) ;
162184 const baseSha = await resolveWorkspaceHead ( workspaceRoot ) ;
163185
164186 const run = store . createRun ( {
165187 name : parsed . meta . name || nameHint ,
166188 source : runSource ,
167- scriptPath : priorScriptPath ?? "pending" ,
189+ scriptPath : "pending" ,
168190 scriptHash,
169191 workspaceRoot,
170192 workspaceId : process . env . DEVSPACE_WORKSPACE_ID ,
@@ -173,19 +195,16 @@ async function runWorkflowRun(args: string[], config: ServerConfig): Promise<voi
173195 baseSha,
174196 } ) ;
175197
176- let persisted = priorScriptPath ;
177- if ( ! persisted ) {
178- const result = await persistWorkflowScriptResult ( {
179- stateDir : config . stateDir ,
180- runId : run . id ,
181- source,
182- preferredName : parsed . meta . name || nameHint ,
183- } ) ;
184- if ( result . isErr ( ) ) throw result . error ;
185- persisted = result . value ;
186- const updated = store . setScriptPathResult ( run . id , persisted ) ;
187- if ( updated . isErr ( ) ) throw updated . error ;
188- }
198+ const result = await persistWorkflowScriptResult ( {
199+ stateDir : config . stateDir ,
200+ runId : run . id ,
201+ source,
202+ preferredName : parsed . meta . name || nameHint ,
203+ } ) ;
204+ if ( result . isErr ( ) ) throw result . error ;
205+ const persisted = result . value ;
206+ const updated = store . setScriptPathResult ( run . id , persisted ) ;
207+ if ( updated . isErr ( ) ) throw updated . error ;
189208
190209 spawnWorkflowWorkerFromCli (
191210 run . id ,
@@ -214,6 +233,7 @@ async function runWorkflowStatus(args: string[], config: ServerConfig): Promise<
214233 const run = runResult . value ;
215234 if ( ! run ) throw new WorkflowNotFoundError ( runId ) ;
216235 console . log ( formatRunLine ( run ) ) ;
236+ console . log ( formatCallSummary ( store . listAgentCalls ( runId ) ) ) ;
217237 if ( follow ) {
218238 await followRun ( store , runId ) ;
219239 return ;
@@ -279,6 +299,40 @@ async function runWorkflowList(config: ServerConfig): Promise<void> {
279299 }
280300}
281301
302+ async function runWorkflowCalls ( args : string [ ] , config : ServerConfig ) : Promise < void > {
303+ const runId = args [ 0 ] ;
304+ if ( ! runId ) throw new Error ( "Usage: devspace workflow calls <runId>" ) ;
305+ const store = createWorkflowStore ( config ) ;
306+ try {
307+ if ( ! store . getRun ( runId ) ) throw new WorkflowNotFoundError ( runId ) ;
308+ const calls = store . listAgentCalls ( runId ) ;
309+ if ( calls . length === 0 ) {
310+ console . log ( "No workflow agent calls." ) ;
311+ return ;
312+ }
313+ for ( const call of calls ) console . log ( formatCallLine ( call ) ) ;
314+ } finally {
315+ store . close ( ) ;
316+ }
317+ }
318+
319+ async function runWorkflowCall ( args : string [ ] , config : ServerConfig ) : Promise < void > {
320+ const runId = args [ 0 ] ;
321+ const callIndex = Number ( args [ 1 ] ) ;
322+ if ( ! runId || ! Number . isInteger ( callIndex ) || callIndex < 0 ) {
323+ throw new Error ( "Usage: devspace workflow call <runId> <callIndex>" ) ;
324+ }
325+ const store = createWorkflowStore ( config ) ;
326+ try {
327+ if ( ! store . getRun ( runId ) ) throw new WorkflowNotFoundError ( runId ) ;
328+ const call = store . getAgentCall ( runId , callIndex ) ;
329+ if ( ! call ) throw new Error ( `Unknown workflow agent call: ${ runId } #${ callIndex } ` ) ;
330+ console . log ( JSON . stringify ( formatCallDetail ( call ) , null , 2 ) ) ;
331+ } finally {
332+ store . close ( ) ;
333+ }
334+ }
335+
282336/** Detached worker entry: claim run, heartbeat, execute, complete/fail. */
283337export async function runWorkflowWorker (
284338 args : string [ ] ,
@@ -501,10 +555,62 @@ function printEvent(event: WorkflowEventRecord): void {
501555}
502556
503557function formatRunLine (
504- run : Pick < WorkflowRunRecord , "id" | "status" | "name" | "error" > ,
558+ run : Pick <
559+ WorkflowRunRecord ,
560+ "id" | "status" | "name" | "error" | "scriptPath" | "scriptHash" | "resumedFromRunId"
561+ > ,
505562) : string {
506563 const err = run . error ? ` error=${ JSON . stringify ( run . error ) } ` : "" ;
507- return `${ run . id } ${ run . status } ${ run . name } ${ err } ` ;
564+ const resumed = run . resumedFromRunId ? ` resumedFrom=${ run . resumedFromRunId } ` : "" ;
565+ return `${ run . id } ${ run . status } ${ run . name } scriptPath=${ JSON . stringify ( run . scriptPath ) } scriptHash=${ run . scriptHash } ${ resumed } ${ err } ` ;
566+ }
567+
568+ function formatCallLine ( call : WorkflowAgentCallRecord ) : string {
569+ const label = call . label ? ` label=${ JSON . stringify ( call . label ) } ` : "" ;
570+ const phase = call . phase ? ` phase=${ JSON . stringify ( call . phase ) } ` : "" ;
571+ const model = call . model ? ` model=${ call . model } ` : "" ;
572+ const duration = callDurationMs ( call ) ;
573+ const replay = call . fromCache
574+ ? ` replay=${ call . replayMatch ?? "cached" } :${ call . replayedFromRunId ?? "?" } #${ call . replayedFromCallIndex ?? "?" } `
575+ : call . replayReason
576+ ? ` replayMiss=${ call . replayReason } `
577+ : "" ;
578+ const worktree = call . worktreePath
579+ ? ` worktree=${ JSON . stringify ( call . worktreePath ) } dirty=${ String ( call . dirty ) } `
580+ : "" ;
581+ return `#${ call . callIndex } ${ call . status } ${ call . provider } ${ model } ${ label } ${ phase } durationMs=${ duration } ${ replay } ${ worktree } ` ;
582+ }
583+
584+ function formatCallSummary ( calls : WorkflowAgentCallRecord [ ] ) : string {
585+ const reused = calls . filter ( ( call ) => call . fromCache ) . length ;
586+ const failed = calls . filter ( ( call ) => call . status === "failed" ) . length ;
587+ const live = calls . filter (
588+ ( call ) => ! call . fromCache && call . status === "completed" ,
589+ ) . length ;
590+ const running = calls . filter ( ( call ) => call . status === "running" ) . length ;
591+ return `calls reused=${ reused } live=${ live } failed=${ failed } running=${ running } total=${ calls . length } ` ;
592+ }
593+
594+ function formatCallDetail ( call : WorkflowAgentCallRecord ) : Record < string , unknown > {
595+ return {
596+ ...call ,
597+ durationMs : callDurationMs ( call ) ,
598+ schema : call . schemaJson ? safeParseJson ( call . schemaJson ) : undefined ,
599+ structured : call . structuredJson ? safeParseJson ( call . structuredJson ) : undefined ,
600+ } ;
601+ }
602+
603+ function callDurationMs ( call : WorkflowAgentCallRecord ) : number | undefined {
604+ if ( ! call . startedAt || ! call . completedAt ) return undefined ;
605+ return Math . max ( 0 , Date . parse ( call . completedAt ) - Date . parse ( call . startedAt ) ) ;
606+ }
607+
608+ function safeParseJson ( text : string ) : unknown {
609+ try {
610+ return JSON . parse ( text ) as unknown ;
611+ } catch {
612+ return text ;
613+ }
508614}
509615
510616function resolveEnabledProviders (
0 commit comments