@yycholla/pi-dynamic-workflows 3.3.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/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +15 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +27 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- package/src/worktree.ts +76 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-specific error types.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Dependency-neutral diagnostic payload retained by capability contract failures. */
|
|
6
|
+
export interface CapabilityErrorDiagnostic {
|
|
7
|
+
code: string;
|
|
8
|
+
severity: "error" | "warning" | "information";
|
|
9
|
+
subject: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Dependency-neutral skill-loading payload retained by generation failures. */
|
|
14
|
+
export interface ModelGenerationSkillLoadingEvidence {
|
|
15
|
+
discovered: boolean;
|
|
16
|
+
loaded: boolean;
|
|
17
|
+
toolCalls: Array<{ tool: string; path?: string }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Dependency-neutral provider-usage payload retained by generation failures. */
|
|
21
|
+
export interface ModelGenerationTokenUsage {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
total: number;
|
|
25
|
+
cost: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Stable runtime and persistence failure codes exposed to callers and UI surfaces. */
|
|
31
|
+
export enum WorkflowErrorCode {
|
|
32
|
+
/** Agent exceeded timeout. */
|
|
33
|
+
AGENT_TIMEOUT = "AGENT_TIMEOUT",
|
|
34
|
+
/** Workflow was aborted by user. */
|
|
35
|
+
WORKFLOW_ABORTED = "WORKFLOW_ABORTED",
|
|
36
|
+
/** Agent limit exceeded. */
|
|
37
|
+
AGENT_LIMIT_EXCEEDED = "AGENT_LIMIT_EXCEEDED",
|
|
38
|
+
/** Token budget exhausted. */
|
|
39
|
+
TOKEN_BUDGET_EXHAUSTED = "TOKEN_BUDGET_EXHAUSTED",
|
|
40
|
+
/**
|
|
41
|
+
* The provider's subscription/usage/quota/rate limit was hit. Distinct from the
|
|
42
|
+
* user's self-imposed TOKEN_BUDGET_EXHAUSTED: a provider limit refills on its own,
|
|
43
|
+
* so the run is checkpointed (paused) and replayed by resume() rather than failed.
|
|
44
|
+
*/
|
|
45
|
+
PROVIDER_USAGE_LIMIT = "PROVIDER_USAGE_LIMIT",
|
|
46
|
+
/** Script validation failed. */
|
|
47
|
+
SCRIPT_VALIDATION_ERROR = "SCRIPT_VALIDATION_ERROR",
|
|
48
|
+
/** A schema agent never produced valid structured_output (after repair + extraction). */
|
|
49
|
+
SCHEMA_NONCOMPLIANCE = "SCHEMA_NONCOMPLIANCE",
|
|
50
|
+
/** A non-schema agent completed without any assistant text output. */
|
|
51
|
+
AGENT_EMPTY_OUTPUT = "AGENT_EMPTY_OUTPUT",
|
|
52
|
+
/** Agent execution failed. */
|
|
53
|
+
AGENT_EXECUTION_ERROR = "AGENT_EXECUTION_ERROR",
|
|
54
|
+
/** Run state persistence failed. */
|
|
55
|
+
PERSISTENCE_ERROR = "PERSISTENCE_ERROR",
|
|
56
|
+
/** Unknown error. */
|
|
57
|
+
UNKNOWN = "UNKNOWN",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Classified workflow failure with recoverability and optional agent/provider context. */
|
|
61
|
+
export class WorkflowError extends Error {
|
|
62
|
+
readonly code: WorkflowErrorCode;
|
|
63
|
+
readonly recoverable: boolean;
|
|
64
|
+
readonly agentLabel?: string;
|
|
65
|
+
readonly details?: unknown;
|
|
66
|
+
/** For PROVIDER_USAGE_LIMIT: the provider's human reset hint, e.g. "Resets in ~3h" (verbatim). */
|
|
67
|
+
readonly resetHint?: string;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
message: string,
|
|
71
|
+
code: WorkflowErrorCode,
|
|
72
|
+
options: { recoverable?: boolean; agentLabel?: string; details?: unknown; resetHint?: string } = {},
|
|
73
|
+
) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "WorkflowError";
|
|
76
|
+
this.code = code;
|
|
77
|
+
this.recoverable = options.recoverable ?? false;
|
|
78
|
+
this.agentLabel = options.agentLabel;
|
|
79
|
+
this.details = options.details;
|
|
80
|
+
this.resetHint = options.resetHint;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Contract failure that retains every definition or assembly diagnostic. */
|
|
85
|
+
export class WorkflowCapabilityContractError extends Error {
|
|
86
|
+
readonly diagnostics: readonly CapabilityErrorDiagnostic[];
|
|
87
|
+
|
|
88
|
+
constructor(message: string, diagnostics: readonly CapabilityErrorDiagnostic[]) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "WorkflowCapabilityContractError";
|
|
91
|
+
this.diagnostics = diagnostics;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Generation failure that retains loading and token evidence for diagnosis. */
|
|
96
|
+
export class ModelGenerationError extends Error {
|
|
97
|
+
readonly skillLoadingEvidence: ModelGenerationSkillLoadingEvidence;
|
|
98
|
+
readonly tokenUsage: ModelGenerationTokenUsage;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
message: string,
|
|
102
|
+
skillLoadingEvidence: ModelGenerationSkillLoadingEvidence,
|
|
103
|
+
tokenUsage: ModelGenerationTokenUsage,
|
|
104
|
+
) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "ModelGenerationError";
|
|
107
|
+
this.skillLoadingEvidence = skillLoadingEvidence;
|
|
108
|
+
this.tokenUsage = tokenUsage;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Narrow an unknown failure to WorkflowError. */
|
|
113
|
+
export function isWorkflowError(error: unknown): error is WorkflowError {
|
|
114
|
+
return error instanceof WorkflowError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Report whether an unknown failure is a provider usage-limit checkpoint condition. */
|
|
118
|
+
export function isProviderUsageLimit(error: unknown): error is WorkflowError {
|
|
119
|
+
return isWorkflowError(error) && error.code === WorkflowErrorCode.PROVIDER_USAGE_LIMIT;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect a provider subscription/usage/quota/rate-limit exhaustion from free-form
|
|
124
|
+
* error text, and extract the provider's human reset hint when present.
|
|
125
|
+
*
|
|
126
|
+
* The pi SDK does NOT throw these — it records them as an assistant message with
|
|
127
|
+
* stopReason "error" and an errorMessage like "Codex usage limit reached (plus
|
|
128
|
+
* plan). Resets in ~3h.". Callers reading message metadata MUST gate on
|
|
129
|
+
* stopReason === "error" before trusting this, so a task whose own output merely
|
|
130
|
+
* mentions "rate limit" is never misclassified. Patterns mirror the SDK's own
|
|
131
|
+
* non-retryable-limit table. Deliberately excludes transient overloaded/5xx
|
|
132
|
+
* errors, which stay recoverable and keep retrying.
|
|
133
|
+
*/
|
|
134
|
+
export function classifyProviderLimit(text: string | undefined): { matched: boolean; resetHint?: string } {
|
|
135
|
+
if (!text) return { matched: false };
|
|
136
|
+
const matched =
|
|
137
|
+
/usage limit|limit reached|insufficient[_\s]?quota|quota exceeded|exceeded your current quota|out of budget|available balance|\bquota\b|rate.?limit|too many requests|\b429\b|GoUsageLimitError|FreeUsageLimitError|\bbilling\b/i.test(
|
|
138
|
+
text,
|
|
139
|
+
);
|
|
140
|
+
if (!matched) return { matched: false };
|
|
141
|
+
const reset = text.match(/resets?\s+(?:in|at)\s+[^.\n]+/i);
|
|
142
|
+
return { matched: true, resetHint: reset?.[0]?.trim() };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Recognize abort-like Error messages without assuming a provider-specific class. */
|
|
146
|
+
export function isAbortError(error: unknown): boolean {
|
|
147
|
+
if (!(error instanceof Error)) return false;
|
|
148
|
+
return /\babort(?:ed)?\b/i.test(error.message);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Recognize timeout-like errors by name or message. */
|
|
152
|
+
export function isTimeoutError(error: unknown): boolean {
|
|
153
|
+
if (!(error instanceof Error)) return false;
|
|
154
|
+
return /\btimeout\b/i.test(error.message) || error.name === "TimeoutError";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Wrap an unknown error into a WorkflowError with appropriate classification.
|
|
159
|
+
*/
|
|
160
|
+
export function wrapError(error: unknown, context?: { agentLabel?: string }): WorkflowError {
|
|
161
|
+
if (isWorkflowError(error)) return error;
|
|
162
|
+
|
|
163
|
+
if (isAbortError(error)) {
|
|
164
|
+
return new WorkflowError(
|
|
165
|
+
error instanceof Error ? error.message : "Workflow was aborted",
|
|
166
|
+
WorkflowErrorCode.WORKFLOW_ABORTED,
|
|
167
|
+
{ recoverable: true },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isTimeoutError(error)) {
|
|
172
|
+
return new WorkflowError(
|
|
173
|
+
error instanceof Error ? error.message : "Agent timed out",
|
|
174
|
+
WorkflowErrorCode.AGENT_TIMEOUT,
|
|
175
|
+
{ recoverable: true, agentLabel: context?.agentLabel },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Defense-in-depth: today the SDK buries provider usage/quota limits in an
|
|
180
|
+
// assistant message (detected in agent.ts), but a future SDK might throw them.
|
|
181
|
+
// Classify a thrown limit here too — recoverable:false so the run checkpoints
|
|
182
|
+
// (paused) instead of being retried into the same wall or silently nulled.
|
|
183
|
+
if (error instanceof Error) {
|
|
184
|
+
const limit = classifyProviderLimit(error.message);
|
|
185
|
+
if (limit.matched) {
|
|
186
|
+
return new WorkflowError(error.message, WorkflowErrorCode.PROVIDER_USAGE_LIMIT, {
|
|
187
|
+
recoverable: false,
|
|
188
|
+
agentLabel: context?.agentLabel,
|
|
189
|
+
resetHint: limit.resetHint,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return new WorkflowError(
|
|
195
|
+
error instanceof Error ? error.message : String(error),
|
|
196
|
+
WorkflowErrorCode.AGENT_EXECUTION_ERROR,
|
|
197
|
+
{ recoverable: true, agentLabel: context?.agentLabel, details: error },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
2
|
+
import type { EffortState } from "./effort-command.js";
|
|
3
|
+
import type { WorkflowManager } from "./workflow-manager.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live extension state that Pi may hand from one extension generation to the
|
|
7
|
+
* next during `/reload`. This deliberately stays process-local: run snapshots
|
|
8
|
+
* and journals already provide the durable cold-start path, while the live
|
|
9
|
+
* manager is what owns in-flight promises, abort controllers, and event streams.
|
|
10
|
+
*/
|
|
11
|
+
export const WORKFLOW_EXTENSION_VERSION = packageJson.version;
|
|
12
|
+
|
|
13
|
+
export interface WorkflowReloadRuntime {
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** Package version that created this manager. Only an exact match is retained. */
|
|
16
|
+
extensionVersion: string;
|
|
17
|
+
manager: WorkflowManager;
|
|
18
|
+
effort: EffortState;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkflowRuntimeClaim {
|
|
22
|
+
compatible?: WorkflowReloadRuntime;
|
|
23
|
+
versionMismatch?: WorkflowReloadRuntime;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HandoffEntry {
|
|
27
|
+
runtime: WorkflowReloadRuntime;
|
|
28
|
+
timer: ReturnType<typeof setTimeout>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RELOAD_HANDOFF_KEY = Symbol.for("@quintinshaw/pi-dynamic-workflows:reload-handoffs");
|
|
32
|
+
const RELOAD_HANDOFF_TTL_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
function handoffs(): Map<string, HandoffEntry> {
|
|
35
|
+
const root = globalThis as typeof globalThis & { [RELOAD_HANDOFF_KEY]?: Map<string, HandoffEntry> };
|
|
36
|
+
const existing = root[RELOAD_HANDOFF_KEY];
|
|
37
|
+
if (existing) return existing;
|
|
38
|
+
const created = new Map<string, HandoffEntry>();
|
|
39
|
+
root[RELOAD_HANDOFF_KEY] = created;
|
|
40
|
+
return created;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Stage a live runtime immediately before Pi tears down the old extension runner. */
|
|
44
|
+
export function handoffWorkflowRuntime(runtime: WorkflowReloadRuntime): void {
|
|
45
|
+
const store = handoffs();
|
|
46
|
+
const previous = store.get(runtime.cwd);
|
|
47
|
+
if (previous) clearTimeout(previous.timer);
|
|
48
|
+
|
|
49
|
+
const entry = {} as HandoffEntry;
|
|
50
|
+
entry.runtime = runtime;
|
|
51
|
+
entry.timer = setTimeout(() => {
|
|
52
|
+
if (store.get(runtime.cwd) === entry) store.delete(runtime.cwd);
|
|
53
|
+
}, RELOAD_HANDOFF_TTL_MS);
|
|
54
|
+
entry.timer.unref?.();
|
|
55
|
+
store.set(runtime.cwd, entry);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Claim a staged runtime from the extension generation that `/reload` just stopped. */
|
|
59
|
+
export function takeWorkflowRuntime(cwd: string): WorkflowReloadRuntime | undefined {
|
|
60
|
+
const store = handoffs();
|
|
61
|
+
const entry = store.get(cwd);
|
|
62
|
+
if (!entry) return undefined;
|
|
63
|
+
clearTimeout(entry.timer);
|
|
64
|
+
store.delete(cwd);
|
|
65
|
+
return entry.runtime;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Claim a staged runtime and compare its package version with this extension
|
|
70
|
+
* generation. Any package update falls back to a fresh manager; only reloads
|
|
71
|
+
* within the exact same installed version retain live workflow state.
|
|
72
|
+
*/
|
|
73
|
+
export function claimWorkflowRuntime(cwd: string): WorkflowRuntimeClaim {
|
|
74
|
+
const runtime = takeWorkflowRuntime(cwd);
|
|
75
|
+
if (!runtime) return {};
|
|
76
|
+
return runtime.extensionVersion === WORKFLOW_EXTENSION_VERSION
|
|
77
|
+
? { compatible: runtime }
|
|
78
|
+
: { versionMismatch: runtime };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Move live runs from a replaced extension version onto the existing journal
|
|
83
|
+
* recovery path before the fresh manager is constructed.
|
|
84
|
+
*/
|
|
85
|
+
export function pauseVersionMismatchWorkflowRuntime(runtime: WorkflowReloadRuntime): number {
|
|
86
|
+
let paused = 0;
|
|
87
|
+
for (const run of runtime.manager.listRuns()) {
|
|
88
|
+
if (run.status === "running" && runtime.manager.pause(run.runId)) paused++;
|
|
89
|
+
}
|
|
90
|
+
return paused;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test/cleanup helper; identity guard avoids deleting a newer handoff. */
|
|
94
|
+
export function discardWorkflowRuntime(cwd: string, runtime?: WorkflowReloadRuntime): void {
|
|
95
|
+
const store = handoffs();
|
|
96
|
+
const entry = store.get(cwd);
|
|
97
|
+
if (!entry || (runtime && entry.runtime !== runtime)) return;
|
|
98
|
+
clearTimeout(entry.timer);
|
|
99
|
+
store.delete(cwd);
|
|
100
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem primitives for JSON-backed persistence.
|
|
3
|
+
*
|
|
4
|
+
* Both run-persistence.ts (workflow runs) and workflow-saved.ts (saved
|
|
5
|
+
* workflow commands) persist plain-JSON records to per-record files under a
|
|
6
|
+
* project/user directory, and both need the same three guarantees:
|
|
7
|
+
*
|
|
8
|
+
* 1. Atomic writes with a recovery backup — a crash mid-write must never
|
|
9
|
+
* corrupt the live file, and a later-discovered-truncated primary must
|
|
10
|
+
* still be recoverable from the last good write.
|
|
11
|
+
* 2. Corrupt-file recovery on read — a truncated/corrupt primary falls back
|
|
12
|
+
* to its `.bak` sidecar instead of losing the record.
|
|
13
|
+
* 3. A missing or unreadable directory degrades to "no files" rather than
|
|
14
|
+
* throwing — a listing must never crash because one storage location is
|
|
15
|
+
* temporarily inaccessible (not yet created, deleted mid-race, EACCES).
|
|
16
|
+
*
|
|
17
|
+
* This module is the single implementation of all three; run-persistence.ts
|
|
18
|
+
* and workflow-saved.ts both call into it rather than maintaining parallel
|
|
19
|
+
* copies.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
renameSync,
|
|
28
|
+
statSync,
|
|
29
|
+
unlinkSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
|
|
33
|
+
/** Filesystem operations used by JSON persistence. Exposed for testing. */
|
|
34
|
+
export type PersistenceFsLayer = {
|
|
35
|
+
existsSync: typeof existsSync;
|
|
36
|
+
mkdirSync: typeof mkdirSync;
|
|
37
|
+
readdirSync: typeof readdirSync;
|
|
38
|
+
readFileSync: typeof readFileSync;
|
|
39
|
+
renameSync: typeof renameSync;
|
|
40
|
+
statSync: typeof statSync;
|
|
41
|
+
unlinkSync: typeof unlinkSync;
|
|
42
|
+
writeFileSync: typeof writeFileSync;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The real node:fs implementations. */
|
|
46
|
+
export function defaultPersistenceFs(): PersistenceFsLayer {
|
|
47
|
+
return { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Merge a partial test override on top of the real node:fs implementations. */
|
|
51
|
+
export function resolvePersistenceFs(overrides?: Partial<PersistenceFsLayer>): PersistenceFsLayer {
|
|
52
|
+
const base = defaultPersistenceFs();
|
|
53
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Ensure `dir` exists (recursive mkdir), idempotent. */
|
|
57
|
+
export function ensureDir(fs: PersistenceFsLayer, dir: string): void {
|
|
58
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Atomically write JSON to `path`: tmp-write + rename (atomic on the same
|
|
63
|
+
* filesystem, so a crash mid-write can't corrupt the live file), then
|
|
64
|
+
* best-effort refresh a `.bak` sidecar from the just-written good state —
|
|
65
|
+
* the recovery fallback readJsonWithBackupRecovery() uses if the primary is
|
|
66
|
+
* later found truncated (e.g. a rename that itself got interrupted by a
|
|
67
|
+
* power loss on a filesystem/OS combination where rename isn't fully atomic).
|
|
68
|
+
*/
|
|
69
|
+
export function writeJsonAtomicWithBackup(fs: PersistenceFsLayer, path: string, data: unknown): void {
|
|
70
|
+
const json = JSON.stringify(data, null, 2);
|
|
71
|
+
fs.writeFileSync(`${path}.tmp`, json);
|
|
72
|
+
fs.renameSync(`${path}.tmp`, path);
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(`${path}.bak`, json);
|
|
75
|
+
} catch {
|
|
76
|
+
// Backup is best-effort; the primary write already succeeded.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read JSON from `path`, falling back to `path.bak` if the primary is
|
|
82
|
+
* missing or fails to parse. Returns null if neither candidate parses.
|
|
83
|
+
*/
|
|
84
|
+
export function readJsonWithBackupRecovery<T>(fs: PersistenceFsLayer, path: string): T | null {
|
|
85
|
+
for (const candidate of [path, `${path}.bak`]) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(candidate)) continue;
|
|
88
|
+
return JSON.parse(fs.readFileSync(candidate, "utf-8")) as T;
|
|
89
|
+
} catch {
|
|
90
|
+
// Corrupt candidate -> fall through to the next candidate.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List `.json` record files in `dir`. A missing directory (never created
|
|
98
|
+
* yet) or an unreadable one (deleted between the existsSync check and
|
|
99
|
+
* readdirSync, permission-denied, etc.) both degrade to an empty list
|
|
100
|
+
* rather than throwing — callers (run listings, saved-workflow listings)
|
|
101
|
+
* must never crash a navigator/listing because one storage location is
|
|
102
|
+
* temporarily inaccessible.
|
|
103
|
+
*/
|
|
104
|
+
export function listJsonFilesSafe(fs: PersistenceFsLayer, dir: string): string[] {
|
|
105
|
+
try {
|
|
106
|
+
if (!fs.existsSync(dir)) return [];
|
|
107
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Best-effort unlink; ignores missing-file/permission errors, reports whether it deleted anything. */
|
|
114
|
+
export function unlinkIfExistsSafe(fs: PersistenceFsLayer, path: string): boolean {
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(path)) {
|
|
117
|
+
fs.unlinkSync(path);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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 { listAvailableModelSpecs, listAvailableModels, WorkflowAgent } from "./agent.js";
|
|
5
|
+
export type { AgentHistoryEntry, AgentHistoryKind, AgentHistoryRole } from "./agent-history.js";
|
|
6
|
+
export { compactAgentHistory } from "./agent-history.js";
|
|
7
|
+
export type { AgentDefinition, AgentRegistry } from "./agent-registry.js";
|
|
8
|
+
export { applyToolPolicy, listAgentTypes, loadAgentRegistry, resolveAgentType } from "./agent-registry.js";
|
|
9
|
+
export { registerBuiltinWorkflows } from "./builtin-commands.js";
|
|
10
|
+
export { generateCodeReviewWorkflow, MAX_DIFF_CHARS } from "./code-review.js";
|
|
11
|
+
export * from "./config.js";
|
|
12
|
+
export type { DeepResearchConfig } from "./deep-research.js";
|
|
13
|
+
export { generateCodebaseAuditWorkflow, generateDeepResearchWorkflow } from "./deep-research.js";
|
|
14
|
+
export type {
|
|
15
|
+
WorkflowAgentSnapshot,
|
|
16
|
+
WorkflowAgentStatus,
|
|
17
|
+
WorkflowDisplay,
|
|
18
|
+
WorkflowDisplayOptions,
|
|
19
|
+
WorkflowSnapshot,
|
|
20
|
+
} from "./display.js";
|
|
21
|
+
export {
|
|
22
|
+
createToolUpdateWorkflowDisplay,
|
|
23
|
+
createWidgetWorkflowDisplay,
|
|
24
|
+
createWorkflowSnapshot,
|
|
25
|
+
preview,
|
|
26
|
+
recomputeWorkflowSnapshot,
|
|
27
|
+
renderWorkflowLines,
|
|
28
|
+
renderWorkflowText,
|
|
29
|
+
} from "./display.js";
|
|
30
|
+
export {
|
|
31
|
+
createEffortState,
|
|
32
|
+
type EffortLevel,
|
|
33
|
+
type EffortState,
|
|
34
|
+
effortDirective,
|
|
35
|
+
isSubstantive,
|
|
36
|
+
registerEffortCommand,
|
|
37
|
+
} from "./effort-command.js";
|
|
38
|
+
export {
|
|
39
|
+
isAbortError,
|
|
40
|
+
isTimeoutError,
|
|
41
|
+
isWorkflowError,
|
|
42
|
+
WorkflowError,
|
|
43
|
+
WorkflowErrorCode,
|
|
44
|
+
wrapError,
|
|
45
|
+
} from "./errors.js";
|
|
46
|
+
export type { WorkflowLogger, WorkflowLoggerOptions } from "./logger.js";
|
|
47
|
+
export { createWorkflowLogger } from "./logger.js";
|
|
48
|
+
export type { ModelRoute, ModelRoutingConfig } from "./model-routing.js";
|
|
49
|
+
export { parseModelRoutingFromMeta, resolveModelForPhase } from "./model-routing.js";
|
|
50
|
+
export type { ModelThinkingLevel, ResolvedModelSpec } from "./model-spec.js";
|
|
51
|
+
export {
|
|
52
|
+
canonicalModelSpec,
|
|
53
|
+
formatModelSpecWithThinking,
|
|
54
|
+
isThinkingLevel,
|
|
55
|
+
resolveModelSpecWithThinking,
|
|
56
|
+
splitModelSpecThinking,
|
|
57
|
+
THINKING_LEVELS,
|
|
58
|
+
} from "./model-spec.js";
|
|
59
|
+
export type { ModelTierConfig, RankableModel } from "./model-tier-config.js";
|
|
60
|
+
export {
|
|
61
|
+
buildDefaultTierConfig,
|
|
62
|
+
formatTierFallbackNotice,
|
|
63
|
+
getModelTierConfigPath,
|
|
64
|
+
loadModelTierConfig,
|
|
65
|
+
resolveTierModel,
|
|
66
|
+
saveModelTierConfig,
|
|
67
|
+
sortedTierNames,
|
|
68
|
+
} from "./model-tier-config.js";
|
|
69
|
+
export type { PersistedRunState, RunPersistence, RunStatus } from "./run-persistence.js";
|
|
70
|
+
export { createRunPersistence, generateRunId } from "./run-persistence.js";
|
|
71
|
+
export {
|
|
72
|
+
parseCommandArgs,
|
|
73
|
+
registerAllSavedWorkflows,
|
|
74
|
+
registerSavedWorkflow,
|
|
75
|
+
} from "./saved-commands.js";
|
|
76
|
+
export { SharedStore } from "./shared-store.js";
|
|
77
|
+
export type { StructuredOutputCapture, StructuredOutputToolOptions } from "./structured-output.js";
|
|
78
|
+
export { createStructuredOutputTool } from "./structured-output.js";
|
|
79
|
+
export { deliverText, installResultDelivery, installTaskPanel, type TaskPanelOptions } from "./task-panel.js";
|
|
80
|
+
export type {
|
|
81
|
+
AutoResumeDelayParams,
|
|
82
|
+
SchedulableWorkflowManager,
|
|
83
|
+
TimerHandle,
|
|
84
|
+
UsageLimitSchedulerOptions,
|
|
85
|
+
} from "./usage-limit-scheduler.js";
|
|
86
|
+
export { computeAutoResumeDelayMs, parseResetHintMs, UsageLimitScheduler } from "./usage-limit-scheduler.js";
|
|
87
|
+
export { createWebFetchTool, createWebSearchTool, createWebTools } from "./web-tools.js";
|
|
88
|
+
export type {
|
|
89
|
+
AgentOptions,
|
|
90
|
+
JournalEntry,
|
|
91
|
+
SharedRuntime,
|
|
92
|
+
WorkflowMeta,
|
|
93
|
+
WorkflowMetaPhase,
|
|
94
|
+
WorkflowRunOptions,
|
|
95
|
+
WorkflowRunResult,
|
|
96
|
+
} from "./workflow.js";
|
|
97
|
+
export { parseWorkflowScript, runWorkflow } from "./workflow.js";
|
|
98
|
+
export type {
|
|
99
|
+
AlignmentEvidence,
|
|
100
|
+
CapabilityDescriptor,
|
|
101
|
+
CapabilityDiagnostic,
|
|
102
|
+
DynamicReferenceDescriptor,
|
|
103
|
+
OptionDescriptor,
|
|
104
|
+
OptionShape,
|
|
105
|
+
PresentAtVersion,
|
|
106
|
+
RuntimeBindingAssembly,
|
|
107
|
+
StaticCapabilityFact,
|
|
108
|
+
WorkflowCapabilityContract,
|
|
109
|
+
WorkflowCapabilityDefinition,
|
|
110
|
+
WorkflowRuntimeImplementations,
|
|
111
|
+
} from "./workflow-capability-contract.js";
|
|
112
|
+
export {
|
|
113
|
+
CapabilityClassification,
|
|
114
|
+
CapabilityOrigin,
|
|
115
|
+
CapabilitySupport,
|
|
116
|
+
DiagnosticSeverity,
|
|
117
|
+
DiscoveryPlacement,
|
|
118
|
+
defineWorkflowCapabilityContract,
|
|
119
|
+
WORKFLOW_CAPABILITY_CONTRACT,
|
|
120
|
+
WORKFLOW_CAPABILITY_DEFINITION,
|
|
121
|
+
WorkflowCapabilityContractError,
|
|
122
|
+
} from "./workflow-capability-contract.js";
|
|
123
|
+
export { registerWorkflowCommands } from "./workflow-commands.js";
|
|
124
|
+
export type {
|
|
125
|
+
WorkflowControlInput,
|
|
126
|
+
WorkflowControlRunDetails,
|
|
127
|
+
WorkflowControlToolOptions,
|
|
128
|
+
} from "./workflow-control-tool.js";
|
|
129
|
+
export { createWorkflowControlTool } from "./workflow-control-tool.js";
|
|
130
|
+
export {
|
|
131
|
+
type ArmReason,
|
|
132
|
+
buildArmedWorkflowPrompt,
|
|
133
|
+
buildForcedWorkflowPrompt,
|
|
134
|
+
endsWithTrigger,
|
|
135
|
+
hasTrigger,
|
|
136
|
+
type InstallWorkflowKeywordArmingOptions,
|
|
137
|
+
installWorkflowKeywordArming,
|
|
138
|
+
registerWorkflowProgressCommands,
|
|
139
|
+
registerWorkflowTriggerCommand,
|
|
140
|
+
type WorkflowModeState,
|
|
141
|
+
} from "./workflow-editor.js";
|
|
142
|
+
export type { ManagedRun, WorkflowManagerOptions } from "./workflow-manager.js";
|
|
143
|
+
export { WorkflowManager } from "./workflow-manager.js";
|
|
144
|
+
export type { WorkflowProjectPaths } from "./workflow-paths.js";
|
|
145
|
+
export {
|
|
146
|
+
WORKFLOW_HOME_RELATIVE_DIR,
|
|
147
|
+
WORKFLOW_PROJECTS_SUBDIR,
|
|
148
|
+
workflowHomeDir,
|
|
149
|
+
workflowProjectKey,
|
|
150
|
+
workflowProjectPaths,
|
|
151
|
+
workflowUserSavedDir,
|
|
152
|
+
} from "./workflow-paths.js";
|
|
153
|
+
export type { SavedWorkflow, WorkflowStorage } from "./workflow-saved.js";
|
|
154
|
+
export { assertSafeSavedWorkflowName, createWorkflowStorage, isSafeSavedWorkflowName } from "./workflow-saved.js";
|
|
155
|
+
export type { WorkflowSettings, WorkflowSettingsOptions, WorkflowSettingsStore } from "./workflow-settings.js";
|
|
156
|
+
export {
|
|
157
|
+
getWorkflowProjectSettingsPath,
|
|
158
|
+
getWorkflowSettingsPath,
|
|
159
|
+
loadWorkflowSettings,
|
|
160
|
+
saveWorkflowSettings,
|
|
161
|
+
saveWorkflowSettingsForCwd,
|
|
162
|
+
} from "./workflow-settings.js";
|
|
163
|
+
export type { WorkflowToolInput, WorkflowToolOptions } from "./workflow-tool.js";
|
|
164
|
+
export { backgroundStartedText, createWorkflowTool } from "./workflow-tool.js";
|
|
165
|
+
export {
|
|
166
|
+
keyToAction,
|
|
167
|
+
type NavAction,
|
|
168
|
+
NavigatorModel,
|
|
169
|
+
NavigatorState,
|
|
170
|
+
openWorkflowNavigator,
|
|
171
|
+
renderNavigator,
|
|
172
|
+
type ViewKind,
|
|
173
|
+
} from "./workflow-ui.js";
|
|
174
|
+
export { registerWorkflowModelsCommand } from "./workflows-models-command.js";
|
|
175
|
+
export type { Worktree } from "./worktree.js";
|
|
176
|
+
export { createWorktree, removeWorktree } from "./worktree.js";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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 { workflowProjectPaths } from "./workflow-paths.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
|
+
const runsDir = workflowProjectPaths(cwd).runsDir;
|
|
34
|
+
let logFile: string | null = null;
|
|
35
|
+
|
|
36
|
+
const write = (level: string, message: string) => {
|
|
37
|
+
const timestamp = new Date().toISOString();
|
|
38
|
+
const entry = `[${timestamp}] [${level}] ${message}`;
|
|
39
|
+
logs.push(entry);
|
|
40
|
+
options.onLog?.(message);
|
|
41
|
+
|
|
42
|
+
if (persistLogs && logFile) {
|
|
43
|
+
try {
|
|
44
|
+
appendFileSync(logFile, `${entry}\n`);
|
|
45
|
+
} catch {
|
|
46
|
+
// Silent fail for log persistence
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const logger: WorkflowLogger = {
|
|
52
|
+
log(message: string) {
|
|
53
|
+
write("INFO", message);
|
|
54
|
+
},
|
|
55
|
+
error(message: string) {
|
|
56
|
+
write("ERROR", message);
|
|
57
|
+
},
|
|
58
|
+
warn(message: string) {
|
|
59
|
+
write("WARN", message);
|
|
60
|
+
},
|
|
61
|
+
getLogs() {
|
|
62
|
+
return [...logs];
|
|
63
|
+
},
|
|
64
|
+
persist() {
|
|
65
|
+
if (!persistLogs) return null;
|
|
66
|
+
try {
|
|
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
|
+
mkdirSync(runsDir, { recursive: true });
|
|
81
|
+
logFile = join(runsDir, `${runId}.log`);
|
|
82
|
+
} catch {
|
|
83
|
+
// Silent fail
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return logger;
|
|
88
|
+
}
|