@agent-compose/sdk 0.5.1 → 0.5.2
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/dist/active-step.d.ts +60 -0
- package/dist/agent/agent-loop-steer.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +46 -0
- package/dist/agent/async-queue.d.ts +29 -0
- package/dist/agent/protocol.d.ts +9 -1
- package/dist/agent/resolve-agent-id.test.d.ts +1 -0
- package/dist/agent/run-agent.d.ts +16 -3
- package/dist/agent/steer-control.d.ts +57 -0
- package/dist/agent/steer-control.test.d.ts +1 -0
- package/dist/client.d.ts +161 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1339 -157
- package/dist/pause/__tests__/agent-loop-checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/errors.test.d.ts +1 -0
- package/dist/pause/__tests__/manager.test.d.ts +1 -0
- package/dist/pause/__tests__/pause-core.test.d.ts +1 -0
- package/dist/pause/__tests__/state-dir.test.d.ts +1 -0
- package/dist/pause/__tests__/wrappers.test.d.ts +1 -0
- package/dist/pause/checkpoint.d.ts +28 -0
- package/dist/pause/errors.d.ts +52 -0
- package/dist/pause/manager.d.ts +63 -0
- package/dist/pause/pause-core.d.ts +101 -0
- package/dist/pause/state-dir.d.ts +80 -0
- package/dist/pause/wrappers.d.ts +41 -0
- package/dist/request-context/request-context.d.ts +12 -0
- package/dist/runtimes/claude.d.ts +6 -0
- package/dist/runtimes/openai-desktop.d.ts +2 -0
- package/dist/runtimes/openai-desktop.js +1336 -156
- package/dist/runtimes/vercel.d.ts +12 -0
- package/dist/runtimes/vercel.js +50 -7
- package/dist/runtimes/vercel.test.d.ts +1 -0
- package/dist/sse.d.ts +2 -3
- package/dist/step-invocation/index.d.ts +2 -2
- package/dist/step-invocation/invoker.d.ts +3 -0
- package/dist/step-invocation/protocol.d.ts +12 -0
- package/dist/step-invocation/server.d.ts +1 -0
- package/dist/step-invocation/types.d.ts +40 -5
- package/dist/types/events.d.ts +9 -0
- package/dist/types/execution-context.d.ts +25 -0
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/runtime.d.ts +55 -0
- package/dist/types/sandbox.d.ts +4 -4
- package/dist/utils/schemas.d.ts +2 -0
- package/dist/workflow-steps/__tests__/pause-wiring.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +2 -0
- package/dist/workflow-steps/observability.d.ts +43 -11
- package/dist/workflow-steps/run-callback.d.ts +39 -0
- package/dist/workflow-steps/runner.d.ts +8 -0
- package/package.json +1 -1
- package/src/active-step.ts +124 -0
- package/src/agent/agent-loop.ts +253 -19
- package/src/agent/async-queue.ts +61 -0
- package/src/agent/protocol.ts +12 -2
- package/src/agent/run-agent.ts +184 -8
- package/src/agent/steer-control.ts +125 -0
- package/src/client.ts +277 -0
- package/src/index.ts +18 -2
- package/src/pause/checkpoint.ts +44 -0
- package/src/pause/errors.ts +70 -0
- package/src/pause/manager.ts +177 -0
- package/src/pause/pause-core.ts +267 -0
- package/src/pause/state-dir.ts +262 -0
- package/src/pause/wrappers.ts +79 -0
- package/src/request-context/request-context.ts +17 -2
- package/src/runtimes/claude.ts +101 -6
- package/src/runtimes/openai-desktop.ts +11 -0
- package/src/runtimes/vercel.ts +26 -0
- package/src/sandbox.ts +39 -20
- package/src/sse.ts +8 -6
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +107 -29
- package/src/step-invocation/protocol.ts +16 -0
- package/src/step-invocation/server.ts +45 -12
- package/src/step-invocation/types.ts +43 -7
- package/src/tools/coding.ts +16 -5
- package/src/types/events.ts +9 -0
- package/src/types/execution-context.ts +25 -0
- package/src/types/protocol.ts +8 -0
- package/src/types/runtime.ts +52 -0
- package/src/types/sandbox.ts +8 -4
- package/src/types/workflow.ts +6 -1
- package/src/utils/bundler.ts +8 -3
- package/src/utils/schemas.ts +2 -0
- package/src/workflow-steps/index.ts +3 -0
- package/src/workflow-steps/observability.ts +84 -13
- package/src/workflow-steps/run-callback.ts +72 -0
- package/src/workflow-steps/runner.ts +70 -8
- package/dist/utils/discovery.d.ts +0 -2
- package/src/utils/discovery.ts +0 -4
package/src/sandbox.ts
CHANGED
|
@@ -118,9 +118,28 @@ interface SandboxProviderDef {
|
|
|
118
118
|
// ── E2B helpers ───────────────────────────────────────────────────────────────
|
|
119
119
|
|
|
120
120
|
export function makeSandboxProvider(sb: Sandbox | Desktop): SandboxProvider {
|
|
121
|
+
const commands = sb.commands as { run: (cmd: string, opts?: unknown) => Promise<{ exitCode: number; stdout: string; stderr?: string }> };
|
|
121
122
|
return {
|
|
122
123
|
sandboxId: sb.sandboxId,
|
|
123
|
-
commands:
|
|
124
|
+
commands: {
|
|
125
|
+
async run(cmd, opts) {
|
|
126
|
+
let stderr = "";
|
|
127
|
+
const result = await commands.run(cmd, {
|
|
128
|
+
...opts,
|
|
129
|
+
onStderr: (chunk: string) => {
|
|
130
|
+
stderr += chunk;
|
|
131
|
+
opts?.onStderr?.(chunk);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
exitCode: result.exitCode,
|
|
136
|
+
stdout: result.stdout,
|
|
137
|
+
stderr: typeof (result as { stderr?: unknown }).stderr === "string"
|
|
138
|
+
? (result as { stderr: string }).stderr
|
|
139
|
+
: stderr,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
},
|
|
124
143
|
files: {
|
|
125
144
|
async write(path, content) { await sb.files.write(path, content); },
|
|
126
145
|
},
|
|
@@ -161,7 +180,7 @@ export async function parseSseExecStream(
|
|
|
161
180
|
body: ReadableStream<Uint8Array>,
|
|
162
181
|
opts?: ParseSseExecStreamOptions,
|
|
163
182
|
): Promise<SandboxCommandResult> {
|
|
164
|
-
let stdout = "", exitCode = 0, exited = false;
|
|
183
|
+
let stdout = "", stderr = "", exitCode = 0, exited = false;
|
|
165
184
|
const reader = body.getReader(), decoder = new TextDecoder();
|
|
166
185
|
let buf = "";
|
|
167
186
|
while (true) {
|
|
@@ -173,9 +192,10 @@ export async function parseSseExecStream(
|
|
|
173
192
|
for (const line of lines) {
|
|
174
193
|
if (!line.startsWith("data: ")) continue;
|
|
175
194
|
let event: SseEvent;
|
|
176
|
-
try { event = JSON.parse(line.slice(6)) as SseEvent; }
|
|
195
|
+
try { event = JSON.parse(line.slice(6)) as SseEvent; }
|
|
196
|
+
catch (err) { throw new Error(`Malformed sandbox exec event: ${err instanceof Error ? err.message : String(err)}`); }
|
|
177
197
|
if (event.type === "stdout") { stdout += event.data ?? ""; opts?.onStdout?.(event.data ?? ""); }
|
|
178
|
-
else if (event.type === "stderr") { opts?.onStderr?.(event.data ?? ""); }
|
|
198
|
+
else if (event.type === "stderr") { stderr += event.data ?? ""; opts?.onStderr?.(event.data ?? ""); }
|
|
179
199
|
else if (event.type === "exit") { exitCode = event.exitCode ?? 0; exited = true; }
|
|
180
200
|
else if (event.type === "error") { throw new Error(`Sandbox exec error: ${event.data}`); }
|
|
181
201
|
}
|
|
@@ -183,7 +203,8 @@ export async function parseSseExecStream(
|
|
|
183
203
|
// QUIC tunnels can delay the stream-end signal even after all data has arrived.
|
|
184
204
|
if (exited) break;
|
|
185
205
|
}
|
|
186
|
-
|
|
206
|
+
if (!exited) throw new Error("Sandbox exec stream ended without an exit event");
|
|
207
|
+
return { exitCode, stdout, stderr };
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
// ── Vercel helpers ────────────────────────────────────────────────────────────
|
|
@@ -200,31 +221,28 @@ function makeVercelSandboxProvider(sb: any, globalEnvs?: Record<string, string>)
|
|
|
200
221
|
sandboxId: sb.sandboxId,
|
|
201
222
|
commands: {
|
|
202
223
|
async run(cmd, opts) {
|
|
203
|
-
// Vercel's runCommand takes a native `sudo` flag — pass it on the `sh`
|
|
204
|
-
// invocation so the whole shell (and every command it spawns) runs as
|
|
205
|
-
// root, matching `sb.runCommand({ cmd, args, sudo: true })` semantics.
|
|
206
|
-
const sudo = opts?.sudo === true;
|
|
207
|
-
if (opts?.background) {
|
|
208
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
209
|
-
void (sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts.cwd, env: mergeEnvs(opts.envs), detached: true, ...(sudo ? { sudo: true } : {}) }) as Promise<any>)
|
|
210
|
-
.catch((err: unknown) => console.error(`[sandbox] background command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
211
|
-
return { exitCode: 0, stdout: "" };
|
|
212
|
-
}
|
|
213
224
|
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
214
225
|
let stdout = "";
|
|
226
|
+
let stderr = "";
|
|
227
|
+
// `sudo: true` is Vercel's native root flag — applied to the `sh`
|
|
228
|
+
// invocation so the whole shell (and its children) runs as root.
|
|
215
229
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
const handle: any = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...(sudo ? { sudo: true } : {}) });
|
|
230
|
+
const handle: any = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...(opts?.sudo ? { sudo: true } : {}) });
|
|
217
231
|
// Reconnect to the already-running command on transient stream failures (e.g. BrotliDecompressionError).
|
|
232
|
+
// `h.logs()` replays from the start on reconnect, so reset accumulators per
|
|
233
|
+
// attempt to avoid double-counting. The streaming callbacks may still fire
|
|
234
|
+
// for duplicate chunks during retries — an acceptable tradeoff for resilience.
|
|
218
235
|
await pRetry(async (attempt) => {
|
|
219
236
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
220
237
|
const h: any = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
238
|
+
if (attempt > 1) { stdout = ""; stderr = ""; }
|
|
221
239
|
for await (const log of h.logs()) {
|
|
222
240
|
if (log.stream === "stdout") { stdout += log.data; opts?.onStdout?.(log.data); }
|
|
223
|
-
else { opts?.onStderr?.(log.data); }
|
|
241
|
+
else { stderr += log.data; opts?.onStderr?.(log.data); }
|
|
224
242
|
}
|
|
225
243
|
}, { retries: 3, minTimeout: 1_000, factor: 2 });
|
|
226
244
|
const finished = await handle.wait();
|
|
227
|
-
return { exitCode: finished.exitCode, stdout };
|
|
245
|
+
return { exitCode: finished.exitCode, stdout, stderr };
|
|
228
246
|
},
|
|
229
247
|
},
|
|
230
248
|
files: {
|
|
@@ -277,12 +295,13 @@ export function makeLocalSandboxProvider(): SandboxProvider {
|
|
|
277
295
|
stdio: ["ignore", "pipe", "pipe"],
|
|
278
296
|
});
|
|
279
297
|
let stdout = "";
|
|
298
|
+
let stderr = "";
|
|
280
299
|
proc.stdout?.setEncoding("utf8");
|
|
281
300
|
proc.stderr?.setEncoding("utf8");
|
|
282
301
|
proc.stdout?.on("data", (chunk: string) => { stdout += chunk; opts?.onStdout?.(chunk); });
|
|
283
|
-
proc.stderr?.on("data", (chunk: string) => { opts?.onStderr?.(chunk); });
|
|
302
|
+
proc.stderr?.on("data", (chunk: string) => { stderr += chunk; opts?.onStderr?.(chunk); });
|
|
284
303
|
proc.on("error", reject);
|
|
285
|
-
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout }));
|
|
304
|
+
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
|
|
286
305
|
});
|
|
287
306
|
},
|
|
288
307
|
},
|
package/src/sse.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* - `event`: event name (from `event:` line; `""` if absent)
|
|
8
8
|
* - `data`: parsed JSON payload (the SDK's stream events are always JSON)
|
|
9
9
|
*
|
|
10
|
-
* Malformed payloads
|
|
11
|
-
*
|
|
12
|
-
* responsible for breaking on terminal events.
|
|
10
|
+
* Malformed payloads throw. A dropped terminal event is worse than a loud
|
|
11
|
+
* protocol error for log consumers.
|
|
13
12
|
*
|
|
14
13
|
* Output type stays loose (`Record<string, unknown>`) on purpose: typed
|
|
15
14
|
* unions like `RunEvent` aren't structurally narrowable from
|
|
@@ -42,10 +41,13 @@ export async function* parseSseStream(
|
|
|
42
41
|
if (line.startsWith("event:")) { event = line.slice(6).trim(); continue; }
|
|
43
42
|
if (line.startsWith("data:")) { dataLines.push(line.slice(5).trim()); continue; }
|
|
44
43
|
if (line === "" && dataLines.length > 0) {
|
|
44
|
+
let data: Record<string, unknown>;
|
|
45
45
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
data = JSON.parse(dataLines.join("\n")) as Record<string, unknown>;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(`Malformed SSE JSON payload for event "${event || "message"}": ${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
}
|
|
50
|
+
yield { id: seq, event, data };
|
|
49
51
|
event = ""; seq = 0; dataLines = [];
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
export {
|
|
22
22
|
STEP_RESULT_PREFIX,
|
|
23
|
+
STEP_PAUSE_PREFIX,
|
|
23
24
|
STEP_ENV,
|
|
24
25
|
RUNNER_BUNDLE_PATH,
|
|
25
26
|
RUNNER_COMMAND,
|
|
@@ -30,4 +31,4 @@ export { invokeStep, parseStepResult, buildStepEnvs } from "./invoker.js";
|
|
|
30
31
|
export { serveStep } from "./server.js";
|
|
31
32
|
export type { StepHandler, ServeStepRequest, StepHandlerResult } from "./server.js";
|
|
32
33
|
export { StepExecutionError } from "./types.js";
|
|
33
|
-
export type { StepRequest, StepResult, StepInvocationError } from "./types.js";
|
|
34
|
+
export type { StepRequest, StepResult, StepInvocationError, StepPauseRequest } from "./types.js";
|
|
@@ -22,12 +22,50 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { z } from "zod";
|
|
25
26
|
import type { SandboxProvider } from "../types/sandbox.js";
|
|
26
|
-
import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, requestContextPath, stepInputPath } from "./protocol.js";
|
|
27
|
+
import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, stepPauseLinePrefix, requestContextPath, stepInputPath } from "./protocol.js";
|
|
28
|
+
import { StepPauseRequestSchema } from "./types.js";
|
|
27
29
|
import type { StepRequest, StepResult } from "./types.js";
|
|
30
|
+
import type { StepObservability } from "../workflow-steps/observability.js";
|
|
28
31
|
|
|
29
32
|
export { RUNNER_COMMAND } from "./protocol.js";
|
|
30
33
|
|
|
34
|
+
const StepObservabilitySchema = z.object({
|
|
35
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
36
|
+
events: z.array(z.unknown()).optional(),
|
|
37
|
+
subSteps: z.array(z.object({
|
|
38
|
+
name: z.string(),
|
|
39
|
+
startedAt: z.number(),
|
|
40
|
+
durationMs: z.number(),
|
|
41
|
+
status: z.enum(["completed", "failed"]),
|
|
42
|
+
error: z.string().optional(),
|
|
43
|
+
})).optional(),
|
|
44
|
+
}).passthrough();
|
|
45
|
+
|
|
46
|
+
const StepResultEnvelopeSchema = z.discriminatedUnion("ok", [
|
|
47
|
+
z.object({
|
|
48
|
+
ok: z.literal(true),
|
|
49
|
+
output: z.unknown().optional(),
|
|
50
|
+
observability: StepObservabilitySchema.optional(),
|
|
51
|
+
}),
|
|
52
|
+
z.object({
|
|
53
|
+
ok: z.literal(false),
|
|
54
|
+
kind: z.enum(["protocol", "user-step"]),
|
|
55
|
+
error: z.string().optional(),
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Wire schema for the pause sentinel — what the runner emits on stdout
|
|
60
|
+
* when user code calls `ctx.pause(...)`. The inner `pauseRequest` schema
|
|
61
|
+
* is the canonical `StepPauseRequestSchema` from types.ts so the runtime
|
|
62
|
+
* type (`StepPauseRequest`) and the parsed shape can't drift. */
|
|
63
|
+
const StepPauseEnvelopeSchema = z.object({
|
|
64
|
+
pauseId: z.string().min(1),
|
|
65
|
+
pauseRequest: StepPauseRequestSchema,
|
|
66
|
+
observability: StepObservabilitySchema.optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
31
69
|
/** Build the env map for one step invocation. Pure function; tests use it
|
|
32
70
|
* to assert the env shape (and to assert that no server-held credentials
|
|
33
71
|
* leak in) without spawning a sandbox. */
|
|
@@ -35,6 +73,7 @@ export function buildStepEnvs(args: {
|
|
|
35
73
|
runId: string;
|
|
36
74
|
stepIndex: number;
|
|
37
75
|
resultToken: string;
|
|
76
|
+
isResume?: boolean;
|
|
38
77
|
}): Record<string, string> {
|
|
39
78
|
return {
|
|
40
79
|
[STEP_ENV.RUN_ID]: args.runId,
|
|
@@ -43,6 +82,7 @@ export function buildStepEnvs(args: {
|
|
|
43
82
|
[STEP_ENV.STEP_INPUT_PATH]: stepInputPath(args.stepIndex),
|
|
44
83
|
[STEP_ENV.REQUEST_CONTEXT_PATH]: requestContextPath(args.stepIndex),
|
|
45
84
|
[STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken,
|
|
85
|
+
[STEP_ENV.STEP_RESUME]: args.isResume ? "1" : "0",
|
|
46
86
|
};
|
|
47
87
|
}
|
|
48
88
|
|
|
@@ -59,35 +99,61 @@ export function parseStepResult<TOutput = unknown>(
|
|
|
59
99
|
stdout: string,
|
|
60
100
|
resultToken: string,
|
|
61
101
|
): StepResult<TOutput> | null {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
102
|
+
// Pause sentinel takes precedence: a step that called `ctx.pause(...)`
|
|
103
|
+
// exits without ever emitting a result, but if the runner did emit a
|
|
104
|
+
// partial / stale result in a prior buffer chunk, the pause sentinel
|
|
105
|
+
// is the load-bearing signal. Scan one pass, dispatch by prefix.
|
|
106
|
+
const resultPrefix = stepResultLinePrefix(resultToken);
|
|
107
|
+
const pausePrefix = stepPauseLinePrefix(resultToken);
|
|
108
|
+
let resultLine: string | undefined;
|
|
109
|
+
let pauseLine: string | undefined;
|
|
110
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
111
|
+
if (!pauseLine && line.startsWith(pausePrefix)) pauseLine = line;
|
|
112
|
+
if (!resultLine && line.startsWith(resultPrefix)) resultLine = line;
|
|
113
|
+
if (pauseLine && resultLine) break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (pauseLine) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
|
|
119
|
+
const parsed = StepPauseEnvelopeSchema.safeParse(raw);
|
|
120
|
+
if (!parsed.success) {
|
|
121
|
+
return { ok: false, error: { kind: "protocol", message: `step pause schema validation failed: ${parsed.error.message}` } };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: "paused",
|
|
125
|
+
pauseId: parsed.data.pauseId,
|
|
126
|
+
pauseRequest: parsed.data.pauseRequest,
|
|
127
|
+
...(parsed.data.observability ? { observability: parsed.data.observability as StepObservability } : {}),
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { ok: false, error: { kind: "protocol", message: "step pause JSON parse failed" } };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!resultLine) return null;
|
|
135
|
+
let parsed: z.infer<typeof StepResultEnvelopeSchema>;
|
|
72
136
|
try {
|
|
73
|
-
|
|
137
|
+
const raw = JSON.parse(resultLine.slice(resultPrefix.length));
|
|
138
|
+
const result = StepResultEnvelopeSchema.safeParse(raw);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
const kind = raw && typeof raw === "object" && typeof (raw as { kind?: unknown }).kind === "string"
|
|
141
|
+
? ` (kind=${(raw as { kind: string }).kind})`
|
|
142
|
+
: "";
|
|
143
|
+
return { ok: false, error: { kind: "protocol", message: `step result schema validation failed${kind}: ${result.error.message}` } };
|
|
144
|
+
}
|
|
145
|
+
parsed = result.data;
|
|
74
146
|
} catch {
|
|
75
147
|
return { ok: false, error: { kind: "protocol", message: "step result JSON parse failed" } };
|
|
76
148
|
}
|
|
77
149
|
if (parsed.ok) {
|
|
78
150
|
return parsed.observability
|
|
79
|
-
? { ok: true, output: parsed.output as TOutput, observability: parsed.observability }
|
|
151
|
+
? { ok: true, output: parsed.output as TOutput, observability: parsed.observability as StepObservability }
|
|
80
152
|
: { ok: true, output: parsed.output as TOutput };
|
|
81
153
|
}
|
|
82
|
-
const kind
|
|
154
|
+
const kind = parsed.kind;
|
|
83
155
|
const baseMessage = parsed.error ?? "step body failed without a message";
|
|
84
|
-
|
|
85
|
-
// doesn't recognise — most likely a version-skew deploy. Preserve the
|
|
86
|
-
// original kind string in the message so operators see the actual value
|
|
87
|
-
// instead of silently re-classifying it as "protocol".
|
|
88
|
-
const message = parsed.kind && parsed.kind !== "user-step" && parsed.kind !== "protocol"
|
|
89
|
-
? `(unknown kind "${parsed.kind}") ${baseMessage}`
|
|
90
|
-
: baseMessage;
|
|
156
|
+
const message = baseMessage;
|
|
91
157
|
return { ok: false, error: { kind, message } };
|
|
92
158
|
}
|
|
93
159
|
|
|
@@ -101,6 +167,8 @@ export function parseStepResult<TOutput = unknown>(
|
|
|
101
167
|
*/
|
|
102
168
|
export interface InvokeStepOptions {
|
|
103
169
|
envs?: Record<string, string>;
|
|
170
|
+
/** True when re-entering a step after resolving or expiring a pause. */
|
|
171
|
+
isResume?: boolean;
|
|
104
172
|
/** Live stdout / stderr from the runner subprocess, line-by-line. Called
|
|
105
173
|
* from inside `sandbox.commands.run` as chunks arrive. The sentinel line
|
|
106
174
|
* carrying the protocol result token is filtered out before delivery so
|
|
@@ -131,17 +199,21 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
131
199
|
runId: request.runId,
|
|
132
200
|
stepIndex: request.stepIndex,
|
|
133
201
|
resultToken,
|
|
202
|
+
isResume: opts?.isResume,
|
|
134
203
|
}),
|
|
135
204
|
};
|
|
136
205
|
|
|
137
206
|
// The sandbox emits stdout as raw chunks, not lines. Buffer between
|
|
138
207
|
// emissions so a `console.log` split across two chunks (or a partial
|
|
139
208
|
// trailing line) is delivered to onStdout/onStderr as one logical line.
|
|
140
|
-
//
|
|
141
|
-
// never see protocol bytes in user-log capture.
|
|
142
|
-
//
|
|
143
|
-
// so the filter and the parser can't drift.
|
|
144
|
-
const
|
|
209
|
+
// Sentinel lines (carrying `resultToken`) are filtered out so callers
|
|
210
|
+
// never see protocol bytes in user-log capture. Both the result and
|
|
211
|
+
// pause prefixes are built from the same shared helpers `parseStepResult`
|
|
212
|
+
// uses, so the filter and the parser can't drift apart.
|
|
213
|
+
const resultSentinel = stepResultLinePrefix(resultToken);
|
|
214
|
+
const pauseSentinel = stepPauseLinePrefix(resultToken);
|
|
215
|
+
const isSentinel = (line: string) =>
|
|
216
|
+
line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
|
|
145
217
|
const makeLineSplitter = (sink: ((line: string) => void) | undefined, filterSentinel: boolean) => {
|
|
146
218
|
if (!sink) return { onChunk: undefined, flush: () => {} };
|
|
147
219
|
let buf = "";
|
|
@@ -152,7 +224,7 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
152
224
|
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
153
225
|
const line = buf.slice(0, nl);
|
|
154
226
|
buf = buf.slice(nl + 1);
|
|
155
|
-
if (filterSentinel && line
|
|
227
|
+
if (filterSentinel && isSentinel(line)) continue;
|
|
156
228
|
sink(line);
|
|
157
229
|
}
|
|
158
230
|
},
|
|
@@ -164,7 +236,7 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
164
236
|
if (buf.length === 0) return;
|
|
165
237
|
const line = buf;
|
|
166
238
|
buf = "";
|
|
167
|
-
if (filterSentinel && line
|
|
239
|
+
if (filterSentinel && isSentinel(line)) return;
|
|
168
240
|
sink(line);
|
|
169
241
|
},
|
|
170
242
|
};
|
|
@@ -188,11 +260,17 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
188
260
|
// before emitting" (runner-exit) from "runner exited cleanly but didn't
|
|
189
261
|
// speak the protocol" (protocol) — the exit code is the evidence.
|
|
190
262
|
if (result.exitCode !== 0) {
|
|
263
|
+
const stderrTail = result.stderr.trim().slice(-2000);
|
|
264
|
+
const stdoutTail = result.stdout.trim().slice(-2000);
|
|
265
|
+
const details = [
|
|
266
|
+
stderrTail ? `stderr:\n${stderrTail}` : "",
|
|
267
|
+
stdoutTail ? `stdout:\n${stdoutTail}` : "",
|
|
268
|
+
].filter(Boolean).join("\n");
|
|
191
269
|
return {
|
|
192
270
|
ok: false,
|
|
193
271
|
error: {
|
|
194
272
|
kind: "runner-exit",
|
|
195
|
-
message: `runner subprocess exited ${result.exitCode} before emitting a step result`,
|
|
273
|
+
message: `runner subprocess exited ${result.exitCode} before emitting a step result${details ? `\n${details}` : ""}`,
|
|
196
274
|
exitCode: result.exitCode,
|
|
197
275
|
},
|
|
198
276
|
};
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* with the same prefix but a wrong token is rejected. */
|
|
15
15
|
export const STEP_RESULT_PREFIX = "__AC_STEP_RESULT__";
|
|
16
16
|
|
|
17
|
+
/** Parallel sentinel for pause requests. The runner emits this — instead
|
|
18
|
+
* of (not in addition to) a step result — when user code calls
|
|
19
|
+
* `ctx.pause(...)`. Exits cleanly afterwards so the activity can snapshot
|
|
20
|
+
* the now-frozen sandbox and durably wait via Temporal `condition()`.
|
|
21
|
+
* See ADR-0006 §"How it actually pauses — Temporal-native, end-to-end". */
|
|
22
|
+
export const STEP_PAUSE_PREFIX = "__AC_STEP_PAUSE__";
|
|
23
|
+
|
|
17
24
|
/** Build the line prefix the runner emits and the invoker scans for. The
|
|
18
25
|
* runner-side serveStep writes `<prefix><token>:<json>\n`; the invoker's
|
|
19
26
|
* result parser AND the stdout line splitter both match on this exact
|
|
@@ -23,6 +30,14 @@ export function stepResultLinePrefix(token: string): string {
|
|
|
23
30
|
return `${STEP_RESULT_PREFIX}${token}:`;
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/** Build the pause sentinel line prefix. Same per-invocation token as the
|
|
34
|
+
* result sentinel so the two channels share one secret — a user
|
|
35
|
+
* `console.log` can't forge either without knowing the token, and the
|
|
36
|
+
* invoker can filter both prefixes from captured stdout with one token. */
|
|
37
|
+
export function stepPauseLinePrefix(token: string): string {
|
|
38
|
+
return `${STEP_PAUSE_PREFIX}${token}:`;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/** Sandbox-side path where dispatch writes the compiled runner bundle.
|
|
27
42
|
* Both modes (full-mode `dispatch.ts` and step-mode `invokeStep`) spawn
|
|
28
43
|
* the runner from this path; single source of truth. */
|
|
@@ -41,6 +56,7 @@ export const STEP_ENV = {
|
|
|
41
56
|
STEP_INPUT_PATH: "AC_STEP_INPUT_PATH",
|
|
42
57
|
REQUEST_CONTEXT_PATH: "AC_REQUEST_CONTEXT_PATH",
|
|
43
58
|
STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN",
|
|
59
|
+
STEP_RESUME: "AC_STEP_RESUME",
|
|
44
60
|
} as const;
|
|
45
61
|
|
|
46
62
|
/** Sandbox-side path where the invoker writes the JSON-encoded step input.
|
|
@@ -23,14 +23,17 @@
|
|
|
23
23
|
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
25
|
import type { RequestContextWire } from "../request-context/request-context.js";
|
|
26
|
+
import { RequestContextWireSchema } from "../request-context/request-context.js";
|
|
26
27
|
import type { StepObservability } from "../workflow-steps/observability.js";
|
|
27
|
-
import { STEP_ENV, STEP_RESULT_PREFIX } from "./protocol.js";
|
|
28
|
+
import { STEP_ENV, STEP_PAUSE_PREFIX, STEP_RESULT_PREFIX } from "./protocol.js";
|
|
29
|
+
import { PauseSignal, isPauseSignal } from "../pause/pause-core.js";
|
|
28
30
|
|
|
29
31
|
/** What the handler receives. The serveStep already parsed the input
|
|
30
32
|
* and request context from their JSON files. */
|
|
31
33
|
export interface ServeStepRequest<TInput = unknown> {
|
|
32
34
|
runId: string;
|
|
33
35
|
stepIndex: number;
|
|
36
|
+
isResume: boolean;
|
|
34
37
|
input: TInput;
|
|
35
38
|
requestContext: RequestContextWire;
|
|
36
39
|
}
|
|
@@ -64,9 +67,8 @@ type SentinelPayload =
|
|
|
64
67
|
* immediately after a non-awaited write can drop the data on Linux
|
|
65
68
|
* pipes, leaving the invoker with empty stdout and the runner-exit
|
|
66
69
|
* classification pointing at the wrong thing. */
|
|
67
|
-
function
|
|
70
|
+
function writeSentinelLine(line: string): Promise<void> {
|
|
68
71
|
return new Promise<void>((resolve, reject) => {
|
|
69
|
-
const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}\n`;
|
|
70
72
|
process.stdout.write(line, (err) => {
|
|
71
73
|
if (err) reject(err);
|
|
72
74
|
else resolve();
|
|
@@ -74,6 +76,24 @@ function emitResult(token: string, payload: SentinelPayload): Promise<void> {
|
|
|
74
76
|
});
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function emitResult(token: string, payload: SentinelPayload): Promise<void> {
|
|
80
|
+
return writeSentinelLine(`${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}\n`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Emit the tokenised pause sentinel — `__AC_STEP_PAUSE__<token>:{pauseId,
|
|
84
|
+
* pauseRequest}` — when the handler threw a `PauseSignal` instead of
|
|
85
|
+
* returning. Same per-invocation token + awaited-flush discipline as
|
|
86
|
+
* `emitResult`; the invoker's `parseStepResult` matches this exact framing
|
|
87
|
+
* (and pause takes precedence over any stray result line). */
|
|
88
|
+
function emitPause(token: string, signal: PauseSignal): Promise<void> {
|
|
89
|
+
const body = JSON.stringify({
|
|
90
|
+
pauseId: signal.pauseId,
|
|
91
|
+
pauseRequest: signal.pauseRequest,
|
|
92
|
+
...(signal.observability ? { observability: signal.observability } : {}),
|
|
93
|
+
});
|
|
94
|
+
return writeSentinelLine(`${STEP_PAUSE_PREFIX}${token}:${body}\n`);
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
/** Delete step-invocation transport envs so the user's workflow code
|
|
78
98
|
* (loaded inside the handler) can't read them. Matters most for
|
|
79
99
|
* AC_STEP_RESULT_TOKEN: if it stayed in env, a `console.log` from a dep
|
|
@@ -89,6 +109,7 @@ function scrubProtocolEnvs(): void {
|
|
|
89
109
|
STEP_ENV.STEP_INPUT_PATH,
|
|
90
110
|
STEP_ENV.REQUEST_CONTEXT_PATH,
|
|
91
111
|
STEP_ENV.STEP_RESULT_TOKEN,
|
|
112
|
+
STEP_ENV.STEP_RESUME,
|
|
92
113
|
]) {
|
|
93
114
|
delete process.env[key];
|
|
94
115
|
}
|
|
@@ -129,13 +150,19 @@ export async function serveStep<TInput, TOutput>(
|
|
|
129
150
|
// and re-emitted as a step failure.
|
|
130
151
|
let exitCode = 0;
|
|
131
152
|
let payload!: SentinelPayload;
|
|
153
|
+
// A pause is not a result. When the handler throws `PauseSignal` (user code
|
|
154
|
+
// called `ctx.pause`), emit the pause sentinel — not a result — and exit 0,
|
|
155
|
+
// so the activity snapshots the now-frozen sandbox and waits durably.
|
|
156
|
+
// ADR-0006 §"How it actually pauses".
|
|
157
|
+
let pauseSignal: PauseSignal | undefined;
|
|
132
158
|
|
|
133
|
-
let setupResult: { runId: string; stepIndex: number; input: TInput; requestContext: RequestContextWire } | undefined;
|
|
159
|
+
let setupResult: { runId: string; stepIndex: number; isResume: boolean; input: TInput; requestContext: RequestContextWire } | undefined;
|
|
134
160
|
try {
|
|
135
161
|
const runId = process.env[STEP_ENV.RUN_ID];
|
|
136
162
|
const stepIndexEnv = process.env[STEP_ENV.STEP_INDEX];
|
|
137
163
|
const inputPath = process.env[STEP_ENV.STEP_INPUT_PATH];
|
|
138
164
|
const ctxPath = process.env[STEP_ENV.REQUEST_CONTEXT_PATH];
|
|
165
|
+
const isResume = process.env[STEP_ENV.STEP_RESUME] === "1";
|
|
139
166
|
const stepIndex = stepIndexEnv === undefined ? Number.NaN : Number(stepIndexEnv);
|
|
140
167
|
|
|
141
168
|
if (!runId || !inputPath || !ctxPath || !Number.isFinite(stepIndex)) {
|
|
@@ -150,13 +177,9 @@ export async function serveStep<TInput, TOutput>(
|
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
const input = JSON.parse(readFileSync(inputPath, "utf8")) as TInput;
|
|
153
|
-
|
|
154
|
-
// server-controlled, malformed shape is a server bug, and we want a
|
|
155
|
-
// useful failure reason in `failRun`. Strict validation belongs at
|
|
156
|
-
// the workflow-level RequestContext.parse boundary, not here.
|
|
157
|
-
const requestContext = JSON.parse(readFileSync(ctxPath, "utf8")) as RequestContextWire;
|
|
180
|
+
const requestContext = RequestContextWireSchema.parse(JSON.parse(readFileSync(ctxPath, "utf8")));
|
|
158
181
|
|
|
159
|
-
setupResult = { runId, stepIndex, input, requestContext };
|
|
182
|
+
setupResult = { runId, stepIndex, isResume, input, requestContext };
|
|
160
183
|
} catch (err) {
|
|
161
184
|
exitCode = 1;
|
|
162
185
|
payload = { ok: false, kind: "protocol", error: err instanceof Error ? err.message : String(err) };
|
|
@@ -174,11 +197,21 @@ export async function serveStep<TInput, TOutput>(
|
|
|
174
197
|
? { ok: true, output, observability }
|
|
175
198
|
: { ok: true, output };
|
|
176
199
|
} catch (err) {
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
// PauseSignal is control flow, not a failure — emit a pause, not a
|
|
201
|
+
// user-step error. Checked first so it can't be misclassified.
|
|
202
|
+
if (isPauseSignal(err)) {
|
|
203
|
+
pauseSignal = err;
|
|
204
|
+
} else {
|
|
205
|
+
exitCode = 1;
|
|
206
|
+
payload = { ok: false, kind: "user-step", error: err instanceof Error ? err.message : String(err) };
|
|
207
|
+
}
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
if (pauseSignal) {
|
|
212
|
+
await emitPause(token, pauseSignal);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
182
215
|
await emitResult(token, payload);
|
|
183
216
|
process.exit(exitCode);
|
|
184
217
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the discriminated error union that classifies failure modes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import type { RequestContextWire } from "../request-context/request-context.js";
|
|
7
8
|
import type { StepObservability } from "../workflow-steps/observability.js";
|
|
8
9
|
|
|
@@ -19,15 +20,50 @@ export interface StepRequest<TInput = unknown> {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* Outcome of one step invocation.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Outcome of one step invocation. Three terminal states:
|
|
24
|
+
*
|
|
25
|
+
* - `ok: true` — step body resolved with `output`.
|
|
26
|
+
* - `ok: false` — step body or runner errored; kind classifies why.
|
|
27
|
+
* - `ok: "paused"` — step body called `ctx.pause(...)` and exited cleanly.
|
|
28
|
+
* The activity captures a sandbox snapshot and the workflow waits on
|
|
29
|
+
* Temporal `condition()` until something resumes the pause (HTTP
|
|
30
|
+
* resume route, TTL expiry, cancellation). `pauseRequest` is what the
|
|
31
|
+
* caller passed to `ctx.pause` — the route surfaces it on the
|
|
32
|
+
* pending-pauses dashboard so an operator sees the question being
|
|
33
|
+
* asked. `observability` carries any ctx metadata/sub-step/agent events
|
|
34
|
+
* recorded before the pause unwound. See ADR-0006.
|
|
27
35
|
*/
|
|
28
36
|
export type StepResult<TOutput = unknown> =
|
|
29
|
-
| { ok: true;
|
|
30
|
-
| { ok:
|
|
37
|
+
| { ok: true; output: TOutput; observability?: StepObservability }
|
|
38
|
+
| { ok: "paused"; pauseId: string; pauseRequest: StepPauseRequest; observability?: StepObservability }
|
|
39
|
+
| { ok: false; error: StepInvocationError };
|
|
40
|
+
|
|
41
|
+
/** Wire schema for one pause request. Canonical here — `parseStepResult`
|
|
42
|
+
* imports it for validation, and `StepPauseRequest` is `z.infer`'d from
|
|
43
|
+
* it so the runtime type and the parsed shape can't drift.
|
|
44
|
+
*
|
|
45
|
+
* Loose by design (the inner zod shape stops at orchestration fields
|
|
46
|
+
* the engine needs): the SDK wrapper layer (`requestDecision` /
|
|
47
|
+
* `sleep` / `waitForEvent`) owns its own payload contract, and the
|
|
48
|
+
* engine treats `payload` as opaque. */
|
|
49
|
+
export const StepPauseRequestSchema = z.object({
|
|
50
|
+
/** Short human-readable label; surfaces on the pending-pauses feed. */
|
|
51
|
+
reason: z.string().min(1),
|
|
52
|
+
/** Wrapper discriminator: which SDK helper produced this pause. */
|
|
53
|
+
kind: z.enum(["decision", "sleep", "event", "custom"]),
|
|
54
|
+
/** Caller-supplied payload — opaque to the engine. */
|
|
55
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
56
|
+
/** Pause TTL in milliseconds. Bounded by Temporal's sleep durability. */
|
|
57
|
+
ttlMs: z.number().int().positive().optional(),
|
|
58
|
+
/** Optional second-key resume route — pending pause is unique per
|
|
59
|
+
* (run, correlationKey) so event-driven senders can resume without
|
|
60
|
+
* knowing the pause id. */
|
|
61
|
+
correlationKey: z.string().min(1).optional(),
|
|
62
|
+
/** Skip the per-pause sandbox snapshot. Default behaviour is to
|
|
63
|
+
* snapshot; only the lightweight wrappers (`ctx.sleep`) opt out. */
|
|
64
|
+
snapshot: z.boolean().optional(),
|
|
65
|
+
});
|
|
66
|
+
export type StepPauseRequest = z.infer<typeof StepPauseRequestSchema>;
|
|
31
67
|
|
|
32
68
|
/**
|
|
33
69
|
* Discriminated error union.
|
package/src/tools/coding.ts
CHANGED
|
@@ -10,6 +10,13 @@ function resolvePath(path: string, cwd?: string): string {
|
|
|
10
10
|
return cwd && !isAbsolute(path) ? join(cwd, path) : path;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function formatCommandFailure(command: string, result: { exitCode: number; stdout: string; stderr: string }): string {
|
|
14
|
+
const parts = [`Command failed with exit code ${result.exitCode}: ${command}`];
|
|
15
|
+
if (result.stderr.trim()) parts.push(`stderr:\n${result.stderr}`);
|
|
16
|
+
if (result.stdout.trim()) parts.push(`stdout:\n${result.stdout}`);
|
|
17
|
+
return parts.join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
async function readFile(sandbox: SandboxProvider, path: string, opts?: { offset?: number; limit?: number; cwd?: string }): Promise<string> {
|
|
14
21
|
const script = `
|
|
15
22
|
const fs = require("fs");
|
|
@@ -26,14 +33,18 @@ for (let i = start; i <= end; i++) console.log(String(i) + ": " + lines[i - 1]);
|
|
|
26
33
|
.filter(Boolean)
|
|
27
34
|
.map(q)
|
|
28
35
|
.join(" ");
|
|
29
|
-
const
|
|
30
|
-
|
|
36
|
+
const command = `node -e ${q(script)} ${args}`;
|
|
37
|
+
const result = await sandbox.commands.run(command, { cwd: opts?.cwd, timeoutMs: 30_000 });
|
|
38
|
+
if (result.exitCode !== 0) throw new Error(formatCommandFailure(command, result));
|
|
39
|
+
return result.stdout;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
async function readRawFile(sandbox: SandboxProvider, path: string, cwd?: string): Promise<string> {
|
|
34
43
|
const script = `const fs = require("fs"); process.stdout.write(fs.readFileSync(process.argv[1], "utf8"));`;
|
|
35
|
-
const
|
|
36
|
-
|
|
44
|
+
const command = `node -e ${q(script)} ${q(path)}`;
|
|
45
|
+
const result = await sandbox.commands.run(command, { cwd, timeoutMs: 30_000 });
|
|
46
|
+
if (result.exitCode !== 0) throw new Error(formatCommandFailure(command, result));
|
|
47
|
+
return result.stdout;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export interface CodingTool<TInput extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -118,7 +129,7 @@ export const bashTool: CodingTool<{
|
|
|
118
129
|
cwd: cwd ?? ctx.cwd,
|
|
119
130
|
timeoutMs: timeoutMs ?? 120_000,
|
|
120
131
|
});
|
|
121
|
-
if (result.exitCode !== 0) throw new Error(
|
|
132
|
+
if (result.exitCode !== 0) throw new Error(formatCommandFailure(command, result));
|
|
122
133
|
return result.stdout;
|
|
123
134
|
},
|
|
124
135
|
};
|