@agent-compose/sdk 0.5.8 → 0.6.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/dist/agent/agent-context.d.ts +1 -1
- package/dist/agent/agent-loop.d.ts +14 -12
- package/dist/agent/pause-client.d.ts +50 -0
- package/dist/agent/pause-client.test.d.ts +1 -0
- package/dist/agent/steer-control.d.ts +22 -6
- package/dist/client.d.ts +12 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +2379 -1463
- package/dist/pause/checkpoint.d.ts +27 -10
- package/dist/pause/manager.d.ts +1 -0
- package/dist/pause/pause-core.d.ts +23 -0
- package/dist/pause/state-dir.d.ts +1 -1
- package/dist/processors/builtins.d.ts +20 -1
- package/dist/processors/gate-pause.d.ts +46 -0
- package/dist/processors/gate-pause.test.d.ts +1 -0
- package/dist/processors/index.d.ts +3 -1
- package/dist/processors/processor.d.ts +13 -0
- package/dist/runtimes/_acp-client.d.ts +140 -0
- package/dist/runtimes/_cli-agent.d.ts +155 -3
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/cli-agent-acp-live.test.d.ts +30 -0
- package/dist/runtimes/cli-agent.test.d.ts +22 -6
- package/dist/runtimes/codex.d.ts +7 -2
- package/dist/runtimes/openai-desktop.js +2365 -1463
- package/dist/runtimes/vercel.js +389 -2
- package/dist/sandbox.d.ts +113 -19
- package/dist/step-invocation/__tests__/background-invoker.test.d.ts +1 -0
- package/dist/step-invocation/index.d.ts +2 -1
- package/dist/step-invocation/invoker.d.ts +36 -0
- package/dist/types/__tests__/environment-build-flag.test.d.ts +1 -0
- package/dist/types/__tests__/workflow-metadata-provider.test.d.ts +1 -0
- package/dist/types/execution-context.d.ts +0 -8
- package/dist/types/protocol.d.ts +32 -1
- package/dist/types/runtime.d.ts +14 -0
- package/dist/types/sandbox-environment.d.ts +6 -1
- package/dist/types/sandbox.d.ts +86 -6
- package/dist/types/workflow-metadata.d.ts +40 -10
- package/dist/types/workflow.d.ts +22 -6
- package/dist/utils/bundler.d.ts +5 -1
- package/dist/workflow-steps/observability.d.ts +28 -2
- package/dist/workflow-steps/types.d.ts +11 -7
- package/dist/workflow-steps/workflow.d.ts +3 -2
- package/package.json +3 -2
- package/src/agent/agent-context.ts +14 -6
- package/src/agent/agent-loop.ts +32 -10
- package/src/agent/pause-client.ts +108 -0
- package/src/agent/run-agent.ts +9 -4
- package/src/agent/steer-control.ts +21 -7
- package/src/client.ts +35 -1
- package/src/index.ts +20 -2
- package/src/pause/checkpoint.ts +33 -14
- package/src/pause/manager.ts +2 -2
- package/src/pause/pause-core.ts +35 -0
- package/src/pause/state-dir.ts +2 -2
- package/src/processors/builtins.ts +44 -1
- package/src/processors/gate-pause.ts +94 -0
- package/src/processors/index.ts +7 -0
- package/src/processors/processor.ts +13 -0
- package/src/runtimes/_acp-client.ts +516 -0
- package/src/runtimes/_cli-agent.ts +416 -3
- package/src/runtimes/claude.ts +31 -3
- package/src/runtimes/codex.ts +21 -1
- package/src/runtimes/vercel.ts +4 -1
- package/src/sandbox.ts +426 -56
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +195 -84
- package/src/types/execution-context.ts +0 -8
- package/src/types/protocol.ts +27 -1
- package/src/types/runtime.ts +14 -0
- package/src/types/sandbox-environment.ts +12 -1
- package/src/types/sandbox.ts +84 -6
- package/src/types/workflow-metadata.ts +42 -10
- package/src/types/workflow.ts +22 -7
- package/src/utils/bundler.ts +6 -1
- package/src/workflow-steps/observability.ts +51 -5
- package/src/workflow-steps/runner.ts +9 -5
- package/src/workflow-steps/types.ts +11 -7
- package/src/workflow-steps/workflow.ts +3 -2
package/dist/runtimes/vercel.js
CHANGED
|
@@ -330,6 +330,391 @@ class RequestContext {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// src/pause/pause-core.ts
|
|
334
|
+
import { createHash } from "node:crypto";
|
|
335
|
+
|
|
336
|
+
// src/step-invocation/types.ts
|
|
337
|
+
import { z as z3 } from "zod";
|
|
338
|
+
var StepPauseRequestSchema = z3.object({
|
|
339
|
+
reason: z3.string().min(1),
|
|
340
|
+
kind: z3.enum(["decision", "sleep", "event", "custom"]),
|
|
341
|
+
payload: z3.record(z3.string(), z3.unknown()).optional(),
|
|
342
|
+
ttlMs: z3.number().int().positive().optional(),
|
|
343
|
+
correlationKey: z3.string().min(1).optional(),
|
|
344
|
+
snapshot: z3.boolean().optional()
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
class StepExecutionError extends Error {
|
|
348
|
+
kind;
|
|
349
|
+
exitCode;
|
|
350
|
+
constructor(error, stepLabel) {
|
|
351
|
+
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
352
|
+
this.name = "StepExecutionError";
|
|
353
|
+
this.kind = error.kind;
|
|
354
|
+
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/active-step.ts
|
|
359
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
360
|
+
var activeStepStorage = new AsyncLocalStorage;
|
|
361
|
+
var fallbackActive = null;
|
|
362
|
+
var BRIDGE_KEY = Symbol.for("agent-compose.activeStep.bridge");
|
|
363
|
+
function bridgeSlot() {
|
|
364
|
+
const g = globalThis;
|
|
365
|
+
return g[BRIDGE_KEY] ??= { state: null };
|
|
366
|
+
}
|
|
367
|
+
function makeState(step) {
|
|
368
|
+
return {
|
|
369
|
+
stepIndex: step.stepIndex,
|
|
370
|
+
nextAgentCallIndex: 0,
|
|
371
|
+
pauseOrdinalByScope: new Map
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function currentState() {
|
|
375
|
+
return activeStepStorage.getStore() ?? bridgeSlot().state ?? fallbackActive;
|
|
376
|
+
}
|
|
377
|
+
async function runWithActiveStep(step, fn) {
|
|
378
|
+
return activeStepStorage.run(makeState(step), fn);
|
|
379
|
+
}
|
|
380
|
+
function setActiveStepBridge(step) {
|
|
381
|
+
const slot = bridgeSlot();
|
|
382
|
+
const prev = { state: slot.state };
|
|
383
|
+
slot.state = step ? makeState(step) : null;
|
|
384
|
+
return prev;
|
|
385
|
+
}
|
|
386
|
+
function restoreActiveStepBridge(prev) {
|
|
387
|
+
bridgeSlot().state = prev.state;
|
|
388
|
+
}
|
|
389
|
+
function getActiveStep() {
|
|
390
|
+
const state = currentState();
|
|
391
|
+
return state ? { stepIndex: state.stepIndex } : null;
|
|
392
|
+
}
|
|
393
|
+
function nextAgentCallInActiveStep() {
|
|
394
|
+
const state = currentState();
|
|
395
|
+
if (!state)
|
|
396
|
+
return null;
|
|
397
|
+
const callIndex = state.nextAgentCallIndex;
|
|
398
|
+
state.nextAgentCallIndex += 1;
|
|
399
|
+
return { stepIndex: state.stepIndex, callIndex };
|
|
400
|
+
}
|
|
401
|
+
function nextPauseOrdinalInActiveStep(scope) {
|
|
402
|
+
const state = currentState();
|
|
403
|
+
if (!state)
|
|
404
|
+
return null;
|
|
405
|
+
const next = state.pauseOrdinalByScope.get(scope) ?? 0;
|
|
406
|
+
state.pauseOrdinalByScope.set(scope, next + 1);
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/pause/state-dir.ts
|
|
411
|
+
import { promises as fs } from "node:fs";
|
|
412
|
+
import { join as join2 } from "node:path";
|
|
413
|
+
import { randomBytes } from "node:crypto";
|
|
414
|
+
function getStateDir() {
|
|
415
|
+
return process.env.AGENT_COMPOSE_STATE_DIR ?? "/tmp/wf/state";
|
|
416
|
+
}
|
|
417
|
+
var checkpointsDir = () => join2(getStateDir(), "checkpoints");
|
|
418
|
+
var pausesDir = () => join2(getStateDir(), "pauses");
|
|
419
|
+
var agentStatePath = (agentInstanceId) => join2(getStateDir(), `agent-${agentInstanceId}.json`);
|
|
420
|
+
var runtimeStatePath = (agentInstanceId) => join2(getStateDir(), `runtime-${agentInstanceId}.json`);
|
|
421
|
+
var checkpointPath = (name) => join2(checkpointsDir(), `${name}.json`);
|
|
422
|
+
var pausePath = (pauseId) => join2(pausesDir(), `${pauseId}.json`);
|
|
423
|
+
var NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
|
|
424
|
+
function assertSafeName(label, name) {
|
|
425
|
+
if (!NAME_RE.test(name)) {
|
|
426
|
+
throw new Error(`${label} name must match ${NAME_RE} (got ${JSON.stringify(name)})`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function assertSafeCheckpointName(name) {
|
|
430
|
+
assertSafeName("checkpoint", name);
|
|
431
|
+
}
|
|
432
|
+
async function ensureDirs() {
|
|
433
|
+
await fs.mkdir(checkpointsDir(), { recursive: true });
|
|
434
|
+
await fs.mkdir(pausesDir(), { recursive: true });
|
|
435
|
+
}
|
|
436
|
+
async function atomicWriteJSON(path, value) {
|
|
437
|
+
await ensureDirs();
|
|
438
|
+
const tmp = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
439
|
+
const payload = JSON.stringify(value);
|
|
440
|
+
if (payload === undefined) {
|
|
441
|
+
throw new Error("pause state values must be JSON-serialisable");
|
|
442
|
+
}
|
|
443
|
+
const handle = await fs.open(tmp, "w");
|
|
444
|
+
try {
|
|
445
|
+
await handle.writeFile(payload, { encoding: "utf8" });
|
|
446
|
+
await handle.sync();
|
|
447
|
+
} finally {
|
|
448
|
+
await handle.close();
|
|
449
|
+
}
|
|
450
|
+
await fs.rename(tmp, path);
|
|
451
|
+
}
|
|
452
|
+
async function readJSON(path) {
|
|
453
|
+
try {
|
|
454
|
+
const raw = await fs.readFile(path, "utf8");
|
|
455
|
+
return JSON.parse(raw);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err.code === "ENOENT")
|
|
458
|
+
return null;
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function readJSONWithPresence(path) {
|
|
463
|
+
try {
|
|
464
|
+
const raw = await fs.readFile(path, "utf8");
|
|
465
|
+
return { found: true, value: JSON.parse(raw) };
|
|
466
|
+
} catch (err) {
|
|
467
|
+
if (err.code === "ENOENT")
|
|
468
|
+
return { found: false };
|
|
469
|
+
throw err;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function readAgentState(agentInstanceId) {
|
|
473
|
+
assertSafeName("agent", agentInstanceId);
|
|
474
|
+
return readJSON(agentStatePath(agentInstanceId));
|
|
475
|
+
}
|
|
476
|
+
async function writeAgentState(agentInstanceId, state) {
|
|
477
|
+
assertSafeName("agent", agentInstanceId);
|
|
478
|
+
await atomicWriteJSON(agentStatePath(agentInstanceId), state);
|
|
479
|
+
}
|
|
480
|
+
async function readRuntimeCheckpoint(agentInstanceId) {
|
|
481
|
+
assertSafeName("runtime", agentInstanceId);
|
|
482
|
+
return readJSON(runtimeStatePath(agentInstanceId));
|
|
483
|
+
}
|
|
484
|
+
async function writeRuntimeCheckpoint(agentInstanceId, blob) {
|
|
485
|
+
assertSafeName("runtime", agentInstanceId);
|
|
486
|
+
await atomicWriteJSON(runtimeStatePath(agentInstanceId), blob);
|
|
487
|
+
}
|
|
488
|
+
async function readCheckpoint(name) {
|
|
489
|
+
assertSafeCheckpointName(name);
|
|
490
|
+
return readJSONWithPresence(checkpointPath(name));
|
|
491
|
+
}
|
|
492
|
+
async function writeCheckpoint(name, value) {
|
|
493
|
+
assertSafeCheckpointName(name);
|
|
494
|
+
await atomicWriteJSON(checkpointPath(name), value);
|
|
495
|
+
}
|
|
496
|
+
function hasOwn(obj, key) {
|
|
497
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
498
|
+
}
|
|
499
|
+
function validatePauseRecord(value, expectedPauseId) {
|
|
500
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
501
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: expected object`);
|
|
502
|
+
}
|
|
503
|
+
const record = value;
|
|
504
|
+
if (record.pauseId !== undefined && record.pauseId !== expectedPauseId) {
|
|
505
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: pauseId mismatch`);
|
|
506
|
+
}
|
|
507
|
+
if (record.resolved === true) {
|
|
508
|
+
if (typeof record.expired !== "boolean") {
|
|
509
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require boolean expired`);
|
|
510
|
+
}
|
|
511
|
+
if (!hasOwn(record, "resumePayload") || record.resumePayload === undefined) {
|
|
512
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require resumePayload`);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
pauseId: expectedPauseId,
|
|
516
|
+
request: hasOwn(record, "request") ? record.request : null,
|
|
517
|
+
...record
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (record.pauseId !== expectedPauseId) {
|
|
521
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require matching pauseId`);
|
|
522
|
+
}
|
|
523
|
+
if (!hasOwn(record, "request") || record.request === undefined) {
|
|
524
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require request`);
|
|
525
|
+
}
|
|
526
|
+
if (record.resolved !== undefined && record.resolved !== false) {
|
|
527
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved must be true, false, or absent`);
|
|
528
|
+
}
|
|
529
|
+
if (hasOwn(record, "expired") || hasOwn(record, "resumePayload")) {
|
|
530
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records must not include resolution fields`);
|
|
531
|
+
}
|
|
532
|
+
return record;
|
|
533
|
+
}
|
|
534
|
+
async function readPauseRecord(pauseId) {
|
|
535
|
+
assertSafeName("pause", pauseId);
|
|
536
|
+
const record = await readJSON(pausePath(pauseId));
|
|
537
|
+
return record === null ? null : validatePauseRecord(record, pauseId);
|
|
538
|
+
}
|
|
539
|
+
async function writePauseRecord(record) {
|
|
540
|
+
assertSafeName("pause", record.pauseId);
|
|
541
|
+
validatePauseRecord(record, record.pauseId);
|
|
542
|
+
await atomicWriteJSON(pausePath(record.pauseId), record);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/pause/errors.ts
|
|
546
|
+
class PauseError extends Error {
|
|
547
|
+
constructor(message) {
|
|
548
|
+
super(message);
|
|
549
|
+
this.name = new.target.name;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class PauseExpiredError extends PauseError {
|
|
554
|
+
code = "pause_expired";
|
|
555
|
+
constructor(message = "pause expired before it was resolved") {
|
|
556
|
+
super(message);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
class PauseSchemaError extends PauseError {
|
|
561
|
+
code = "pause_schema";
|
|
562
|
+
issues;
|
|
563
|
+
constructor(message, issues) {
|
|
564
|
+
super(message);
|
|
565
|
+
this.issues = issues;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
class PauseRequestError extends PauseError {
|
|
570
|
+
code = "pause_request";
|
|
571
|
+
constructor(message = "pause request is invalid") {
|
|
572
|
+
super(message);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/pause/pause-core.ts
|
|
577
|
+
var PAUSE_SIGNAL_BRAND = Symbol.for("agent-compose.PauseSignal");
|
|
578
|
+
|
|
579
|
+
class PauseSignal {
|
|
580
|
+
pauseId;
|
|
581
|
+
pauseRequest;
|
|
582
|
+
observability;
|
|
583
|
+
[PAUSE_SIGNAL_BRAND] = true;
|
|
584
|
+
constructor(pauseId, pauseRequest, observability) {
|
|
585
|
+
this.pauseId = pauseId;
|
|
586
|
+
this.pauseRequest = pauseRequest;
|
|
587
|
+
this.observability = observability;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function isPauseSignal(err) {
|
|
591
|
+
return typeof err === "object" && err !== null && err[PAUSE_SIGNAL_BRAND] === true;
|
|
592
|
+
}
|
|
593
|
+
function boundProcessorPause(boundary, scope) {
|
|
594
|
+
return (req) => {
|
|
595
|
+
if (!boundary) {
|
|
596
|
+
return Promise.reject(new PauseRequestError("ctx.pause is unavailable here: no pause boundary is wired (a processor can " + "only pause inside a sandboxed workflow step, not in a local/unit-test run)"));
|
|
597
|
+
}
|
|
598
|
+
return boundary(req, scope);
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
var PAUSE_NAMESPACE = "5f3e2b8a-0c41-4e9d-9a7b-6d2c1f8e3a40";
|
|
602
|
+
function uuidToBytes(uuid) {
|
|
603
|
+
return Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
604
|
+
}
|
|
605
|
+
function bytesToUuid(b) {
|
|
606
|
+
const h = b.toString("hex");
|
|
607
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
608
|
+
}
|
|
609
|
+
function uuidv5(name, namespace) {
|
|
610
|
+
const hash = createHash("sha1").update(uuidToBytes(namespace)).update(Buffer.from(name, "utf8")).digest();
|
|
611
|
+
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
612
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
613
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
614
|
+
return bytesToUuid(bytes);
|
|
615
|
+
}
|
|
616
|
+
function scopeOf(coord) {
|
|
617
|
+
return coord.agentScope ? `${coord.runId}:${coord.stepIndex}:${coord.agentScope.agentId}:${coord.agentScope.iteration}` : `${coord.runId}:${coord.stepIndex}:bare`;
|
|
618
|
+
}
|
|
619
|
+
var pauseOrdinalByScope = new Map;
|
|
620
|
+
function nextPauseOrdinal(scope) {
|
|
621
|
+
const activeOrdinal = nextPauseOrdinalInActiveStep(scope);
|
|
622
|
+
if (activeOrdinal !== null)
|
|
623
|
+
return activeOrdinal;
|
|
624
|
+
const next = pauseOrdinalByScope.get(scope) ?? 0;
|
|
625
|
+
pauseOrdinalByScope.set(scope, next + 1);
|
|
626
|
+
return next;
|
|
627
|
+
}
|
|
628
|
+
function nextPauseId(coord) {
|
|
629
|
+
const scope = scopeOf(coord);
|
|
630
|
+
const ordinal = nextPauseOrdinal(scope);
|
|
631
|
+
return uuidv5(`${scope}:${ordinal}`, PAUSE_NAMESPACE);
|
|
632
|
+
}
|
|
633
|
+
function nonJsonPath(value, path = "$", seen = new Set) {
|
|
634
|
+
if (value === null)
|
|
635
|
+
return null;
|
|
636
|
+
const kind = typeof value;
|
|
637
|
+
if (kind === "string" || kind === "boolean")
|
|
638
|
+
return null;
|
|
639
|
+
if (kind === "number")
|
|
640
|
+
return Number.isFinite(value) ? null : path;
|
|
641
|
+
if (kind === "undefined" || kind === "bigint" || kind === "function" || kind === "symbol")
|
|
642
|
+
return path;
|
|
643
|
+
if (kind !== "object")
|
|
644
|
+
return path;
|
|
645
|
+
const obj = value;
|
|
646
|
+
if (seen.has(obj))
|
|
647
|
+
return path;
|
|
648
|
+
seen.add(obj);
|
|
649
|
+
if (Array.isArray(value)) {
|
|
650
|
+
for (let i = 0;i < value.length; i++) {
|
|
651
|
+
const child = nonJsonPath(value[i], `${path}[${i}]`, seen);
|
|
652
|
+
if (child)
|
|
653
|
+
return child;
|
|
654
|
+
}
|
|
655
|
+
seen.delete(obj);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const proto = Object.getPrototypeOf(obj);
|
|
659
|
+
if (proto !== Object.prototype && proto !== null)
|
|
660
|
+
return path;
|
|
661
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
662
|
+
if (typeof key !== "string")
|
|
663
|
+
return `${path}.${String(key)}`;
|
|
664
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
665
|
+
if (!desc?.enumerable)
|
|
666
|
+
return `${path}.${key}`;
|
|
667
|
+
const child = nonJsonPath(value[key], `${path}.${key}`, seen);
|
|
668
|
+
if (child)
|
|
669
|
+
return child;
|
|
670
|
+
}
|
|
671
|
+
seen.delete(obj);
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
function toWireRequest(req, kind) {
|
|
675
|
+
const wire = {
|
|
676
|
+
reason: req.reason,
|
|
677
|
+
kind,
|
|
678
|
+
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
679
|
+
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {},
|
|
680
|
+
...req.correlationKey !== undefined ? { correlationKey: req.correlationKey } : {},
|
|
681
|
+
...req.snapshot !== undefined ? { snapshot: req.snapshot } : {}
|
|
682
|
+
};
|
|
683
|
+
const parsed = StepPauseRequestSchema.safeParse(wire);
|
|
684
|
+
if (!parsed.success) {
|
|
685
|
+
throw new PauseRequestError(`pause request failed validation: ${parsed.error.message}`);
|
|
686
|
+
}
|
|
687
|
+
const badPath = nonJsonPath(parsed.data);
|
|
688
|
+
if (badPath) {
|
|
689
|
+
throw new PauseRequestError(`pause request must be JSON-serializable (invalid value at ${badPath})`);
|
|
690
|
+
}
|
|
691
|
+
return parsed.data;
|
|
692
|
+
}
|
|
693
|
+
async function corePause(req, coord, kind = "custom") {
|
|
694
|
+
const wire = toWireRequest(req, kind);
|
|
695
|
+
const pauseId = nextPauseId(coord);
|
|
696
|
+
const record = await readPauseRecord(pauseId);
|
|
697
|
+
if (record === null || record.resolved !== true) {
|
|
698
|
+
await writePauseRecord({ pauseId, request: wire });
|
|
699
|
+
throw new PauseSignal(pauseId, wire);
|
|
700
|
+
}
|
|
701
|
+
if (record.expired === true) {
|
|
702
|
+
const onExpiry = req.onExpiry ?? { mode: "throw" };
|
|
703
|
+
if (onExpiry.mode === "resolve")
|
|
704
|
+
return onExpiry.value;
|
|
705
|
+
throw new PauseExpiredError(`pause "${req.reason}" expired before it was resolved`);
|
|
706
|
+
}
|
|
707
|
+
const payload = record.resumePayload ?? null;
|
|
708
|
+
if (req.schema) {
|
|
709
|
+
const parsed = req.schema.safeParse(payload);
|
|
710
|
+
if (!parsed.success) {
|
|
711
|
+
throw new PauseSchemaError(`resume payload for pause "${req.reason}" failed schema validation: ${parsed.error.message}`, parsed.error.issues);
|
|
712
|
+
}
|
|
713
|
+
return parsed.data;
|
|
714
|
+
}
|
|
715
|
+
return payload;
|
|
716
|
+
}
|
|
717
|
+
|
|
333
718
|
// src/utils/errors.ts
|
|
334
719
|
function formatError(err) {
|
|
335
720
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -441,12 +826,14 @@ class VercelRunner {
|
|
|
441
826
|
apiKeyScopes: [],
|
|
442
827
|
parentRunId: null
|
|
443
828
|
});
|
|
829
|
+
const gateAgentId = this.options.agentId ?? "agent";
|
|
444
830
|
const gate = await this.gateToolCall(call, {
|
|
445
831
|
requestContext,
|
|
446
832
|
abortSignal: execOpts.abortSignal ?? signal ?? new AbortController().signal,
|
|
447
833
|
retryCount: 0,
|
|
448
|
-
agentId:
|
|
449
|
-
iteration
|
|
834
|
+
agentId: gateAgentId,
|
|
835
|
+
iteration,
|
|
836
|
+
pause: boundProcessorPause(this.options.pause, { agentId: gateAgentId, iteration })
|
|
450
837
|
});
|
|
451
838
|
if (gate.kind === "deny")
|
|
452
839
|
return gate.reason;
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -5,30 +5,35 @@
|
|
|
5
5
|
* Each provider validates its required env vars at creation time.
|
|
6
6
|
*/
|
|
7
7
|
import { Sandbox } from "e2b";
|
|
8
|
+
import type { SandboxNetworkOpts as E2bNetworkOpts } from "e2b";
|
|
8
9
|
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
9
10
|
import type { SandboxProvider, DesktopSandboxProvider, SandboxCommandResult } from "./types/sandbox.js";
|
|
10
11
|
import type { ConnectorRequestRules } from "./types/workflow-metadata.js";
|
|
11
12
|
import type { NetworkPolicy as VercelNetworkPolicy } from "@vercel/sandbox";
|
|
12
|
-
export type { SandboxProvider, DesktopSandboxProvider, SandboxCommandRunOptions, SandboxCommandResult } from "./types/sandbox.js";
|
|
13
|
+
export type { SandboxProvider, DesktopSandboxProvider, SandboxCommandRunOptions, SandboxCommandResult, SandboxDuplexProcess, SandboxSpawnDuplexOptions } from "./types/sandbox.js";
|
|
13
14
|
export type SandboxProviderName = "vercel" | "e2b" | "e2b-desktop";
|
|
14
15
|
export declare const AGENT_COMPOSE_TAG: string;
|
|
15
16
|
/**
|
|
16
17
|
* Network policy for outbound HTTPS requests — ONE shape for every provider;
|
|
17
18
|
* only the enforcement point differs. When a sandbox makes a request matching
|
|
18
19
|
* a domain in `allow`, the egress layer injects the specified headers before
|
|
19
|
-
* forwarding — credentials never exist inside the VM.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* forwarding — credentials never exist inside the VM. Both providers enforce
|
|
21
|
+
* this natively at the platform edge from the resolved policy passed at
|
|
22
|
+
* create: Vercel via its firewall (`requestRules` translate to native `match`
|
|
23
|
+
* rules), E2B via its native network firewall (`toE2bNetwork` →
|
|
24
|
+
* `allowOut`/`denyOut`/`rules`). See `toVercelNetworkPolicy` / `toE2bNetwork`.
|
|
23
25
|
*/
|
|
24
26
|
export interface SandboxNetworkHeaderTransform {
|
|
25
27
|
headers?: Record<string, string>;
|
|
26
28
|
}
|
|
27
29
|
export interface SandboxNetworkAllowRule {
|
|
28
30
|
transform?: SandboxNetworkHeaderTransform[];
|
|
29
|
-
/** Tier-2 request gate (method/path) — the transform
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
/** Tier-2 request gate (method/path) — the transform only applies when the
|
|
32
|
+
* request matches. Honoured on Vercel (translated to native `match` rules).
|
|
33
|
+
* NOT honoured on E2B: its native rules carry only a `transform`, no
|
|
34
|
+
* method/path matcher, so a brokered credential rides ALL requests to an
|
|
35
|
+
* allowed host there (see `toE2bNetwork`). Present on connector-auth rules;
|
|
36
|
+
* see `ConnectorRequestRules`. */
|
|
32
37
|
requestRules?: ConnectorRequestRules;
|
|
33
38
|
}
|
|
34
39
|
export interface SandboxNetworkSubnetPolicy {
|
|
@@ -54,12 +59,56 @@ export type SandboxSize = "2vcpu-4gb" | "4vcpu-8gb" | "8vcpu-16gb" | "32vcpu-64g
|
|
|
54
59
|
/** SKU → Vercel vCPU count (RAM follows at 2048 MB/vCPU). */
|
|
55
60
|
export declare const SANDBOX_VCPUS: Record<SandboxSize, number>;
|
|
56
61
|
/** SDK fallback size when neither the caller nor the deployment specifies one.
|
|
57
|
-
* Deliberately conservative — the OPERATIONAL default is
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
+
* Deliberately conservative — the OPERATIONAL default is the server's
|
|
63
|
+
* `SANDBOX_DEFAULT_SIZE` env var (now also `2vcpu-4gb`). Keeping the default
|
|
64
|
+
* small matters because Vercel rate-limits creation by vCPUs-per-window
|
|
65
|
+
* (`api-sandboxes-vcpus-creation`); a large default 429s bursty/simultaneous
|
|
66
|
+
* creates. Workloads that need more RAM/CPU declare `resources.size` on the
|
|
67
|
+
* workflow rather than inflating the default for everyone. */
|
|
62
68
|
export declare const DEFAULT_SANDBOX_SIZE: SandboxSize;
|
|
69
|
+
/** The E2B sizes we pre-build a template for. E2B sizing is template-baked
|
|
70
|
+
* (no per-create cpu/mem knob), so honouring `resources.size` on E2B means
|
|
71
|
+
* ONE pre-built template per size. `32vcpu-64gb` is absent (E2B has no
|
|
72
|
+
* >8-vCPU equivalent). `8vcpu-16gb` is also absent: it needs 16 GiB RAM, but
|
|
73
|
+
* the E2B account caps memory at 8 GiB (`Template.build` 400s with
|
|
74
|
+
* "Memory can't be higher than 8192 MiB"). Add it back here (and rebuild the
|
|
75
|
+
* templates) only once the account's memory limit is raised. The register/
|
|
76
|
+
* invoke guards reject an unsupported E2B size before it can reach here. */
|
|
77
|
+
export declare const E2B_TEMPLATE_SIZES: readonly SandboxSize[];
|
|
78
|
+
/** Is `size` one E2B can be built/booted at? `32vcpu-64gb` (no >8-vCPU E2B
|
|
79
|
+
* equivalent) and `8vcpu-16gb` (exceeds the account's 8 GiB memory cap) are
|
|
80
|
+
* not — the guards lean on this so the "no E2B equivalent" decision lives in
|
|
81
|
+
* exactly one place. */
|
|
82
|
+
export declare function isE2bSupportedSize(size: SandboxSize): boolean;
|
|
83
|
+
/** Machine spec for a SandboxSize, in the shape `Template.build` wants. RAM is
|
|
84
|
+
* always 2048 MB/vCPU, matching the size name + Vercel parity
|
|
85
|
+
* (`SANDBOX_VCPUS` × 2048). Used by `infra/e2b-template/build.ts` to stamp the
|
|
86
|
+
* per-size base + agent-env templates. */
|
|
87
|
+
export declare function e2bMachineSpec(size: SandboxSize): {
|
|
88
|
+
cpuCount: number;
|
|
89
|
+
memoryMB: number;
|
|
90
|
+
};
|
|
91
|
+
/** Stable E2B template ALIAS for the platform base at a given size
|
|
92
|
+
* (`agent-compose-base-<size>`). Aliases — not snapshot ids — so the refs are
|
|
93
|
+
* multi-account-clean: the same string resolves in any E2B account that built
|
|
94
|
+
* the templates. Built by `infra/e2b-template/build.ts`; the E2B provider
|
|
95
|
+
* boots this when a run on E2B declares no explicit `bootFrom`/template. */
|
|
96
|
+
export declare function e2bBaseTemplate(size: SandboxSize): string;
|
|
97
|
+
/** Stable E2B template ALIAS for the agent runtime (base + claude binary) at a
|
|
98
|
+
* given size (`agent-env-<size>`). The agent default templates boot from this
|
|
99
|
+
* via `bootFrom: { snapshotId: e2bAgentEnvTemplate(size) }`. Multi-account-clean
|
|
100
|
+
* for the same reason as `e2bBaseTemplate`. */
|
|
101
|
+
export declare function e2bAgentEnvTemplate(size: SandboxSize): string;
|
|
102
|
+
/** Is `id` one of the platform-managed E2B template aliases — a
|
|
103
|
+
* `agent-compose-base-<size>` or `agent-env-<size>` for a supported size?
|
|
104
|
+
*
|
|
105
|
+
* These are stable, platform-built, multi-account-clean strings (NOT tenant
|
|
106
|
+
* captures), so any workflow may boot from them: the same alias resolves to
|
|
107
|
+
* the same platform base in every account, and there is no tenant data behind
|
|
108
|
+
* it to leak. The dispatch snapshot gate uses this to vouch a platform E2B
|
|
109
|
+
* boot alias without it having to be a team-owned capture or a published
|
|
110
|
+
* template's bootFrom. */
|
|
111
|
+
export declare function isPlatformE2bTemplateAlias(id: string): boolean;
|
|
63
112
|
export interface SandboxCreateOpts {
|
|
64
113
|
envs: Record<string, string>;
|
|
65
114
|
metadata: Record<string, string>;
|
|
@@ -71,12 +120,10 @@ export interface SandboxCreateOpts {
|
|
|
71
120
|
* (size is template-defined). Omit → `DEFAULT_SANDBOX_SIZE`. */
|
|
72
121
|
size?: SandboxSize;
|
|
73
122
|
/** Outbound request policy with header transforms — ONE shape for every
|
|
74
|
-
* provider; only the enforcement point differs.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* server's per-run egress-policy endpoint — so this field is not passed
|
|
79
|
-
* to E2B's API. See server/src/sandbox/iron-proxy.ts. */
|
|
123
|
+
* provider; only the enforcement point differs. Both providers enforce +
|
|
124
|
+
* inject natively at create from this value: Vercel via its firewall
|
|
125
|
+
* (`toVercelNetworkPolicy`), E2B via its native network firewall
|
|
126
|
+
* (`toE2bNetwork` → `network: { allowOut, denyOut, rules }`). */
|
|
80
127
|
networkPolicy?: SandboxNetworkPolicy;
|
|
81
128
|
}
|
|
82
129
|
/**
|
|
@@ -120,6 +167,16 @@ interface SandboxProviderDef {
|
|
|
120
167
|
* up eventually by the provider's own retention.
|
|
121
168
|
*/
|
|
122
169
|
deleteSnapshot?: (snapshotId: string, env: Record<string, string>) => Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Does this snapshot id resolve on the provider for this account? A cheap
|
|
172
|
+
* metadata lookup — NEVER provisions a sandbox. Used at server boot to
|
|
173
|
+
* validate that the platform base snapshots the default templates boot from
|
|
174
|
+
* actually exist for this deployment (catching e.g. a dev-account snapshot
|
|
175
|
+
* baked into prod). Returns true if it resolves, false if the provider
|
|
176
|
+
* reports it does not exist. Transport/credential errors PROPAGATE — an
|
|
177
|
+
* indeterminate result must not be reported as "missing".
|
|
178
|
+
*/
|
|
179
|
+
snapshotExists?: (snapshotId: string, env: Record<string, string>) => Promise<boolean>;
|
|
123
180
|
}
|
|
124
181
|
export declare function makeSandboxProvider(sb: Sandbox | Desktop): SandboxProvider;
|
|
125
182
|
export declare function makeDesktopSandboxProvider(sb: Desktop): DesktopSandboxProvider;
|
|
@@ -164,6 +221,35 @@ export declare const DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?
|
|
|
164
221
|
* method/path combinations. Pinned by vercel-network-policy.test.ts.
|
|
165
222
|
*/
|
|
166
223
|
export declare function toVercelNetworkPolicy(policy: SandboxNetworkPolicy): VercelNetworkPolicy;
|
|
224
|
+
/**
|
|
225
|
+
* Translate our policy shape into E2B's native `network` config
|
|
226
|
+
* (`SandboxNetworkOpts`) — the E2B analogue of `toVercelNetworkPolicy`.
|
|
227
|
+
*
|
|
228
|
+
* - `allowOut`: the set of allowed egress targets — every host named in
|
|
229
|
+
* `allow` PLUS every CIDR in `subnets.allow`. The Archil raw-TCP
|
|
230
|
+
* data-plane CIDRs ride here as FIRST-CLASS allow entries (E2B has no
|
|
231
|
+
* root-exemption to lean on, unlike the old in-VM iptables model).
|
|
232
|
+
* - `denyOut`: the all-traffic sentinel (`0.0.0.0/0`) so the policy is
|
|
233
|
+
* DEFAULT-DENY — only `allowOut` targets pass. E2B applies allow before
|
|
234
|
+
* deny, so a host in both lists is allowed.
|
|
235
|
+
* - `rules`: per-host header injection. For each host carrying transform(s),
|
|
236
|
+
* emit one rule per transform with `{ transform: { headers } }`. A host
|
|
237
|
+
* with a rule MUST also be in `allowOut` (registering a rule does not
|
|
238
|
+
* grant egress on its own — E2B's API requires the host in `allowOut`).
|
|
239
|
+
*
|
|
240
|
+
* BEHAVIOURAL DIVERGENCE FROM iron-proxy / Vercel — path/method gating is NOT
|
|
241
|
+
* available on E2B native rules. An `SandboxNetworkRule` carries only a
|
|
242
|
+
* `transform` (header injection); there is no method/path matcher. So our
|
|
243
|
+
* Tier-2 `requestRules` (`{ methods, pathPrefixes }`) cannot be enforced here:
|
|
244
|
+
* a brokered credential rides EVERY request to an allowed host on E2B, whereas
|
|
245
|
+
* Vercel (native `match`) and the old iron-proxy gated it to the declared
|
|
246
|
+
* method+path. The host allowlist still confines WHICH hosts the credential
|
|
247
|
+
* can reach; it just can't narrow to a method/path within an allowed host.
|
|
248
|
+
*
|
|
249
|
+
* String policies map straight through: "allow-all" → no restriction (omit
|
|
250
|
+
* `allowOut`/`denyOut`), "deny-all" → block all egress.
|
|
251
|
+
*/
|
|
252
|
+
export declare function toE2bNetwork(policy: SandboxNetworkPolicy): E2bNetworkOpts;
|
|
167
253
|
/** Vercel caps sandbox tags at 5. Build the tag set with the fleet `executor`
|
|
168
254
|
* tag always present and always winning over caller metadata — `listOwned` /
|
|
169
255
|
* orphan reconciliation scope on it, so a clobbered or dropped executor tag
|
|
@@ -186,6 +272,14 @@ export declare function createSandbox(provider: SandboxProviderName, opts: Sandb
|
|
|
186
272
|
export declare function reconnectSandbox(provider: SandboxProviderName, sandboxId: string): Promise<SandboxProvider>;
|
|
187
273
|
/** Delete a snapshot by id on the named provider. No live sandbox needed. */
|
|
188
274
|
export declare function deleteSandboxSnapshot(provider: SandboxProviderName, snapshotId: string): Promise<void>;
|
|
275
|
+
/**
|
|
276
|
+
* Does `snapshotId` resolve on the named provider for this account? A cheap
|
|
277
|
+
* metadata lookup — never provisions a sandbox. `true` = resolves, `false` =
|
|
278
|
+
* provider says it does not exist. Transport/credential errors propagate (an
|
|
279
|
+
* indeterminate result is not "missing"). Used at server boot to validate the
|
|
280
|
+
* platform base snapshots the default templates boot from.
|
|
281
|
+
*/
|
|
282
|
+
export declare function snapshotResolves(provider: SandboxProviderName, snapshotId: string): Promise<boolean>;
|
|
189
283
|
/**
|
|
190
284
|
* Current active-sandbox count per configured provider, for quota gauges.
|
|
191
285
|
* Skips providers whose required env isn't set or which don't implement
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* to test in isolation.
|
|
19
19
|
*/
|
|
20
20
|
export { STEP_RESULT_PREFIX, STEP_PAUSE_PREFIX, STEP_ENV, RUNNER_BUNDLE_PATH, RUNNER_COMMAND, stepInputPath, requestContextPath, } from "./protocol.js";
|
|
21
|
-
export { invokeStep, parseStepResult, buildStepEnvs } from "./invoker.js";
|
|
21
|
+
export { invokeStep, launchStep, reconnectStep, parseStepResult, buildStepEnvs } from "./invoker.js";
|
|
22
|
+
export type { RunningStep, InvokeStepOptions } from "./invoker.js";
|
|
22
23
|
export { serveStep } from "./server.js";
|
|
23
24
|
export type { StepHandler, ServeStepRequest, StepHandlerResult } from "./server.js";
|
|
24
25
|
export { StepExecutionError } from "./types.js";
|
|
@@ -65,4 +65,40 @@ export interface InvokeStepOptions {
|
|
|
65
65
|
onStdout?: (line: string) => void;
|
|
66
66
|
onStderr?: (line: string) => void;
|
|
67
67
|
}
|
|
68
|
+
/** A step running as a background command (ADR-0028). The activity races its
|
|
69
|
+
* `wait()` against a server pause request; on a pause it freezes the VM
|
|
70
|
+
* (`pauseProcess`) and persists `runnerPid` + `resultToken` so the resume
|
|
71
|
+
* activity can `reconnectStep(...)` to the SAME process — no re-run. */
|
|
72
|
+
export interface RunningStep<TOutput = unknown> {
|
|
73
|
+
/** OS pid of the background runner inside the VM — the reconnect handle. */
|
|
74
|
+
runnerPid: number;
|
|
75
|
+
/** Per-invocation token keying the durable result file the runner writes. */
|
|
76
|
+
resultToken: string;
|
|
77
|
+
/** Await the runner's exit and classify its output into a `StepResult`. */
|
|
78
|
+
wait(): Promise<StepResult<TOutput>>;
|
|
79
|
+
}
|
|
68
80
|
export declare function invokeStep<TOutput = unknown>(sandbox: SandboxProvider, request: StepRequest, opts?: InvokeStepOptions): Promise<StepResult<TOutput>>;
|
|
81
|
+
/**
|
|
82
|
+
* Launch the step runner as a BACKGROUND command (ADR-0028) and return a handle
|
|
83
|
+
* the activity drives: it races `wait()` against a server pause request and, on
|
|
84
|
+
* a pause, freezes the VM (`pauseProcess`) and persists `runnerPid` +
|
|
85
|
+
* `resultToken` so `reconnectStep` can continue the SAME process — no re-run.
|
|
86
|
+
* Requires a provider with `commands.runBackground` (E2B); the foreground
|
|
87
|
+
* `invokeStep` is the path for providers without it (Vercel).
|
|
88
|
+
*/
|
|
89
|
+
export declare function launchStep<TOutput = unknown>(sandbox: SandboxProvider, request: StepRequest, opts?: InvokeStepOptions): Promise<RunningStep<TOutput>>;
|
|
90
|
+
/**
|
|
91
|
+
* Resume a previously-paused background runner (ADR-0028) and return a
|
|
92
|
+
* `RunningStep` handle — uniform with `launchStep` so the activity can race
|
|
93
|
+
* `wait()` against a fresh pause request (a resumed step can pause again). After
|
|
94
|
+
* the workflow reconnects the suspended VM (`Sandbox.connect` auto-resumes it),
|
|
95
|
+
* this re-attaches to the still-running runner by `runnerPid`; `wait()` awaits
|
|
96
|
+
* its exit (event-driven — no polling) and classifies the output (the durable
|
|
97
|
+
* result file is authoritative on this path). If the runner already exited
|
|
98
|
+
* during resume, the re-attach fails and `wait()` classifies from the file.
|
|
99
|
+
*/
|
|
100
|
+
export declare function reconnectStep<TOutput = unknown>(sandbox: SandboxProvider, resume: {
|
|
101
|
+
runnerPid: number;
|
|
102
|
+
resultToken: string;
|
|
103
|
+
stepIndex: number;
|
|
104
|
+
}, opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr">): Promise<RunningStep<TOutput>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -21,14 +21,6 @@ export interface BaseExecutionContext {
|
|
|
21
21
|
setMetadata?: (data: Record<string, unknown>) => Promise<void>;
|
|
22
22
|
/** Invoke another registered workflow and wait for it to settle. */
|
|
23
23
|
invokeChild: InvokeChild;
|
|
24
|
-
/**
|
|
25
|
-
* Disk-backed memoise across pause-resume. First call runs `fn` and
|
|
26
|
-
* atomically writes the result to the sandbox; on resume the recorded
|
|
27
|
-
* value is returned and `fn` is NOT re-executed. Use for expensive
|
|
28
|
-
* deterministic transforms; for side effects, use `invokeChild`.
|
|
29
|
-
* See ADR-0006 §"`ctx.checkpoint(name, fn)` — disk-backed memoisation".
|
|
30
|
-
*/
|
|
31
|
-
checkpoint<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
32
24
|
/**
|
|
33
25
|
* Pause for feedback. The step exits and the workflow waits durably until
|
|
34
26
|
* something resolves the pause (a resume call, a TTL expiry); on resume the
|