acpx 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -16
- package/dist/{acp-jsonrpc-BNHXq7qK.js → acp-jsonrpc-C7pPk9Tw.js} +1 -1
- package/dist/{acp-jsonrpc-BNHXq7qK.js.map → acp-jsonrpc-C7pPk9Tw.js.map} +1 -1
- package/dist/cli-5s-E-Y99.js +176 -0
- package/dist/cli-5s-E-Y99.js.map +1 -0
- package/dist/cli.d.ts +1 -118
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +235 -331
- package/dist/cli.js.map +1 -1
- package/dist/flags-BkWInxAq.js +194 -0
- package/dist/flags-BkWInxAq.js.map +1 -0
- package/dist/flows-DnIYoHI1.js +1551 -0
- package/dist/flows-DnIYoHI1.js.map +1 -0
- package/dist/flows.d.ts +292 -0
- package/dist/flows.d.ts.map +1 -0
- package/dist/flows.js +2 -0
- package/dist/{output-BmkPP7qE.js → output-C58ukIo3.js} +137 -14
- package/dist/output-C58ukIo3.js.map +1 -0
- package/dist/{output-render-DEAaMxg8.js → output-render-C7w9NZ2H.js} +10 -10
- package/dist/output-render-C7w9NZ2H.js.map +1 -0
- package/dist/{queue-ipc-EQLpBMKv.js → queue-ipc-CgWf63GN.js} +258 -95
- package/dist/queue-ipc-CgWf63GN.js.map +1 -0
- package/dist/{session-C2Q8ktsN.js → session-BtpTC2pM.js} +687 -138
- package/dist/session-BtpTC2pM.js.map +1 -0
- package/dist/types-CeRKmEQ1.d.ts +137 -0
- package/dist/types-CeRKmEQ1.d.ts.map +1 -0
- package/package.json +36 -16
- package/skills/acpx/SKILL.md +23 -6
- package/dist/output-BmkPP7qE.js.map +0 -1
- package/dist/output-render-DEAaMxg8.js.map +0 -1
- package/dist/queue-ipc-EQLpBMKv.js.map +0 -1
- package/dist/runtime-session-id-C544sPPL.js +0 -31
- package/dist/runtime-session-id-C544sPPL.js.map +0 -1
- package/dist/session-C2Q8ktsN.js.map +0 -1
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
import { L as promptToDisplayText, R as textPrompt, j as SESSION_RECORD_SCHEMA } from "./queue-ipc-CgWf63GN.js";
|
|
2
|
+
import { C as TimeoutError, S as InterruptedError, T as withTimeout, _ as recordClientOperation, a as runOnce, g as createSessionConversation, h as cloneSessionAcpxState, i as createSessionWithClient, m as defaultSessionEventLog, p as resolveSessionRecord, r as cancelSessionPrompt, s as sendSessionDirect, v as recordPromptSubmission, w as withInterrupt, y as recordSessionUpdate } from "./session-BtpTC2pM.js";
|
|
3
|
+
import { t as createOutputFormatter } from "./output-C58ukIo3.js";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
+
//#region src/flows/definition.ts
|
|
10
|
+
function defineFlow(definition) {
|
|
11
|
+
return definition;
|
|
12
|
+
}
|
|
13
|
+
function acp(definition) {
|
|
14
|
+
return {
|
|
15
|
+
nodeType: "acp",
|
|
16
|
+
...definition
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function compute(definition) {
|
|
20
|
+
return {
|
|
21
|
+
nodeType: "compute",
|
|
22
|
+
...definition
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function action(definition) {
|
|
26
|
+
return {
|
|
27
|
+
nodeType: "action",
|
|
28
|
+
...definition
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function shell(definition) {
|
|
32
|
+
return {
|
|
33
|
+
nodeType: "action",
|
|
34
|
+
...definition
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function checkpoint(definition = {}) {
|
|
38
|
+
return {
|
|
39
|
+
nodeType: "checkpoint",
|
|
40
|
+
...definition
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/flows/executors/shell.ts
|
|
45
|
+
function formatShellActionSummary(spec) {
|
|
46
|
+
return `shell: ${renderShellCommand(spec.command, spec.args ?? [])}`;
|
|
47
|
+
}
|
|
48
|
+
function renderShellCommand(command, args) {
|
|
49
|
+
const renderedArgs = args.map((arg) => JSON.stringify(arg)).join(" ");
|
|
50
|
+
return renderedArgs.length > 0 ? `${command} ${renderedArgs}` : command;
|
|
51
|
+
}
|
|
52
|
+
async function runShellAction(spec) {
|
|
53
|
+
const cwd = spec.cwd ?? process.cwd();
|
|
54
|
+
const args = spec.args ?? [];
|
|
55
|
+
const startMs = Date.now();
|
|
56
|
+
const child = spawn(spec.command, args, {
|
|
57
|
+
cwd,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
...spec.env
|
|
61
|
+
},
|
|
62
|
+
shell: spec.shell,
|
|
63
|
+
stdio: [
|
|
64
|
+
"pipe",
|
|
65
|
+
"pipe",
|
|
66
|
+
"pipe"
|
|
67
|
+
],
|
|
68
|
+
windowsHide: true
|
|
69
|
+
});
|
|
70
|
+
let stdout = "";
|
|
71
|
+
let stderr = "";
|
|
72
|
+
let timedOut = false;
|
|
73
|
+
let timeout;
|
|
74
|
+
const finish = new Promise((resolve, reject) => {
|
|
75
|
+
child.stdout.setEncoding("utf8");
|
|
76
|
+
child.stderr.setEncoding("utf8");
|
|
77
|
+
child.stdout.on("data", (chunk) => {
|
|
78
|
+
stdout += chunk;
|
|
79
|
+
});
|
|
80
|
+
child.stderr.on("data", (chunk) => {
|
|
81
|
+
stderr += chunk;
|
|
82
|
+
});
|
|
83
|
+
child.once("error", reject);
|
|
84
|
+
child.once("exit", (exitCode, signal) => {
|
|
85
|
+
const result = {
|
|
86
|
+
command: spec.command,
|
|
87
|
+
args,
|
|
88
|
+
cwd,
|
|
89
|
+
stdout,
|
|
90
|
+
stderr,
|
|
91
|
+
combinedOutput: `${stdout}${stderr}`,
|
|
92
|
+
exitCode,
|
|
93
|
+
signal,
|
|
94
|
+
durationMs: Date.now() - startMs
|
|
95
|
+
};
|
|
96
|
+
if (timedOut) {
|
|
97
|
+
reject(new TimeoutError(spec.timeoutMs ?? 0));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (((exitCode ?? 0) !== 0 || signal != null) && spec.allowNonZeroExit !== true) {
|
|
101
|
+
reject(/* @__PURE__ */ new Error(`Shell action failed (${renderShellCommand(spec.command, args)}): ${signal ? `signal ${signal}` : `exit ${String(exitCode)}`}${stderr.length > 0 ? `\n${stderr.trim()}` : ""}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
resolve(result);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
if (spec.stdin != null) child.stdin.write(spec.stdin);
|
|
108
|
+
child.stdin.end();
|
|
109
|
+
if (spec.timeoutMs != null && spec.timeoutMs > 0) timeout = setTimeout(() => {
|
|
110
|
+
timedOut = true;
|
|
111
|
+
child.kill("SIGTERM");
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
child.kill("SIGKILL");
|
|
114
|
+
}, 1e3).unref();
|
|
115
|
+
}, spec.timeoutMs);
|
|
116
|
+
try {
|
|
117
|
+
return await finish;
|
|
118
|
+
} finally {
|
|
119
|
+
if (timeout) clearTimeout(timeout);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/flows/graph.ts
|
|
124
|
+
function validateFlowDefinition(flow) {
|
|
125
|
+
if (!flow.name.trim()) throw new Error("Flow name must not be empty");
|
|
126
|
+
if (flow.permissions?.reason !== void 0 && !flow.permissions.reason.trim()) throw new Error("Flow permission reason must not be empty");
|
|
127
|
+
if (!flow.nodes[flow.startAt]) throw new Error(`Flow start node is missing: ${flow.startAt}`);
|
|
128
|
+
const outgoingEdges = /* @__PURE__ */ new Set();
|
|
129
|
+
for (const edge of flow.edges) {
|
|
130
|
+
if (!flow.nodes[edge.from]) throw new Error(`Flow edge references unknown from-node: ${edge.from}`);
|
|
131
|
+
if (outgoingEdges.has(edge.from)) throw new Error(`Flow node must not declare multiple outgoing edges: ${edge.from}`);
|
|
132
|
+
outgoingEdges.add(edge.from);
|
|
133
|
+
if ("to" in edge) {
|
|
134
|
+
if (!flow.nodes[edge.to]) throw new Error(`Flow edge references unknown to-node: ${edge.to}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const target of Object.values(edge.switch.cases)) if (!flow.nodes[target]) throw new Error(`Flow switch references unknown to-node: ${target}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function resolveNext(edges, from, output, result) {
|
|
141
|
+
const edge = edges.find((candidate) => candidate.from === from);
|
|
142
|
+
if (!edge) return null;
|
|
143
|
+
if ("to" in edge) return edge.to;
|
|
144
|
+
const value = getBySwitchPath(output, result, edge.switch.on);
|
|
145
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new Error(`Flow switch value must be scalar for ${edge.switch.on}`);
|
|
146
|
+
const next = edge.switch.cases[String(value)];
|
|
147
|
+
if (!next) throw new Error(`No flow switch case for ${edge.switch.on}=${JSON.stringify(value)}`);
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
function resolveNextForOutcome(edges, from, result) {
|
|
151
|
+
const edge = edges.find((candidate) => candidate.from === from);
|
|
152
|
+
if (!edge || "to" in edge) return null;
|
|
153
|
+
if (!edge.switch.on.startsWith("$result.")) return null;
|
|
154
|
+
const value = getBySwitchPath(void 0, result, edge.switch.on);
|
|
155
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new Error(`Flow switch value must be scalar for ${edge.switch.on}`);
|
|
156
|
+
const next = edge.switch.cases[String(value)];
|
|
157
|
+
if (!next) throw new Error(`No flow switch case for ${edge.switch.on}=${JSON.stringify(value)}`);
|
|
158
|
+
return next;
|
|
159
|
+
}
|
|
160
|
+
function getBySwitchPath(output, result, jsonPath) {
|
|
161
|
+
if (jsonPath.startsWith("$result.")) return getByPath(result, `$.${jsonPath.slice(8)}`);
|
|
162
|
+
if (jsonPath.startsWith("$output.")) return getByPath(output, `$.${jsonPath.slice(8)}`);
|
|
163
|
+
return getByPath(output, jsonPath);
|
|
164
|
+
}
|
|
165
|
+
function getByPath(value, jsonPath) {
|
|
166
|
+
if (!jsonPath.startsWith("$.")) throw new Error(`Unsupported JSON path: ${jsonPath}`);
|
|
167
|
+
return jsonPath.slice(2).split(".").reduce((current, key) => {
|
|
168
|
+
if (current == null || typeof current !== "object") return;
|
|
169
|
+
return current[key];
|
|
170
|
+
}, value);
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/flows/store.ts
|
|
174
|
+
const FLOW_BUNDLE_SCHEMA = "acpx.flow-run-bundle.v1";
|
|
175
|
+
const FLOW_TRACE_SCHEMA = "acpx.flow-trace-event.v1";
|
|
176
|
+
const FLOW_SNAPSHOT_SCHEMA = "acpx.flow-definition-snapshot.v1";
|
|
177
|
+
const MANIFEST_PATH = "manifest.json";
|
|
178
|
+
const FLOW_SNAPSHOT_PATH = "flow.json";
|
|
179
|
+
const TRACE_PATH = "trace.ndjson";
|
|
180
|
+
const PROJECTIONS_DIR = "projections";
|
|
181
|
+
const RUN_PROJECTION_PATH = "projections/run.json";
|
|
182
|
+
const LIVE_PROJECTION_PATH = "projections/live.json";
|
|
183
|
+
const STEPS_PROJECTION_PATH = "projections/steps.json";
|
|
184
|
+
const SESSIONS_DIR = "sessions";
|
|
185
|
+
const ARTIFACTS_DIR = "artifacts";
|
|
186
|
+
function flowRunsBaseDir(homeDir = os.homedir()) {
|
|
187
|
+
return path.join(homeDir, ".acpx", "flows", "runs");
|
|
188
|
+
}
|
|
189
|
+
var FlowRunStore = class {
|
|
190
|
+
outputRoot;
|
|
191
|
+
traceSeqByRun = /* @__PURE__ */ new Map();
|
|
192
|
+
sessionSeqByBundle = /* @__PURE__ */ new Map();
|
|
193
|
+
manifestByRun = /* @__PURE__ */ new Map();
|
|
194
|
+
appendChainByPath = /* @__PURE__ */ new Map();
|
|
195
|
+
constructor(outputRoot = flowRunsBaseDir()) {
|
|
196
|
+
this.outputRoot = outputRoot;
|
|
197
|
+
}
|
|
198
|
+
async createRunDir(runId) {
|
|
199
|
+
const runDir = path.join(this.outputRoot, runId);
|
|
200
|
+
await fs.mkdir(path.join(runDir, PROJECTIONS_DIR), { recursive: true });
|
|
201
|
+
await fs.mkdir(path.join(runDir, SESSIONS_DIR), { recursive: true });
|
|
202
|
+
await fs.mkdir(path.join(runDir, ARTIFACTS_DIR), { recursive: true });
|
|
203
|
+
this.traceSeqByRun.set(runDir, 0);
|
|
204
|
+
return runDir;
|
|
205
|
+
}
|
|
206
|
+
async initializeRunBundle(runDir, options) {
|
|
207
|
+
const snapshot = createFlowDefinitionSnapshot(options.flow);
|
|
208
|
+
const manifest = {
|
|
209
|
+
schema: FLOW_BUNDLE_SCHEMA,
|
|
210
|
+
runId: options.state.runId,
|
|
211
|
+
flowName: options.state.flowName,
|
|
212
|
+
runTitle: options.state.runTitle,
|
|
213
|
+
flowPath: options.state.flowPath,
|
|
214
|
+
startedAt: options.state.startedAt,
|
|
215
|
+
finishedAt: options.state.finishedAt,
|
|
216
|
+
status: options.state.status,
|
|
217
|
+
traceSchema: FLOW_TRACE_SCHEMA,
|
|
218
|
+
paths: {
|
|
219
|
+
flow: FLOW_SNAPSHOT_PATH,
|
|
220
|
+
trace: TRACE_PATH,
|
|
221
|
+
runProjection: RUN_PROJECTION_PATH,
|
|
222
|
+
liveProjection: LIVE_PROJECTION_PATH,
|
|
223
|
+
stepsProjection: STEPS_PROJECTION_PATH,
|
|
224
|
+
sessionsDir: SESSIONS_DIR,
|
|
225
|
+
artifactsDir: ARTIFACTS_DIR
|
|
226
|
+
},
|
|
227
|
+
sessions: []
|
|
228
|
+
};
|
|
229
|
+
this.manifestByRun.set(runDir, manifest);
|
|
230
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, FLOW_SNAPSHOT_PATH), snapshot);
|
|
231
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
|
|
232
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, RUN_PROJECTION_PATH), options.state);
|
|
233
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, LIVE_PROJECTION_PATH), createLiveState(options.state));
|
|
234
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, STEPS_PROJECTION_PATH), options.state.steps);
|
|
235
|
+
await ensureFile(this.resolveRunPath(runDir, TRACE_PATH));
|
|
236
|
+
await this.appendTrace(runDir, options.state, {
|
|
237
|
+
scope: "run",
|
|
238
|
+
type: "run_started",
|
|
239
|
+
payload: {
|
|
240
|
+
flowName: options.state.flowName,
|
|
241
|
+
...options.state.runTitle ? { runTitle: options.state.runTitle } : {},
|
|
242
|
+
...options.state.flowPath ? { flowPath: options.state.flowPath } : {},
|
|
243
|
+
...options.inputArtifact ? { inputArtifact: options.inputArtifact } : {}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async writeSnapshot(runDir, state, event) {
|
|
248
|
+
state.updatedAt = isoNow$1();
|
|
249
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, RUN_PROJECTION_PATH), state);
|
|
250
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, LIVE_PROJECTION_PATH), createLiveState(state));
|
|
251
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, STEPS_PROJECTION_PATH), state.steps);
|
|
252
|
+
await this.writeManifest(runDir, state);
|
|
253
|
+
await this.appendTrace(runDir, state, event);
|
|
254
|
+
}
|
|
255
|
+
async writeLive(runDir, state, event) {
|
|
256
|
+
state.updatedAt = isoNow$1();
|
|
257
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, LIVE_PROJECTION_PATH), createLiveState(state));
|
|
258
|
+
await this.writeManifest(runDir, state);
|
|
259
|
+
await this.appendTrace(runDir, state, event);
|
|
260
|
+
}
|
|
261
|
+
async appendTrace(runDir, state, event) {
|
|
262
|
+
const traceEvent = {
|
|
263
|
+
seq: this.nextTraceSeq(runDir),
|
|
264
|
+
at: isoNow$1(),
|
|
265
|
+
runId: state.runId,
|
|
266
|
+
...event
|
|
267
|
+
};
|
|
268
|
+
await this.appendJsonLine(this.resolveRunPath(runDir, TRACE_PATH), traceEvent);
|
|
269
|
+
return traceEvent;
|
|
270
|
+
}
|
|
271
|
+
async writeArtifact(runDir, state, content, options) {
|
|
272
|
+
const buffer = toArtifactBuffer(content, options.mediaType);
|
|
273
|
+
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
|
274
|
+
const relativePath = path.posix.join(ARTIFACTS_DIR, `sha256-${sha256}${normalizeArtifactExtension(options.extension)}`);
|
|
275
|
+
const filePath = this.resolveRunPath(runDir, relativePath);
|
|
276
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
277
|
+
try {
|
|
278
|
+
await fs.access(filePath);
|
|
279
|
+
} catch {
|
|
280
|
+
await fs.writeFile(filePath, buffer);
|
|
281
|
+
}
|
|
282
|
+
const artifact = {
|
|
283
|
+
path: relativePath,
|
|
284
|
+
mediaType: options.mediaType,
|
|
285
|
+
bytes: buffer.byteLength,
|
|
286
|
+
sha256
|
|
287
|
+
};
|
|
288
|
+
if (options.emitTrace !== false) await this.appendTrace(runDir, state, {
|
|
289
|
+
scope: "artifact",
|
|
290
|
+
type: "artifact_written",
|
|
291
|
+
nodeId: options.nodeId,
|
|
292
|
+
attemptId: options.attemptId,
|
|
293
|
+
sessionId: options.sessionId,
|
|
294
|
+
artifact,
|
|
295
|
+
payload: { artifact }
|
|
296
|
+
});
|
|
297
|
+
return artifact;
|
|
298
|
+
}
|
|
299
|
+
async ensureSessionBundle(runDir, state, binding, record) {
|
|
300
|
+
const sessionDir = this.resolveRunPath(runDir, sessionDirPath(binding.bundleId));
|
|
301
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
302
|
+
await writeJsonAtomic(path.join(sessionDir, "binding.json"), binding);
|
|
303
|
+
await ensureFile(path.join(sessionDir, "events.ndjson"));
|
|
304
|
+
if (record) await this.writeSessionRecord(runDir, state, binding, record);
|
|
305
|
+
const manifest = this.getManifest(runDir, state);
|
|
306
|
+
const isNew = !manifest.sessions.find((entry) => entry.id === binding.bundleId);
|
|
307
|
+
if (isNew) {
|
|
308
|
+
const entry = {
|
|
309
|
+
id: binding.bundleId,
|
|
310
|
+
handle: binding.handle,
|
|
311
|
+
bindingPath: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
|
|
312
|
+
recordPath: path.posix.join(sessionDirPath(binding.bundleId), "record.json"),
|
|
313
|
+
eventsPath: path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")
|
|
314
|
+
};
|
|
315
|
+
manifest.sessions.push(entry);
|
|
316
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
|
|
317
|
+
}
|
|
318
|
+
if (isNew) await this.appendTrace(runDir, state, {
|
|
319
|
+
scope: "session",
|
|
320
|
+
type: "session_bound",
|
|
321
|
+
sessionId: binding.bundleId,
|
|
322
|
+
payload: {
|
|
323
|
+
sessionId: binding.bundleId,
|
|
324
|
+
handle: binding.handle,
|
|
325
|
+
bindingArtifact: {
|
|
326
|
+
path: path.posix.join(sessionDirPath(binding.bundleId), "binding.json"),
|
|
327
|
+
mediaType: "application/json",
|
|
328
|
+
sha256: await fileSha256(path.join(sessionDir, "binding.json"))
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async writeSessionRecord(runDir, _state, binding, record) {
|
|
334
|
+
const bundledRecord = createBundledSessionRecord(binding, record, this.sessionSeqByBundle.get(`${runDir}::${binding.bundleId}`) ?? 0);
|
|
335
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "record.json")), bundledRecord);
|
|
336
|
+
}
|
|
337
|
+
async appendSessionEvent(runDir, binding, direction, message) {
|
|
338
|
+
const sessionKey = `${runDir}::${binding.bundleId}`;
|
|
339
|
+
const seq = (this.sessionSeqByBundle.get(sessionKey) ?? 0) + 1;
|
|
340
|
+
this.sessionSeqByBundle.set(sessionKey, seq);
|
|
341
|
+
await this.appendJsonLine(this.resolveRunPath(runDir, path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson")), {
|
|
342
|
+
seq,
|
|
343
|
+
at: isoNow$1(),
|
|
344
|
+
direction,
|
|
345
|
+
message
|
|
346
|
+
});
|
|
347
|
+
return seq;
|
|
348
|
+
}
|
|
349
|
+
getManifest(runDir, state) {
|
|
350
|
+
const existing = this.manifestByRun.get(runDir);
|
|
351
|
+
if (existing) {
|
|
352
|
+
existing.startedAt = state.startedAt;
|
|
353
|
+
existing.finishedAt = state.finishedAt;
|
|
354
|
+
existing.status = state.status;
|
|
355
|
+
existing.flowPath = state.flowPath;
|
|
356
|
+
existing.flowName = state.flowName;
|
|
357
|
+
existing.runTitle = state.runTitle;
|
|
358
|
+
return existing;
|
|
359
|
+
}
|
|
360
|
+
const created = {
|
|
361
|
+
schema: FLOW_BUNDLE_SCHEMA,
|
|
362
|
+
runId: state.runId,
|
|
363
|
+
flowName: state.flowName,
|
|
364
|
+
runTitle: state.runTitle,
|
|
365
|
+
flowPath: state.flowPath,
|
|
366
|
+
startedAt: state.startedAt,
|
|
367
|
+
finishedAt: state.finishedAt,
|
|
368
|
+
status: state.status,
|
|
369
|
+
traceSchema: FLOW_TRACE_SCHEMA,
|
|
370
|
+
paths: {
|
|
371
|
+
flow: FLOW_SNAPSHOT_PATH,
|
|
372
|
+
trace: TRACE_PATH,
|
|
373
|
+
runProjection: RUN_PROJECTION_PATH,
|
|
374
|
+
liveProjection: LIVE_PROJECTION_PATH,
|
|
375
|
+
stepsProjection: STEPS_PROJECTION_PATH,
|
|
376
|
+
sessionsDir: SESSIONS_DIR,
|
|
377
|
+
artifactsDir: ARTIFACTS_DIR
|
|
378
|
+
},
|
|
379
|
+
sessions: []
|
|
380
|
+
};
|
|
381
|
+
this.manifestByRun.set(runDir, created);
|
|
382
|
+
return created;
|
|
383
|
+
}
|
|
384
|
+
async writeManifest(runDir, state) {
|
|
385
|
+
const manifest = this.getManifest(runDir, state);
|
|
386
|
+
await writeJsonAtomic(this.resolveRunPath(runDir, MANIFEST_PATH), manifest);
|
|
387
|
+
}
|
|
388
|
+
nextTraceSeq(runDir) {
|
|
389
|
+
const next = (this.traceSeqByRun.get(runDir) ?? 0) + 1;
|
|
390
|
+
this.traceSeqByRun.set(runDir, next);
|
|
391
|
+
return next;
|
|
392
|
+
}
|
|
393
|
+
resolveRunPath(runDir, relativePath) {
|
|
394
|
+
return path.join(runDir, ...relativePath.split("/"));
|
|
395
|
+
}
|
|
396
|
+
async appendJsonLine(filePath, value) {
|
|
397
|
+
const tracked = (this.appendChainByPath.get(filePath) ?? Promise.resolve()).then(async () => {
|
|
398
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
399
|
+
await fs.appendFile(filePath, `${JSON.stringify(value)}\n`, "utf8");
|
|
400
|
+
}).finally(() => {
|
|
401
|
+
if (this.appendChainByPath.get(filePath) === tracked) this.appendChainByPath.delete(filePath);
|
|
402
|
+
});
|
|
403
|
+
this.appendChainByPath.set(filePath, tracked);
|
|
404
|
+
await tracked;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function createLiveState(state) {
|
|
408
|
+
return {
|
|
409
|
+
runId: state.runId,
|
|
410
|
+
flowName: state.flowName,
|
|
411
|
+
runTitle: state.runTitle,
|
|
412
|
+
flowPath: state.flowPath,
|
|
413
|
+
startedAt: state.startedAt,
|
|
414
|
+
finishedAt: state.finishedAt,
|
|
415
|
+
updatedAt: state.updatedAt,
|
|
416
|
+
status: state.status,
|
|
417
|
+
currentNode: state.currentNode,
|
|
418
|
+
currentAttemptId: state.currentAttemptId,
|
|
419
|
+
currentNodeType: state.currentNodeType,
|
|
420
|
+
currentNodeStartedAt: state.currentNodeStartedAt,
|
|
421
|
+
lastHeartbeatAt: state.lastHeartbeatAt,
|
|
422
|
+
statusDetail: state.statusDetail,
|
|
423
|
+
waitingOn: state.waitingOn,
|
|
424
|
+
error: state.error,
|
|
425
|
+
sessionBindings: state.sessionBindings
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function createFlowDefinitionSnapshot(flow) {
|
|
429
|
+
return {
|
|
430
|
+
schema: FLOW_SNAPSHOT_SCHEMA,
|
|
431
|
+
name: flow.name,
|
|
432
|
+
...flow.run?.title !== void 0 ? { run: { hasTitle: true } } : {},
|
|
433
|
+
...flow.permissions ? { permissions: structuredClone(flow.permissions) } : {},
|
|
434
|
+
startAt: flow.startAt,
|
|
435
|
+
nodes: Object.fromEntries(Object.entries(flow.nodes).map(([nodeId, node]) => [nodeId, snapshotNode(node)])),
|
|
436
|
+
edges: structuredClone(flow.edges)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function snapshotNode(node) {
|
|
440
|
+
const common = {
|
|
441
|
+
nodeType: node.nodeType,
|
|
442
|
+
...node.timeoutMs !== void 0 ? { timeoutMs: node.timeoutMs } : {},
|
|
443
|
+
...node.heartbeatMs !== void 0 ? { heartbeatMs: node.heartbeatMs } : {},
|
|
444
|
+
...node.statusDetail ? { statusDetail: node.statusDetail } : {}
|
|
445
|
+
};
|
|
446
|
+
switch (node.nodeType) {
|
|
447
|
+
case "acp": return {
|
|
448
|
+
...common,
|
|
449
|
+
...node.profile ? { profile: node.profile } : {},
|
|
450
|
+
session: {
|
|
451
|
+
...node.session?.handle ? { handle: node.session.handle } : {},
|
|
452
|
+
...node.session?.isolated ? { isolated: true } : {}
|
|
453
|
+
},
|
|
454
|
+
cwd: snapshotCwd(node.cwd),
|
|
455
|
+
hasPrompt: true,
|
|
456
|
+
hasParse: typeof node.parse === "function"
|
|
457
|
+
};
|
|
458
|
+
case "compute": return {
|
|
459
|
+
...common,
|
|
460
|
+
hasRun: true
|
|
461
|
+
};
|
|
462
|
+
case "action": {
|
|
463
|
+
const actionExecution = "exec" in node ? "shell" : "function";
|
|
464
|
+
return {
|
|
465
|
+
...common,
|
|
466
|
+
actionExecution,
|
|
467
|
+
hasRun: "run" in node,
|
|
468
|
+
hasExec: "exec" in node,
|
|
469
|
+
hasParse: "parse" in node && typeof node.parse === "function"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
case "checkpoint": return {
|
|
473
|
+
...common,
|
|
474
|
+
...node.summary ? { summary: node.summary } : {},
|
|
475
|
+
hasRun: typeof node.run === "function"
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function snapshotCwd(cwd) {
|
|
480
|
+
if (typeof cwd === "function") return { mode: "dynamic" };
|
|
481
|
+
if (typeof cwd === "string") return {
|
|
482
|
+
mode: "static",
|
|
483
|
+
value: cwd
|
|
484
|
+
};
|
|
485
|
+
return { mode: "default" };
|
|
486
|
+
}
|
|
487
|
+
function createBundledSessionRecord(binding, record, bundleLastSeq) {
|
|
488
|
+
return {
|
|
489
|
+
...structuredClone(record),
|
|
490
|
+
lastSeq: bundleLastSeq,
|
|
491
|
+
eventLog: {
|
|
492
|
+
...structuredClone(record.eventLog),
|
|
493
|
+
active_path: path.posix.join(sessionDirPath(binding.bundleId), "events.ndjson"),
|
|
494
|
+
segment_count: 1,
|
|
495
|
+
max_segments: 1
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async function writeJsonAtomic(filePath, value) {
|
|
500
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
501
|
+
const payload = JSON.stringify(value, null, 2);
|
|
502
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
503
|
+
await fs.writeFile(tempPath, `${payload}\n`, "utf8");
|
|
504
|
+
await fs.rename(tempPath, filePath);
|
|
505
|
+
}
|
|
506
|
+
async function ensureFile(filePath) {
|
|
507
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
508
|
+
await fs.appendFile(filePath, "", "utf8");
|
|
509
|
+
}
|
|
510
|
+
async function fileSha256(filePath) {
|
|
511
|
+
const payload = await fs.readFile(filePath);
|
|
512
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
513
|
+
}
|
|
514
|
+
function toArtifactBuffer(content, mediaType) {
|
|
515
|
+
if (typeof content === "string") return Buffer.from(content, "utf8");
|
|
516
|
+
if (Buffer.isBuffer(content)) return content;
|
|
517
|
+
if (content instanceof Uint8Array) return Buffer.from(content);
|
|
518
|
+
if (mediaType === "application/json") return Buffer.from(`${JSON.stringify(content, null, 2)}\n`, "utf8");
|
|
519
|
+
return Buffer.from(String(content), "utf8");
|
|
520
|
+
}
|
|
521
|
+
function normalizeArtifactExtension(extension) {
|
|
522
|
+
if (!extension) return "";
|
|
523
|
+
return extension.startsWith(".") ? extension : `.${extension}`;
|
|
524
|
+
}
|
|
525
|
+
function sessionDirPath(bundleId) {
|
|
526
|
+
return path.posix.join(SESSIONS_DIR, bundleId);
|
|
527
|
+
}
|
|
528
|
+
function isoNow$1() {
|
|
529
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/flows/runtime.ts
|
|
533
|
+
const DEFAULT_FLOW_HEARTBEAT_MS = 5e3;
|
|
534
|
+
const DEFAULT_FLOW_STEP_TIMEOUT_MS = 15 * 6e4;
|
|
535
|
+
var FlowRunner = class {
|
|
536
|
+
resolveAgent;
|
|
537
|
+
defaultCwd;
|
|
538
|
+
permissionMode;
|
|
539
|
+
mcpServers;
|
|
540
|
+
nonInteractivePermissions;
|
|
541
|
+
authCredentials;
|
|
542
|
+
authPolicy;
|
|
543
|
+
timeoutMs;
|
|
544
|
+
defaultNodeTimeoutMs;
|
|
545
|
+
verbose;
|
|
546
|
+
suppressSdkConsoleErrors;
|
|
547
|
+
sessionOptions;
|
|
548
|
+
services;
|
|
549
|
+
store;
|
|
550
|
+
pendingPersistentSessionClients = /* @__PURE__ */ new Map();
|
|
551
|
+
constructor(options) {
|
|
552
|
+
this.resolveAgent = options.resolveAgent;
|
|
553
|
+
this.defaultCwd = options.resolveAgent(void 0).cwd;
|
|
554
|
+
this.permissionMode = options.permissionMode;
|
|
555
|
+
this.mcpServers = options.mcpServers;
|
|
556
|
+
this.nonInteractivePermissions = options.nonInteractivePermissions;
|
|
557
|
+
this.authCredentials = options.authCredentials;
|
|
558
|
+
this.authPolicy = options.authPolicy;
|
|
559
|
+
this.timeoutMs = options.timeoutMs;
|
|
560
|
+
this.defaultNodeTimeoutMs = options.defaultNodeTimeoutMs ?? options.timeoutMs ?? DEFAULT_FLOW_STEP_TIMEOUT_MS;
|
|
561
|
+
this.verbose = options.verbose;
|
|
562
|
+
this.suppressSdkConsoleErrors = options.suppressSdkConsoleErrors;
|
|
563
|
+
this.sessionOptions = options.sessionOptions;
|
|
564
|
+
this.services = options.services ?? {};
|
|
565
|
+
this.store = new FlowRunStore(options.outputRoot);
|
|
566
|
+
}
|
|
567
|
+
async run(flow, input, options = {}) {
|
|
568
|
+
validateFlowDefinition(flow);
|
|
569
|
+
const runId = createRunId(flow.name);
|
|
570
|
+
const runTitle = await resolveFlowRunTitle(flow, input, options.flowPath);
|
|
571
|
+
const runDir = await this.store.createRunDir(runId);
|
|
572
|
+
const state = {
|
|
573
|
+
runId,
|
|
574
|
+
flowName: flow.name,
|
|
575
|
+
runTitle,
|
|
576
|
+
flowPath: options.flowPath,
|
|
577
|
+
startedAt: isoNow(),
|
|
578
|
+
updatedAt: isoNow(),
|
|
579
|
+
status: "running",
|
|
580
|
+
input,
|
|
581
|
+
outputs: {},
|
|
582
|
+
results: {},
|
|
583
|
+
steps: [],
|
|
584
|
+
sessionBindings: {}
|
|
585
|
+
};
|
|
586
|
+
const inputArtifact = await this.store.writeArtifact(runDir, state, input, {
|
|
587
|
+
mediaType: "application/json",
|
|
588
|
+
extension: "json",
|
|
589
|
+
emitTrace: false
|
|
590
|
+
});
|
|
591
|
+
await this.store.initializeRunBundle(runDir, {
|
|
592
|
+
flow,
|
|
593
|
+
state,
|
|
594
|
+
inputArtifact
|
|
595
|
+
});
|
|
596
|
+
let current = flow.startAt;
|
|
597
|
+
const attemptCounts = /* @__PURE__ */ new Map();
|
|
598
|
+
try {
|
|
599
|
+
return await withInterrupt(async () => {
|
|
600
|
+
try {
|
|
601
|
+
while (current) {
|
|
602
|
+
const node = flow.nodes[current];
|
|
603
|
+
if (!node) throw new Error(`Unknown flow node: ${current}`);
|
|
604
|
+
const attemptId = nextAttemptId(attemptCounts, current);
|
|
605
|
+
const startedAt = isoNow();
|
|
606
|
+
const context = this.makeContext(state, input);
|
|
607
|
+
let output;
|
|
608
|
+
let promptText = null;
|
|
609
|
+
let rawText = null;
|
|
610
|
+
let sessionInfo = null;
|
|
611
|
+
let agentInfo = null;
|
|
612
|
+
let trace = null;
|
|
613
|
+
this.markNodeStarted(state, current, attemptId, node.nodeType, startedAt, node.statusDetail);
|
|
614
|
+
await this.store.writeSnapshot(runDir, state, {
|
|
615
|
+
scope: "node",
|
|
616
|
+
type: "node_started",
|
|
617
|
+
nodeId: current,
|
|
618
|
+
attemptId,
|
|
619
|
+
payload: {
|
|
620
|
+
nodeType: node.nodeType,
|
|
621
|
+
...node.timeoutMs !== void 0 ? { timeoutMs: node.timeoutMs ?? this.defaultNodeTimeoutMs } : { timeoutMs: this.defaultNodeTimeoutMs },
|
|
622
|
+
...state.statusDetail ? { statusDetail: state.statusDetail } : {}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
let nodeResult;
|
|
626
|
+
let executionError;
|
|
627
|
+
try {
|
|
628
|
+
({output, promptText, rawText, sessionInfo, agentInfo, trace} = await this.executeNode(runDir, state, flow, current, node, context));
|
|
629
|
+
trace = await this.finalizeStepTrace(runDir, state, current, attemptId, output, trace);
|
|
630
|
+
nodeResult = createNodeResult({
|
|
631
|
+
attemptId,
|
|
632
|
+
nodeId: current,
|
|
633
|
+
nodeType: node.nodeType,
|
|
634
|
+
outcome: "ok",
|
|
635
|
+
startedAt,
|
|
636
|
+
finishedAt: isoNow(),
|
|
637
|
+
output
|
|
638
|
+
});
|
|
639
|
+
} catch (error) {
|
|
640
|
+
executionError = error;
|
|
641
|
+
trace = extractAttachedStepTrace(error) ?? trace;
|
|
642
|
+
trace = await this.finalizeStepTrace(runDir, state, current, attemptId, void 0, trace);
|
|
643
|
+
nodeResult = createNodeResult({
|
|
644
|
+
attemptId,
|
|
645
|
+
nodeId: current,
|
|
646
|
+
nodeType: node.nodeType,
|
|
647
|
+
outcome: outcomeForError(error),
|
|
648
|
+
startedAt,
|
|
649
|
+
finishedAt: isoNow(),
|
|
650
|
+
error: error instanceof Error ? error.message : String(error)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
state.results[current] = nodeResult;
|
|
654
|
+
if (nodeResult.outcome === "ok" && node.nodeType === "checkpoint") {
|
|
655
|
+
state.outputs[current] = output;
|
|
656
|
+
state.waitingOn = current;
|
|
657
|
+
state.updatedAt = isoNow();
|
|
658
|
+
state.status = "waiting";
|
|
659
|
+
this.clearActiveNode(state, output?.summary ?? current);
|
|
660
|
+
state.steps.push({
|
|
661
|
+
attemptId,
|
|
662
|
+
nodeId: current,
|
|
663
|
+
nodeType: node.nodeType,
|
|
664
|
+
outcome: nodeResult.outcome,
|
|
665
|
+
startedAt,
|
|
666
|
+
finishedAt: nodeResult.finishedAt,
|
|
667
|
+
promptText,
|
|
668
|
+
rawText,
|
|
669
|
+
output,
|
|
670
|
+
session: null,
|
|
671
|
+
agent: null,
|
|
672
|
+
...trace ? { trace } : {}
|
|
673
|
+
});
|
|
674
|
+
await this.store.writeSnapshot(runDir, state, {
|
|
675
|
+
scope: "node",
|
|
676
|
+
type: "node_outcome",
|
|
677
|
+
nodeId: current,
|
|
678
|
+
attemptId,
|
|
679
|
+
payload: createNodeOutcomePayload(nodeResult, trace)
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
runDir,
|
|
683
|
+
state
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (nodeResult.outcome === "ok") state.outputs[current] = output;
|
|
687
|
+
state.updatedAt = isoNow();
|
|
688
|
+
this.clearActiveNode(state);
|
|
689
|
+
state.steps.push({
|
|
690
|
+
attemptId,
|
|
691
|
+
nodeId: current,
|
|
692
|
+
nodeType: node.nodeType,
|
|
693
|
+
outcome: nodeResult.outcome,
|
|
694
|
+
startedAt,
|
|
695
|
+
finishedAt: nodeResult.finishedAt,
|
|
696
|
+
promptText,
|
|
697
|
+
rawText,
|
|
698
|
+
output,
|
|
699
|
+
error: nodeResult.error,
|
|
700
|
+
session: sessionInfo,
|
|
701
|
+
agent: agentInfo,
|
|
702
|
+
...trace ? { trace } : {}
|
|
703
|
+
});
|
|
704
|
+
await this.store.writeSnapshot(runDir, state, {
|
|
705
|
+
scope: "node",
|
|
706
|
+
type: "node_outcome",
|
|
707
|
+
nodeId: current,
|
|
708
|
+
attemptId,
|
|
709
|
+
payload: createNodeOutcomePayload(nodeResult, trace)
|
|
710
|
+
});
|
|
711
|
+
if (nodeResult.outcome === "ok") {
|
|
712
|
+
current = resolveNext(flow.edges, current, output, nodeResult);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const next = resolveNextForOutcome(flow.edges, current, nodeResult);
|
|
716
|
+
if (next) {
|
|
717
|
+
current = next;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
throw executionError;
|
|
721
|
+
}
|
|
722
|
+
state.status = "completed";
|
|
723
|
+
state.finishedAt = isoNow();
|
|
724
|
+
state.updatedAt = state.finishedAt;
|
|
725
|
+
this.clearActiveNode(state);
|
|
726
|
+
await this.store.writeSnapshot(runDir, state, {
|
|
727
|
+
scope: "run",
|
|
728
|
+
type: "run_completed",
|
|
729
|
+
payload: { status: state.status }
|
|
730
|
+
});
|
|
731
|
+
return {
|
|
732
|
+
runDir,
|
|
733
|
+
state
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
await this.persistRunFailure(runDir, state, error);
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}, async () => {
|
|
740
|
+
await this.persistRunFailure(runDir, state, new InterruptedError());
|
|
741
|
+
});
|
|
742
|
+
} finally {
|
|
743
|
+
await this.closePendingPersistentSessionClients();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async persistRunFailure(runDir, state, error) {
|
|
747
|
+
if (state.finishedAt !== void 0 && (state.status === "failed" || state.status === "timed_out")) return;
|
|
748
|
+
state.status = error instanceof TimeoutError ? "timed_out" : "failed";
|
|
749
|
+
state.updatedAt = isoNow();
|
|
750
|
+
state.finishedAt = state.updatedAt;
|
|
751
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
752
|
+
state.statusDetail = state.currentNode ? `Failed in ${state.currentNode}: ${state.error}` : state.error;
|
|
753
|
+
await this.store.writeSnapshot(runDir, state, {
|
|
754
|
+
scope: "run",
|
|
755
|
+
type: "run_failed",
|
|
756
|
+
payload: {
|
|
757
|
+
status: state.status,
|
|
758
|
+
error: state.error
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
makeContext(state, input) {
|
|
763
|
+
return {
|
|
764
|
+
input,
|
|
765
|
+
outputs: state.outputs,
|
|
766
|
+
results: state.results,
|
|
767
|
+
state,
|
|
768
|
+
services: this.services
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
async executeNode(runDir, state, flow, nodeId, node, context) {
|
|
772
|
+
switch (node.nodeType) {
|
|
773
|
+
case "compute": return await this.executeComputeNode(runDir, state, node, context);
|
|
774
|
+
case "action": return await this.executeActionNode(runDir, state, node, context);
|
|
775
|
+
case "checkpoint": return await this.executeCheckpointNode(runDir, state, nodeId, node, context);
|
|
776
|
+
case "acp": return await this.executeAcpNode(runDir, state, flow, node, context);
|
|
777
|
+
default: throw new Error(`Unsupported flow node: ${String(node)}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async executeComputeNode(runDir, state, node, context) {
|
|
781
|
+
const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
|
|
782
|
+
return {
|
|
783
|
+
output: await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => await Promise.resolve(node.run(context))),
|
|
784
|
+
promptText: null,
|
|
785
|
+
rawText: null,
|
|
786
|
+
sessionInfo: null,
|
|
787
|
+
agentInfo: null,
|
|
788
|
+
trace: null
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
async executeActionNode(runDir, state, node, context) {
|
|
792
|
+
const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
|
|
793
|
+
if ("run" in node) return {
|
|
794
|
+
output: await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => await Promise.resolve(node.run(context))),
|
|
795
|
+
promptText: null,
|
|
796
|
+
rawText: null,
|
|
797
|
+
sessionInfo: null,
|
|
798
|
+
agentInfo: null,
|
|
799
|
+
trace: { action: { actionType: "function" } }
|
|
800
|
+
};
|
|
801
|
+
const { output, rawText, trace } = await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => {
|
|
802
|
+
const execution = await Promise.resolve(node.exec(context));
|
|
803
|
+
const effectiveExecution = {
|
|
804
|
+
...execution,
|
|
805
|
+
cwd: resolveShellActionCwd(this.defaultCwd, execution.cwd),
|
|
806
|
+
timeoutMs: execution.timeoutMs ?? nodeTimeoutMs
|
|
807
|
+
};
|
|
808
|
+
this.updateStatusDetail(state, formatShellActionSummary(effectiveExecution));
|
|
809
|
+
await this.store.writeLive(runDir, state, {
|
|
810
|
+
scope: "node",
|
|
811
|
+
type: "node_heartbeat",
|
|
812
|
+
nodeId: state.currentNode,
|
|
813
|
+
attemptId: state.currentAttemptId,
|
|
814
|
+
payload: { statusDetail: state.statusDetail }
|
|
815
|
+
});
|
|
816
|
+
await this.store.appendTrace(runDir, state, {
|
|
817
|
+
scope: "action",
|
|
818
|
+
type: "action_prepared",
|
|
819
|
+
nodeId: state.currentNode,
|
|
820
|
+
attemptId: state.currentAttemptId,
|
|
821
|
+
payload: { action: {
|
|
822
|
+
actionType: "shell",
|
|
823
|
+
command: effectiveExecution.command,
|
|
824
|
+
args: effectiveExecution.args ?? [],
|
|
825
|
+
cwd: effectiveExecution.cwd
|
|
826
|
+
} }
|
|
827
|
+
});
|
|
828
|
+
const result = await runShellAction(effectiveExecution);
|
|
829
|
+
const stdoutArtifact = await this.store.writeArtifact(runDir, state, result.stdout, {
|
|
830
|
+
mediaType: "text/plain",
|
|
831
|
+
extension: "txt",
|
|
832
|
+
nodeId: state.currentNode,
|
|
833
|
+
attemptId: state.currentAttemptId
|
|
834
|
+
});
|
|
835
|
+
const stderrArtifact = await this.store.writeArtifact(runDir, state, result.stderr, {
|
|
836
|
+
mediaType: "text/plain",
|
|
837
|
+
extension: "txt",
|
|
838
|
+
nodeId: state.currentNode,
|
|
839
|
+
attemptId: state.currentAttemptId
|
|
840
|
+
});
|
|
841
|
+
await this.store.appendTrace(runDir, state, {
|
|
842
|
+
scope: "action",
|
|
843
|
+
type: "action_completed",
|
|
844
|
+
nodeId: state.currentNode,
|
|
845
|
+
attemptId: state.currentAttemptId,
|
|
846
|
+
payload: {
|
|
847
|
+
action: {
|
|
848
|
+
actionType: "shell",
|
|
849
|
+
command: result.command,
|
|
850
|
+
args: result.args,
|
|
851
|
+
cwd: result.cwd,
|
|
852
|
+
exitCode: result.exitCode,
|
|
853
|
+
signal: result.signal,
|
|
854
|
+
durationMs: result.durationMs
|
|
855
|
+
},
|
|
856
|
+
stdoutArtifact,
|
|
857
|
+
stderrArtifact
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
const trace = {
|
|
861
|
+
action: {
|
|
862
|
+
actionType: "shell",
|
|
863
|
+
command: result.command,
|
|
864
|
+
args: result.args,
|
|
865
|
+
cwd: result.cwd,
|
|
866
|
+
exitCode: result.exitCode,
|
|
867
|
+
signal: result.signal,
|
|
868
|
+
durationMs: result.durationMs
|
|
869
|
+
},
|
|
870
|
+
stdoutArtifact,
|
|
871
|
+
stderrArtifact
|
|
872
|
+
};
|
|
873
|
+
let parsedOutput;
|
|
874
|
+
try {
|
|
875
|
+
parsedOutput = node.parse ? await node.parse(result, context) : result;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
throw attachStepTrace(error, trace);
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
output: parsedOutput,
|
|
881
|
+
rawText: result.combinedOutput,
|
|
882
|
+
trace
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
output,
|
|
887
|
+
promptText: null,
|
|
888
|
+
rawText,
|
|
889
|
+
sessionInfo: null,
|
|
890
|
+
agentInfo: null,
|
|
891
|
+
trace
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
async executeCheckpointNode(runDir, state, nodeId, node, context) {
|
|
895
|
+
const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
|
|
896
|
+
return {
|
|
897
|
+
output: typeof node.run === "function" ? await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => await Promise.resolve(node.run?.(context))) : {
|
|
898
|
+
checkpoint: nodeId,
|
|
899
|
+
summary: node.summary ?? nodeId
|
|
900
|
+
},
|
|
901
|
+
promptText: null,
|
|
902
|
+
rawText: null,
|
|
903
|
+
sessionInfo: null,
|
|
904
|
+
agentInfo: null,
|
|
905
|
+
trace: null
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async executeAcpNode(runDir, state, flow, node, context) {
|
|
909
|
+
const nodeTimeoutMs = node.timeoutMs ?? this.defaultNodeTimeoutMs;
|
|
910
|
+
let boundSession = null;
|
|
911
|
+
return await this.runWithHeartbeat(runDir, state, state.currentNode ?? "", node, nodeTimeoutMs, async () => {
|
|
912
|
+
const resolvedAgent = this.resolveAgent(node.profile);
|
|
913
|
+
const agentInfo = {
|
|
914
|
+
...resolvedAgent,
|
|
915
|
+
cwd: await resolveNodeCwd(resolvedAgent.cwd, node.cwd, context)
|
|
916
|
+
};
|
|
917
|
+
const prompt = normalizePromptInput(await Promise.resolve(node.prompt(context)));
|
|
918
|
+
const promptText = promptToDisplayText(prompt);
|
|
919
|
+
this.updateStatusDetail(state, summarizePrompt(promptText, node.statusDetail));
|
|
920
|
+
await this.store.writeLive(runDir, state, {
|
|
921
|
+
scope: "node",
|
|
922
|
+
type: "node_heartbeat",
|
|
923
|
+
nodeId: state.currentNode,
|
|
924
|
+
attemptId: state.currentAttemptId,
|
|
925
|
+
payload: { statusDetail: state.statusDetail }
|
|
926
|
+
});
|
|
927
|
+
const promptArtifact = await this.store.writeArtifact(runDir, state, promptText, {
|
|
928
|
+
mediaType: "text/plain",
|
|
929
|
+
extension: "txt",
|
|
930
|
+
nodeId: state.currentNode,
|
|
931
|
+
attemptId: state.currentAttemptId
|
|
932
|
+
});
|
|
933
|
+
if (node.session?.isolated) {
|
|
934
|
+
const isolatedBinding = createIsolatedSessionBinding(flow.name, state.runId, state.currentAttemptId ?? randomUUID(), node.profile, agentInfo);
|
|
935
|
+
const initialIsolatedRecord = createSyntheticSessionRecord({
|
|
936
|
+
binding: isolatedBinding,
|
|
937
|
+
createdAt: state.currentNodeStartedAt ?? isoNow(),
|
|
938
|
+
updatedAt: state.currentNodeStartedAt ?? isoNow(),
|
|
939
|
+
conversation: createSessionConversation(state.currentNodeStartedAt ?? isoNow()),
|
|
940
|
+
acpxState: void 0,
|
|
941
|
+
lastSeq: 0
|
|
942
|
+
});
|
|
943
|
+
await this.store.ensureSessionBundle(runDir, state, isolatedBinding, initialIsolatedRecord);
|
|
944
|
+
await this.store.appendTrace(runDir, state, {
|
|
945
|
+
scope: "acp",
|
|
946
|
+
type: "acp_prompt_prepared",
|
|
947
|
+
nodeId: state.currentNode,
|
|
948
|
+
attemptId: state.currentAttemptId,
|
|
949
|
+
sessionId: isolatedBinding.bundleId,
|
|
950
|
+
payload: {
|
|
951
|
+
sessionId: isolatedBinding.bundleId,
|
|
952
|
+
promptArtifact
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
const isolatedPrompt = await this.runIsolatedPrompt(runDir, state, isolatedBinding, agentInfo, prompt, nodeTimeoutMs);
|
|
956
|
+
const rawResponseArtifact = await this.store.writeArtifact(runDir, state, isolatedPrompt.rawText, {
|
|
957
|
+
mediaType: "text/plain",
|
|
958
|
+
extension: "txt",
|
|
959
|
+
nodeId: state.currentNode,
|
|
960
|
+
attemptId: state.currentAttemptId,
|
|
961
|
+
sessionId: isolatedBinding.bundleId
|
|
962
|
+
});
|
|
963
|
+
await this.store.appendTrace(runDir, state, {
|
|
964
|
+
scope: "acp",
|
|
965
|
+
type: "acp_response_parsed",
|
|
966
|
+
nodeId: state.currentNode,
|
|
967
|
+
attemptId: state.currentAttemptId,
|
|
968
|
+
sessionId: isolatedBinding.bundleId,
|
|
969
|
+
payload: {
|
|
970
|
+
sessionId: isolatedBinding.bundleId,
|
|
971
|
+
conversation: isolatedPrompt.conversation,
|
|
972
|
+
rawResponseArtifact
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
const trace = {
|
|
976
|
+
sessionId: isolatedBinding.bundleId,
|
|
977
|
+
promptArtifact,
|
|
978
|
+
rawResponseArtifact,
|
|
979
|
+
conversation: isolatedPrompt.conversation
|
|
980
|
+
};
|
|
981
|
+
let parsedOutput;
|
|
982
|
+
try {
|
|
983
|
+
parsedOutput = node.parse ? await node.parse(isolatedPrompt.rawText, context) : isolatedPrompt.rawText;
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw attachStepTrace(error, trace);
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
output: parsedOutput,
|
|
989
|
+
promptText,
|
|
990
|
+
rawText: isolatedPrompt.rawText,
|
|
991
|
+
sessionInfo: isolatedBinding,
|
|
992
|
+
agentInfo,
|
|
993
|
+
trace
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
boundSession = await this.ensureSessionBinding(runDir, state, flow, node, agentInfo, nodeTimeoutMs);
|
|
997
|
+
await this.store.appendTrace(runDir, state, {
|
|
998
|
+
scope: "acp",
|
|
999
|
+
type: "acp_prompt_prepared",
|
|
1000
|
+
nodeId: state.currentNode,
|
|
1001
|
+
attemptId: state.currentAttemptId,
|
|
1002
|
+
sessionId: boundSession.bundleId,
|
|
1003
|
+
payload: {
|
|
1004
|
+
sessionId: boundSession.bundleId,
|
|
1005
|
+
promptArtifact
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
const persistentPrompt = await this.runPersistentPrompt(runDir, state, boundSession, prompt, nodeTimeoutMs);
|
|
1009
|
+
const rawResponseArtifact = await this.store.writeArtifact(runDir, state, persistentPrompt.rawText, {
|
|
1010
|
+
mediaType: "text/plain",
|
|
1011
|
+
extension: "txt",
|
|
1012
|
+
nodeId: state.currentNode,
|
|
1013
|
+
attemptId: state.currentAttemptId,
|
|
1014
|
+
sessionId: persistentPrompt.sessionInfo.bundleId
|
|
1015
|
+
});
|
|
1016
|
+
await this.store.appendTrace(runDir, state, {
|
|
1017
|
+
scope: "acp",
|
|
1018
|
+
type: "acp_response_parsed",
|
|
1019
|
+
nodeId: state.currentNode,
|
|
1020
|
+
attemptId: state.currentAttemptId,
|
|
1021
|
+
sessionId: persistentPrompt.sessionInfo.bundleId,
|
|
1022
|
+
payload: {
|
|
1023
|
+
sessionId: persistentPrompt.sessionInfo.bundleId,
|
|
1024
|
+
conversation: persistentPrompt.conversation,
|
|
1025
|
+
rawResponseArtifact
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
const trace = {
|
|
1029
|
+
sessionId: persistentPrompt.sessionInfo.bundleId,
|
|
1030
|
+
promptArtifact,
|
|
1031
|
+
rawResponseArtifact,
|
|
1032
|
+
conversation: persistentPrompt.conversation
|
|
1033
|
+
};
|
|
1034
|
+
let parsedOutput;
|
|
1035
|
+
try {
|
|
1036
|
+
parsedOutput = node.parse ? await node.parse(persistentPrompt.rawText, context) : persistentPrompt.rawText;
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
throw attachStepTrace(error, trace);
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
output: parsedOutput,
|
|
1042
|
+
promptText,
|
|
1043
|
+
rawText: persistentPrompt.rawText,
|
|
1044
|
+
sessionInfo: persistentPrompt.sessionInfo,
|
|
1045
|
+
agentInfo,
|
|
1046
|
+
trace
|
|
1047
|
+
};
|
|
1048
|
+
}, async () => {
|
|
1049
|
+
if (!boundSession) return;
|
|
1050
|
+
await cancelSessionPrompt({ sessionId: boundSession.acpxRecordId });
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
markNodeStarted(state, nodeId, attemptId, nodeType, startedAt, detail) {
|
|
1054
|
+
state.status = "running";
|
|
1055
|
+
state.waitingOn = void 0;
|
|
1056
|
+
state.currentNode = nodeId;
|
|
1057
|
+
state.currentAttemptId = attemptId;
|
|
1058
|
+
state.currentNodeType = nodeType;
|
|
1059
|
+
state.currentNodeStartedAt = startedAt;
|
|
1060
|
+
state.lastHeartbeatAt = startedAt;
|
|
1061
|
+
state.statusDetail = detail ?? `Running ${nodeType} node ${nodeId}`;
|
|
1062
|
+
}
|
|
1063
|
+
clearActiveNode(state, detail) {
|
|
1064
|
+
state.currentNode = void 0;
|
|
1065
|
+
state.currentAttemptId = void 0;
|
|
1066
|
+
state.currentNodeType = void 0;
|
|
1067
|
+
state.currentNodeStartedAt = void 0;
|
|
1068
|
+
state.lastHeartbeatAt = void 0;
|
|
1069
|
+
state.statusDetail = detail;
|
|
1070
|
+
}
|
|
1071
|
+
updateStatusDetail(state, detail) {
|
|
1072
|
+
if (!detail) return;
|
|
1073
|
+
state.statusDetail = detail;
|
|
1074
|
+
}
|
|
1075
|
+
async finalizeStepTrace(runDir, state, nodeId, attemptId, output, baseTrace) {
|
|
1076
|
+
const trace = baseTrace ? structuredClone(baseTrace) : {};
|
|
1077
|
+
if (output !== void 0) {
|
|
1078
|
+
const inlineOutput = toInlineOutput(output);
|
|
1079
|
+
if (inlineOutput !== void 0) trace.outputInline = inlineOutput;
|
|
1080
|
+
else trace.outputArtifact = await this.store.writeArtifact(runDir, state, output, {
|
|
1081
|
+
mediaType: outputArtifactMediaType(output),
|
|
1082
|
+
extension: outputArtifactExtension(output),
|
|
1083
|
+
nodeId,
|
|
1084
|
+
attemptId
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return Object.keys(trace).length > 0 ? trace : null;
|
|
1088
|
+
}
|
|
1089
|
+
async runWithHeartbeat(runDir, state, nodeId, node, timeoutMs, run, onTimeout) {
|
|
1090
|
+
const heartbeatMs = Math.max(0, Math.round(node.heartbeatMs ?? DEFAULT_FLOW_HEARTBEAT_MS));
|
|
1091
|
+
let timer;
|
|
1092
|
+
let active = true;
|
|
1093
|
+
const heartbeat = async () => {
|
|
1094
|
+
if (!active) return;
|
|
1095
|
+
state.lastHeartbeatAt = isoNow();
|
|
1096
|
+
state.updatedAt = state.lastHeartbeatAt;
|
|
1097
|
+
await this.store.writeLive(runDir, state, {
|
|
1098
|
+
scope: "node",
|
|
1099
|
+
type: "node_heartbeat",
|
|
1100
|
+
nodeId,
|
|
1101
|
+
attemptId: state.currentAttemptId,
|
|
1102
|
+
payload: { statusDetail: state.statusDetail }
|
|
1103
|
+
});
|
|
1104
|
+
};
|
|
1105
|
+
if (heartbeatMs > 0) timer = setInterval(() => {
|
|
1106
|
+
heartbeat();
|
|
1107
|
+
}, heartbeatMs);
|
|
1108
|
+
try {
|
|
1109
|
+
return await withTimeout(run(), timeoutMs);
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
if (error instanceof TimeoutError && onTimeout) await onTimeout().catch(() => {});
|
|
1112
|
+
throw error;
|
|
1113
|
+
} finally {
|
|
1114
|
+
active = false;
|
|
1115
|
+
if (timer) clearInterval(timer);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async ensureSessionBinding(runDir, state, flow, node, agent, timeoutMs) {
|
|
1119
|
+
const handle = node.session?.handle ?? "main";
|
|
1120
|
+
const key = createSessionBindingKey(agent.agentCommand, agent.cwd, handle);
|
|
1121
|
+
const existing = state.sessionBindings[key];
|
|
1122
|
+
if (existing) {
|
|
1123
|
+
await this.store.ensureSessionBundle(runDir, state, existing);
|
|
1124
|
+
return existing;
|
|
1125
|
+
}
|
|
1126
|
+
const name = createSessionName(flow.name, handle, agent.cwd, state.runId);
|
|
1127
|
+
const created = await createSessionWithClient({
|
|
1128
|
+
agentCommand: agent.agentCommand,
|
|
1129
|
+
cwd: agent.cwd,
|
|
1130
|
+
name,
|
|
1131
|
+
mcpServers: this.mcpServers,
|
|
1132
|
+
permissionMode: this.permissionMode,
|
|
1133
|
+
nonInteractivePermissions: this.nonInteractivePermissions,
|
|
1134
|
+
authCredentials: this.authCredentials,
|
|
1135
|
+
authPolicy: this.authPolicy,
|
|
1136
|
+
timeoutMs,
|
|
1137
|
+
verbose: this.verbose,
|
|
1138
|
+
sessionOptions: this.sessionOptions
|
|
1139
|
+
});
|
|
1140
|
+
const binding = {
|
|
1141
|
+
key,
|
|
1142
|
+
handle,
|
|
1143
|
+
bundleId: createSessionBundleId(handle, key),
|
|
1144
|
+
name,
|
|
1145
|
+
profile: node.profile,
|
|
1146
|
+
agentName: agent.agentName,
|
|
1147
|
+
agentCommand: agent.agentCommand,
|
|
1148
|
+
cwd: agent.cwd,
|
|
1149
|
+
acpxRecordId: created.record.acpxRecordId,
|
|
1150
|
+
acpSessionId: created.record.acpSessionId,
|
|
1151
|
+
agentSessionId: created.record.agentSessionId
|
|
1152
|
+
};
|
|
1153
|
+
state.sessionBindings[key] = binding;
|
|
1154
|
+
this.pendingPersistentSessionClients.set(binding.key, created.client);
|
|
1155
|
+
await this.store.ensureSessionBundle(runDir, state, binding, created.record);
|
|
1156
|
+
return binding;
|
|
1157
|
+
}
|
|
1158
|
+
async refreshSessionBinding(binding) {
|
|
1159
|
+
const record = await resolveSessionRecord(binding.acpxRecordId);
|
|
1160
|
+
return {
|
|
1161
|
+
...binding,
|
|
1162
|
+
acpSessionId: record.acpSessionId,
|
|
1163
|
+
agentSessionId: record.agentSessionId
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async runPersistentPrompt(runDir, state, binding, prompt, timeoutMs) {
|
|
1167
|
+
const capture = createQuietCaptureOutput();
|
|
1168
|
+
const beforeRecord = await resolveSessionRecord(binding.acpxRecordId);
|
|
1169
|
+
let eventStartSeq;
|
|
1170
|
+
let eventEndSeq;
|
|
1171
|
+
const pendingEventWrites = [];
|
|
1172
|
+
const initialClient = this.pendingPersistentSessionClients.get(binding.key);
|
|
1173
|
+
if (initialClient) this.pendingPersistentSessionClients.delete(binding.key);
|
|
1174
|
+
try {
|
|
1175
|
+
await sendSessionDirect({
|
|
1176
|
+
sessionId: binding.acpxRecordId,
|
|
1177
|
+
prompt,
|
|
1178
|
+
resumePolicy: "same-session-only",
|
|
1179
|
+
mcpServers: this.mcpServers,
|
|
1180
|
+
permissionMode: this.permissionMode,
|
|
1181
|
+
nonInteractivePermissions: this.nonInteractivePermissions,
|
|
1182
|
+
authCredentials: this.authCredentials,
|
|
1183
|
+
authPolicy: this.authPolicy,
|
|
1184
|
+
outputFormatter: capture.formatter,
|
|
1185
|
+
onAcpMessage: (direction, message) => {
|
|
1186
|
+
const pending = this.store.appendSessionEvent(runDir, binding, direction, message).then((seq) => {
|
|
1187
|
+
eventStartSeq = eventStartSeq === void 0 ? seq : Math.min(eventStartSeq, seq);
|
|
1188
|
+
eventEndSeq = eventEndSeq === void 0 ? seq : Math.max(eventEndSeq, seq);
|
|
1189
|
+
});
|
|
1190
|
+
pendingEventWrites.push(pending);
|
|
1191
|
+
},
|
|
1192
|
+
suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
|
|
1193
|
+
timeoutMs,
|
|
1194
|
+
verbose: this.verbose,
|
|
1195
|
+
client: initialClient
|
|
1196
|
+
});
|
|
1197
|
+
await Promise.all(pendingEventWrites);
|
|
1198
|
+
const sessionInfo = await this.refreshSessionBinding(binding);
|
|
1199
|
+
state.sessionBindings[sessionInfo.key] = sessionInfo;
|
|
1200
|
+
await this.store.ensureSessionBundle(runDir, state, sessionInfo);
|
|
1201
|
+
const afterRecord = await resolveSessionRecord(sessionInfo.acpxRecordId);
|
|
1202
|
+
await this.store.writeSessionRecord(runDir, state, sessionInfo, afterRecord);
|
|
1203
|
+
const messageStartResolved = findConversationDeltaStart(beforeRecord.messages, afterRecord.messages);
|
|
1204
|
+
return {
|
|
1205
|
+
rawText: capture.read(),
|
|
1206
|
+
sessionInfo,
|
|
1207
|
+
conversation: {
|
|
1208
|
+
sessionId: sessionInfo.bundleId,
|
|
1209
|
+
messageStart: messageStartResolved,
|
|
1210
|
+
messageEnd: Math.max(messageStartResolved, afterRecord.messages.length - 1),
|
|
1211
|
+
eventStartSeq: eventStartSeq ?? (() => {
|
|
1212
|
+
throw new Error(`Missing ACP event capture for session ${sessionInfo.bundleId}`);
|
|
1213
|
+
})(),
|
|
1214
|
+
eventEndSeq: eventEndSeq ?? (() => {
|
|
1215
|
+
throw new Error(`Missing ACP event capture for session ${sessionInfo.bundleId}`);
|
|
1216
|
+
})()
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
} finally {
|
|
1220
|
+
if (initialClient) await initialClient.close().catch(() => {});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async closePendingPersistentSessionClients() {
|
|
1224
|
+
const pendingClients = [...this.pendingPersistentSessionClients.values()];
|
|
1225
|
+
this.pendingPersistentSessionClients.clear();
|
|
1226
|
+
await Promise.all(pendingClients.map(async (client) => {
|
|
1227
|
+
await client.close().catch(() => {});
|
|
1228
|
+
}));
|
|
1229
|
+
}
|
|
1230
|
+
async runIsolatedPrompt(runDir, state, binding, agent, prompt, timeoutMs) {
|
|
1231
|
+
const capture = createQuietCaptureOutput();
|
|
1232
|
+
const conversation = createSessionConversation(state.currentNodeStartedAt ?? isoNow());
|
|
1233
|
+
let acpxState;
|
|
1234
|
+
recordPromptSubmission(conversation, prompt, state.currentNodeStartedAt ?? isoNow());
|
|
1235
|
+
let eventStartSeq;
|
|
1236
|
+
let eventEndSeq;
|
|
1237
|
+
const pendingEventWrites = [];
|
|
1238
|
+
const result = await runOnce({
|
|
1239
|
+
agentCommand: agent.agentCommand,
|
|
1240
|
+
cwd: agent.cwd,
|
|
1241
|
+
prompt,
|
|
1242
|
+
mcpServers: this.mcpServers,
|
|
1243
|
+
permissionMode: this.permissionMode,
|
|
1244
|
+
nonInteractivePermissions: this.nonInteractivePermissions,
|
|
1245
|
+
authCredentials: this.authCredentials,
|
|
1246
|
+
authPolicy: this.authPolicy,
|
|
1247
|
+
outputFormatter: capture.formatter,
|
|
1248
|
+
onAcpMessage: (direction, message) => {
|
|
1249
|
+
const pending = this.store.appendSessionEvent(runDir, binding, direction, message).then((seq) => {
|
|
1250
|
+
eventStartSeq = eventStartSeq === void 0 ? seq : Math.min(eventStartSeq, seq);
|
|
1251
|
+
eventEndSeq = eventEndSeq === void 0 ? seq : Math.max(eventEndSeq, seq);
|
|
1252
|
+
});
|
|
1253
|
+
pendingEventWrites.push(pending);
|
|
1254
|
+
},
|
|
1255
|
+
onSessionUpdate: (notification) => {
|
|
1256
|
+
acpxState = recordSessionUpdate(conversation, acpxState, notification);
|
|
1257
|
+
},
|
|
1258
|
+
onClientOperation: (operation) => {
|
|
1259
|
+
acpxState = recordClientOperation(conversation, acpxState, operation);
|
|
1260
|
+
},
|
|
1261
|
+
suppressSdkConsoleErrors: this.suppressSdkConsoleErrors,
|
|
1262
|
+
timeoutMs,
|
|
1263
|
+
verbose: this.verbose,
|
|
1264
|
+
sessionOptions: this.sessionOptions
|
|
1265
|
+
});
|
|
1266
|
+
await Promise.all(pendingEventWrites);
|
|
1267
|
+
const sessionInfo = {
|
|
1268
|
+
...binding,
|
|
1269
|
+
acpxRecordId: result.sessionId,
|
|
1270
|
+
acpSessionId: result.sessionId
|
|
1271
|
+
};
|
|
1272
|
+
await this.store.ensureSessionBundle(runDir, state, sessionInfo);
|
|
1273
|
+
const syntheticRecord = createSyntheticSessionRecord({
|
|
1274
|
+
binding: sessionInfo,
|
|
1275
|
+
createdAt: state.currentNodeStartedAt ?? isoNow(),
|
|
1276
|
+
updatedAt: conversation.updated_at,
|
|
1277
|
+
conversation,
|
|
1278
|
+
acpxState: cloneSessionAcpxState(acpxState),
|
|
1279
|
+
lastSeq: eventEndSeq ?? 0
|
|
1280
|
+
});
|
|
1281
|
+
await this.store.writeSessionRecord(runDir, state, sessionInfo, syntheticRecord);
|
|
1282
|
+
return {
|
|
1283
|
+
rawText: capture.read(),
|
|
1284
|
+
sessionInfo,
|
|
1285
|
+
conversation: {
|
|
1286
|
+
sessionId: sessionInfo.bundleId,
|
|
1287
|
+
messageStart: 0,
|
|
1288
|
+
messageEnd: Math.max(0, conversation.messages.length - 1),
|
|
1289
|
+
eventStartSeq: eventStartSeq ?? (() => {
|
|
1290
|
+
throw new Error(`Missing ACP event capture for session ${sessionInfo.bundleId}`);
|
|
1291
|
+
})(),
|
|
1292
|
+
eventEndSeq: eventEndSeq ?? (() => {
|
|
1293
|
+
throw new Error(`Missing ACP event capture for session ${sessionInfo.bundleId}`);
|
|
1294
|
+
})()
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
function normalizePromptInput(prompt) {
|
|
1300
|
+
return typeof prompt === "string" ? textPrompt(prompt) : prompt;
|
|
1301
|
+
}
|
|
1302
|
+
async function resolveNodeCwd(defaultCwd, cwd, context) {
|
|
1303
|
+
if (typeof cwd === "function") {
|
|
1304
|
+
const resolved = await cwd(context) ?? defaultCwd;
|
|
1305
|
+
return path.resolve(defaultCwd, resolved);
|
|
1306
|
+
}
|
|
1307
|
+
return path.resolve(defaultCwd, cwd ?? defaultCwd);
|
|
1308
|
+
}
|
|
1309
|
+
function resolveShellActionCwd(defaultCwd, cwd) {
|
|
1310
|
+
return path.resolve(defaultCwd, cwd ?? defaultCwd);
|
|
1311
|
+
}
|
|
1312
|
+
function summarizePrompt(promptText, explicitDetail) {
|
|
1313
|
+
if (explicitDetail) return explicitDetail;
|
|
1314
|
+
const line = promptText.split("\n").map((candidate) => candidate.trim()).find((candidate) => candidate.length > 0);
|
|
1315
|
+
if (!line) return "Running ACP prompt";
|
|
1316
|
+
return `ACP: ${line.length > 120 ? `${line.slice(0, 117)}...` : line}`;
|
|
1317
|
+
}
|
|
1318
|
+
function createQuietCaptureOutput() {
|
|
1319
|
+
const chunks = [];
|
|
1320
|
+
return {
|
|
1321
|
+
formatter: createOutputFormatter("quiet", { stdout: { write(chunk) {
|
|
1322
|
+
chunks.push(chunk);
|
|
1323
|
+
} } }),
|
|
1324
|
+
read: () => chunks.join("").trim()
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
async function resolveFlowRunTitle(flow, input, flowPath) {
|
|
1328
|
+
const titleDefinition = flow.run?.title;
|
|
1329
|
+
if (titleDefinition === void 0) return;
|
|
1330
|
+
return normalizeFlowRunTitle(typeof titleDefinition === "function" ? await Promise.resolve(titleDefinition({
|
|
1331
|
+
input,
|
|
1332
|
+
flowName: flow.name,
|
|
1333
|
+
flowPath
|
|
1334
|
+
})) : titleDefinition);
|
|
1335
|
+
}
|
|
1336
|
+
function normalizeFlowRunTitle(value) {
|
|
1337
|
+
const trimmed = value?.trim();
|
|
1338
|
+
return trimmed ? trimmed : void 0;
|
|
1339
|
+
}
|
|
1340
|
+
function createRunId(flowName) {
|
|
1341
|
+
return `${isoNow().replaceAll(":", "").replaceAll(".", "")}-${flowName.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}-${randomUUID().slice(0, 8)}`;
|
|
1342
|
+
}
|
|
1343
|
+
function createSessionBindingKey(agentCommand, cwd, handle) {
|
|
1344
|
+
return `${agentCommand}::${cwd}::${handle}`;
|
|
1345
|
+
}
|
|
1346
|
+
function createSessionName(flowName, handle, cwd, runId) {
|
|
1347
|
+
return `${flowName}-${handle}-${stableShortHash(cwd)}-${runId.slice(-8)}`;
|
|
1348
|
+
}
|
|
1349
|
+
function createSessionBundleId(handle, key) {
|
|
1350
|
+
return `${handle.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "session"}-${stableShortHash(key)}`;
|
|
1351
|
+
}
|
|
1352
|
+
function createIsolatedSessionBinding(flowName, runId, attemptId, profile, agent) {
|
|
1353
|
+
const key = `isolated::${attemptId}`;
|
|
1354
|
+
const handle = "isolated";
|
|
1355
|
+
return {
|
|
1356
|
+
key,
|
|
1357
|
+
handle,
|
|
1358
|
+
bundleId: createSessionBundleId(`${handle}-${attemptId}`, `${key}::${agent.cwd}`),
|
|
1359
|
+
name: `${flowName}-${attemptId}-${runId.slice(-8)}`,
|
|
1360
|
+
profile,
|
|
1361
|
+
agentName: agent.agentName,
|
|
1362
|
+
agentCommand: agent.agentCommand,
|
|
1363
|
+
cwd: agent.cwd,
|
|
1364
|
+
acpxRecordId: key,
|
|
1365
|
+
acpSessionId: key
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function createSyntheticSessionRecord(options) {
|
|
1369
|
+
return {
|
|
1370
|
+
schema: SESSION_RECORD_SCHEMA,
|
|
1371
|
+
acpxRecordId: options.binding.acpxRecordId,
|
|
1372
|
+
acpSessionId: options.binding.acpSessionId,
|
|
1373
|
+
agentSessionId: options.binding.agentSessionId,
|
|
1374
|
+
agentCommand: options.binding.agentCommand,
|
|
1375
|
+
cwd: options.binding.cwd,
|
|
1376
|
+
name: options.binding.name,
|
|
1377
|
+
createdAt: options.createdAt,
|
|
1378
|
+
lastUsedAt: options.updatedAt,
|
|
1379
|
+
lastSeq: options.lastSeq,
|
|
1380
|
+
lastRequestId: void 0,
|
|
1381
|
+
eventLog: defaultSessionEventLog(options.binding.acpxRecordId),
|
|
1382
|
+
closed: true,
|
|
1383
|
+
closedAt: options.updatedAt,
|
|
1384
|
+
title: options.conversation.title,
|
|
1385
|
+
messages: options.conversation.messages,
|
|
1386
|
+
updated_at: options.conversation.updated_at,
|
|
1387
|
+
cumulative_token_usage: options.conversation.cumulative_token_usage,
|
|
1388
|
+
request_token_usage: options.conversation.request_token_usage,
|
|
1389
|
+
acpx: options.acpxState
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function createNodeResult(options) {
|
|
1393
|
+
return {
|
|
1394
|
+
attemptId: options.attemptId,
|
|
1395
|
+
nodeId: options.nodeId,
|
|
1396
|
+
nodeType: options.nodeType,
|
|
1397
|
+
outcome: options.outcome,
|
|
1398
|
+
startedAt: options.startedAt,
|
|
1399
|
+
finishedAt: options.finishedAt,
|
|
1400
|
+
durationMs: new Date(options.finishedAt).getTime() - new Date(options.startedAt).getTime(),
|
|
1401
|
+
output: options.output,
|
|
1402
|
+
error: options.error
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function outcomeForError(error) {
|
|
1406
|
+
if (error instanceof TimeoutError) return "timed_out";
|
|
1407
|
+
if (error instanceof InterruptedError) return "cancelled";
|
|
1408
|
+
return "failed";
|
|
1409
|
+
}
|
|
1410
|
+
function stableShortHash(value) {
|
|
1411
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 8);
|
|
1412
|
+
}
|
|
1413
|
+
function nextAttemptId(attemptCounts, nodeId) {
|
|
1414
|
+
const next = (attemptCounts.get(nodeId) ?? 0) + 1;
|
|
1415
|
+
attemptCounts.set(nodeId, next);
|
|
1416
|
+
return `${nodeId}#${next}`;
|
|
1417
|
+
}
|
|
1418
|
+
function createNodeOutcomePayload(result, trace) {
|
|
1419
|
+
return {
|
|
1420
|
+
nodeType: result.nodeType,
|
|
1421
|
+
outcome: result.outcome,
|
|
1422
|
+
durationMs: result.durationMs,
|
|
1423
|
+
error: result.error ?? null,
|
|
1424
|
+
...trace
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function attachStepTrace(error, trace) {
|
|
1428
|
+
const attached = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
|
|
1429
|
+
attached.flowStepTrace = trace;
|
|
1430
|
+
return attached;
|
|
1431
|
+
}
|
|
1432
|
+
function extractAttachedStepTrace(error) {
|
|
1433
|
+
if (!(error instanceof Error)) return;
|
|
1434
|
+
return error.flowStepTrace;
|
|
1435
|
+
}
|
|
1436
|
+
function toInlineOutput(value) {
|
|
1437
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
1438
|
+
if (typeof value === "string") return value.length <= 200 && !value.includes("\n") ? value : void 0;
|
|
1439
|
+
try {
|
|
1440
|
+
const serialized = JSON.stringify(value);
|
|
1441
|
+
if (serialized.length <= 200 && !serialized.includes("\n")) return value;
|
|
1442
|
+
} catch {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function outputArtifactMediaType(value) {
|
|
1447
|
+
return typeof value === "string" ? "text/plain" : "application/json";
|
|
1448
|
+
}
|
|
1449
|
+
function outputArtifactExtension(value) {
|
|
1450
|
+
return typeof value === "string" ? "txt" : "json";
|
|
1451
|
+
}
|
|
1452
|
+
function findConversationDeltaStart(before, after) {
|
|
1453
|
+
const maxOverlap = Math.min(before.length, after.length);
|
|
1454
|
+
for (let overlap = maxOverlap; overlap >= 0; overlap -= 1) {
|
|
1455
|
+
let matches = true;
|
|
1456
|
+
for (let index = 0; index < overlap; index += 1) {
|
|
1457
|
+
const beforeMessage = before[before.length - overlap + index];
|
|
1458
|
+
const afterMessage = after[index];
|
|
1459
|
+
if (!deepEqualJson(beforeMessage, afterMessage)) {
|
|
1460
|
+
matches = false;
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (matches) return overlap;
|
|
1465
|
+
}
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
function deepEqualJson(left, right) {
|
|
1469
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1470
|
+
}
|
|
1471
|
+
function isoNow() {
|
|
1472
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1473
|
+
}
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region src/flows/json.ts
|
|
1476
|
+
function parseJsonObject(text, options = {}) {
|
|
1477
|
+
const trimmed = String(text ?? "").trim();
|
|
1478
|
+
if (!trimmed) throw new Error("Expected JSON output, got empty text");
|
|
1479
|
+
const mode = options.mode ?? "compat";
|
|
1480
|
+
const direct = tryParse(trimmed);
|
|
1481
|
+
if (direct.ok) return direct.value;
|
|
1482
|
+
if (mode === "fenced" || mode === "compat") {
|
|
1483
|
+
const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1484
|
+
if (fencedMatch) {
|
|
1485
|
+
const fenced = tryParse(fencedMatch[1].trim());
|
|
1486
|
+
if (fenced.ok) return fenced.value;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (mode === "compat") for (const candidate of extractBalancedJsonCandidates(trimmed)) {
|
|
1490
|
+
const parsed = tryParse(candidate);
|
|
1491
|
+
if (parsed.ok) return parsed.value;
|
|
1492
|
+
}
|
|
1493
|
+
throw new Error(`Could not parse JSON from assistant output:\n${trimmed}`);
|
|
1494
|
+
}
|
|
1495
|
+
function parseStrictJsonObject(text) {
|
|
1496
|
+
return parseJsonObject(text, { mode: "strict" });
|
|
1497
|
+
}
|
|
1498
|
+
function extractJsonObject(text) {
|
|
1499
|
+
return parseJsonObject(text, { mode: "compat" });
|
|
1500
|
+
}
|
|
1501
|
+
function tryParse(text) {
|
|
1502
|
+
try {
|
|
1503
|
+
return {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
value: JSON.parse(text)
|
|
1506
|
+
};
|
|
1507
|
+
} catch {
|
|
1508
|
+
return { ok: false };
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
function extractBalancedJsonCandidates(text) {
|
|
1512
|
+
const candidates = [];
|
|
1513
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1514
|
+
if (text[index] !== "{" && text[index] !== "[") continue;
|
|
1515
|
+
const result = scanBalanced(text, index);
|
|
1516
|
+
if (result) candidates.push(result);
|
|
1517
|
+
}
|
|
1518
|
+
return candidates;
|
|
1519
|
+
}
|
|
1520
|
+
function scanBalanced(text, startIndex) {
|
|
1521
|
+
const stack = [];
|
|
1522
|
+
let inString = false;
|
|
1523
|
+
let escaped = false;
|
|
1524
|
+
for (let index = startIndex; index < text.length; index += 1) {
|
|
1525
|
+
const char = text[index];
|
|
1526
|
+
if (inString) {
|
|
1527
|
+
if (escaped) escaped = false;
|
|
1528
|
+
else if (char === "\\") escaped = true;
|
|
1529
|
+
else if (char === "\"") inString = false;
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
if (char === "\"") {
|
|
1533
|
+
inString = true;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (char === "{" || char === "[") {
|
|
1537
|
+
stack.push(char);
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (char !== "}" && char !== "]") continue;
|
|
1541
|
+
const last = stack.at(-1);
|
|
1542
|
+
if (last === "{" && char !== "}" || last === "[" && char !== "]") return null;
|
|
1543
|
+
stack.pop();
|
|
1544
|
+
if (stack.length === 0) return text.slice(startIndex, index + 1);
|
|
1545
|
+
}
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1549
|
+
export { flowRunsBaseDir as a, checkpoint as c, shell as d, FlowRunner as i, compute as l, parseJsonObject as n, acp as o, parseStrictJsonObject as r, action as s, extractJsonObject as t, defineFlow as u };
|
|
1550
|
+
|
|
1551
|
+
//# sourceMappingURL=flows-DnIYoHI1.js.map
|