pi-workflow-engine 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/pi-workflow-engine/index.ts +134 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +276 -24
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +222 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +46 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +83 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +15 -69
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +24 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +200 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +27 -1
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +96 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +66 -4
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +81 -13
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +14 -5
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +238 -0
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +10 -5
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +12 -8
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +6 -2
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +6 -2
- package/README.md +11 -1
- package/USAGE.md +53 -8
- package/package.json +8 -8
|
@@ -3,14 +3,21 @@ import { Type } from "typebox";
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { SelectList, Text, truncateToWidth, type Component, type SelectItem, type SelectListTheme, type TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import type { WorkflowProgressSnapshot } from "./src/progress.ts";
|
|
6
|
-
import type { WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunOptions } from "./src/types.ts";
|
|
6
|
+
import type { WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
|
|
7
7
|
import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
|
|
8
8
|
import type { PerfSink, PerfSnapshot } from "./src/perf.ts";
|
|
9
|
-
import {
|
|
9
|
+
import type { WorkflowUsageSnapshot } from "./src/usage.ts";
|
|
10
10
|
import { registerDynamax } from "./src/dynamax.ts";
|
|
11
11
|
import { handleReviewViewerAction } from "./src/review/review-actions.ts";
|
|
12
12
|
import { decideReviewResultsPresentation, extensionContextMode, maybeShowReviewResultsViewer } from "./src/review/review-results-flow.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
formatWorkflowDetailLines,
|
|
15
|
+
isWorkflowResult,
|
|
16
|
+
renderWorkflowResult,
|
|
17
|
+
type WorkflowPerfDetails,
|
|
18
|
+
type WorkflowResultEnvelope,
|
|
19
|
+
} from "./src/ui/workflow-result-renderer.ts";
|
|
20
|
+
import { parseWorkflowBudgetString, WORKFLOW_BUDGET_MAX, WORKFLOW_BUDGET_MIN } from "./src/options.ts";
|
|
14
21
|
|
|
15
22
|
/** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
|
|
16
23
|
const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -22,15 +29,25 @@ function summarize(result: unknown): string {
|
|
|
22
29
|
return typeof result === "string" ? result : "Workflow finished.";
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
function formatMessageContent(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
function formatMessageContent(
|
|
33
|
+
name: string,
|
|
34
|
+
result: unknown,
|
|
35
|
+
usage?: WorkflowUsageSnapshot,
|
|
36
|
+
perf?: WorkflowPerfDetails,
|
|
37
|
+
metadata?: WorkflowRunMetadata,
|
|
38
|
+
): string {
|
|
39
|
+
const details = formatWorkflowDetailLines({ usage, perf, metadata });
|
|
29
40
|
return `## Workflow: ${name}\n\n${summarize(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
function workflowEnvelope(
|
|
33
|
-
|
|
43
|
+
function workflowEnvelope(
|
|
44
|
+
name: string,
|
|
45
|
+
result: unknown,
|
|
46
|
+
usage?: WorkflowUsageSnapshot,
|
|
47
|
+
perf?: WorkflowPerfDetails,
|
|
48
|
+
metadata?: WorkflowRunMetadata,
|
|
49
|
+
): WorkflowResultEnvelope {
|
|
50
|
+
return { name, result, completedAt: Date.now(), usage, perf, runId: metadata?.runId, resumedFromRunId: metadata?.resumedFromRunId };
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
|
|
@@ -38,12 +55,6 @@ function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDe
|
|
|
38
55
|
return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
function formatPerfLine(perf: WorkflowPerfDetails | undefined): string | undefined {
|
|
42
|
-
if (!perf) return undefined;
|
|
43
|
-
const parts = perf.aggregates.slice(0, 4).map((aggregate) => `${aggregate.name} ${Math.round(aggregate.total)}ms`);
|
|
44
|
-
return parts.length > 0 ? `Perf: ${parts.join(" · ")}` : "Perf: no samples";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
58
|
type DiscoveryModule = typeof import("./src/discovery.ts");
|
|
48
59
|
type EngineModule = typeof import("./src/engine.ts");
|
|
49
60
|
type InlineWorkflowModule = typeof import("./src/inline-workflow.ts");
|
|
@@ -216,6 +227,7 @@ export interface WorkflowInvocation {
|
|
|
216
227
|
args: string;
|
|
217
228
|
options: WorkflowRunOptions;
|
|
218
229
|
refreshDiscovery?: boolean;
|
|
230
|
+
optionErrors?: string[];
|
|
219
231
|
authorBrief?: string;
|
|
220
232
|
}
|
|
221
233
|
|
|
@@ -224,14 +236,21 @@ export function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
|
224
236
|
const space = trimmed.indexOf(" ");
|
|
225
237
|
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
226
238
|
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
227
|
-
const { args, options, refreshDiscovery } = parseWorkflowOptions(rest);
|
|
228
|
-
|
|
239
|
+
const { args, options, refreshDiscovery, optionErrors } = parseWorkflowOptions(rest);
|
|
240
|
+
const invocation: WorkflowInvocation = { name, args, options };
|
|
241
|
+
if (refreshDiscovery) invocation.refreshDiscovery = refreshDiscovery;
|
|
242
|
+
if (optionErrors) invocation.optionErrors = optionErrors;
|
|
243
|
+
return invocation;
|
|
229
244
|
}
|
|
230
245
|
|
|
231
|
-
|
|
246
|
+
const INVALID_BUDGET_OPTION = "--budget requires a positive integer output-token count";
|
|
247
|
+
const INVALID_RESUME_OPTION = "--resume requires a workflow run id";
|
|
248
|
+
|
|
249
|
+
function parseWorkflowOptions(input: string): { args: string; options: WorkflowRunOptions; refreshDiscovery?: boolean; optionErrors?: string[] } {
|
|
232
250
|
const tokens = input.split(/\s+/).filter(Boolean);
|
|
233
251
|
const kept: string[] = [];
|
|
234
252
|
const options: WorkflowRunOptions = {};
|
|
253
|
+
const optionErrors: string[] = [];
|
|
235
254
|
let refreshDiscovery = false;
|
|
236
255
|
for (let i = 0; i < tokens.length; i++) {
|
|
237
256
|
const token = tokens[i];
|
|
@@ -275,9 +294,46 @@ function parseWorkflowOptions(input: string): { args: string; options: WorkflowR
|
|
|
275
294
|
if (next !== undefined) i++;
|
|
276
295
|
continue;
|
|
277
296
|
}
|
|
297
|
+
if (token.startsWith("--budget=")) {
|
|
298
|
+
const parsed = parseBudgetOption(token.slice("--budget=".length));
|
|
299
|
+
if (parsed === undefined) optionErrors.push(INVALID_BUDGET_OPTION);
|
|
300
|
+
else options.budget = parsed;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (token === "--budget") {
|
|
304
|
+
const next = tokens[i + 1];
|
|
305
|
+
const parsed = next === undefined ? undefined : parseBudgetOption(next);
|
|
306
|
+
if (parsed === undefined) {
|
|
307
|
+
optionErrors.push(INVALID_BUDGET_OPTION);
|
|
308
|
+
} else {
|
|
309
|
+
options.budget = parsed;
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (token.startsWith("--resume=")) {
|
|
315
|
+
const value = token.slice("--resume=".length).trim();
|
|
316
|
+
if (value === "") optionErrors.push(INVALID_RESUME_OPTION);
|
|
317
|
+
else options.resumeFromRunId = value;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (token === "--resume") {
|
|
321
|
+
const next = tokens[i + 1];
|
|
322
|
+
if (next === undefined || next.startsWith("--")) {
|
|
323
|
+
optionErrors.push(INVALID_RESUME_OPTION);
|
|
324
|
+
} else {
|
|
325
|
+
options.resumeFromRunId = next;
|
|
326
|
+
i++;
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
278
330
|
kept.push(token);
|
|
279
331
|
}
|
|
280
|
-
return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined };
|
|
332
|
+
return { args: kept.join(" ").trim(), options, refreshDiscovery: refreshDiscovery || undefined, optionErrors: optionErrors.length > 0 ? optionErrors : undefined };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseBudgetOption(value: string): number | undefined {
|
|
336
|
+
return parseWorkflowBudgetString(value);
|
|
281
337
|
}
|
|
282
338
|
|
|
283
339
|
function parseNumericOption(value: string | undefined): number | undefined {
|
|
@@ -302,12 +358,15 @@ Use the workflow tool with a script argument, not a saved workflow name.
|
|
|
302
358
|
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
303
359
|
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
304
360
|
Set thinkingLevel explicitly on each agent() call.
|
|
361
|
+
When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
|
|
362
|
+
Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
|
|
305
363
|
Do not edit files unless the user explicitly requested edits.`;
|
|
306
364
|
}
|
|
307
365
|
|
|
308
366
|
export interface WorkflowToolRequestParams {
|
|
309
367
|
readonly name?: string;
|
|
310
368
|
readonly script?: string;
|
|
369
|
+
readonly resumeFromRunId?: string;
|
|
311
370
|
}
|
|
312
371
|
|
|
313
372
|
export type WorkflowToolRequest =
|
|
@@ -374,6 +433,7 @@ export async function sendWorkflowResult(
|
|
|
374
433
|
const { runWorkflow } = await loadEngine();
|
|
375
434
|
let perfSnapshot: PerfSnapshot | undefined;
|
|
376
435
|
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
436
|
+
let runMetadata: WorkflowRunMetadata | undefined;
|
|
377
437
|
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
378
438
|
const result = await runWorkflow(ctx, mod, args, {
|
|
379
439
|
...options,
|
|
@@ -398,6 +458,10 @@ export async function sendWorkflowResult(
|
|
|
398
458
|
usageSnapshot = snapshot;
|
|
399
459
|
options.onUsageSnapshot?.(snapshot);
|
|
400
460
|
},
|
|
461
|
+
onRunMetadata(metadata) {
|
|
462
|
+
runMetadata = metadata;
|
|
463
|
+
options.onRunMetadata?.(metadata);
|
|
464
|
+
},
|
|
401
465
|
onProgressSnapshot(snapshot) {
|
|
402
466
|
lastWorkflowInspection = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
|
|
403
467
|
options.onProgressSnapshot?.(snapshot);
|
|
@@ -417,7 +481,12 @@ export async function sendWorkflowResult(
|
|
|
417
481
|
await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
|
|
418
482
|
}
|
|
419
483
|
pi.sendMessage(
|
|
420
|
-
{
|
|
484
|
+
{
|
|
485
|
+
customType: "workflow-result",
|
|
486
|
+
content: formatMessageContent(name, result, usageSnapshot, perf, runMetadata),
|
|
487
|
+
display: true,
|
|
488
|
+
details: workflowEnvelope(name, result, usageSnapshot, perf, runMetadata),
|
|
489
|
+
},
|
|
421
490
|
{ triggerTurn: false },
|
|
422
491
|
);
|
|
423
492
|
}
|
|
@@ -427,7 +496,12 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
427
496
|
|
|
428
497
|
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
429
498
|
const details = message.details;
|
|
430
|
-
if (isWorkflowResult(details))
|
|
499
|
+
if (isWorkflowResult(details)) {
|
|
500
|
+
return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
|
|
501
|
+
runId: details.runId,
|
|
502
|
+
resumedFromRunId: details.resumedFromRunId,
|
|
503
|
+
}, details.perf);
|
|
504
|
+
}
|
|
431
505
|
return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
|
|
432
506
|
});
|
|
433
507
|
|
|
@@ -448,6 +522,10 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
448
522
|
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
449
523
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
450
524
|
const direct = parseWorkflowInvocation(args);
|
|
525
|
+
if (direct.optionErrors?.length) {
|
|
526
|
+
ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
451
529
|
const perfRecorder = await createInvocationPerf(direct.options);
|
|
452
530
|
const { discoverWorkflows } = await loadDiscovery();
|
|
453
531
|
const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
|
|
@@ -487,6 +565,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
487
565
|
"Use workflow with `script` for a new one-off inline workflow; the script must start with `export const meta = { ... }` and default-export an async workflow function.",
|
|
488
566
|
"Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
|
|
489
567
|
"Inline scripts may compose registered workflows in-process via `api.workflow(\"<name>\", args)` (e.g. `await api.workflow(\"code-review\", \"HEAD~3\")`); it returns the sub-workflow's result and nests one level only.",
|
|
568
|
+
"Subagents receive no skills by default. In inline workflows, pass `skills: [\"skill-name\"]` per `agent()` call when the user asks for a skill or a stage should use one; grant only the needed skills.",
|
|
569
|
+
"If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
|
|
570
|
+
"`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
|
|
490
571
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
491
572
|
],
|
|
492
573
|
parameters: Type.Object({
|
|
@@ -495,7 +576,15 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
495
576
|
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
496
577
|
concurrency: Type.Optional(Type.Number({ description: "Optional per-run agent concurrency cap" })),
|
|
497
578
|
parallelSubmissionLimit: Type.Optional(Type.Number({ description: "Optional limit for eagerly submitted parallel thunks" })),
|
|
579
|
+
budget: Type.Optional(
|
|
580
|
+
Type.Integer({
|
|
581
|
+
minimum: WORKFLOW_BUDGET_MIN,
|
|
582
|
+
maximum: WORKFLOW_BUDGET_MAX,
|
|
583
|
+
description: "Optional output-token ceiling for the run; agent() throws once it is exceeded",
|
|
584
|
+
}),
|
|
585
|
+
),
|
|
498
586
|
perf: Type.Optional(Type.Boolean({ description: "Include workflow performance timing aggregates in the result details" })),
|
|
587
|
+
resumeFromRunId: Type.Optional(Type.String({ minLength: 1, description: "Workflow run id to resume from by replaying matching completed agent results" })),
|
|
499
588
|
}),
|
|
500
589
|
renderCall(args, theme) {
|
|
501
590
|
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
@@ -509,7 +598,12 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
509
598
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
510
599
|
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
511
600
|
const details = result.details;
|
|
512
|
-
if (isWorkflowResult(details))
|
|
601
|
+
if (isWorkflowResult(details)) {
|
|
602
|
+
return renderWorkflowResult(details.name, details.result, expanded, theme, details.usage, {
|
|
603
|
+
runId: details.runId,
|
|
604
|
+
resumedFromRunId: details.resumedFromRunId,
|
|
605
|
+
}, details.perf);
|
|
606
|
+
}
|
|
513
607
|
const first = result.content[0];
|
|
514
608
|
const text = first?.type === "text" ? first.text : "Workflow finished.";
|
|
515
609
|
return new Text(theme.fg("muted", text), 0, 0);
|
|
@@ -517,12 +611,21 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
517
611
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
518
612
|
const request = normalizeWorkflowToolRequest(params);
|
|
519
613
|
if (request.kind === "error") return invalidWorkflowInvocationResult();
|
|
614
|
+
const resumeFromRunId = params.resumeFromRunId?.trim();
|
|
615
|
+
if (params.resumeFromRunId !== undefined && resumeFromRunId === "") {
|
|
616
|
+
return {
|
|
617
|
+
content: [{ type: "text", text: "resumeFromRunId must be non-empty." }],
|
|
618
|
+
details: { error: "invalid_resume_from_run_id" },
|
|
619
|
+
};
|
|
620
|
+
}
|
|
520
621
|
|
|
521
622
|
const runOptions: WorkflowRunOptions = {
|
|
522
623
|
inspect: ctx.hasUI,
|
|
523
624
|
concurrency: params.concurrency,
|
|
524
625
|
parallelSubmissionLimit: params.parallelSubmissionLimit,
|
|
626
|
+
budget: params.budget,
|
|
525
627
|
perf: params.perf,
|
|
628
|
+
resumeFromRunId,
|
|
526
629
|
signal,
|
|
527
630
|
};
|
|
528
631
|
const perfRecorder = await createInvocationPerf(runOptions);
|
|
@@ -556,6 +659,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
556
659
|
const { runWorkflow } = await loadEngine();
|
|
557
660
|
let perfSnapshot: PerfSnapshot | undefined;
|
|
558
661
|
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
662
|
+
let runMetadata: WorkflowRunMetadata | undefined;
|
|
559
663
|
let liveInspection: ActiveWorkflowInspection | undefined;
|
|
560
664
|
const resultArgs = params.args ?? "";
|
|
561
665
|
const result = await runWorkflow(ctx, mod, resultArgs, {
|
|
@@ -578,6 +682,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
578
682
|
onUsageSnapshot: (snapshot) => {
|
|
579
683
|
usageSnapshot = snapshot;
|
|
580
684
|
},
|
|
685
|
+
onRunMetadata: (metadata) => {
|
|
686
|
+
runMetadata = metadata;
|
|
687
|
+
},
|
|
581
688
|
onProgressSnapshot: (snapshot) => {
|
|
582
689
|
// Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
|
|
583
690
|
// previously uninspectable, unlike the /workflow command path.
|
|
@@ -585,10 +692,11 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
585
692
|
},
|
|
586
693
|
});
|
|
587
694
|
const perf = compactPerfSnapshot(perfSnapshot);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
695
|
+
const detailLines = formatWorkflowDetailLines({ usage: usageSnapshot, perf, metadata: runMetadata });
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: `${summarize(result)}${detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : ""}` }],
|
|
698
|
+
details: workflowEnvelope(resultName, result, usageSnapshot, perf, runMetadata),
|
|
699
|
+
};
|
|
592
700
|
},
|
|
593
701
|
});
|
|
594
702
|
}
|