agentweaver 0.1.16 → 0.1.17
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 +50 -10
- package/dist/artifacts.js +73 -3
- package/dist/doctor/checks/executors.js +2 -2
- package/dist/flow-state.js +138 -1
- package/dist/index.js +175 -61
- package/dist/interactive/controller.js +56 -23
- package/dist/interactive/ink/index.js +22 -1
- package/dist/interactive/tree.js +2 -2
- package/dist/pipeline/auto-flow.js +9 -6
- package/dist/pipeline/context.js +6 -5
- package/dist/pipeline/declarative-flows.js +39 -20
- package/dist/pipeline/flow-catalog.js +36 -14
- package/dist/pipeline/flow-specs/auto-common.json +1 -0
- package/dist/pipeline/flow-specs/auto-golang.json +27 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +13 -1
- package/dist/pipeline/flow-specs/plan.json +4 -2
- package/dist/pipeline/launch-profile-config.js +30 -18
- package/dist/pipeline/node-contract.js +1 -0
- package/dist/pipeline/node-registry.js +74 -5
- package/dist/pipeline/nodes/flow-run-node.js +188 -173
- package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
- package/dist/pipeline/plugin-loader.js +389 -0
- package/dist/pipeline/plugin-types.js +1 -0
- package/dist/pipeline/registry.js +71 -4
- package/dist/pipeline/spec-compiler.js +1 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-validator.js +6 -0
- package/dist/pipeline/value-resolver.js +2 -1
- package/dist/plugin-sdk.js +1 -0
- package/dist/runtime/artifact-registry.js +3 -0
- package/dist/runtime/execution-routing.js +25 -19
- package/dist/runtime/interactive-execution-routing.js +66 -57
- package/docs/example/.flows/examples/claude-example.json +50 -0
- package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/examples/.flows/claude-example.json +50 -0
- package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/plugin-sdk.md +731 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -34,13 +34,21 @@ In practice, this means you can treat an agent workflow like an engineered syste
|
|
|
34
34
|
|
|
35
35
|
## Core Concepts
|
|
36
36
|
|
|
37
|
-
- `flow spec`: declarative JSON under `src/pipeline/flow-specs
|
|
37
|
+
- `flow spec`: declarative JSON under `src/pipeline/flow-specs/`, global `~/.agentweaver/.flows/`, or project-local `.agentweaver/.flows/`
|
|
38
38
|
- `node`: reusable runtime unit from `src/pipeline/nodes/`
|
|
39
39
|
- `executor`: integration layer for Jira, Codex, OpenCode, GitLab, shell/process execution, Telegram notifications, and related actions
|
|
40
40
|
- `scope`: isolated workspace key for artifacts and flow state; usually based on Jira task, otherwise derived from git context
|
|
41
41
|
- `artifact`: file produced or consumed by flows, used as the stable contract between stages
|
|
42
42
|
- `flow state`: compact persisted execution metadata used for resume/restart in long-running flows such as `auto-golang`
|
|
43
43
|
|
|
44
|
+
## Семантика Запуска
|
|
45
|
+
|
|
46
|
+
- `resume` возобновляет только реально прерванный запуск и использует сохранённое состояние исполнения без пересборки уже выполненных шагов
|
|
47
|
+
- `continue` предназначен для завершённых итерационных циклов и запускает следующую итерацию от последних валидных артефактов без удаления исторических артефактов
|
|
48
|
+
- `restart` считается новым запуском: текущая активная попытка архивируется в `.agentweaver/scopes/<scope>/.artifacts/restart-archives/attempt-XXXX`, после чего создаётся новая активная попытка
|
|
49
|
+
- Для неоднозначных запусков оператор должен явно выбрать действие: в интерактивном режиме через подтверждение, в неинтерактивном режиме через `--resume`, `--continue` или `--restart`
|
|
50
|
+
- Контракт распространяется на `auto-common`, `auto-simple`, `auto-golang`, `instant-task`, `review-loop`, `run-go-linter-loop` и `run-go-tests-loop`
|
|
51
|
+
|
|
44
52
|
## Declarative Workflow Model
|
|
45
53
|
|
|
46
54
|
The center of the system is the declarative flow spec:
|
|
@@ -118,6 +126,34 @@ node dist/index.js --help
|
|
|
118
126
|
|
|
119
127
|
Global install after publishing:
|
|
120
128
|
|
|
129
|
+
## Plugin SDK
|
|
130
|
+
|
|
131
|
+
AgentWeaver supports local plugins and custom declarative flows from both global and project-local `.agentweaver` directories.
|
|
132
|
+
|
|
133
|
+
Plugin authors must use only the public SDK subpath: `agentweaver/plugin-sdk`.
|
|
134
|
+
The package root `agentweaver`, internal paths such as `agentweaver/dist/*` and `agentweaver/src/*`, and repository-relative source imports are not part of the supported SDK contract.
|
|
135
|
+
|
|
136
|
+
Supported plugin manifest locations are:
|
|
137
|
+
|
|
138
|
+
- `~/.agentweaver/.plugins/<plugin-id>/plugin.json`
|
|
139
|
+
- `.agentweaver/.plugins/<plugin-id>/plugin.json`
|
|
140
|
+
|
|
141
|
+
The plugin directory name and manifest `id` must match exactly.
|
|
142
|
+
|
|
143
|
+
Use the dedicated guide at [docs/plugin-sdk.md](docs/plugin-sdk.md) for:
|
|
144
|
+
|
|
145
|
+
- the executor versus node architecture
|
|
146
|
+
- manifest and entrypoint rules
|
|
147
|
+
- optional routing metadata for plugin LLM executors
|
|
148
|
+
- runtime context APIs available to plugin code
|
|
149
|
+
- global and project-local flow wiring under `~/.agentweaver/.flows/` and `.agentweaver/.flows/`
|
|
150
|
+
- compatibility, testing, troubleshooting, and a complete end-to-end walkthrough
|
|
151
|
+
|
|
152
|
+
Repository reference examples live under `docs/examples/`, for example:
|
|
153
|
+
|
|
154
|
+
- `docs/examples/.plugins/claude-example-plugin/`
|
|
155
|
+
- `docs/examples/.flows/claude-example.json`
|
|
156
|
+
|
|
121
157
|
```bash
|
|
122
158
|
npm install -g agentweaver
|
|
123
159
|
agentweaver --help
|
|
@@ -183,7 +219,7 @@ OPENCODE_MODEL=minimax-coding-plan/MiniMax-M2.7
|
|
|
183
219
|
|
|
184
220
|
The full-screen TUI is not a cosmetic wrapper. It is the operator console for the harness:
|
|
185
221
|
|
|
186
|
-
- browse built-in and project-local workflows
|
|
222
|
+
- browse built-in, global, and project-local workflows
|
|
187
223
|
- launch flows in the current scope
|
|
188
224
|
- inspect progress by phase and step
|
|
189
225
|
- follow activity, prompts, summaries, and statuses
|
|
@@ -332,27 +368,31 @@ Current layout:
|
|
|
332
368
|
Flow discovery behavior:
|
|
333
369
|
|
|
334
370
|
- built-in flows are loaded from `src/pipeline/flow-specs/`
|
|
335
|
-
-
|
|
336
|
-
-
|
|
337
|
-
-
|
|
338
|
-
-
|
|
371
|
+
- global custom flows are loaded from `~/.agentweaver/.flows/`
|
|
372
|
+
- project-local custom flows are loaded from `.agentweaver/.flows/`
|
|
373
|
+
- all discovered flow specs are validated at load time
|
|
374
|
+
- duplicate flow ids fail fast across built-in, global, and project-local sources
|
|
375
|
+
- custom flows are shown separately in the UI as global and project-local groups
|
|
339
376
|
|
|
340
|
-
##
|
|
377
|
+
## Custom Flows
|
|
341
378
|
|
|
342
|
-
You can add
|
|
379
|
+
You can add custom flow specs under either:
|
|
343
380
|
|
|
344
381
|
```bash
|
|
382
|
+
~/.agentweaver/.flows/**/*.json
|
|
345
383
|
.agentweaver/.flows/**/*.json
|
|
346
384
|
```
|
|
347
385
|
|
|
348
|
-
|
|
386
|
+
Custom flows:
|
|
349
387
|
|
|
350
388
|
- are discovered recursively
|
|
351
389
|
- get their flow id from the relative path without `.json`
|
|
352
390
|
- share the same validator and runtime as built-in flows
|
|
353
391
|
- cannot conflict with an existing built-in or other discovered flow id
|
|
354
392
|
|
|
355
|
-
|
|
393
|
+
Use the global directory for reusable personal flows and plugins across repositories, and the project-local directory for repo-specific wiring.
|
|
394
|
+
|
|
395
|
+
Nested `flow-run` steps can reference built-in, global, or project-local specs by file name, as long as the name resolves unambiguously.
|
|
356
396
|
|
|
357
397
|
## Development
|
|
358
398
|
|
package/dist/artifacts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { TaskRunnerError } from "./errors.js";
|
|
@@ -214,8 +214,78 @@ export function gitlabDiffReviewInputJsonFile(taskKey) {
|
|
|
214
214
|
export function flowStateFile(scopeKey, flowId) {
|
|
215
215
|
return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${encodeURIComponent(flowId)}.json`);
|
|
216
216
|
}
|
|
217
|
-
export function
|
|
218
|
-
return
|
|
217
|
+
export function restartArchivesDir(scopeKey) {
|
|
218
|
+
return scopeArtifactsFile(scopeKey, "restart-archives");
|
|
219
|
+
}
|
|
220
|
+
function nextRestartArchiveName(scopeKey) {
|
|
221
|
+
const archiveRoot = restartArchivesDir(scopeKey);
|
|
222
|
+
if (!existsSync(archiveRoot)) {
|
|
223
|
+
return "attempt-0001";
|
|
224
|
+
}
|
|
225
|
+
const attemptNumbers = readdirSync(archiveRoot, { withFileTypes: true })
|
|
226
|
+
.filter((entry) => entry.isDirectory())
|
|
227
|
+
.map((entry) => /^attempt-(\d{4})$/.exec(entry.name)?.[1] ?? null)
|
|
228
|
+
.filter((value) => value !== null)
|
|
229
|
+
.map((value) => Number.parseInt(value, 10));
|
|
230
|
+
const nextNumber = (attemptNumbers.length === 0 ? 0 : Math.max(...attemptNumbers)) + 1;
|
|
231
|
+
return `attempt-${String(nextNumber).padStart(4, "0")}`;
|
|
232
|
+
}
|
|
233
|
+
export function archiveActiveAttempt(scopeKey) {
|
|
234
|
+
const workspaceDir = scopeWorkspaceDir(scopeKey);
|
|
235
|
+
if (!existsSync(workspaceDir)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const workspaceEntries = readdirSync(workspaceDir, { withFileTypes: true })
|
|
239
|
+
.filter((entry) => entry.name !== ".artifacts");
|
|
240
|
+
const artifactEntries = readdirSync(scopeArtifactsDir(scopeKey), { withFileTypes: true })
|
|
241
|
+
.filter((entry) => entry.name !== "restart-archives");
|
|
242
|
+
if (workspaceEntries.length === 0 && artifactEntries.length === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const archiveRoot = restartArchivesDir(scopeKey);
|
|
246
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
247
|
+
const archiveDir = path.join(archiveRoot, nextRestartArchiveName(scopeKey));
|
|
248
|
+
const workspaceArchiveDir = path.join(archiveDir, "workspace");
|
|
249
|
+
const artifactsArchiveDir = path.join(archiveDir, "artifacts");
|
|
250
|
+
mkdirSync(workspaceArchiveDir, { recursive: true });
|
|
251
|
+
mkdirSync(artifactsArchiveDir, { recursive: true });
|
|
252
|
+
try {
|
|
253
|
+
for (const entry of workspaceEntries) {
|
|
254
|
+
cpSync(path.join(workspaceDir, entry.name), path.join(workspaceArchiveDir, entry.name), {
|
|
255
|
+
recursive: true,
|
|
256
|
+
errorOnExist: true,
|
|
257
|
+
force: false,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
for (const entry of artifactEntries) {
|
|
261
|
+
cpSync(path.join(scopeArtifactsDir(scopeKey), entry.name), path.join(artifactsArchiveDir, entry.name), {
|
|
262
|
+
recursive: true,
|
|
263
|
+
errorOnExist: true,
|
|
264
|
+
force: false,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
rmSync(archiveDir, { recursive: true, force: true });
|
|
270
|
+
throw new TaskRunnerError(`Failed to archive active attempt for restart: ${error.message}`);
|
|
271
|
+
}
|
|
272
|
+
for (const entry of workspaceEntries) {
|
|
273
|
+
rmSync(path.join(workspaceDir, entry.name), { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
for (const entry of artifactEntries) {
|
|
276
|
+
rmSync(path.join(scopeArtifactsDir(scopeKey), entry.name), { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
return archiveDir;
|
|
279
|
+
}
|
|
280
|
+
export function planArtifacts(taskKey, iteration) {
|
|
281
|
+
return [
|
|
282
|
+
designFile(taskKey, iteration),
|
|
283
|
+
designJsonFile(taskKey, iteration),
|
|
284
|
+
planFile(taskKey, iteration),
|
|
285
|
+
planJsonFile(taskKey, iteration),
|
|
286
|
+
qaFile(taskKey, iteration),
|
|
287
|
+
qaJsonFile(taskKey, iteration),
|
|
288
|
+
];
|
|
219
289
|
}
|
|
220
290
|
export function bugAnalyzeArtifacts(taskKey) {
|
|
221
291
|
return [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { DoctorStatus } from "../types.js";
|
|
3
3
|
import { CATEGORY } from "./category.js";
|
|
4
|
-
import {
|
|
4
|
+
import { allowedModelsForExecutor } from "../../pipeline/launch-profile-config.js";
|
|
5
5
|
import { findCmdPath, isExecutable } from "../../runtime/command-resolution.js";
|
|
6
6
|
function getEnvVarName(executorId) {
|
|
7
7
|
return executorId === "codex" ? "CODEX_BIN" : "OPENCODE_BIN";
|
|
@@ -72,7 +72,7 @@ function checkExecutor(executorId) {
|
|
|
72
72
|
if (versionOutput === null) {
|
|
73
73
|
return createResult(executorId, DoctorStatus.Fail, `${executorId} --version check failed`, `${executorId} --version did not produce expected output`, `path: ${resolution.path}, source: ${resolution.source}`, resolution, null);
|
|
74
74
|
}
|
|
75
|
-
const allowedModels =
|
|
75
|
+
const allowedModels = allowedModelsForExecutor(executorId);
|
|
76
76
|
const modelWarnings = [];
|
|
77
77
|
for (const model of allowedModels) {
|
|
78
78
|
const modelResult = spawnSync(resolution.path, ["--model", model, "--version"], { encoding: "utf8", stdio: "pipe" });
|
package/dist/flow-state.js
CHANGED
|
@@ -4,7 +4,25 @@ import { ensureScopeWorkspaceDir, flowStateFile } from "./artifacts.js";
|
|
|
4
4
|
import { TaskRunnerError } from "./errors.js";
|
|
5
5
|
import { isFlowRunResumeEnvelope } from "./pipeline/flow-run-resume.js";
|
|
6
6
|
import { resolveStoredExecutionRoutingSnapshot, singleLaunchProfileExecutionRouting } from "./runtime/execution-routing.js";
|
|
7
|
-
const FLOW_STATE_SCHEMA_VERSION =
|
|
7
|
+
const FLOW_STATE_SCHEMA_VERSION = 3;
|
|
8
|
+
const CONTINUABLE_FLOW_KINDS = new Set([
|
|
9
|
+
"design-review-loop-flow",
|
|
10
|
+
"review-loop-flow",
|
|
11
|
+
"review-project-loop-flow",
|
|
12
|
+
"run-go-linter-loop-flow",
|
|
13
|
+
"run-go-tests-loop-flow",
|
|
14
|
+
]);
|
|
15
|
+
const CONTINUABLE_PARENT_FLOW_IDS = new Set([
|
|
16
|
+
"auto-common",
|
|
17
|
+
"auto-simple",
|
|
18
|
+
"auto-golang",
|
|
19
|
+
"instant-task",
|
|
20
|
+
]);
|
|
21
|
+
const CONTINUABLE_DIRECT_FLOW_IDS = new Set([
|
|
22
|
+
"review-loop",
|
|
23
|
+
"run-go-linter-loop",
|
|
24
|
+
"run-go-tests-loop",
|
|
25
|
+
]);
|
|
8
26
|
function nowIso8601() {
|
|
9
27
|
return new Date().toISOString();
|
|
10
28
|
}
|
|
@@ -45,6 +63,7 @@ export function createFlowRunState(scopeKey, flowId, executionState, jiraRef, la
|
|
|
45
63
|
ensurePublicationRunId(executionState);
|
|
46
64
|
const effectiveExecutionRouting = executionRouting ?? (launchProfile ? singleLaunchProfileExecutionRouting(launchProfile) : undefined);
|
|
47
65
|
const effectiveLaunchProfile = launchProfile ?? effectiveExecutionRouting?.defaultRoute;
|
|
66
|
+
const continuation = inferContinuationMetadata(flowId, executionState);
|
|
48
67
|
return {
|
|
49
68
|
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
50
69
|
flowId,
|
|
@@ -56,6 +75,7 @@ export function createFlowRunState(scopeKey, flowId, executionState, jiraRef, la
|
|
|
56
75
|
...(effectiveLaunchProfile ? { launchProfile: effectiveLaunchProfile } : {}),
|
|
57
76
|
...(effectiveExecutionRouting ? { executionRouting: effectiveExecutionRouting, routingFingerprint: effectiveExecutionRouting.fingerprint } : {}),
|
|
58
77
|
...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
|
|
78
|
+
continuation,
|
|
59
79
|
executionState: stripExecutionStatePayload(executionState),
|
|
60
80
|
};
|
|
61
81
|
}
|
|
@@ -66,6 +86,43 @@ function upgradeFlowRunStateV1(state) {
|
|
|
66
86
|
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
67
87
|
...(executionRouting ? { executionRouting, routingFingerprint: executionRouting.fingerprint } : {}),
|
|
68
88
|
...(executionRouting ? { selectedRoutingPreset: { kind: "custom", label: "Legacy launch profile" } } : {}),
|
|
89
|
+
continuation: {
|
|
90
|
+
continueEligible: false,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function upgradeFlowRunStateV2(state) {
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
98
|
+
continuation: {
|
|
99
|
+
continueEligible: false,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseTerminationLocation(terminationReason) {
|
|
104
|
+
if (typeof terminationReason !== "string") {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const match = /^Stopped by ([^:]+):(.+)$/.exec(terminationReason.trim());
|
|
108
|
+
if (!match) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
const stopPhaseId = match[1];
|
|
112
|
+
const stopStepId = match[2];
|
|
113
|
+
return {
|
|
114
|
+
...(stopPhaseId ? { stopPhaseId } : {}),
|
|
115
|
+
...(stopStepId ? { stopStepId } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function inferContinuationMetadata(flowId, executionState) {
|
|
119
|
+
const stopLocation = parseTerminationLocation(executionState.terminationReason);
|
|
120
|
+
const continueEligible = CONTINUABLE_FLOW_KINDS.has(executionState.flowKind)
|
|
121
|
+
|| (CONTINUABLE_PARENT_FLOW_IDS.has(flowId) && Boolean(stopLocation.stopPhaseId && stopLocation.stopStepId));
|
|
122
|
+
return {
|
|
123
|
+
continueEligible,
|
|
124
|
+
...(stopLocation.stopPhaseId ? { stopPhaseId: stopLocation.stopPhaseId } : {}),
|
|
125
|
+
...(stopLocation.stopStepId ? { stopStepId: stopLocation.stopStepId } : {}),
|
|
69
126
|
};
|
|
70
127
|
}
|
|
71
128
|
function normalizeFlowRunState(raw, flowId, filePath) {
|
|
@@ -77,6 +134,9 @@ function normalizeFlowRunState(raw, flowId, filePath) {
|
|
|
77
134
|
if (schemaVersion === 1) {
|
|
78
135
|
state = upgradeFlowRunStateV1(raw);
|
|
79
136
|
}
|
|
137
|
+
else if (schemaVersion === 2) {
|
|
138
|
+
state = upgradeFlowRunStateV2(raw);
|
|
139
|
+
}
|
|
80
140
|
else if (schemaVersion === FLOW_STATE_SCHEMA_VERSION) {
|
|
81
141
|
state = raw;
|
|
82
142
|
}
|
|
@@ -97,6 +157,11 @@ function normalizeFlowRunState(raw, flowId, filePath) {
|
|
|
97
157
|
state.executionRouting = executionRouting;
|
|
98
158
|
state.routingFingerprint = executionRouting.fingerprint;
|
|
99
159
|
}
|
|
160
|
+
const inferredContinuation = inferContinuationMetadata(state.flowId, state.executionState);
|
|
161
|
+
state.continuation = {
|
|
162
|
+
...inferredContinuation,
|
|
163
|
+
continueEligible: inferredContinuation.continueEligible && state.continuation?.continueEligible !== false,
|
|
164
|
+
};
|
|
100
165
|
return state;
|
|
101
166
|
}
|
|
102
167
|
export function loadFlowRunState(scopeKey, flowId) {
|
|
@@ -125,6 +190,7 @@ export function saveFlowRunState(state) {
|
|
|
125
190
|
state.executionRouting = singleLaunchProfileExecutionRouting(state.launchProfile);
|
|
126
191
|
state.routingFingerprint = state.executionRouting.fingerprint;
|
|
127
192
|
}
|
|
193
|
+
state.continuation = inferContinuationMetadata(state.flowId, state.executionState);
|
|
128
194
|
ensureScopeWorkspaceDir(state.scopeKey);
|
|
129
195
|
writeFileSync(flowStateFile(state.scopeKey, state.flowId), `${JSON.stringify({
|
|
130
196
|
...state,
|
|
@@ -154,6 +220,53 @@ export function hasResumableFlowState(state) {
|
|
|
154
220
|
}
|
|
155
221
|
return state.executionState.phases.some((phase) => phase.steps.some((step) => step.status === "done" || step.status === "running"));
|
|
156
222
|
}
|
|
223
|
+
function hasContinuableFlowState(state) {
|
|
224
|
+
if (!state) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (!state.executionState.terminated && state.status !== "completed") {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return state.continuation?.continueEligible === true;
|
|
231
|
+
}
|
|
232
|
+
export function classifyFlowLaunchAvailability(state) {
|
|
233
|
+
if (!state) {
|
|
234
|
+
return {
|
|
235
|
+
hasExistingState: false,
|
|
236
|
+
requiresExplicitChoice: false,
|
|
237
|
+
resume: { available: false, reason: "No saved state found." },
|
|
238
|
+
continue: { available: false, reason: "No saved state found." },
|
|
239
|
+
restart: { available: true, reason: "Start a fresh attempt." },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const resumeAvailable = hasResumableFlowState(state);
|
|
243
|
+
const continueAvailable = hasContinuableFlowState(state);
|
|
244
|
+
const availability = {
|
|
245
|
+
hasExistingState: true,
|
|
246
|
+
requiresExplicitChoice: resumeAvailable || continueAvailable,
|
|
247
|
+
resume: resumeAvailable
|
|
248
|
+
? { available: true, reason: "Continue the interrupted execution state." }
|
|
249
|
+
: {
|
|
250
|
+
available: false,
|
|
251
|
+
reason: state.executionState.terminated || state.status === "completed"
|
|
252
|
+
? "The saved run already terminated and cannot be resumed."
|
|
253
|
+
: "The saved state is not resumable.",
|
|
254
|
+
},
|
|
255
|
+
continue: continueAvailable
|
|
256
|
+
? { available: true, reason: "Start the next iteration from the latest active artifacts." }
|
|
257
|
+
: {
|
|
258
|
+
available: false,
|
|
259
|
+
reason: state.schemaVersion < FLOW_STATE_SCHEMA_VERSION
|
|
260
|
+
? "Legacy flow state lacks safe continuation metadata."
|
|
261
|
+
: "The saved run does not expose a continuable loop boundary.",
|
|
262
|
+
},
|
|
263
|
+
restart: {
|
|
264
|
+
available: true,
|
|
265
|
+
reason: "Archive the active attempt and start a fresh run.",
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
return availability;
|
|
269
|
+
}
|
|
157
270
|
function normalizeStepState(step) {
|
|
158
271
|
if (step.status !== "running") {
|
|
159
272
|
return step;
|
|
@@ -237,3 +350,27 @@ export function prepareFlowStateForResume(state) {
|
|
|
237
350
|
delete state.executionState.terminationReason;
|
|
238
351
|
return state;
|
|
239
352
|
}
|
|
353
|
+
export function prepareFlowStateForContinue(state, orderedPhases) {
|
|
354
|
+
state.status = "pending";
|
|
355
|
+
state.lastError = null;
|
|
356
|
+
state.currentStep = null;
|
|
357
|
+
const flowKind = state.executionState.flowKind;
|
|
358
|
+
if (CONTINUABLE_FLOW_KINDS.has(flowKind) || CONTINUABLE_DIRECT_FLOW_IDS.has(state.flowId)) {
|
|
359
|
+
state.executionState = {
|
|
360
|
+
...state.executionState,
|
|
361
|
+
publicationRunId: randomUUID(),
|
|
362
|
+
terminated: false,
|
|
363
|
+
phases: orderedPhases.map(createPendingPhaseState),
|
|
364
|
+
};
|
|
365
|
+
delete state.executionState.terminationReason;
|
|
366
|
+
delete state.executionState.terminationOutcome;
|
|
367
|
+
return state;
|
|
368
|
+
}
|
|
369
|
+
const targetPhaseId = state.continuation?.stopPhaseId ?? parseTerminationLocation(state.executionState.terminationReason).stopPhaseId;
|
|
370
|
+
if (!targetPhaseId) {
|
|
371
|
+
throw new TaskRunnerError("Continue is impossible because the stop phase could not be determined safely. Use restart.");
|
|
372
|
+
}
|
|
373
|
+
rewindFlowRunStateToPhase(state, orderedPhases, targetPhaseId);
|
|
374
|
+
state.executionState.publicationRunId = randomUUID();
|
|
375
|
+
return state;
|
|
376
|
+
}
|