@quintinshaw/pi-dynamic-workflows 1.0.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/README.md +159 -0
- package/dist/adversarial-review.d.ts +20 -0
- package/dist/adversarial-review.js +87 -0
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +90 -0
- package/dist/auto-workflow.d.ts +26 -0
- package/dist/auto-workflow.js +121 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +17 -0
- package/dist/deep-research.d.ts +22 -0
- package/dist/deep-research.js +110 -0
- package/dist/display.d.ts +62 -0
- package/dist/display.js +163 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +63 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +67 -0
- package/dist/model-routing.d.ts +33 -0
- package/dist/model-routing.js +57 -0
- package/dist/run-persistence.d.ts +53 -0
- package/dist/run-persistence.js +78 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/workflow-manager.d.ts +74 -0
- package/dist/workflow-manager.js +241 -0
- package/dist/workflow-saved.d.ts +35 -0
- package/dist/workflow-saved.js +91 -0
- package/dist/workflow-tool.d.ts +22 -0
- package/dist/workflow-tool.js +216 -0
- package/dist/workflow.d.ts +75 -0
- package/dist/workflow.js +364 -0
- package/extensions/workflow.ts +14 -0
- package/package.json +70 -0
- package/src/adversarial-review.ts +107 -0
- package/src/agent.ts +135 -0
- package/src/auto-workflow.ts +146 -0
- package/src/config.ts +24 -0
- package/src/deep-research.ts +128 -0
- package/src/display.ts +236 -0
- package/src/errors.ts +85 -0
- package/src/index.ts +55 -0
- package/src/logger.ts +89 -0
- package/src/model-routing.ts +80 -0
- package/src/run-persistence.ts +132 -0
- package/src/structured-output.ts +47 -0
- package/src/workflow-manager.ts +294 -0
- package/src/workflow-saved.ts +131 -0
- package/src/workflow-tool.ts +254 -0
- package/src/workflow.ts +492 -0
package/src/display.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WorkflowMeta } from "./workflow.js";
|
|
3
|
+
|
|
4
|
+
export type WorkflowAgentStatus = "queued" | "running" | "done" | "error" | "skipped";
|
|
5
|
+
|
|
6
|
+
export interface WorkflowAgentSnapshot {
|
|
7
|
+
id: number;
|
|
8
|
+
label: string;
|
|
9
|
+
phase?: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
status: WorkflowAgentStatus;
|
|
12
|
+
resultPreview?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
/** Tokens used by this agent. */
|
|
15
|
+
tokens?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowSnapshot {
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
phases: string[];
|
|
22
|
+
currentPhase?: string;
|
|
23
|
+
logs: string[];
|
|
24
|
+
agents: WorkflowAgentSnapshot[];
|
|
25
|
+
agentCount: number;
|
|
26
|
+
runningCount: number;
|
|
27
|
+
doneCount: number;
|
|
28
|
+
errorCount: number;
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
result?: unknown;
|
|
31
|
+
tokenUsage?: {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
total: number;
|
|
35
|
+
};
|
|
36
|
+
runId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkflowDisplay {
|
|
40
|
+
update(snapshot: WorkflowSnapshot): void;
|
|
41
|
+
complete(snapshot: WorkflowSnapshot): void;
|
|
42
|
+
clear(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WorkflowDisplayOptions {
|
|
46
|
+
key?: string;
|
|
47
|
+
placement?: "aboveEditor" | "belowEditor";
|
|
48
|
+
maxAgents?: number;
|
|
49
|
+
maxLogs?: number;
|
|
50
|
+
showStatus?: boolean;
|
|
51
|
+
showResultPreviews?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createWorkflowSnapshot(meta: WorkflowMeta): WorkflowSnapshot {
|
|
55
|
+
return {
|
|
56
|
+
name: meta.name,
|
|
57
|
+
description: meta.description,
|
|
58
|
+
phases: meta.phases?.map((phase) => phase.title) ?? [],
|
|
59
|
+
logs: [],
|
|
60
|
+
agents: [],
|
|
61
|
+
agentCount: 0,
|
|
62
|
+
runningCount: 0,
|
|
63
|
+
doneCount: 0,
|
|
64
|
+
errorCount: 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function recomputeWorkflowSnapshot(snapshot: WorkflowSnapshot): WorkflowSnapshot {
|
|
69
|
+
const runningCount = snapshot.agents.filter((agent) => agent.status === "running").length;
|
|
70
|
+
const doneCount = snapshot.agents.filter((agent) => agent.status === "done").length;
|
|
71
|
+
const errorCount = snapshot.agents.filter((agent) => agent.status === "error").length;
|
|
72
|
+
return { ...snapshot, agentCount: snapshot.agents.length, runningCount, doneCount, errorCount };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createWidgetWorkflowDisplay(
|
|
76
|
+
ctx: Pick<ExtensionContext, "ui" | "hasUI">,
|
|
77
|
+
options: WorkflowDisplayOptions = {},
|
|
78
|
+
): WorkflowDisplay {
|
|
79
|
+
const key = options.key ?? "workflow";
|
|
80
|
+
const placement = options.placement ?? "belowEditor";
|
|
81
|
+
const showStatus = options.showStatus ?? false;
|
|
82
|
+
|
|
83
|
+
const render = (snapshot: WorkflowSnapshot, completed = false) => {
|
|
84
|
+
if (!ctx.hasUI) return;
|
|
85
|
+
if (showStatus) ctx.ui.setStatus(key, statusLine(snapshot, completed));
|
|
86
|
+
ctx.ui.setWidget(key, renderWorkflowLines(snapshot, options), { placement });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
update(snapshot) {
|
|
91
|
+
render(snapshot, false);
|
|
92
|
+
},
|
|
93
|
+
complete(snapshot) {
|
|
94
|
+
render(snapshot, true);
|
|
95
|
+
},
|
|
96
|
+
clear() {
|
|
97
|
+
if (!ctx.hasUI) return;
|
|
98
|
+
if (showStatus) ctx.ui.setStatus(key, undefined);
|
|
99
|
+
ctx.ui.setWidget(key, undefined);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createToolUpdateWorkflowDisplay(
|
|
105
|
+
onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: unknown }) => void) | undefined,
|
|
106
|
+
ctx?: Pick<ExtensionContext, "ui" | "hasUI">,
|
|
107
|
+
options: WorkflowDisplayOptions & { streamToolUpdates?: boolean } = {},
|
|
108
|
+
): WorkflowDisplay {
|
|
109
|
+
const widget = ctx ? createWidgetWorkflowDisplay(ctx, options) : undefined;
|
|
110
|
+
const streamToolUpdates = options.streamToolUpdates ?? !ctx?.hasUI;
|
|
111
|
+
|
|
112
|
+
const emit = (snapshot: WorkflowSnapshot, completed = false) => {
|
|
113
|
+
if (streamToolUpdates) {
|
|
114
|
+
onUpdate?.({
|
|
115
|
+
content: [{ type: "text", text: renderWorkflowText(snapshot, completed) }],
|
|
116
|
+
details: snapshot,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (completed) widget?.complete(snapshot);
|
|
120
|
+
else widget?.update(snapshot);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
update(snapshot) {
|
|
125
|
+
emit(snapshot, false);
|
|
126
|
+
},
|
|
127
|
+
complete(snapshot) {
|
|
128
|
+
emit(snapshot, true);
|
|
129
|
+
},
|
|
130
|
+
clear() {
|
|
131
|
+
widget?.clear();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderWorkflowLines(snapshot: WorkflowSnapshot, options: WorkflowDisplayOptions = {}): string[] {
|
|
137
|
+
const maxAgents = options.maxAgents ?? 8;
|
|
138
|
+
const maxLogs = options.maxLogs ?? 2;
|
|
139
|
+
const showResultPreviews = options.showResultPreviews ?? false;
|
|
140
|
+
const state =
|
|
141
|
+
snapshot.errorCount > 0
|
|
142
|
+
? `, ${snapshot.errorCount} errors`
|
|
143
|
+
: snapshot.runningCount > 0
|
|
144
|
+
? `, ${snapshot.runningCount} running`
|
|
145
|
+
: "";
|
|
146
|
+
// Build header with token info
|
|
147
|
+
const tokenInfo = snapshot.tokenUsage ? ` · ${snapshot.tokenUsage.total.toLocaleString()} tokens` : "";
|
|
148
|
+
const lines = [
|
|
149
|
+
`◆ Workflow: ${snapshot.name} (${snapshot.doneCount}/${snapshot.agentCount} done${state}${tokenInfo})`,
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const phaseNames = snapshot.phases.length
|
|
153
|
+
? snapshot.phases
|
|
154
|
+
: unique(snapshot.agents.map((agent) => agent.phase).filter(Boolean) as string[]);
|
|
155
|
+
const rendered = new Set<WorkflowAgentSnapshot>();
|
|
156
|
+
|
|
157
|
+
for (const phase of phaseNames) {
|
|
158
|
+
const agents = snapshot.agents.filter((agent) => agent.phase === phase);
|
|
159
|
+
for (const agent of agents) rendered.add(agent);
|
|
160
|
+
const done = agents.filter((agent) => agent.status === "done").length;
|
|
161
|
+
const running = agents.filter((agent) => agent.status === "running").length;
|
|
162
|
+
const errors = agents.filter((agent) => agent.status === "error").length;
|
|
163
|
+
const skipped = agents.filter((agent) => agent.status === "skipped").length;
|
|
164
|
+
const complete = agents.length > 0 && done + errors + skipped === agents.length;
|
|
165
|
+
const marker = running > 0 || (!complete && snapshot.currentPhase === phase) ? "▶" : complete ? "✓" : " ";
|
|
166
|
+
lines.push(
|
|
167
|
+
` ${marker} ${phase} ${done}/${agents.length}${running ? ` · ${running} running` : ""}${errors ? ` · ${errors} errors` : ""}${skipped ? ` · ${skipped} skipped` : ""}`,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const visibleAgents = agents.slice(-maxAgents);
|
|
171
|
+
for (const agent of visibleAgents) {
|
|
172
|
+
const order = `#${agent.id}`;
|
|
173
|
+
const result = showResultPreviews && agent.resultPreview ? ` — ${agent.resultPreview}` : "";
|
|
174
|
+
const agentTokens = agent.tokens ? ` [${agent.tokens.toLocaleString()} tok]` : "";
|
|
175
|
+
lines.push(` ${order} ${statusIcon(agent.status)} ${shorten(agent.label, 48)}${agentTokens}${result}`);
|
|
176
|
+
}
|
|
177
|
+
if (agents.length > visibleAgents.length)
|
|
178
|
+
lines.push(` … ${agents.length - visibleAgents.length} earlier agents`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const unphased = snapshot.agents.filter((agent) => !rendered.has(agent));
|
|
182
|
+
if (unphased.length) {
|
|
183
|
+
lines.push(" Unphased");
|
|
184
|
+
for (const agent of unphased.slice(-maxAgents)) {
|
|
185
|
+
const result = showResultPreviews && agent.resultPreview ? ` — ${agent.resultPreview}` : "";
|
|
186
|
+
const agentTokens = agent.tokens ? ` [${agent.tokens.toLocaleString()} tok]` : "";
|
|
187
|
+
lines.push(` #${agent.id} ${statusIcon(agent.status)} ${shorten(agent.label, 48)}${agentTokens}${result}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const log of snapshot.logs.slice(-maxLogs)) lines.push(` log: ${log}`);
|
|
192
|
+
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function renderWorkflowText(snapshot: WorkflowSnapshot, completed = false): string {
|
|
197
|
+
const header = completed ? "Workflow completed" : "Workflow running";
|
|
198
|
+
return [header, ...renderWorkflowLines(snapshot)].join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function statusLine(snapshot: WorkflowSnapshot, completed: boolean): string {
|
|
202
|
+
if (completed) return `workflow ✓ ${snapshot.name}: ${snapshot.doneCount}/${snapshot.agentCount}`;
|
|
203
|
+
if (snapshot.runningCount > 0)
|
|
204
|
+
return `workflow ${snapshot.name}: ${snapshot.runningCount} running, ${snapshot.doneCount}/${snapshot.agentCount} done`;
|
|
205
|
+
return `workflow ${snapshot.name}: ${snapshot.doneCount}/${snapshot.agentCount} done`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function statusIcon(status: WorkflowAgentStatus): string {
|
|
209
|
+
switch (status) {
|
|
210
|
+
case "queued":
|
|
211
|
+
return "○";
|
|
212
|
+
case "running":
|
|
213
|
+
return "●";
|
|
214
|
+
case "done":
|
|
215
|
+
return "✓";
|
|
216
|
+
case "error":
|
|
217
|
+
return "✗";
|
|
218
|
+
case "skipped":
|
|
219
|
+
return "-";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function unique(values: string[]): string[] {
|
|
224
|
+
return [...new Set(values)];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function shorten(value: string, max: number): string {
|
|
228
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
229
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function preview(value: unknown, max = 80): string {
|
|
233
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
234
|
+
if (!text) return "";
|
|
235
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
236
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-specific error types.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum WorkflowErrorCode {
|
|
6
|
+
/** Agent exceeded timeout. */
|
|
7
|
+
AGENT_TIMEOUT = "AGENT_TIMEOUT",
|
|
8
|
+
/** Workflow was aborted by user. */
|
|
9
|
+
WORKFLOW_ABORTED = "WORKFLOW_ABORTED",
|
|
10
|
+
/** Agent limit exceeded. */
|
|
11
|
+
AGENT_LIMIT_EXCEEDED = "AGENT_LIMIT_EXCEEDED",
|
|
12
|
+
/** Token budget exhausted. */
|
|
13
|
+
TOKEN_BUDGET_EXHAUSTED = "TOKEN_BUDGET_EXHAUSTED",
|
|
14
|
+
/** Script validation failed. */
|
|
15
|
+
SCRIPT_VALIDATION_ERROR = "SCRIPT_VALIDATION_ERROR",
|
|
16
|
+
/** Agent execution failed. */
|
|
17
|
+
AGENT_EXECUTION_ERROR = "AGENT_EXECUTION_ERROR",
|
|
18
|
+
/** Run state persistence failed. */
|
|
19
|
+
PERSISTENCE_ERROR = "PERSISTENCE_ERROR",
|
|
20
|
+
/** Unknown error. */
|
|
21
|
+
UNKNOWN = "UNKNOWN",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class WorkflowError extends Error {
|
|
25
|
+
readonly code: WorkflowErrorCode;
|
|
26
|
+
readonly recoverable: boolean;
|
|
27
|
+
readonly agentLabel?: string;
|
|
28
|
+
readonly details?: unknown;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
code: WorkflowErrorCode,
|
|
33
|
+
options: { recoverable?: boolean; agentLabel?: string; details?: unknown } = {},
|
|
34
|
+
) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "WorkflowError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.recoverable = options.recoverable ?? false;
|
|
39
|
+
this.agentLabel = options.agentLabel;
|
|
40
|
+
this.details = options.details;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isWorkflowError(error: unknown): error is WorkflowError {
|
|
45
|
+
return error instanceof WorkflowError;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isAbortError(error: unknown): boolean {
|
|
49
|
+
if (!(error instanceof Error)) return false;
|
|
50
|
+
return /\babort(?:ed)?\b/i.test(error.message);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isTimeoutError(error: unknown): boolean {
|
|
54
|
+
if (!(error instanceof Error)) return false;
|
|
55
|
+
return /\btimeout\b/i.test(error.message) || error.name === "TimeoutError";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap an unknown error into a WorkflowError with appropriate classification.
|
|
60
|
+
*/
|
|
61
|
+
export function wrapError(error: unknown, context?: { agentLabel?: string }): WorkflowError {
|
|
62
|
+
if (isWorkflowError(error)) return error;
|
|
63
|
+
|
|
64
|
+
if (isAbortError(error)) {
|
|
65
|
+
return new WorkflowError(
|
|
66
|
+
error instanceof Error ? error.message : "Workflow was aborted",
|
|
67
|
+
WorkflowErrorCode.WORKFLOW_ABORTED,
|
|
68
|
+
{ recoverable: true },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isTimeoutError(error)) {
|
|
73
|
+
return new WorkflowError(
|
|
74
|
+
error instanceof Error ? error.message : "Agent timed out",
|
|
75
|
+
WorkflowErrorCode.AGENT_TIMEOUT,
|
|
76
|
+
{ recoverable: true, agentLabel: context?.agentLabel },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new WorkflowError(
|
|
81
|
+
error instanceof Error ? error.message : String(error),
|
|
82
|
+
WorkflowErrorCode.AGENT_EXECUTION_ERROR,
|
|
83
|
+
{ recoverable: true, agentLabel: context?.agentLabel, details: error },
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type { AdversarialReviewConfig } from "./adversarial-review.js";
|
|
2
|
+
export { generateAdversarialReviewWorkflow, generateMultiPerspectiveWorkflow } from "./adversarial-review.js";
|
|
3
|
+
export type { AgentRunOptions, AgentRunResult, WorkflowAgentOptions } from "./agent.js";
|
|
4
|
+
export { WorkflowAgent } from "./agent.js";
|
|
5
|
+
export type { AutoWorkflowConfig } from "./auto-workflow.js";
|
|
6
|
+
export { shouldUseWorkflow, suggestWorkflowScript } from "./auto-workflow.js";
|
|
7
|
+
export * from "./config.js";
|
|
8
|
+
export type { DeepResearchConfig } from "./deep-research.js";
|
|
9
|
+
export { generateCodebaseAuditWorkflow, generateDeepResearchWorkflow } from "./deep-research.js";
|
|
10
|
+
export type {
|
|
11
|
+
WorkflowAgentSnapshot,
|
|
12
|
+
WorkflowAgentStatus,
|
|
13
|
+
WorkflowDisplay,
|
|
14
|
+
WorkflowDisplayOptions,
|
|
15
|
+
WorkflowSnapshot,
|
|
16
|
+
} from "./display.js";
|
|
17
|
+
export {
|
|
18
|
+
createToolUpdateWorkflowDisplay,
|
|
19
|
+
createWidgetWorkflowDisplay,
|
|
20
|
+
createWorkflowSnapshot,
|
|
21
|
+
preview,
|
|
22
|
+
recomputeWorkflowSnapshot,
|
|
23
|
+
renderWorkflowLines,
|
|
24
|
+
renderWorkflowText,
|
|
25
|
+
} from "./display.js";
|
|
26
|
+
export {
|
|
27
|
+
isAbortError,
|
|
28
|
+
isTimeoutError,
|
|
29
|
+
isWorkflowError,
|
|
30
|
+
WorkflowError,
|
|
31
|
+
WorkflowErrorCode,
|
|
32
|
+
wrapError,
|
|
33
|
+
} from "./errors.js";
|
|
34
|
+
export type { WorkflowLogger, WorkflowLoggerOptions } from "./logger.js";
|
|
35
|
+
export { createWorkflowLogger } from "./logger.js";
|
|
36
|
+
export type { ModelRoute, ModelRoutingConfig } from "./model-routing.js";
|
|
37
|
+
export { buildModelRoutingInstructions, parseModelRoutingFromMeta, resolveModelForPhase } from "./model-routing.js";
|
|
38
|
+
export type { PersistedRunState, RunPersistence, RunStatus } from "./run-persistence.js";
|
|
39
|
+
export { createRunPersistence, generateRunId } from "./run-persistence.js";
|
|
40
|
+
export type { StructuredOutputCapture, StructuredOutputToolOptions } from "./structured-output.js";
|
|
41
|
+
export { createStructuredOutputTool } from "./structured-output.js";
|
|
42
|
+
export type {
|
|
43
|
+
AgentOptions,
|
|
44
|
+
WorkflowMeta,
|
|
45
|
+
WorkflowMetaPhase,
|
|
46
|
+
WorkflowRunOptions,
|
|
47
|
+
WorkflowRunResult,
|
|
48
|
+
} from "./workflow.js";
|
|
49
|
+
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
50
|
+
export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
|
|
51
|
+
export { WorkflowManager } from "./workflow-manager.js";
|
|
52
|
+
export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
|
|
53
|
+
export { createWorkflowStorage } from "./workflow-saved.js";
|
|
54
|
+
export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
|
|
55
|
+
export { createWorkflowTool } from "./workflow-tool.js";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow logger with file persistence.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { WORKFLOW_RUNS_DIR } from "./config.js";
|
|
8
|
+
|
|
9
|
+
export interface WorkflowLogger {
|
|
10
|
+
log(message: string): void;
|
|
11
|
+
error(message: string): void;
|
|
12
|
+
warn(message: string): void;
|
|
13
|
+
getLogs(): string[];
|
|
14
|
+
persist(): string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkflowLoggerOptions {
|
|
18
|
+
/** Run ID for persistence. */
|
|
19
|
+
runId?: string;
|
|
20
|
+
/** Working directory for file paths. */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
/** Whether to persist logs to disk. */
|
|
23
|
+
persist?: boolean;
|
|
24
|
+
/** Callback for each log entry. */
|
|
25
|
+
onLog?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createWorkflowLogger(options: WorkflowLoggerOptions = {}): WorkflowLogger {
|
|
29
|
+
const logs: string[] = [];
|
|
30
|
+
const persistLogs = options.persist ?? true;
|
|
31
|
+
const cwd = options.cwd ?? process.cwd();
|
|
32
|
+
const runId = options.runId ?? `run-${Date.now()}`;
|
|
33
|
+
let logFile: string | null = null;
|
|
34
|
+
|
|
35
|
+
const write = (level: string, message: string) => {
|
|
36
|
+
const timestamp = new Date().toISOString();
|
|
37
|
+
const entry = `[${timestamp}] [${level}] ${message}`;
|
|
38
|
+
logs.push(entry);
|
|
39
|
+
options.onLog?.(message);
|
|
40
|
+
|
|
41
|
+
if (persistLogs && logFile) {
|
|
42
|
+
try {
|
|
43
|
+
appendFileSync(logFile, `${entry}\n`);
|
|
44
|
+
} catch {
|
|
45
|
+
// Silent fail for log persistence
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const logger: WorkflowLogger = {
|
|
51
|
+
log(message: string) {
|
|
52
|
+
write("INFO", message);
|
|
53
|
+
},
|
|
54
|
+
error(message: string) {
|
|
55
|
+
write("ERROR", message);
|
|
56
|
+
},
|
|
57
|
+
warn(message: string) {
|
|
58
|
+
write("WARN", message);
|
|
59
|
+
},
|
|
60
|
+
getLogs() {
|
|
61
|
+
return [...logs];
|
|
62
|
+
},
|
|
63
|
+
persist() {
|
|
64
|
+
if (!persistLogs) return null;
|
|
65
|
+
try {
|
|
66
|
+
const runsDir = join(cwd, WORKFLOW_RUNS_DIR);
|
|
67
|
+
mkdirSync(runsDir, { recursive: true });
|
|
68
|
+
logFile = join(runsDir, `${runId}.log`);
|
|
69
|
+
writeFileSync(logFile, `${logs.join("\n")}\n`);
|
|
70
|
+
return logFile;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Initialize log file if persisting
|
|
78
|
+
if (persistLogs) {
|
|
79
|
+
try {
|
|
80
|
+
const runsDir = join(cwd, WORKFLOW_RUNS_DIR);
|
|
81
|
+
mkdirSync(runsDir, { recursive: true });
|
|
82
|
+
logFile = join(runsDir, `${runId}.log`);
|
|
83
|
+
} catch {
|
|
84
|
+
// Silent fail
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return logger;
|
|
89
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-stage model routing for workflows.
|
|
3
|
+
* Allows different phases to use different models.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ModelRoute {
|
|
7
|
+
/** Phase name pattern (regex or exact match). */
|
|
8
|
+
phasePattern: string;
|
|
9
|
+
/** Model to use for this phase. */
|
|
10
|
+
model: string;
|
|
11
|
+
/** Whether to use regex matching. */
|
|
12
|
+
useRegex?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModelRoutingConfig {
|
|
16
|
+
/** Default model for all phases. */
|
|
17
|
+
defaultModel?: string;
|
|
18
|
+
/** Per-phase model overrides. */
|
|
19
|
+
routes: ModelRoute[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve which model to use for a given phase.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveModelForPhase(phase: string | undefined, config: ModelRoutingConfig): string | undefined {
|
|
26
|
+
if (!phase || !config.routes.length) {
|
|
27
|
+
return config.defaultModel;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const route of config.routes) {
|
|
31
|
+
if (route.useRegex) {
|
|
32
|
+
try {
|
|
33
|
+
const regex = new RegExp(route.phasePattern, "i");
|
|
34
|
+
if (regex.test(phase)) {
|
|
35
|
+
return route.model;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Invalid regex, skip
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
if (phase.toLowerCase().includes(route.phasePattern.toLowerCase())) {
|
|
42
|
+
return route.model;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return config.defaultModel;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build model routing instructions for a workflow agent.
|
|
52
|
+
*/
|
|
53
|
+
export function buildModelRoutingInstructions(
|
|
54
|
+
phase: string | undefined,
|
|
55
|
+
config: ModelRoutingConfig,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
const model = resolveModelForPhase(phase, config);
|
|
58
|
+
if (!model) return undefined;
|
|
59
|
+
return `Use model: ${model}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse model routing from workflow meta phases.
|
|
64
|
+
*/
|
|
65
|
+
export function parseModelRoutingFromMeta(phases?: Array<{ title: string; model?: string }>): ModelRoutingConfig {
|
|
66
|
+
const routes: ModelRoute[] = [];
|
|
67
|
+
|
|
68
|
+
if (phases) {
|
|
69
|
+
for (const phase of phases) {
|
|
70
|
+
if (phase.model) {
|
|
71
|
+
routes.push({
|
|
72
|
+
phasePattern: phase.title,
|
|
73
|
+
model: phase.model,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { routes };
|
|
80
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow run state persistence for pause/resume support.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { WORKFLOW_RUNS_DIR } from "./config.js";
|
|
8
|
+
|
|
9
|
+
export type RunStatus = "pending" | "running" | "paused" | "completed" | "failed" | "aborted";
|
|
10
|
+
|
|
11
|
+
export interface PersistedAgentState {
|
|
12
|
+
id: number;
|
|
13
|
+
label: string;
|
|
14
|
+
phase?: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
status: "queued" | "running" | "done" | "error" | "skipped";
|
|
17
|
+
result?: unknown;
|
|
18
|
+
error?: string;
|
|
19
|
+
startedAt?: string;
|
|
20
|
+
endedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PersistedRunState {
|
|
24
|
+
runId: string;
|
|
25
|
+
workflowName: string;
|
|
26
|
+
script: string;
|
|
27
|
+
args?: unknown;
|
|
28
|
+
status: RunStatus;
|
|
29
|
+
phases: string[];
|
|
30
|
+
currentPhase?: string;
|
|
31
|
+
agents: PersistedAgentState[];
|
|
32
|
+
logs: string[];
|
|
33
|
+
result?: unknown;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
completedAt?: string;
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
tokenUsage?: {
|
|
39
|
+
input: number;
|
|
40
|
+
output: number;
|
|
41
|
+
total: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunPersistence {
|
|
46
|
+
/** Save current run state. */
|
|
47
|
+
save(state: PersistedRunState): void;
|
|
48
|
+
/** Load a persisted run by ID. */
|
|
49
|
+
load(runId: string): PersistedRunState | null;
|
|
50
|
+
/** List all persisted runs. */
|
|
51
|
+
list(): PersistedRunState[];
|
|
52
|
+
/** Delete a persisted run. */
|
|
53
|
+
delete(runId: string): boolean;
|
|
54
|
+
/** Get runs directory path. */
|
|
55
|
+
getRunsDir(): string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createRunPersistence(cwd: string): RunPersistence {
|
|
59
|
+
const runsDir = join(cwd, WORKFLOW_RUNS_DIR);
|
|
60
|
+
|
|
61
|
+
const ensureDir = () => {
|
|
62
|
+
if (!existsSync(runsDir)) {
|
|
63
|
+
mkdirSync(runsDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const runPath = (runId: string) => join(runsDir, `${runId}.json`);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
save(state: PersistedRunState) {
|
|
71
|
+
ensureDir();
|
|
72
|
+
state.updatedAt = new Date().toISOString();
|
|
73
|
+
writeFileSync(runPath(state.runId), JSON.stringify(state, null, 2));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
load(runId: string): PersistedRunState | null {
|
|
77
|
+
try {
|
|
78
|
+
const path = runPath(runId);
|
|
79
|
+
if (!existsSync(path)) return null;
|
|
80
|
+
return JSON.parse(readFileSync(path, "utf-8")) as PersistedRunState;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
list(): PersistedRunState[] {
|
|
87
|
+
ensureDir();
|
|
88
|
+
try {
|
|
89
|
+
const files = readdirSync(runsDir).filter((f) => f.endsWith(".json"));
|
|
90
|
+
const runs: PersistedRunState[] = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
try {
|
|
93
|
+
const state = JSON.parse(readFileSync(join(runsDir, file), "utf-8")) as PersistedRunState;
|
|
94
|
+
runs.push(state);
|
|
95
|
+
} catch {
|
|
96
|
+
// Skip corrupted files
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return runs.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
delete(runId: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
const path = runPath(runId);
|
|
108
|
+
if (existsSync(path)) {
|
|
109
|
+
const { unlinkSync } = require("node:fs");
|
|
110
|
+
unlinkSync(path);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
getRunsDir(): string {
|
|
120
|
+
return runsDir;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate a unique run ID.
|
|
127
|
+
*/
|
|
128
|
+
export function generateRunId(): string {
|
|
129
|
+
const timestamp = Date.now().toString(36);
|
|
130
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
131
|
+
return `${timestamp}-${random}`;
|
|
132
|
+
}
|