Skip to content

Commit c8f0b65

Browse files
committed
feat(workflow): add script iteration and call inspection
1 parent f9a1dc7 commit c8f0b65

2 files changed

Lines changed: 209 additions & 54 deletions

File tree

src/workflow-cli.ts

Lines changed: 132 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

94103
async 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. */
283337
export async function runWorkflowWorker(
284338
args: string[],
@@ -501,10 +555,62 @@ function printEvent(event: WorkflowEventRecord): void {
501555
}
502556

503557
function 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

510616
function resolveEnabledProviders(

0 commit comments

Comments
 (0)