agentweaver 0.1.17 → 0.1.19
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 +112 -23
- package/dist/artifacts.js +41 -0
- package/dist/index.js +258 -29
- package/dist/interactive/controller.js +323 -13
- package/dist/interactive/ink/index.js +2 -2
- package/dist/interactive/state.js +10 -0
- package/dist/interactive/web/index.js +326 -0
- package/dist/interactive/web/protocol.js +160 -0
- package/dist/interactive/web/server.js +1011 -0
- package/dist/interactive/web/static/app.js +1580 -0
- package/dist/interactive/web/static/index.html +114 -0
- package/dist/interactive/web/static/styles.css +2 -0
- package/dist/interactive/web/static/styles.input.css +849 -0
- package/dist/pipeline/flow-catalog.js +4 -0
- package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
- package/dist/pipeline/flow-specs/auto-common.json +3 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
- package/dist/pipeline/flow-specs/design-review.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +3 -1
- package/dist/pipeline/flow-specs/plan.json +4 -0
- package/dist/pipeline/flow-specs/playbook-init.json +199 -0
- package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
- package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
- package/dist/pipeline/flow-specs/review/review.json +2 -0
- package/dist/pipeline/node-registry.js +45 -0
- package/dist/pipeline/nodes/flow-run-node.js +13 -1
- package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
- package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
- package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
- package/dist/pipeline/nodes/playbook-write-node.js +243 -0
- package/dist/pipeline/nodes/project-guidance-node.js +69 -0
- package/dist/pipeline/prompt-registry.js +4 -1
- package/dist/pipeline/prompt-runtime.js +6 -2
- package/dist/pipeline/spec-types.js +19 -0
- package/dist/pipeline/value-resolver.js +39 -1
- package/dist/playbook/practice-candidates.js +12 -0
- package/dist/playbook/repo-inventory.js +208 -0
- package/dist/prompts.js +31 -0
- package/dist/runtime/artifact-catalog.js +379 -0
- package/dist/runtime/playbook.js +485 -0
- package/dist/runtime/project-guidance.js +339 -0
- package/dist/structured-artifact-schema-registry.js +8 -0
- package/dist/structured-artifact-schemas.json +235 -0
- package/dist/structured-artifacts.js +7 -1
- package/docs/declarative-workflows.md +565 -0
- package/docs/features.md +77 -0
- package/docs/playbook.md +327 -0
- package/package.json +8 -3
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
3
|
+
import { FlowInterruptedError } from "../../errors.js";
|
|
4
|
+
import { listArtifactCatalog } from "../../runtime/artifact-catalog.js";
|
|
5
|
+
import { createArtifactRegistry } from "../../runtime/artifact-registry.js";
|
|
6
|
+
import { InteractiveSessionController } from "../controller.js";
|
|
7
|
+
import { startWebServer, } from "./server.js";
|
|
8
|
+
function actionId(action) {
|
|
9
|
+
return "actionId" in action ? action.actionId : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function createWebInteractiveSession(options, webOptions = {}) {
|
|
12
|
+
const controller = new InteractiveSessionController(options);
|
|
13
|
+
let server = null;
|
|
14
|
+
let unsubscribe = null;
|
|
15
|
+
let mounted = false;
|
|
16
|
+
let shuttingDown = false;
|
|
17
|
+
let activeScopeKey = options.scopeKey;
|
|
18
|
+
let artifactRestoreGeneration = 0;
|
|
19
|
+
const artifactCatalogProvider = webOptions.getArtifactCatalog ?? ((input) => {
|
|
20
|
+
const explorerScopeKey = controller.getViewModel().artifactExplorer.scopeKey;
|
|
21
|
+
const requestedScopeKey = input?.scopeKey;
|
|
22
|
+
const scopeKey = requestedScopeKey && requestedScopeKey === explorerScopeKey ? requestedScopeKey : activeScopeKey;
|
|
23
|
+
return listArtifactCatalog({
|
|
24
|
+
scopeKey,
|
|
25
|
+
artifactRegistry: createArtifactRegistry(),
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
function snapshot() {
|
|
29
|
+
return { type: "snapshot", viewModel: controller.getViewModel() };
|
|
30
|
+
}
|
|
31
|
+
function sendError(client, message, id) {
|
|
32
|
+
const event = { type: "error", message, ...(id ? { actionId: id } : {}) };
|
|
33
|
+
if (client) {
|
|
34
|
+
client.send(event);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
server?.broadcast(event);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function uniqueStrings(values) {
|
|
41
|
+
return values.filter((value, index, allValues) => (typeof value === "string" && value.length > 0 && allValues.indexOf(value) === index));
|
|
42
|
+
}
|
|
43
|
+
function collectPublishedArtifactRunIds(executionState) {
|
|
44
|
+
const runIds = [];
|
|
45
|
+
for (const phase of executionState?.phases ?? []) {
|
|
46
|
+
for (const step of phase.steps) {
|
|
47
|
+
for (const artifact of step.publishedArtifacts ?? []) {
|
|
48
|
+
const runId = artifact.manifest?.run_id;
|
|
49
|
+
if (runId) {
|
|
50
|
+
runIds.push(runId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return uniqueStrings(runIds);
|
|
56
|
+
}
|
|
57
|
+
function candidateRunIds() {
|
|
58
|
+
const executionState = controller.getCurrentFlowExecutionState();
|
|
59
|
+
return uniqueStrings([
|
|
60
|
+
executionState?.runId ?? null,
|
|
61
|
+
executionState?.publicationRunId ?? null,
|
|
62
|
+
...collectPublishedArtifactRunIds(executionState),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
async function resolveArtifactExplorerRunMetadata(scopeKey) {
|
|
66
|
+
const candidates = candidateRunIds();
|
|
67
|
+
const preferredRunId = candidates[0] ?? null;
|
|
68
|
+
try {
|
|
69
|
+
const catalog = await artifactCatalogProvider({
|
|
70
|
+
scopeKey,
|
|
71
|
+
...(candidates.length === 1 && preferredRunId ? { runId: preferredRunId, runIds: candidates } : {}),
|
|
72
|
+
...(candidates.length > 1 ? { runIds: candidates } : {}),
|
|
73
|
+
});
|
|
74
|
+
if (!catalog || catalog.scopeKey !== scopeKey) {
|
|
75
|
+
return { runId: preferredRunId };
|
|
76
|
+
}
|
|
77
|
+
if (candidates.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
runId: null,
|
|
80
|
+
artifactCount: catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const matchingRunIds = candidates.filter((candidate) => (catalog.items.some((item) => item.scopeKey === scopeKey && item.runId === candidate && item.kind === "markdown")));
|
|
84
|
+
if (matchingRunIds.length > 0) {
|
|
85
|
+
const matchingRunIdSet = new Set(matchingRunIds);
|
|
86
|
+
return {
|
|
87
|
+
runId: matchingRunIds[0] ?? preferredRunId,
|
|
88
|
+
...(matchingRunIds.length > 1 ? { runIds: matchingRunIds } : {}),
|
|
89
|
+
artifactCount: catalog.items.filter((item) => (item.scopeKey === scopeKey
|
|
90
|
+
&& item.kind === "markdown"
|
|
91
|
+
&& item.runId !== null
|
|
92
|
+
&& matchingRunIdSet.has(item.runId))).length,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
runId: preferredRunId,
|
|
97
|
+
artifactCount: catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown" && item.runId === preferredRunId).length,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { runId: preferredRunId };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function markArtifactExplorerForCompletedRun(status) {
|
|
105
|
+
artifactRestoreGeneration += 1;
|
|
106
|
+
const scopeKey = activeScopeKey;
|
|
107
|
+
const { runId, runIds, artifactCount } = await resolveArtifactExplorerRunMetadata(scopeKey);
|
|
108
|
+
controller.setArtifactExplorerAvailability({
|
|
109
|
+
scopeKey,
|
|
110
|
+
runId,
|
|
111
|
+
...(runIds ? { runIds } : {}),
|
|
112
|
+
status,
|
|
113
|
+
...(artifactCount !== undefined ? { artifactCount } : {}),
|
|
114
|
+
open: !controller.hasActiveInput(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function restoreArtifactExplorerFromScope(scopeKey) {
|
|
118
|
+
const generation = ++artifactRestoreGeneration;
|
|
119
|
+
try {
|
|
120
|
+
const catalog = await artifactCatalogProvider({ scopeKey });
|
|
121
|
+
if (generation !== artifactRestoreGeneration || shuttingDown || activeScopeKey !== scopeKey) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!catalog || catalog.scopeKey !== scopeKey) {
|
|
125
|
+
controller.setArtifactExplorerUnavailable();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const artifactCount = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length;
|
|
129
|
+
if (artifactCount === 0) {
|
|
130
|
+
controller.setArtifactExplorerUnavailable("No markdown artifacts were found for the current scope.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
controller.setArtifactExplorerAvailability({
|
|
134
|
+
scopeKey,
|
|
135
|
+
runId: null,
|
|
136
|
+
status: "completed",
|
|
137
|
+
artifactCount,
|
|
138
|
+
open: false,
|
|
139
|
+
label: "Artifacts available",
|
|
140
|
+
message: "Markdown artifacts from this scope are available for review.",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
if (generation === artifactRestoreGeneration && !shuttingDown && activeScopeKey === scopeKey) {
|
|
145
|
+
controller.setArtifactExplorerUnavailable("Artifact Explorer could not inspect the current scope.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function dispatch(action, client) {
|
|
150
|
+
let acceptedRunConfirmation = false;
|
|
151
|
+
try {
|
|
152
|
+
if (action.type === "flow.select") {
|
|
153
|
+
if (action.key) {
|
|
154
|
+
controller.selectFlowKey(action.key);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
controller.selectFlowIndex(action.index ?? 0);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (action.type === "folder.toggle") {
|
|
162
|
+
controller.toggleFolderKey(action.key);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (action.type === "run.openConfirm") {
|
|
166
|
+
await controller.openRunConfirm(action.flowId, action.key);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (action.type === "confirm.select") {
|
|
170
|
+
controller.selectConfirmAction(action.action);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (action.type === "confirm.accept") {
|
|
174
|
+
const confirmation = controller.getViewModel().confirmation;
|
|
175
|
+
const acceptedAction = action.action ?? confirmation?.selectedAction;
|
|
176
|
+
acceptedRunConfirmation = confirmation?.kind === "run" && acceptedAction !== "cancel";
|
|
177
|
+
if (action.action) {
|
|
178
|
+
controller.selectConfirmAction(action.action);
|
|
179
|
+
}
|
|
180
|
+
await controller.acceptConfirmation();
|
|
181
|
+
if (acceptedRunConfirmation) {
|
|
182
|
+
await markArtifactExplorerForCompletedRun("completed");
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (action.type === "confirm.cancel") {
|
|
187
|
+
controller.cancelConfirmation();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action.type === "form.update") {
|
|
191
|
+
controller.updateActiveFormValues(action.values);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (action.type === "form.fieldUpdate") {
|
|
195
|
+
controller.updateFormField(action.fieldId, action.value);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (action.type === "form.submit") {
|
|
199
|
+
controller.submitForm(action.values);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (action.type === "form.cancel") {
|
|
203
|
+
controller.cancelForm();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (action.type === "flow.interrupt") {
|
|
207
|
+
await controller.interruptCurrentFlow(action.flowId);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (action.type === "interrupt.openConfirm") {
|
|
211
|
+
controller.openInterruptConfirm();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (action.type === "log.clear") {
|
|
215
|
+
controller.clearLog();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (action.type === "artifactExplorer.open") {
|
|
219
|
+
controller.openArtifactExplorer();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (action.type === "artifactExplorer.close") {
|
|
223
|
+
controller.closeArtifactExplorer();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (action.type === "help.toggle") {
|
|
227
|
+
controller.showHelp(action.visible ?? !controller.getViewModel().helpVisible);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
controller.scrollPane(action.pane, { ...(action.delta !== undefined ? { delta: action.delta } : {}), ...(action.offset !== undefined ? { offset: action.offset } : {}) });
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (acceptedRunConfirmation) {
|
|
234
|
+
await markArtifactExplorerForCompletedRun("failed");
|
|
235
|
+
}
|
|
236
|
+
const message = error.message;
|
|
237
|
+
controller.appendLog(`Web action failed: ${message}`);
|
|
238
|
+
sendError(client, message, actionId(action));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
mount() {
|
|
243
|
+
if (mounted) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
mounted = true;
|
|
247
|
+
controller.mount();
|
|
248
|
+
void restoreArtifactExplorerFromScope(activeScopeKey);
|
|
249
|
+
unsubscribe = controller.subscribe((event) => {
|
|
250
|
+
if (event.type === "log") {
|
|
251
|
+
server?.broadcast({ type: "log.append", appendedLines: event.appendedLines });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
server?.broadcast(snapshot());
|
|
255
|
+
});
|
|
256
|
+
void startWebServer({
|
|
257
|
+
...(webOptions.noOpen !== undefined ? { noOpen: webOptions.noOpen } : {}),
|
|
258
|
+
...(webOptions.host !== undefined ? { host: webOptions.host } : {}),
|
|
259
|
+
...(webOptions.auth !== undefined ? { auth: webOptions.auth } : {}),
|
|
260
|
+
printInfo: (message) => {
|
|
261
|
+
webOptions.printInfo?.(message);
|
|
262
|
+
controller.appendLog(message);
|
|
263
|
+
},
|
|
264
|
+
...(webOptions.openBrowser ? { openBrowser: webOptions.openBrowser } : {}),
|
|
265
|
+
getArtifactCatalog: (input) => artifactCatalogProvider(input),
|
|
266
|
+
onClientAction: (action, client) => {
|
|
267
|
+
void dispatch(action, client);
|
|
268
|
+
},
|
|
269
|
+
onClientConnected: (client) => {
|
|
270
|
+
client.send(snapshot());
|
|
271
|
+
},
|
|
272
|
+
onExitRequested: () => {
|
|
273
|
+
options.onExit();
|
|
274
|
+
},
|
|
275
|
+
}).then((started) => {
|
|
276
|
+
if (shuttingDown) {
|
|
277
|
+
void started.close().catch((error) => {
|
|
278
|
+
process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
server = started;
|
|
283
|
+
webOptions.onServerReady?.(started);
|
|
284
|
+
}).catch((error) => {
|
|
285
|
+
const message = `Web UI startup failed: ${error.message}`;
|
|
286
|
+
controller.appendLog(message);
|
|
287
|
+
writeSync(process.stderr.fd, `${message}\n`);
|
|
288
|
+
options.onExit();
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
destroy() {
|
|
292
|
+
if (shuttingDown) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
shuttingDown = true;
|
|
296
|
+
controller.interruptActiveForm("Web UI session closed.");
|
|
297
|
+
unsubscribe?.();
|
|
298
|
+
unsubscribe = null;
|
|
299
|
+
const closePromise = server?.close();
|
|
300
|
+
server = null;
|
|
301
|
+
if (closePromise) {
|
|
302
|
+
void closePromise.catch((error) => {
|
|
303
|
+
process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
controller.destroy();
|
|
307
|
+
},
|
|
308
|
+
requestUserInput: (form) => controller.requestUserInput(form),
|
|
309
|
+
setSummary: (markdown) => controller.setSummary(markdown),
|
|
310
|
+
clearSummary: () => controller.clearSummary(),
|
|
311
|
+
setScope: (scopeKey, jiraIssueKey, gitBranchName) => {
|
|
312
|
+
activeScopeKey = scopeKey;
|
|
313
|
+
controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
|
|
314
|
+
controller.setArtifactExplorerUnavailable();
|
|
315
|
+
void restoreArtifactExplorerFromScope(scopeKey);
|
|
316
|
+
},
|
|
317
|
+
appendLog: (text) => controller.appendLog(text),
|
|
318
|
+
setFlowFailed: (flowId) => controller.setFlowFailed(flowId),
|
|
319
|
+
interruptActiveForm: (message = "Flow interrupted by user.") => {
|
|
320
|
+
controller.interruptActiveForm(message);
|
|
321
|
+
if (message) {
|
|
322
|
+
controller.appendLog(new FlowInterruptedError(message).message);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const ACTION_TYPES = new Set([
|
|
2
|
+
"flow.select",
|
|
3
|
+
"folder.toggle",
|
|
4
|
+
"run.openConfirm",
|
|
5
|
+
"confirm.select",
|
|
6
|
+
"confirm.accept",
|
|
7
|
+
"confirm.cancel",
|
|
8
|
+
"form.update",
|
|
9
|
+
"form.fieldUpdate",
|
|
10
|
+
"form.submit",
|
|
11
|
+
"form.cancel",
|
|
12
|
+
"interrupt.openConfirm",
|
|
13
|
+
"flow.interrupt",
|
|
14
|
+
"log.clear",
|
|
15
|
+
"artifactExplorer.open",
|
|
16
|
+
"artifactExplorer.close",
|
|
17
|
+
"help.toggle",
|
|
18
|
+
"scroll",
|
|
19
|
+
]);
|
|
20
|
+
const SCROLL_PANES = new Set(["flows", "progress", "summary", "log", "help"]);
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function optionalActionId(value) {
|
|
25
|
+
if (value.actionId === undefined) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value.actionId !== "string" || value.actionId.trim().length === 0) {
|
|
29
|
+
throw new Error("actionId must be a non-empty string when provided.");
|
|
30
|
+
}
|
|
31
|
+
return value.actionId;
|
|
32
|
+
}
|
|
33
|
+
function requireNonEmptyString(value, fieldName) {
|
|
34
|
+
const field = value[fieldName];
|
|
35
|
+
if (typeof field !== "string" || field.trim().length === 0) {
|
|
36
|
+
throw new Error(`${fieldName} must be a non-empty string.`);
|
|
37
|
+
}
|
|
38
|
+
return field;
|
|
39
|
+
}
|
|
40
|
+
function optionalNonEmptyString(value, fieldName) {
|
|
41
|
+
if (value[fieldName] === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return requireNonEmptyString(value, fieldName);
|
|
45
|
+
}
|
|
46
|
+
function optionalInteger(value, fieldName) {
|
|
47
|
+
if (value[fieldName] === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const field = value[fieldName];
|
|
51
|
+
if (typeof field !== "number" || !Number.isInteger(field)) {
|
|
52
|
+
throw new Error(`${fieldName} must be an integer.`);
|
|
53
|
+
}
|
|
54
|
+
return field;
|
|
55
|
+
}
|
|
56
|
+
function requireValues(value, fieldName = "values") {
|
|
57
|
+
const values = value[fieldName];
|
|
58
|
+
if (!isRecord(values)) {
|
|
59
|
+
throw new Error(`${fieldName} must be an object.`);
|
|
60
|
+
}
|
|
61
|
+
return values;
|
|
62
|
+
}
|
|
63
|
+
export function parseClientAction(raw) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error("Protocol message must be valid JSON.");
|
|
70
|
+
}
|
|
71
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string") {
|
|
72
|
+
throw new Error("Protocol message requires a string type.");
|
|
73
|
+
}
|
|
74
|
+
if (!ACTION_TYPES.has(parsed.type)) {
|
|
75
|
+
throw new Error(`Unknown protocol action: ${parsed.type}`);
|
|
76
|
+
}
|
|
77
|
+
const actionId = optionalActionId(parsed);
|
|
78
|
+
if (parsed.type === "flow.select") {
|
|
79
|
+
const index = optionalInteger(parsed, "index");
|
|
80
|
+
const key = optionalNonEmptyString(parsed, "key");
|
|
81
|
+
if (index === undefined && key === undefined) {
|
|
82
|
+
throw new Error("flow.select requires index or key.");
|
|
83
|
+
}
|
|
84
|
+
return { type: "flow.select", ...(index !== undefined ? { index } : {}), ...(key ? { key } : {}), ...(actionId ? { actionId } : {}) };
|
|
85
|
+
}
|
|
86
|
+
if (parsed.type === "folder.toggle") {
|
|
87
|
+
return { type: "folder.toggle", key: requireNonEmptyString(parsed, "key"), ...(actionId ? { actionId } : {}) };
|
|
88
|
+
}
|
|
89
|
+
if (parsed.type === "run.openConfirm") {
|
|
90
|
+
const flowId = optionalNonEmptyString(parsed, "flowId");
|
|
91
|
+
const key = optionalNonEmptyString(parsed, "key");
|
|
92
|
+
return { type: "run.openConfirm", ...(flowId ? { flowId } : {}), ...(key ? { key } : {}), ...(actionId ? { actionId } : {}) };
|
|
93
|
+
}
|
|
94
|
+
if (parsed.type === "confirm.select") {
|
|
95
|
+
return { type: "confirm.select", action: requireNonEmptyString(parsed, "action"), ...(actionId ? { actionId } : {}) };
|
|
96
|
+
}
|
|
97
|
+
if (parsed.type === "confirm.accept") {
|
|
98
|
+
const action = optionalNonEmptyString(parsed, "action");
|
|
99
|
+
return { type: "confirm.accept", ...(action ? { action } : {}), ...(actionId ? { actionId } : {}) };
|
|
100
|
+
}
|
|
101
|
+
if (parsed.type === "confirm.cancel"
|
|
102
|
+
|| parsed.type === "form.cancel"
|
|
103
|
+
|| parsed.type === "log.clear"
|
|
104
|
+
|| parsed.type === "artifactExplorer.open"
|
|
105
|
+
|| parsed.type === "artifactExplorer.close") {
|
|
106
|
+
return { type: parsed.type, ...(actionId ? { actionId } : {}) };
|
|
107
|
+
}
|
|
108
|
+
if (parsed.type === "form.update") {
|
|
109
|
+
return { type: "form.update", values: requireValues(parsed), ...(actionId ? { actionId } : {}) };
|
|
110
|
+
}
|
|
111
|
+
if (parsed.type === "form.fieldUpdate") {
|
|
112
|
+
const fieldId = requireNonEmptyString(parsed, "fieldId");
|
|
113
|
+
if (!("value" in parsed)) {
|
|
114
|
+
throw new Error("value is required.");
|
|
115
|
+
}
|
|
116
|
+
const value = parsed.value;
|
|
117
|
+
if (typeof value !== "string"
|
|
118
|
+
&& typeof value !== "boolean"
|
|
119
|
+
&& (!Array.isArray(value) || value.some((item) => typeof item !== "string"))) {
|
|
120
|
+
throw new Error("value must be a string, boolean, or string array.");
|
|
121
|
+
}
|
|
122
|
+
return { type: "form.fieldUpdate", fieldId, value, ...(actionId ? { actionId } : {}) };
|
|
123
|
+
}
|
|
124
|
+
if (parsed.type === "form.submit") {
|
|
125
|
+
return {
|
|
126
|
+
type: "form.submit",
|
|
127
|
+
...(parsed.values !== undefined ? { values: requireValues(parsed) } : {}),
|
|
128
|
+
...(actionId ? { actionId } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (parsed.type === "interrupt.openConfirm") {
|
|
132
|
+
return { type: "interrupt.openConfirm", ...(actionId ? { actionId } : {}) };
|
|
133
|
+
}
|
|
134
|
+
if (parsed.type === "flow.interrupt") {
|
|
135
|
+
const flowId = optionalNonEmptyString(parsed, "flowId");
|
|
136
|
+
return { type: "flow.interrupt", ...(flowId ? { flowId } : {}), ...(actionId ? { actionId } : {}) };
|
|
137
|
+
}
|
|
138
|
+
if (parsed.type === "help.toggle") {
|
|
139
|
+
if (parsed.visible !== undefined && typeof parsed.visible !== "boolean") {
|
|
140
|
+
throw new Error("visible must be a boolean when provided.");
|
|
141
|
+
}
|
|
142
|
+
return { type: "help.toggle", ...(parsed.visible !== undefined ? { visible: parsed.visible } : {}), ...(actionId ? { actionId } : {}) };
|
|
143
|
+
}
|
|
144
|
+
const pane = requireNonEmptyString(parsed, "pane");
|
|
145
|
+
if (!SCROLL_PANES.has(pane)) {
|
|
146
|
+
throw new Error("scroll pane must be one of flows, progress, summary, log, or help.");
|
|
147
|
+
}
|
|
148
|
+
const delta = optionalInteger(parsed, "delta");
|
|
149
|
+
const offset = optionalInteger(parsed, "offset");
|
|
150
|
+
if (delta === undefined && offset === undefined) {
|
|
151
|
+
throw new Error("scroll requires delta or offset.");
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
type: "scroll",
|
|
155
|
+
pane: pane,
|
|
156
|
+
...(delta !== undefined ? { delta } : {}),
|
|
157
|
+
...(offset !== undefined ? { offset } : {}),
|
|
158
|
+
...(actionId ? { actionId } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|