pi-workflow-engine 0.11.0 → 0.13.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 +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +26 -4
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +82 -10
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +13 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +127 -41
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +52 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +151 -178
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +47 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +270 -17
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
clearWorkflowModelProfile,
|
|
5
|
+
isWorkflowModelProfileName,
|
|
6
|
+
isWorkflowThinkingLevel,
|
|
7
|
+
resolveWorkflowModelProfiles,
|
|
8
|
+
setWorkflowModelProfile,
|
|
9
|
+
workflowModelProfilePaths,
|
|
10
|
+
WORKFLOW_MODEL_PROFILE_NAMES,
|
|
11
|
+
WORKFLOW_THINKING_LEVELS,
|
|
12
|
+
type ResolvedWorkflowModelProfiles,
|
|
13
|
+
type WorkflowModelProfilePaths,
|
|
14
|
+
type WorkflowModelProfileName,
|
|
15
|
+
} from "./model-profiles.ts";
|
|
16
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
17
|
+
import { completeCurrentArgument, splitArgumentPrefix } from "./command-completions.ts";
|
|
18
|
+
|
|
19
|
+
type ConfigScope = "user" | "project";
|
|
20
|
+
|
|
21
|
+
type WorkflowModelsCommand =
|
|
22
|
+
| { readonly kind: "status" }
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "set";
|
|
25
|
+
readonly scope: ConfigScope;
|
|
26
|
+
readonly profile: WorkflowModelProfileName;
|
|
27
|
+
readonly model: string;
|
|
28
|
+
readonly thinkingLevel?: Parameters<typeof setWorkflowModelProfile>[0]["profile"]["thinkingLevel"];
|
|
29
|
+
}
|
|
30
|
+
| { readonly kind: "clear"; readonly scope: ConfigScope; readonly profile: WorkflowModelProfileName }
|
|
31
|
+
| { readonly kind: "error"; readonly message: string };
|
|
32
|
+
|
|
33
|
+
const USAGE =
|
|
34
|
+
"Usage: /workflow:models [status | set <small|medium|big> <provider/model> [thinkingLevel] [--user|--project] | clear <small|medium|big> [--user|--project]]";
|
|
35
|
+
const MODEL_COMMAND_ACTIONS = [
|
|
36
|
+
{ value: "status", description: "Show resolved workflow model profiles" },
|
|
37
|
+
{ value: "set", description: "Configure an exact model for a profile" },
|
|
38
|
+
{ value: "clear", description: "Remove a configured profile" },
|
|
39
|
+
] as const;
|
|
40
|
+
const MODEL_COMMAND_SCOPES = [
|
|
41
|
+
{ value: "--user", description: "Write the user-level workflow model config" },
|
|
42
|
+
{ value: "--project", description: "Write the project-level workflow model config" },
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export function registerWorkflowModelProfileCommand(pi: ExtensionAPI): void {
|
|
46
|
+
pi.registerCommand("workflow:models", {
|
|
47
|
+
description: "Inspect or configure exact small, medium, and big workflow model profiles",
|
|
48
|
+
getArgumentCompletions: workflowModelProfileArgumentCompletions,
|
|
49
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
50
|
+
const command = parseWorkflowModelsCommand(args);
|
|
51
|
+
if (command.kind === "error") {
|
|
52
|
+
ctx.ui.notify(`${command.message}\n${USAGE}`, "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const paths = workflowModelProfilePaths(ctx.cwd);
|
|
58
|
+
if (command.kind === "set") {
|
|
59
|
+
setWorkflowModelProfile({
|
|
60
|
+
configPath: paths[command.scope],
|
|
61
|
+
name: command.profile,
|
|
62
|
+
profile: {
|
|
63
|
+
model: command.model,
|
|
64
|
+
...(command.thinkingLevel === undefined ? {} : { thinkingLevel: command.thinkingLevel }),
|
|
65
|
+
},
|
|
66
|
+
modelRegistry: ctx.modelRegistry,
|
|
67
|
+
});
|
|
68
|
+
} else if (command.kind === "clear") {
|
|
69
|
+
clearWorkflowModelProfile(paths[command.scope], command.profile);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const profiles = resolveWorkflowModelProfiles({
|
|
73
|
+
cwd: ctx.cwd,
|
|
74
|
+
modelRegistry: ctx.modelRegistry,
|
|
75
|
+
hostModel: ctx.model,
|
|
76
|
+
paths,
|
|
77
|
+
});
|
|
78
|
+
const prefix = command.kind === "status"
|
|
79
|
+
? "Workflow model profiles"
|
|
80
|
+
: `${command.profile} ${command.kind === "set" ? "updated" : "cleared"} in ${command.scope} config`;
|
|
81
|
+
ctx.ui.notify(`${prefix}\n${formatWorkflowModelProfiles(profiles, paths)}`, "info");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
ctx.ui.notify(unknownErrorMessage(error), "error");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function workflowModelProfileArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
90
|
+
const parts = splitArgumentPrefix(argumentPrefix);
|
|
91
|
+
const completedWithoutScopes = parts.completed.filter((token) => token !== "--user" && token !== "--project");
|
|
92
|
+
const action = completedWithoutScopes[0];
|
|
93
|
+
const scopes = parts.completed.some((token) => token === "--user" || token === "--project")
|
|
94
|
+
? []
|
|
95
|
+
: MODEL_COMMAND_SCOPES;
|
|
96
|
+
|
|
97
|
+
if (completedWithoutScopes.length === 0) {
|
|
98
|
+
return completeCurrentArgument(argumentPrefix, MODEL_COMMAND_ACTIONS);
|
|
99
|
+
}
|
|
100
|
+
if (action === "status") return null;
|
|
101
|
+
if (completedWithoutScopes.length === 1) {
|
|
102
|
+
return completeCurrentArgument(
|
|
103
|
+
argumentPrefix,
|
|
104
|
+
WORKFLOW_MODEL_PROFILE_NAMES.map((value) => ({ value, description: `${value} workflow model profile` })),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (action === "clear") return completeCurrentArgument(argumentPrefix, scopes);
|
|
108
|
+
if (action !== "set") return null;
|
|
109
|
+
|
|
110
|
+
if (completedWithoutScopes.length === 2) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (completedWithoutScopes.length === 3) {
|
|
114
|
+
return completeCurrentArgument(argumentPrefix, [
|
|
115
|
+
...WORKFLOW_THINKING_LEVELS.map((value) => ({ value, description: "Thinking level" })),
|
|
116
|
+
...scopes,
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
return completeCurrentArgument(argumentPrefix, scopes);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseWorkflowModelsCommand(args: string): WorkflowModelsCommand {
|
|
123
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
124
|
+
if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "status")) return { kind: "status" };
|
|
125
|
+
|
|
126
|
+
const userScope = tokens.includes("--user");
|
|
127
|
+
const projectScope = tokens.includes("--project");
|
|
128
|
+
if (userScope && projectScope) return { kind: "error", message: "Choose either --user or --project, not both." };
|
|
129
|
+
const scope: ConfigScope = projectScope ? "project" : "user";
|
|
130
|
+
const positional = tokens.filter((token) => token !== "--user" && token !== "--project");
|
|
131
|
+
const [action, profile, model, thinkingLevel, ...extra] = positional;
|
|
132
|
+
if (extra.length > 0) return { kind: "error", message: "Too many /workflow:models arguments." };
|
|
133
|
+
if (!profile || !isWorkflowModelProfileName(profile)) {
|
|
134
|
+
return { kind: "error", message: "Profile must be small, medium, or big." };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (action === "clear") {
|
|
138
|
+
if (model !== undefined) return { kind: "error", message: "The clear action accepts only a profile and optional scope." };
|
|
139
|
+
return { kind: "clear", scope, profile };
|
|
140
|
+
}
|
|
141
|
+
if (action !== "set") return { kind: "error", message: `Unknown /workflow:models action "${action ?? ""}".` };
|
|
142
|
+
if (!model) return { kind: "error", message: "The set action requires an exact provider/model identity." };
|
|
143
|
+
if (thinkingLevel !== undefined && !isWorkflowThinkingLevel(thinkingLevel)) {
|
|
144
|
+
return { kind: "error", message: "Thinking level must be off, minimal, low, medium, high, xhigh, or max." };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
kind: "set",
|
|
148
|
+
scope,
|
|
149
|
+
profile,
|
|
150
|
+
model,
|
|
151
|
+
...(thinkingLevel === undefined ? {} : { thinkingLevel }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatWorkflowModelProfiles(
|
|
156
|
+
profiles: ResolvedWorkflowModelProfiles,
|
|
157
|
+
paths: WorkflowModelProfilePaths,
|
|
158
|
+
): string {
|
|
159
|
+
const lines = WORKFLOW_MODEL_PROFILE_NAMES.map((name) => {
|
|
160
|
+
const profile = profiles[name];
|
|
161
|
+
const model = profile.model ? `${profile.model.provider}/${profile.model.id}` : "(no host model)";
|
|
162
|
+
const thinking = profile.thinkingLevel ?? "host";
|
|
163
|
+
const source = profile.source === "host" ? "host fallback" : `${profile.source}: ${profile.configPath}`;
|
|
164
|
+
return `${name}: ${model} · thinking ${thinking} · ${source}`;
|
|
165
|
+
});
|
|
166
|
+
return [...lines, `user: ${paths.user}`, `project: ${paths.project} (overrides user)`].join("\n");
|
|
167
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { isMissingPathError } from "./filesystem-error.ts";
|
|
8
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
9
|
+
|
|
10
|
+
export const WORKFLOW_MODEL_PROFILE_NAMES = ["small", "medium", "big"] as const;
|
|
11
|
+
export type WorkflowModelProfileName = (typeof WORKFLOW_MODEL_PROFILE_NAMES)[number];
|
|
12
|
+
|
|
13
|
+
export interface WorkflowModelProfileConfig {
|
|
14
|
+
readonly model: string;
|
|
15
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkflowModelProfileFile {
|
|
19
|
+
readonly profiles: Partial<Record<WorkflowModelProfileName, WorkflowModelProfileConfig>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkflowModelProfilePaths {
|
|
23
|
+
readonly user: string;
|
|
24
|
+
readonly project: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ResolvedWorkflowModelProfileBase {
|
|
28
|
+
readonly name: WorkflowModelProfileName;
|
|
29
|
+
readonly model: Model<Api> | undefined;
|
|
30
|
+
readonly thinkingLevel: ThinkingLevel | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ResolvedWorkflowModelProfile = ResolvedWorkflowModelProfileBase & (
|
|
34
|
+
| { readonly source: "host" }
|
|
35
|
+
| { readonly source: "project" | "user"; readonly configPath: string }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type ResolvedWorkflowModelProfiles = Readonly<Record<WorkflowModelProfileName, ResolvedWorkflowModelProfile>>;
|
|
39
|
+
|
|
40
|
+
export interface WorkflowModelRouteRequest {
|
|
41
|
+
readonly model?: string;
|
|
42
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
43
|
+
readonly profile?: WorkflowModelProfileName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class WorkflowModelProfileConfigError extends Error {
|
|
47
|
+
override readonly name = "WorkflowModelProfileConfigError";
|
|
48
|
+
readonly code = "WORKFLOW_MODEL_PROFILE_CONFIG";
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
readonly configPath: string,
|
|
53
|
+
readonly profile?: WorkflowModelProfileName,
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const WORKFLOW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
60
|
+
const THINKING_LEVELS = new Set<ThinkingLevel>(WORKFLOW_THINKING_LEVELS);
|
|
61
|
+
const HOST_FALLBACK_THINKING: Record<WorkflowModelProfileName, ThinkingLevel> = {
|
|
62
|
+
small: "low",
|
|
63
|
+
medium: "medium",
|
|
64
|
+
big: "high",
|
|
65
|
+
};
|
|
66
|
+
const CONFIG_FILE_NAME = "workflow-models.json";
|
|
67
|
+
|
|
68
|
+
export function workflowModelProfilePaths(
|
|
69
|
+
cwd: string,
|
|
70
|
+
agentDir: string = getAgentDir(),
|
|
71
|
+
): WorkflowModelProfilePaths {
|
|
72
|
+
return {
|
|
73
|
+
user: join(agentDir, CONFIG_FILE_NAME),
|
|
74
|
+
project: join(cwd, ".pi", CONFIG_FILE_NAME),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveWorkflowModelProfiles(input: {
|
|
79
|
+
readonly cwd: string;
|
|
80
|
+
readonly modelRegistry: Pick<ModelRegistry, "find">;
|
|
81
|
+
readonly hostModel: Model<Api> | undefined;
|
|
82
|
+
readonly paths?: WorkflowModelProfilePaths;
|
|
83
|
+
}): ResolvedWorkflowModelProfiles {
|
|
84
|
+
const paths = input.paths ?? workflowModelProfilePaths(input.cwd);
|
|
85
|
+
const user = readWorkflowModelProfileFile(paths.user);
|
|
86
|
+
const project = readWorkflowModelProfileFile(paths.project);
|
|
87
|
+
const resolved = { ...hostWorkflowModelProfiles(input.hostModel) };
|
|
88
|
+
|
|
89
|
+
for (const name of WORKFLOW_MODEL_PROFILE_NAMES) {
|
|
90
|
+
const projectProfile = project.profiles[name];
|
|
91
|
+
const configured = projectProfile ?? user.profiles[name];
|
|
92
|
+
if (!configured) continue;
|
|
93
|
+
|
|
94
|
+
const source = projectProfile ? "project" : "user";
|
|
95
|
+
const configPath = paths[source];
|
|
96
|
+
resolved[name] = {
|
|
97
|
+
name,
|
|
98
|
+
model: resolveConfiguredModel(name, configured.model, input.modelRegistry, configPath),
|
|
99
|
+
thinkingLevel: configured.thinkingLevel,
|
|
100
|
+
source,
|
|
101
|
+
configPath,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hostWorkflowModelProfiles(
|
|
109
|
+
hostModel: Model<Api> | undefined,
|
|
110
|
+
): ResolvedWorkflowModelProfiles {
|
|
111
|
+
return {
|
|
112
|
+
small: { name: "small", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.small, source: "host" },
|
|
113
|
+
medium: { name: "medium", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.medium, source: "host" },
|
|
114
|
+
big: { name: "big", model: hostModel, thinkingLevel: HOST_FALLBACK_THINKING.big, source: "host" },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolveAgentModelProfile(
|
|
119
|
+
input: {
|
|
120
|
+
readonly request: WorkflowModelRouteRequest;
|
|
121
|
+
readonly profiles: ResolvedWorkflowModelProfiles;
|
|
122
|
+
readonly resolveExplicitModel: (modelRef: string) => Model<Api> | undefined;
|
|
123
|
+
readonly hostModel: Model<Api> | undefined;
|
|
124
|
+
},
|
|
125
|
+
): { readonly model: Model<Api> | undefined; readonly thinkingLevel: ThinkingLevel | undefined } {
|
|
126
|
+
const profile = input.request.profile === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: input.profiles[assertWorkflowModelProfileName(input.request.profile)];
|
|
129
|
+
return {
|
|
130
|
+
model: input.request.model === undefined
|
|
131
|
+
? (profile?.model ?? input.hostModel)
|
|
132
|
+
: input.resolveExplicitModel(input.request.model),
|
|
133
|
+
thinkingLevel: input.request.thinkingLevel ?? profile?.thinkingLevel,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readWorkflowModelProfileFile(configPath: string): WorkflowModelProfileFile {
|
|
138
|
+
let contents: string;
|
|
139
|
+
try {
|
|
140
|
+
contents = readFileSync(configPath, "utf8");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isMissingPathError(error)) return { profiles: {} };
|
|
143
|
+
throw new WorkflowModelProfileConfigError(
|
|
144
|
+
`Could not read workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}. Fix the file permissions or remove the file.`,
|
|
145
|
+
configPath,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
let parsed: unknown;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(contents);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new WorkflowModelProfileConfigError(
|
|
153
|
+
`Invalid JSON in workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}. Fix or remove the file.`,
|
|
154
|
+
configPath,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return parseWorkflowModelProfileFile(parsed, configPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function setWorkflowModelProfile(input: {
|
|
161
|
+
readonly configPath: string;
|
|
162
|
+
readonly name: WorkflowModelProfileName;
|
|
163
|
+
readonly profile: WorkflowModelProfileConfig;
|
|
164
|
+
readonly modelRegistry: Pick<ModelRegistry, "find">;
|
|
165
|
+
}): void {
|
|
166
|
+
const current = readWorkflowModelProfileFile(input.configPath);
|
|
167
|
+
const next = { ...current.profiles, [input.name]: input.profile };
|
|
168
|
+
for (const name of WORKFLOW_MODEL_PROFILE_NAMES) {
|
|
169
|
+
const profile = next[name];
|
|
170
|
+
if (profile) resolveConfiguredModel(name, profile.model, input.modelRegistry, input.configPath);
|
|
171
|
+
}
|
|
172
|
+
writeWorkflowModelProfileFile(input.configPath, { profiles: next });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function clearWorkflowModelProfile(configPath: string, name: WorkflowModelProfileName): void {
|
|
176
|
+
const current = readWorkflowModelProfileFile(configPath);
|
|
177
|
+
if (!current.profiles[name]) return;
|
|
178
|
+
const profiles = { ...current.profiles };
|
|
179
|
+
delete profiles[name];
|
|
180
|
+
writeWorkflowModelProfileFile(configPath, { profiles });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isWorkflowModelProfileName(value: string): value is WorkflowModelProfileName {
|
|
184
|
+
return WORKFLOW_MODEL_PROFILE_NAMES.includes(value as WorkflowModelProfileName);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isWorkflowThinkingLevel(value: string): value is ThinkingLevel {
|
|
188
|
+
return THINKING_LEVELS.has(value as ThinkingLevel);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseWorkflowModelProfileFile(value: unknown, configPath: string): WorkflowModelProfileFile {
|
|
192
|
+
if (!isRecord(value)) throw invalidConfig(configPath, "the root must be an object");
|
|
193
|
+
assertOnlyKeys(value, ["profiles"], configPath);
|
|
194
|
+
if (!isRecord(value.profiles)) throw invalidConfig(configPath, '"profiles" must be an object');
|
|
195
|
+
|
|
196
|
+
const profiles: Partial<Record<WorkflowModelProfileName, WorkflowModelProfileConfig>> = {};
|
|
197
|
+
for (const [name, profile] of Object.entries(value.profiles)) {
|
|
198
|
+
if (!isWorkflowModelProfileName(name)) {
|
|
199
|
+
throw invalidConfig(configPath, `unknown profile "${name}"; expected small, medium, or big`);
|
|
200
|
+
}
|
|
201
|
+
if (!isRecord(profile)) throw invalidConfig(configPath, `profile "${name}" must be an object`, name);
|
|
202
|
+
assertOnlyKeys(profile, ["model", "thinkingLevel"], configPath, name);
|
|
203
|
+
if (typeof profile.model !== "string") {
|
|
204
|
+
throw invalidConfig(configPath, `profile "${name}" requires a provider-qualified "model" string`, name);
|
|
205
|
+
}
|
|
206
|
+
assertThinkingLevel(profile.thinkingLevel, configPath, name);
|
|
207
|
+
profiles[name] = {
|
|
208
|
+
model: profile.model,
|
|
209
|
+
...(profile.thinkingLevel === undefined ? {} : { thinkingLevel: profile.thinkingLevel }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { profiles };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveConfiguredModel(
|
|
216
|
+
name: WorkflowModelProfileName,
|
|
217
|
+
modelRef: string,
|
|
218
|
+
modelRegistry: Pick<ModelRegistry, "find">,
|
|
219
|
+
configPath: string,
|
|
220
|
+
): Model<Api> {
|
|
221
|
+
const slash = modelRef.indexOf("/");
|
|
222
|
+
if (modelRef.trim() !== modelRef || slash <= 0 || slash === modelRef.length - 1) {
|
|
223
|
+
throw invalidConfig(
|
|
224
|
+
configPath,
|
|
225
|
+
`profile "${name}" model must be an exact provider/model identity; received "${modelRef}"`,
|
|
226
|
+
name,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const provider = modelRef.slice(0, slash);
|
|
230
|
+
const modelId = modelRef.slice(slash + 1);
|
|
231
|
+
if (/\s/.test(provider) || modelId.startsWith("/") || modelId.endsWith("/") || /\s/.test(modelId)) {
|
|
232
|
+
throw invalidConfig(
|
|
233
|
+
configPath,
|
|
234
|
+
`profile "${name}" model must be an exact provider/model identity; received "${modelRef}"`,
|
|
235
|
+
name,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const model = modelRegistry.find(provider, modelId);
|
|
239
|
+
if (!model) {
|
|
240
|
+
throw invalidConfig(
|
|
241
|
+
configPath,
|
|
242
|
+
`profile "${name}" references unavailable model "${modelRef}"; choose an exact model shown by /model and update it with /workflow:models set ${name} provider/model`,
|
|
243
|
+
name,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return model;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assertWorkflowModelProfileName(value: string): WorkflowModelProfileName {
|
|
250
|
+
if (isWorkflowModelProfileName(value)) return value;
|
|
251
|
+
throw new Error(`Unknown workflow model profile "${value}"; expected small, medium, or big.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function assertThinkingLevel(
|
|
255
|
+
value: unknown,
|
|
256
|
+
configPath: string,
|
|
257
|
+
profile: WorkflowModelProfileName,
|
|
258
|
+
): asserts value is ThinkingLevel | undefined {
|
|
259
|
+
if (value === undefined || (typeof value === "string" && isWorkflowThinkingLevel(value))) return;
|
|
260
|
+
throw invalidConfig(
|
|
261
|
+
configPath,
|
|
262
|
+
`profile "${profile}" thinkingLevel must be off, minimal, low, medium, high, xhigh, or max`,
|
|
263
|
+
profile,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function assertOnlyKeys(
|
|
268
|
+
value: Record<string, unknown>,
|
|
269
|
+
allowed: readonly string[],
|
|
270
|
+
configPath: string,
|
|
271
|
+
profile?: WorkflowModelProfileName,
|
|
272
|
+
): void {
|
|
273
|
+
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
274
|
+
if (unexpected) throw invalidConfig(configPath, `unexpected key "${unexpected}"`, profile);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeWorkflowModelProfileFile(configPath: string, config: WorkflowModelProfileFile): void {
|
|
278
|
+
const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
279
|
+
try {
|
|
280
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
281
|
+
writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
282
|
+
renameSync(temporaryPath, configPath);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
try {
|
|
285
|
+
unlinkSync(temporaryPath);
|
|
286
|
+
} catch {
|
|
287
|
+
// Best effort: preserve the original write error.
|
|
288
|
+
}
|
|
289
|
+
throw new WorkflowModelProfileConfigError(
|
|
290
|
+
`Could not write workflow model profiles at ${configPath}: ${unknownErrorMessage(error)}.`,
|
|
291
|
+
configPath,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function invalidConfig(
|
|
297
|
+
configPath: string,
|
|
298
|
+
reason: string,
|
|
299
|
+
profile?: WorkflowModelProfileName,
|
|
300
|
+
): WorkflowModelProfileConfigError {
|
|
301
|
+
return new WorkflowModelProfileConfigError(
|
|
302
|
+
`Invalid workflow model profile config at ${configPath}: ${reason}.`,
|
|
303
|
+
configPath,
|
|
304
|
+
profile,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
309
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
310
|
+
}
|
|
@@ -3,14 +3,48 @@ import type { WorkflowRunOptions } from "./types.ts";
|
|
|
3
3
|
|
|
4
4
|
export const WORKFLOW_BUDGET_MIN = 1;
|
|
5
5
|
export const WORKFLOW_BUDGET_MAX = 1_000_000_000;
|
|
6
|
+
export const WORKFLOW_MAX_AGENTS_MIN = 1;
|
|
7
|
+
export const WORKFLOW_MAX_AGENTS_MAX = 10_000;
|
|
8
|
+
export const DEFAULT_WORKFLOW_MAX_AGENTS = 64;
|
|
9
|
+
export const WORKFLOW_AGENT_TIMEOUT_MIN_MS = 1_000;
|
|
10
|
+
export const WORKFLOW_AGENT_TIMEOUT_MAX_MS = 86_400_000;
|
|
11
|
+
export const DEFAULT_WORKFLOW_AGENT_TIMEOUT_MS = 1_800_000;
|
|
12
|
+
export const WORKFLOW_AGENT_RETRIES_MIN = 0;
|
|
13
|
+
export const WORKFLOW_AGENT_RETRIES_MAX = 10;
|
|
14
|
+
export const DEFAULT_WORKFLOW_AGENT_RETRIES = 0;
|
|
15
|
+
export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN = 1;
|
|
16
|
+
export const WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX = 10;
|
|
17
|
+
export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS = 3;
|
|
18
|
+
export const WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS = 5_000;
|
|
19
|
+
export const WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS = 86_400_000;
|
|
20
|
+
export const DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS = 21_600_000;
|
|
6
21
|
|
|
7
22
|
export type ResolvedWorkflowRunOptions = Omit<
|
|
8
23
|
WorkflowRunOptions,
|
|
9
|
-
|
|
24
|
+
| "perf"
|
|
25
|
+
| "concurrency"
|
|
26
|
+
| "parallelSubmissionLimit"
|
|
27
|
+
| "maxAgents"
|
|
28
|
+
| "agentTimeoutMs"
|
|
29
|
+
| "agentRetries"
|
|
30
|
+
| "autoResumeOnUsageLimit"
|
|
31
|
+
| "usageLimitMaxAttempts"
|
|
32
|
+
| "usageLimitMaxDelayMs"
|
|
33
|
+
| "usageLimitAttempt"
|
|
34
|
+
| "resumeEditedWorkflow"
|
|
35
|
+
| "budget"
|
|
10
36
|
> & {
|
|
11
37
|
readonly perf: boolean;
|
|
12
38
|
readonly concurrency: number;
|
|
13
39
|
readonly parallelSubmissionLimit: number | null;
|
|
40
|
+
readonly maxAgents: number;
|
|
41
|
+
readonly agentTimeoutMs: number;
|
|
42
|
+
readonly agentRetries: number;
|
|
43
|
+
readonly autoResumeOnUsageLimit: boolean;
|
|
44
|
+
readonly usageLimitMaxAttempts: number;
|
|
45
|
+
readonly usageLimitMaxDelayMs: number;
|
|
46
|
+
readonly usageLimitAttempt: number;
|
|
47
|
+
readonly resumeEditedWorkflow: boolean;
|
|
14
48
|
readonly budget: number | null;
|
|
15
49
|
};
|
|
16
50
|
|
|
@@ -28,16 +62,63 @@ export function resolveWorkflowRunOptions(
|
|
|
28
62
|
1,
|
|
29
63
|
10_000,
|
|
30
64
|
);
|
|
65
|
+
const maxAgents = clampInteger(
|
|
66
|
+
input.maxAgents ?? parseWorkflowIntegerString(env.PI_WORKFLOW_MAX_AGENTS),
|
|
67
|
+
WORKFLOW_MAX_AGENTS_MIN,
|
|
68
|
+
WORKFLOW_MAX_AGENTS_MAX,
|
|
69
|
+
DEFAULT_WORKFLOW_MAX_AGENTS,
|
|
70
|
+
);
|
|
71
|
+
const agentTimeoutMs = clampInteger(
|
|
72
|
+
input.agentTimeoutMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_TIMEOUT_MS),
|
|
73
|
+
WORKFLOW_AGENT_TIMEOUT_MIN_MS,
|
|
74
|
+
WORKFLOW_AGENT_TIMEOUT_MAX_MS,
|
|
75
|
+
DEFAULT_WORKFLOW_AGENT_TIMEOUT_MS,
|
|
76
|
+
);
|
|
77
|
+
const agentRetries = clampInteger(
|
|
78
|
+
input.agentRetries ?? parseWorkflowIntegerString(env.PI_WORKFLOW_AGENT_RETRIES),
|
|
79
|
+
WORKFLOW_AGENT_RETRIES_MIN,
|
|
80
|
+
WORKFLOW_AGENT_RETRIES_MAX,
|
|
81
|
+
DEFAULT_WORKFLOW_AGENT_RETRIES,
|
|
82
|
+
);
|
|
31
83
|
const budget = resolveBudget(input.budget, env.PI_WORKFLOW_BUDGET);
|
|
84
|
+
const usageLimitMaxAttempts = clampInteger(
|
|
85
|
+
input.usageLimitMaxAttempts ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS),
|
|
86
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MIN,
|
|
87
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
88
|
+
DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_ATTEMPTS,
|
|
89
|
+
);
|
|
90
|
+
const usageLimitMaxDelayMs = clampInteger(
|
|
91
|
+
input.usageLimitMaxDelayMs ?? parseWorkflowIntegerString(env.PI_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS),
|
|
92
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
93
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MAX_MS,
|
|
94
|
+
DEFAULT_WORKFLOW_USAGE_LIMIT_MAX_DELAY_MS,
|
|
95
|
+
);
|
|
32
96
|
return {
|
|
33
97
|
...input,
|
|
34
98
|
perf: input.perf ?? env.PI_WORKFLOW_PERF === "1",
|
|
35
99
|
concurrency,
|
|
36
100
|
parallelSubmissionLimit: parallelSubmissionLimit ?? null,
|
|
101
|
+
maxAgents,
|
|
102
|
+
agentTimeoutMs,
|
|
103
|
+
agentRetries,
|
|
104
|
+
autoResumeOnUsageLimit: input.autoResumeOnUsageLimit ?? env.PI_WORKFLOW_USAGE_LIMIT_AUTO_RESUME === "1",
|
|
105
|
+
usageLimitMaxAttempts,
|
|
106
|
+
usageLimitMaxDelayMs,
|
|
107
|
+
usageLimitAttempt: clampInteger(
|
|
108
|
+
input.usageLimitAttempt,
|
|
109
|
+
0,
|
|
110
|
+
WORKFLOW_USAGE_LIMIT_ATTEMPTS_MAX,
|
|
111
|
+
0,
|
|
112
|
+
),
|
|
113
|
+
resumeEditedWorkflow: input.resumeEditedWorkflow === true && hasResumeRunId(input.resumeFromRunId),
|
|
37
114
|
budget: budget ?? null,
|
|
38
115
|
};
|
|
39
116
|
}
|
|
40
117
|
|
|
118
|
+
function hasResumeRunId(value: string | undefined): boolean {
|
|
119
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
41
122
|
function optionalClampedInteger(value: number | undefined, min: number, max: number): number | undefined {
|
|
42
123
|
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
43
124
|
return clampInteger(value, min, max, value);
|
|
@@ -54,6 +135,12 @@ function parseInteger(value: string | undefined): number | undefined {
|
|
|
54
135
|
return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
|
|
55
136
|
}
|
|
56
137
|
|
|
138
|
+
export function parseWorkflowIntegerString(value: string | undefined): number | undefined {
|
|
139
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
140
|
+
const parsed = Number(value);
|
|
141
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
57
144
|
function resolveBudget(inputBudget: number | undefined, envBudget: string | undefined): number | undefined {
|
|
58
145
|
if (inputBudget !== undefined) return normalizeExplicitBudget(inputBudget);
|
|
59
146
|
return envBudget === undefined ? undefined : parseWorkflowBudgetString(envBudget);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const MINIMUM_PI_VERSION = "0.80.10";
|
|
2
|
+
|
|
3
|
+
const MINIMUM_PI_CORE = [0, 80, 10] as const;
|
|
4
|
+
|
|
5
|
+
/** Fail during extension registration instead of failing later in a workflow. */
|
|
6
|
+
export function assertSupportedPiVersion(version: string): void {
|
|
7
|
+
const core = parseVersionCore(version);
|
|
8
|
+
if (!core || compareVersionCore(core, MINIMUM_PI_CORE) < 0) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`pi-workflow-engine requires pi ${MINIMUM_PI_VERSION} or newer; detected ${JSON.stringify(version)}. Update pi before loading this extension.`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseVersionCore(version: string): readonly [number, number, number] | undefined {
|
|
16
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
17
|
+
if (!match) return undefined;
|
|
18
|
+
const major = Number(match[1]);
|
|
19
|
+
const minor = Number(match[2]);
|
|
20
|
+
const patch = Number(match[3]);
|
|
21
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) return undefined;
|
|
22
|
+
return [major, minor, patch];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compareVersionCore(
|
|
26
|
+
left: readonly [number, number, number],
|
|
27
|
+
right: readonly [number, number, number],
|
|
28
|
+
): number {
|
|
29
|
+
const [leftMajor, leftMinor, leftPatch] = left;
|
|
30
|
+
const [rightMajor, rightMinor, rightPatch] = right;
|
|
31
|
+
return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch;
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
2
|
+
|
|
3
|
+
export type AgentRowStatus = "queued" | "running" | "done" | "failed";
|
|
4
|
+
export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
|
|
5
|
+
|
|
6
|
+
export interface AgentRowSnapshot {
|
|
7
|
+
readonly id: number;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly status: AgentRowStatus;
|
|
10
|
+
readonly startedAt?: number;
|
|
11
|
+
readonly doneAt?: number;
|
|
12
|
+
readonly toolUses: number;
|
|
13
|
+
readonly lastTool?: string;
|
|
14
|
+
readonly error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PhaseSnapshot {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly agents: readonly AgentRowSnapshot[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkflowCounterSnapshot {
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly value: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkflowLaneItemSnapshot {
|
|
29
|
+
readonly lane: string;
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly subtitle?: string;
|
|
32
|
+
readonly status: WorkflowLaneItemStatus;
|
|
33
|
+
readonly details?: string;
|
|
34
|
+
readonly createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkflowProgressSnapshot {
|
|
38
|
+
readonly runId: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly startedAt: number;
|
|
41
|
+
readonly doneAt?: number;
|
|
42
|
+
readonly currentPhase: string;
|
|
43
|
+
readonly phases: readonly PhaseSnapshot[];
|
|
44
|
+
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
45
|
+
readonly summary: readonly [string, string | number][];
|
|
46
|
+
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
47
|
+
readonly laneOverflow: readonly [string, number][];
|
|
48
|
+
readonly logs: readonly string[];
|
|
49
|
+
readonly usage?: WorkflowUsageSnapshot;
|
|
50
|
+
}
|