pi-workflow-engine 0.3.0 → 0.5.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 +369 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +83 -0
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +396 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
- package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
- package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-result-renderer.ts +11 -8
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +57 -0
- package/{src → .pi/extensions/pi-workflow-engine/src}/workflows.ts +8 -2
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +57 -125
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/diagnose.ts +12 -73
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/perf-review.ts +41 -148
- package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/refactor-scout.ts +43 -151
- package/.pi/settings.json +3 -0
- package/README.md +101 -27
- package/USAGE.md +183 -0
- package/assets/preview-candidate-core.png +0 -0
- package/assets/preview-candidate-relic-emblem.png +0 -0
- package/assets/preview-candidate-robot-squad.png +0 -0
- package/assets/preview.png +0 -0
- package/package.json +33 -6
- package/src/agent-runner.ts +0 -122
- package/src/concurrency.ts +0 -55
- package/src/discovery.ts +0 -54
- package/src/index.ts +0 -141
- package/src/ui/workflow-widget.ts +0 -143
- package/workflows/ping.ts +0 -28
- /package/{src → .pi/extensions/pi-workflow-engine/src}/advisory-schema.ts +0 -0
package/src/discovery.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import type { WorkflowModule } from "./types.ts";
|
|
6
|
-
import { BUILTIN_WORKFLOWS } from "./workflows.ts";
|
|
7
|
-
|
|
8
|
-
function isWorkflowModule(value: unknown): value is WorkflowModule {
|
|
9
|
-
if (!value || typeof value !== "object") return false;
|
|
10
|
-
const candidate = value as { meta?: { name?: unknown }; default?: unknown };
|
|
11
|
-
return typeof candidate.meta?.name === "string" && typeof candidate.default === "function";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Best-effort dynamic load of every `*.ts` workflow in a directory. */
|
|
15
|
-
async function loadDir(dir: string): Promise<WorkflowModule[]> {
|
|
16
|
-
let entries: string[];
|
|
17
|
-
try {
|
|
18
|
-
entries = await readdir(dir);
|
|
19
|
-
} catch {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const modules: WorkflowModule[] = [];
|
|
24
|
-
for (const name of entries) {
|
|
25
|
-
if (!name.endsWith(".ts") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
26
|
-
try {
|
|
27
|
-
const loaded: unknown = await import(pathToFileURL(join(dir, name)).href);
|
|
28
|
-
if (isWorkflowModule(loaded)) modules.push(loaded);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
// Drop-in loading depends on the runtime resolving TS + the bundled typebox.
|
|
31
|
-
// Failures here are non-fatal: the static registry still works.
|
|
32
|
-
console.error(`[workflow-engine] skipped ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return modules;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* All available workflows by name. Static registry wins on name collisions, so the
|
|
40
|
-
* bundled example is always the verified one even if a same-named file is dropped in.
|
|
41
|
-
*/
|
|
42
|
-
export async function discoverWorkflows(repoDir: string): Promise<Map<string, WorkflowModule>> {
|
|
43
|
-
const byName = new Map<string, WorkflowModule>();
|
|
44
|
-
for (const mod of BUILTIN_WORKFLOWS) byName.set(mod.meta.name, mod);
|
|
45
|
-
|
|
46
|
-
const dynamic = [
|
|
47
|
-
...(await loadDir(join(repoDir, "workflows"))),
|
|
48
|
-
...(await loadDir(join(homedir(), ".pi", "agent", "workflows"))),
|
|
49
|
-
];
|
|
50
|
-
for (const mod of dynamic) {
|
|
51
|
-
if (!byName.has(mod.meta.name)) byName.set(mod.meta.name, mod);
|
|
52
|
-
}
|
|
53
|
-
return byName;
|
|
54
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { discoverWorkflows } from "./discovery.ts";
|
|
6
|
-
import { runWorkflow } from "./engine.ts";
|
|
7
|
-
import type { WorkflowModule, WorkflowRunOptions } from "./types.ts";
|
|
8
|
-
import { isWorkflowResult, renderWorkflowResult, type WorkflowResultEnvelope } from "./ui/workflow-result-renderer.ts";
|
|
9
|
-
|
|
10
|
-
/** Repo root (this file lives in <repo>/src/index.ts). */
|
|
11
|
-
const REPO_DIR = fileURLToPath(new URL("..", import.meta.url));
|
|
12
|
-
|
|
13
|
-
function summarize(result: unknown): string {
|
|
14
|
-
if (result && typeof result === "object" && typeof (result as { summary?: unknown }).summary === "string") {
|
|
15
|
-
return (result as { summary: string }).summary;
|
|
16
|
-
}
|
|
17
|
-
return typeof result === "string" ? result : "Workflow finished.";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatReport(name: string, result: unknown): string {
|
|
21
|
-
return [`## Workflow: ${name}`, "", "```json", JSON.stringify(result, null, 2), "```"].join("\n");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function workflowEnvelope(name: string, result: unknown): WorkflowResultEnvelope {
|
|
25
|
-
return { name, result, completedAt: Date.now() };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface WorkflowInvocation {
|
|
29
|
-
name: string;
|
|
30
|
-
args: string;
|
|
31
|
-
options: WorkflowRunOptions;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseWorkflowInvocation(input: string): WorkflowInvocation {
|
|
35
|
-
const trimmed = input.trim();
|
|
36
|
-
const space = trimmed.indexOf(" ");
|
|
37
|
-
const name = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
38
|
-
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
39
|
-
const inspect = /(?:^|\s)--inspect(?:\s|$)/.test(rest);
|
|
40
|
-
const cleanedArgs = rest.replace(/(?:^|\s)--inspect(?=\s|$)/g, " ").trim();
|
|
41
|
-
return { name, args: cleanedArgs, options: inspect ? { inspect: true } : {} };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function pickWorkflow(
|
|
45
|
-
workflows: ReadonlyMap<string, WorkflowModule>,
|
|
46
|
-
ctx: ExtensionCommandContext,
|
|
47
|
-
): Promise<WorkflowInvocation | undefined> {
|
|
48
|
-
const choices = [...workflows.values()].map((mod) => `${mod.meta.name} — ${mod.meta.description}`);
|
|
49
|
-
const choice = await ctx.ui.select("Run workflow", choices);
|
|
50
|
-
if (!choice) return undefined;
|
|
51
|
-
|
|
52
|
-
const separator = choice.indexOf(" — ");
|
|
53
|
-
const name = separator === -1 ? choice : choice.slice(0, separator);
|
|
54
|
-
const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
|
|
55
|
-
const inspect = await ctx.ui.confirm("Open inspector?", "Open a live workflow inspector while this workflow runs?");
|
|
56
|
-
return { name, args, options: { inspect } };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function sendWorkflowResult(
|
|
60
|
-
pi: ExtensionAPI,
|
|
61
|
-
ctx: ExtensionCommandContext,
|
|
62
|
-
name: string,
|
|
63
|
-
mod: WorkflowModule,
|
|
64
|
-
args: string,
|
|
65
|
-
options: WorkflowRunOptions,
|
|
66
|
-
): Promise<void> {
|
|
67
|
-
const result = await runWorkflow(ctx, mod, args, options);
|
|
68
|
-
pi.sendMessage(
|
|
69
|
-
{ customType: "workflow-result", content: formatReport(name, result), display: true, details: workflowEnvelope(name, result) },
|
|
70
|
-
{ triggerTurn: false },
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export default function workflowEngine(pi: ExtensionAPI): void {
|
|
75
|
-
pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
|
|
76
|
-
const details = message.details;
|
|
77
|
-
if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
|
|
78
|
-
return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// /workflow <name> [args] — user-invoked.
|
|
82
|
-
pi.registerCommand("workflow", {
|
|
83
|
-
description: "Run a multi-agent workflow: /workflow <name> [args]",
|
|
84
|
-
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
85
|
-
const workflows = await discoverWorkflows(REPO_DIR);
|
|
86
|
-
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
87
|
-
const direct = parseWorkflowInvocation(args);
|
|
88
|
-
const invocation = direct.name ? direct : ctx.hasUI ? await pickWorkflow(workflows, ctx) : undefined;
|
|
89
|
-
|
|
90
|
-
if (!invocation) {
|
|
91
|
-
ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const mod = workflows.get(invocation.name);
|
|
96
|
-
if (!mod) {
|
|
97
|
-
ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options);
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// workflow tool — lets the host agent fan out mid-conversation.
|
|
106
|
-
pi.registerTool({
|
|
107
|
-
name: "workflow",
|
|
108
|
-
label: "Workflow",
|
|
109
|
-
description:
|
|
110
|
-
"Run a named multi-agent workflow (fan-out → verify → synthesize) and return its structured result. Use for thorough reviews or audits.",
|
|
111
|
-
parameters: Type.Object({
|
|
112
|
-
name: Type.String({ description: "Workflow name, e.g. code-review" }),
|
|
113
|
-
args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
|
|
114
|
-
}),
|
|
115
|
-
renderCall(args, theme) {
|
|
116
|
-
const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
|
|
117
|
-
return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name)}${suffix}`, 0, 0);
|
|
118
|
-
},
|
|
119
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
120
|
-
if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
|
|
121
|
-
const details = result.details;
|
|
122
|
-
if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
|
|
123
|
-
const first = result.content[0];
|
|
124
|
-
const text = first?.type === "text" ? first.text : "Workflow finished.";
|
|
125
|
-
return new Text(theme.fg("muted", text), 0, 0);
|
|
126
|
-
},
|
|
127
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
128
|
-
const workflows = await discoverWorkflows(REPO_DIR);
|
|
129
|
-
const mod = workflows.get(params.name);
|
|
130
|
-
if (!mod) {
|
|
131
|
-
const available = [...workflows.keys()].join(", ") || "(none)";
|
|
132
|
-
return {
|
|
133
|
-
content: [{ type: "text", text: `Unknown workflow "${params.name}". Available: ${available}` }],
|
|
134
|
-
details: { error: "unknown_workflow", available },
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
const result = await runWorkflow(ctx, mod, params.args ?? "");
|
|
138
|
-
return { content: [{ type: "text", text: summarize(result) }], details: workflowEnvelope(params.name, result) };
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
|
|
3
|
-
import { formatCount, formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
|
|
4
|
-
|
|
5
|
-
const MAX_WIDGET_LINES = 12;
|
|
6
|
-
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
|
|
8
|
-
export interface WorkflowWidget {
|
|
9
|
-
nextFrame(): void;
|
|
10
|
-
render(width: number, theme: Theme): string[];
|
|
11
|
-
invalidate(): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createWorkflowWidget(snapshotProvider: () => WorkflowProgressSnapshot): WorkflowWidget {
|
|
15
|
-
return new LiveWorkflowWidget(snapshotProvider);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function renderWorkflowWidgetLines(
|
|
19
|
-
snapshot: WorkflowProgressSnapshot,
|
|
20
|
-
frame: number,
|
|
21
|
-
width: number,
|
|
22
|
-
theme: Theme,
|
|
23
|
-
): string[] {
|
|
24
|
-
const safeWidth = Math.max(1, width);
|
|
25
|
-
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
26
|
-
const running = agents.filter((agent) => agent.status === "running").length;
|
|
27
|
-
const queued = agents.filter((agent) => agent.status === "queued").length;
|
|
28
|
-
const done = agents.filter((agent) => agent.status === "done").length;
|
|
29
|
-
const failed = agents.filter((agent) => agent.status === "failed").length;
|
|
30
|
-
const active = running + queued;
|
|
31
|
-
const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
|
|
32
|
-
const spinner = active > 0 ? theme.fg("accent", SPINNER[frame % SPINNER.length]) : theme.fg("success", "✓");
|
|
33
|
-
|
|
34
|
-
const headingParts = [`${done}/${agents.length} done`, elapsed];
|
|
35
|
-
if (running > 0) headingParts.unshift(`${running} running`);
|
|
36
|
-
if (queued > 0) headingParts.unshift(`${queued} queued`);
|
|
37
|
-
if (failed > 0) headingParts.unshift(theme.fg("error", `${failed} failed`));
|
|
38
|
-
|
|
39
|
-
const lines: string[] = [
|
|
40
|
-
truncateDisplay(
|
|
41
|
-
`${spinner} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
|
|
42
|
-
safeWidth,
|
|
43
|
-
),
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const footer = footerLine(snapshot, theme);
|
|
47
|
-
const bodyBudget = Math.max(0, MAX_WIDGET_LINES - lines.length - (footer ? 1 : 0));
|
|
48
|
-
const bodyLines = prioritizedBodyLines(snapshot.phases, theme);
|
|
49
|
-
const visibleBody = bodyLines.slice(0, bodyBudget);
|
|
50
|
-
const hidden = bodyLines.length - visibleBody.length;
|
|
51
|
-
|
|
52
|
-
for (const line of visibleBody) lines.push(truncateDisplay(line, safeWidth));
|
|
53
|
-
if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
54
|
-
lines.push(truncateDisplay(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`, safeWidth));
|
|
55
|
-
}
|
|
56
|
-
if (footer && lines.length < MAX_WIDGET_LINES) lines.push(truncateDisplay(footer, safeWidth));
|
|
57
|
-
|
|
58
|
-
return lines.slice(0, MAX_WIDGET_LINES).map((line) => truncateDisplay(line, safeWidth));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
class LiveWorkflowWidget implements WorkflowWidget {
|
|
62
|
-
private frame = 0;
|
|
63
|
-
private cachedWidth: number | undefined;
|
|
64
|
-
private cachedFrame: number | undefined;
|
|
65
|
-
private cachedDoneAt: number | undefined;
|
|
66
|
-
private cachedLines: string[] | undefined;
|
|
67
|
-
|
|
68
|
-
constructor(private readonly snapshotProvider: () => WorkflowProgressSnapshot) {}
|
|
69
|
-
|
|
70
|
-
nextFrame(): void {
|
|
71
|
-
this.frame = (this.frame + 1) % SPINNER.length;
|
|
72
|
-
this.invalidate();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
render(width: number, theme: Theme): string[] {
|
|
76
|
-
const snapshot = this.snapshotProvider();
|
|
77
|
-
if (
|
|
78
|
-
this.cachedLines &&
|
|
79
|
-
this.cachedWidth === width &&
|
|
80
|
-
this.cachedFrame === this.frame &&
|
|
81
|
-
this.cachedDoneAt === snapshot.doneAt
|
|
82
|
-
) {
|
|
83
|
-
return this.cachedLines;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.cachedLines = renderWorkflowWidgetLines(snapshot, this.frame, width, theme);
|
|
87
|
-
this.cachedWidth = width;
|
|
88
|
-
this.cachedFrame = this.frame;
|
|
89
|
-
this.cachedDoneAt = snapshot.doneAt;
|
|
90
|
-
return this.cachedLines;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
invalidate(): void {
|
|
94
|
-
this.cachedWidth = undefined;
|
|
95
|
-
this.cachedFrame = undefined;
|
|
96
|
-
this.cachedDoneAt = undefined;
|
|
97
|
-
this.cachedLines = undefined;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function prioritizedBodyLines(phases: readonly PhaseSnapshot[], theme: Theme): string[] {
|
|
102
|
-
const activePhases = phases.filter((phase) => phase.agents.some((agent) => agent.status === "running" || agent.status === "queued"));
|
|
103
|
-
const inactivePhases = phases.filter((phase) => !activePhases.includes(phase));
|
|
104
|
-
return [...activePhases, ...inactivePhases].flatMap((phase) => phaseLines(phase, theme));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function phaseLines(phase: PhaseSnapshot, theme: Theme): string[] {
|
|
108
|
-
if (phase.agents.length === 0 && phase.title === "Workflow") return [];
|
|
109
|
-
const running = phase.agents.filter((agent) => agent.status === "running").length;
|
|
110
|
-
const done = phase.agents.filter((agent) => agent.status === "done").length;
|
|
111
|
-
const failed = phase.agents.filter((agent) => agent.status === "failed").length;
|
|
112
|
-
const parts: string[] = [];
|
|
113
|
-
if (running > 0) parts.push(`${running} running`);
|
|
114
|
-
if (done > 0) parts.push(`${done} done`);
|
|
115
|
-
if (failed > 0) parts.push(`${failed} failed`);
|
|
116
|
-
|
|
117
|
-
const lines = [`${theme.fg("dim", "├─")} ${theme.fg(running > 0 ? "accent" : "muted", phase.title)}${parts.length ? ` ${theme.fg("dim", `(${parts.join(" · ")})`)}` : ""}`];
|
|
118
|
-
const active = phase.agents.filter((agent) => agent.status === "running" || agent.status === "queued");
|
|
119
|
-
const failedAgents = phase.agents.filter((agent) => agent.status === "failed");
|
|
120
|
-
const completed = phase.agents.filter((agent) => agent.status === "done");
|
|
121
|
-
for (const agent of [...active, ...failedAgents, ...completed]) lines.push(agentLine(agent, theme));
|
|
122
|
-
return lines;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
|
|
126
|
-
const detailParts: string[] = [];
|
|
127
|
-
if (agent.toolUses > 0) detailParts.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
|
|
128
|
-
if (agent.status === "running" && agent.lastTool) detailParts.push(agent.lastTool);
|
|
129
|
-
if (agent.startedAt !== undefined) detailParts.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
|
|
130
|
-
if (agent.status === "failed" && agent.error) detailParts.push(agent.error);
|
|
131
|
-
|
|
132
|
-
const activity = detailParts.length > 0 ? ` ${theme.fg("dim", `· ${detailParts.join(" · ")}`)}` : "";
|
|
133
|
-
return `${theme.fg("dim", "│ ")} ${statusIcon(agent.status, theme)} ${theme.fg(agent.status === "running" ? "text" : "muted", agent.label)}${activity}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
137
|
-
const counters = snapshot.counters.slice(0, 4).map((counter) => `${counter.label} ${formatCount(counter.value)}`);
|
|
138
|
-
const latestLog = snapshot.logs[snapshot.logs.length - 1];
|
|
139
|
-
const parts = counters.length > 0 ? counters : [];
|
|
140
|
-
if (latestLog) parts.push(latestLog);
|
|
141
|
-
if (parts.length === 0) return undefined;
|
|
142
|
-
return `${theme.fg("dim", "└─")} ${theme.fg("dim", parts.join(" · "))}`;
|
|
143
|
-
}
|
package/workflows/ping.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
|
|
3
|
-
|
|
4
|
-
/** Minimal engine smoke test: a single structured-output agent call. Fast + cheap. */
|
|
5
|
-
export const meta: WorkflowMeta = {
|
|
6
|
-
name: "ping",
|
|
7
|
-
description: "Engine smoke test — one structured agent call, no tools.",
|
|
8
|
-
phases: [{ title: "Ping" }],
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export default async function run(api: WorkflowApi): Promise<unknown> {
|
|
12
|
-
const { agent, phase, log } = api;
|
|
13
|
-
phase("Ping");
|
|
14
|
-
log("calling one structured agent");
|
|
15
|
-
const result = await agent(
|
|
16
|
-
"You are a code-review workflow engine. Return a one-word headline and a one-sentence summary of what you do.",
|
|
17
|
-
{
|
|
18
|
-
phase: "Ping",
|
|
19
|
-
label: "ping",
|
|
20
|
-
tools: [],
|
|
21
|
-
schema: Type.Object({
|
|
22
|
-
headline: Type.String({ description: "One word" }),
|
|
23
|
-
summary: Type.String({ description: "One sentence" }),
|
|
24
|
-
}),
|
|
25
|
-
},
|
|
26
|
-
);
|
|
27
|
-
return result ?? { headline: "none", summary: "no structured answer returned" };
|
|
28
|
-
}
|
|
File without changes
|