@agent-compose/sdk 0.5.8 → 0.5.9
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 +22 -12
- package/dist/agent/local-pause-request.d.ts +49 -0
- package/dist/agent/local-pause-request.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 +4 -2
- package/dist/index.js +1275 -527
- 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/index.d.ts +1 -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 +1263 -527
- package/dist/runtimes/vercel.js +389 -2
- package/dist/sandbox.d.ts +113 -19
- 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 +7 -0
- package/dist/types/sandbox-environment.d.ts +6 -1
- package/dist/types/sandbox.d.ts +41 -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 +59 -10
- package/src/agent/local-pause-request.ts +90 -0
- package/src/agent/run-agent.ts +6 -2
- package/src/agent/steer-control.ts +21 -7
- package/src/client.ts +35 -1
- package/src/index.ts +10 -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/index.ts +1 -0
- package/src/processors/processor.ts +13 -0
- package/src/runtimes/_acp-client.ts +516 -0
- package/src/runtimes/_cli-agent.ts +418 -3
- package/src/runtimes/claude.ts +27 -3
- package/src/runtimes/codex.ts +21 -1
- package/src/runtimes/vercel.ts +4 -1
- package/src/sandbox.ts +361 -54
- package/src/types/execution-context.ts +0 -8
- package/src/types/protocol.ts +27 -1
- package/src/types/runtime.ts +7 -0
- package/src/types/sandbox-environment.ts +12 -1
- package/src/types/sandbox.ts +40 -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
|
@@ -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;
|
|
@@ -520,7 +907,7 @@ async function listVercelRuntimeModels() {
|
|
|
520
907
|
return models.filter((m) => m.modelType == null || m.modelType === "language").map((m) => ({ id: m.id, name: m.name })).sort((a, b) => a.id.localeCompare(b.id));
|
|
521
908
|
}
|
|
522
909
|
// src/types/workflow.ts
|
|
523
|
-
import { z as
|
|
910
|
+
import { z as z4 } from "zod";
|
|
524
911
|
|
|
525
912
|
// src/workflow-steps/types.ts
|
|
526
913
|
var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
|
|
@@ -550,6 +937,8 @@ function extractMetadata(source) {
|
|
|
550
937
|
out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
551
938
|
if (source.invokePolicy !== undefined)
|
|
552
939
|
out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
940
|
+
if (source.environmentBuild !== undefined)
|
|
941
|
+
out.environmentBuild = source.environmentBuild;
|
|
553
942
|
return Object.freeze(out);
|
|
554
943
|
}
|
|
555
944
|
function freezeMetadataValue(value) {
|
|
@@ -601,7 +990,7 @@ function isWorkflow(value) {
|
|
|
601
990
|
|
|
602
991
|
// src/types/workflow.ts
|
|
603
992
|
function compileRunForm(def, metadata) {
|
|
604
|
-
const unknownSchema =
|
|
993
|
+
const unknownSchema = z4.unknown();
|
|
605
994
|
const inputSchema = def.input ?? unknownSchema;
|
|
606
995
|
const outputSchema = def.output ?? unknownSchema;
|
|
607
996
|
const step = {
|
|
@@ -617,7 +1006,6 @@ function compileRunForm(def, metadata) {
|
|
|
617
1006
|
setMetadata: stepCtx.setMetadata,
|
|
618
1007
|
step: stepCtx.step,
|
|
619
1008
|
agentEvents: stepCtx.agentEvents,
|
|
620
|
-
checkpoint: stepCtx.checkpoint,
|
|
621
1009
|
pause: stepCtx.pause,
|
|
622
1010
|
sleep: stepCtx.sleep,
|
|
623
1011
|
waitForEvent: stepCtx.waitForEvent,
|
|
@@ -651,7 +1039,9 @@ function defineSandboxEnvironment(env) {
|
|
|
651
1039
|
}
|
|
652
1040
|
return defineWorkflow({
|
|
653
1041
|
...env.description !== undefined ? { description: env.description } : {},
|
|
1042
|
+
...env.resources !== undefined ? { resources: env.resources } : {},
|
|
654
1043
|
snapshots: env.snapshots ?? { saveLatest: true },
|
|
1044
|
+
environmentBuild: true,
|
|
655
1045
|
run: async (_ctx, sandbox) => env.setup(sandbox)
|
|
656
1046
|
});
|
|
657
1047
|
}
|
|
@@ -662,6 +1052,7 @@ var Verdict = {
|
|
|
662
1052
|
abort: (reason) => ({ kind: "abort", reason })
|
|
663
1053
|
};
|
|
664
1054
|
// src/processors/builtins.ts
|
|
1055
|
+
import { z as z5 } from "zod";
|
|
665
1056
|
function denyTools(names) {
|
|
666
1057
|
const banned = new Set(names);
|
|
667
1058
|
return {
|
|
@@ -669,6 +1060,26 @@ function denyTools(names) {
|
|
|
669
1060
|
processToolCall: (call) => banned.has(call.toolName) ? Verdict.deny(`tool "${call.toolName}" is denied by policy`) : Verdict.continue(call)
|
|
670
1061
|
};
|
|
671
1062
|
}
|
|
1063
|
+
var ApprovalDecision = z5.object({
|
|
1064
|
+
approved: z5.boolean(),
|
|
1065
|
+
reason: z5.string().optional()
|
|
1066
|
+
});
|
|
1067
|
+
function humanApproval(opts) {
|
|
1068
|
+
const gated = opts?.tools ? new Set(opts.tools) : null;
|
|
1069
|
+
return {
|
|
1070
|
+
name: "humanApproval",
|
|
1071
|
+
async processToolCall(call, ctx) {
|
|
1072
|
+
if (gated && !gated.has(call.toolName))
|
|
1073
|
+
return Verdict.continue(call);
|
|
1074
|
+
const decision = await ctx.pause({
|
|
1075
|
+
reason: opts?.reason?.(call) ?? `Approve tool call: ${call.toolName}`,
|
|
1076
|
+
payload: { tool: call.toolName, input: call.toolInput, toolUseId: call.toolUseId },
|
|
1077
|
+
schema: ApprovalDecision
|
|
1078
|
+
});
|
|
1079
|
+
return decision.approved ? Verdict.continue(call) : Verdict.deny(decision.reason ?? `tool "${call.toolName}" denied by human reviewer`);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
672
1083
|
function requireScope(required) {
|
|
673
1084
|
return {
|
|
674
1085
|
name: "requireScope",
|
|
@@ -1063,6 +1474,22 @@ class AgentComposeClient {
|
|
|
1063
1474
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1064
1475
|
return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
|
|
1065
1476
|
}
|
|
1477
|
+
setFactorySecret(key, value, opts) {
|
|
1478
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1479
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`, {
|
|
1480
|
+
method: "POST",
|
|
1481
|
+
body: { key, value }
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async listFactorySecrets(opts) {
|
|
1485
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1486
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`);
|
|
1487
|
+
return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
1488
|
+
}
|
|
1489
|
+
deleteFactorySecret(key, opts) {
|
|
1490
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1491
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
|
|
1492
|
+
}
|
|
1066
1493
|
createApiKey(input) {
|
|
1067
1494
|
return this.fetch("/api-keys", { method: "POST", body: input });
|
|
1068
1495
|
}
|
|
@@ -1101,7 +1528,7 @@ class AgentComposeClient {
|
|
|
1101
1528
|
}
|
|
1102
1529
|
}
|
|
1103
1530
|
// src/utils/bundler.ts
|
|
1104
|
-
import { createHash } from "node:crypto";
|
|
1531
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1105
1532
|
import { parse as babelParse } from "@babel/parser";
|
|
1106
1533
|
|
|
1107
1534
|
// src/utils/source-loader.ts
|
|
@@ -1210,7 +1637,7 @@ function findDefaultExportExpression(body) {
|
|
|
1210
1637
|
return null;
|
|
1211
1638
|
}
|
|
1212
1639
|
function sha256(text) {
|
|
1213
|
-
return
|
|
1640
|
+
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
1214
1641
|
}
|
|
1215
1642
|
async function bundleWorkflow(workflowPath, overrides) {
|
|
1216
1643
|
const label = `"${workflowPath}"`;
|
|
@@ -1242,16 +1669,17 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1242
1669
|
...metadata.resources !== undefined ? { resources: metadata.resources } : {},
|
|
1243
1670
|
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1244
1671
|
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1245
|
-
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
|
|
1672
|
+
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {},
|
|
1673
|
+
...metadata.environmentBuild !== undefined ? { environmentBuild: metadata.environmentBuild } : {}
|
|
1246
1674
|
};
|
|
1247
1675
|
}
|
|
1248
1676
|
function extractIOSchema(zodSchema) {
|
|
1249
1677
|
let json;
|
|
1250
1678
|
try {
|
|
1251
|
-
const
|
|
1252
|
-
if (!
|
|
1679
|
+
const z6 = __require("zod");
|
|
1680
|
+
if (!z6.toJSONSchema)
|
|
1253
1681
|
return;
|
|
1254
|
-
json =
|
|
1682
|
+
json = z6.toJSONSchema(zodSchema);
|
|
1255
1683
|
} catch {
|
|
1256
1684
|
return;
|
|
1257
1685
|
}
|
|
@@ -1284,26 +1712,26 @@ function extractIOSchema(zodSchema) {
|
|
|
1284
1712
|
return out;
|
|
1285
1713
|
}
|
|
1286
1714
|
// src/utils/schemas.ts
|
|
1287
|
-
import { z as
|
|
1288
|
-
var AgentStatusSchema =
|
|
1289
|
-
summary:
|
|
1290
|
-
completed:
|
|
1291
|
-
blockers:
|
|
1292
|
-
exit_signal:
|
|
1293
|
-
needs_input:
|
|
1294
|
-
question:
|
|
1715
|
+
import { z as z6 } from "zod";
|
|
1716
|
+
var AgentStatusSchema = z6.object({
|
|
1717
|
+
summary: z6.string(),
|
|
1718
|
+
completed: z6.array(z6.string()),
|
|
1719
|
+
blockers: z6.array(z6.string()),
|
|
1720
|
+
exit_signal: z6.boolean(),
|
|
1721
|
+
needs_input: z6.boolean().optional(),
|
|
1722
|
+
question: z6.string().optional()
|
|
1295
1723
|
});
|
|
1296
1724
|
// src/runtimes/claude.ts
|
|
1297
1725
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1298
1726
|
|
|
1299
1727
|
// src/agent/agent-loop.ts
|
|
1300
|
-
import { z as
|
|
1728
|
+
import { z as z10 } from "zod";
|
|
1301
1729
|
|
|
1302
1730
|
// src/agent/protocol.ts
|
|
1303
|
-
import { z as
|
|
1304
|
-
var AgentMessageSchema =
|
|
1305
|
-
type:
|
|
1306
|
-
timestamp:
|
|
1731
|
+
import { z as z7 } from "zod";
|
|
1732
|
+
var AgentMessageSchema = z7.object({
|
|
1733
|
+
type: z7.enum(["init", "text", "thinking", "tool_use", "tool_result", "done", "error", "usage"]),
|
|
1734
|
+
timestamp: z7.string()
|
|
1307
1735
|
}).passthrough();
|
|
1308
1736
|
function parseAgentResponse(text) {
|
|
1309
1737
|
const match = text.match(/<response>([\s\S]*?)<\/response>/);
|
|
@@ -1320,170 +1748,33 @@ function parseAgentResponse(text) {
|
|
|
1320
1748
|
import { randomUUID } from "node:crypto";
|
|
1321
1749
|
|
|
1322
1750
|
// src/pause/manager.ts
|
|
1323
|
-
import { z as
|
|
1324
|
-
|
|
1325
|
-
// src/pause/state-dir.ts
|
|
1326
|
-
import { promises as fs } from "node:fs";
|
|
1327
|
-
import { join as join2 } from "node:path";
|
|
1328
|
-
import { randomBytes } from "node:crypto";
|
|
1329
|
-
function getStateDir() {
|
|
1330
|
-
return process.env.AGENT_COMPOSE_STATE_DIR ?? "/tmp/wf/state";
|
|
1331
|
-
}
|
|
1332
|
-
var checkpointsDir = () => join2(getStateDir(), "checkpoints");
|
|
1333
|
-
var pausesDir = () => join2(getStateDir(), "pauses");
|
|
1334
|
-
var agentStatePath = (agentInstanceId) => join2(getStateDir(), `agent-${agentInstanceId}.json`);
|
|
1335
|
-
var runtimeStatePath = (agentInstanceId) => join2(getStateDir(), `runtime-${agentInstanceId}.json`);
|
|
1336
|
-
var checkpointPath = (name) => join2(checkpointsDir(), `${name}.json`);
|
|
1337
|
-
var pausePath = (pauseId) => join2(pausesDir(), `${pauseId}.json`);
|
|
1338
|
-
var NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
|
|
1339
|
-
function assertSafeName(label, name) {
|
|
1340
|
-
if (!NAME_RE.test(name)) {
|
|
1341
|
-
throw new Error(`${label} name must match ${NAME_RE} (got ${JSON.stringify(name)})`);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
function assertSafeCheckpointName(name) {
|
|
1345
|
-
assertSafeName("checkpoint", name);
|
|
1346
|
-
}
|
|
1347
|
-
async function ensureDirs() {
|
|
1348
|
-
await fs.mkdir(checkpointsDir(), { recursive: true });
|
|
1349
|
-
await fs.mkdir(pausesDir(), { recursive: true });
|
|
1350
|
-
}
|
|
1351
|
-
async function atomicWriteJSON(path, value) {
|
|
1352
|
-
await ensureDirs();
|
|
1353
|
-
const tmp = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
1354
|
-
const payload = JSON.stringify(value);
|
|
1355
|
-
if (payload === undefined) {
|
|
1356
|
-
throw new Error("pause state values must be JSON-serialisable");
|
|
1357
|
-
}
|
|
1358
|
-
const handle = await fs.open(tmp, "w");
|
|
1359
|
-
try {
|
|
1360
|
-
await handle.writeFile(payload, { encoding: "utf8" });
|
|
1361
|
-
await handle.sync();
|
|
1362
|
-
} finally {
|
|
1363
|
-
await handle.close();
|
|
1364
|
-
}
|
|
1365
|
-
await fs.rename(tmp, path);
|
|
1366
|
-
}
|
|
1367
|
-
async function readJSON(path) {
|
|
1368
|
-
try {
|
|
1369
|
-
const raw = await fs.readFile(path, "utf8");
|
|
1370
|
-
return JSON.parse(raw);
|
|
1371
|
-
} catch (err) {
|
|
1372
|
-
if (err.code === "ENOENT")
|
|
1373
|
-
return null;
|
|
1374
|
-
throw err;
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
async function readJSONWithPresence(path) {
|
|
1378
|
-
try {
|
|
1379
|
-
const raw = await fs.readFile(path, "utf8");
|
|
1380
|
-
return { found: true, value: JSON.parse(raw) };
|
|
1381
|
-
} catch (err) {
|
|
1382
|
-
if (err.code === "ENOENT")
|
|
1383
|
-
return { found: false };
|
|
1384
|
-
throw err;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
async function readAgentState(agentInstanceId) {
|
|
1388
|
-
assertSafeName("agent", agentInstanceId);
|
|
1389
|
-
return readJSON(agentStatePath(agentInstanceId));
|
|
1390
|
-
}
|
|
1391
|
-
async function writeAgentState(agentInstanceId, state) {
|
|
1392
|
-
assertSafeName("agent", agentInstanceId);
|
|
1393
|
-
await atomicWriteJSON(agentStatePath(agentInstanceId), state);
|
|
1394
|
-
}
|
|
1395
|
-
async function readRuntimeCheckpoint(agentInstanceId) {
|
|
1396
|
-
assertSafeName("runtime", agentInstanceId);
|
|
1397
|
-
return readJSON(runtimeStatePath(agentInstanceId));
|
|
1398
|
-
}
|
|
1399
|
-
async function writeRuntimeCheckpoint(agentInstanceId, blob) {
|
|
1400
|
-
assertSafeName("runtime", agentInstanceId);
|
|
1401
|
-
await atomicWriteJSON(runtimeStatePath(agentInstanceId), blob);
|
|
1402
|
-
}
|
|
1403
|
-
async function readCheckpoint(name) {
|
|
1404
|
-
assertSafeCheckpointName(name);
|
|
1405
|
-
return readJSONWithPresence(checkpointPath(name));
|
|
1406
|
-
}
|
|
1407
|
-
async function writeCheckpoint(name, value) {
|
|
1408
|
-
assertSafeCheckpointName(name);
|
|
1409
|
-
await atomicWriteJSON(checkpointPath(name), value);
|
|
1410
|
-
}
|
|
1411
|
-
function hasOwn(obj, key) {
|
|
1412
|
-
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1413
|
-
}
|
|
1414
|
-
function validatePauseRecord(value, expectedPauseId) {
|
|
1415
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1416
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: expected object`);
|
|
1417
|
-
}
|
|
1418
|
-
const record = value;
|
|
1419
|
-
if (record.pauseId !== undefined && record.pauseId !== expectedPauseId) {
|
|
1420
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: pauseId mismatch`);
|
|
1421
|
-
}
|
|
1422
|
-
if (record.resolved === true) {
|
|
1423
|
-
if (typeof record.expired !== "boolean") {
|
|
1424
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require boolean expired`);
|
|
1425
|
-
}
|
|
1426
|
-
if (!hasOwn(record, "resumePayload") || record.resumePayload === undefined) {
|
|
1427
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require resumePayload`);
|
|
1428
|
-
}
|
|
1429
|
-
return {
|
|
1430
|
-
pauseId: expectedPauseId,
|
|
1431
|
-
request: hasOwn(record, "request") ? record.request : null,
|
|
1432
|
-
...record
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
if (record.pauseId !== expectedPauseId) {
|
|
1436
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require matching pauseId`);
|
|
1437
|
-
}
|
|
1438
|
-
if (!hasOwn(record, "request") || record.request === undefined) {
|
|
1439
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require request`);
|
|
1440
|
-
}
|
|
1441
|
-
if (record.resolved !== undefined && record.resolved !== false) {
|
|
1442
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: resolved must be true, false, or absent`);
|
|
1443
|
-
}
|
|
1444
|
-
if (hasOwn(record, "expired") || hasOwn(record, "resumePayload")) {
|
|
1445
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records must not include resolution fields`);
|
|
1446
|
-
}
|
|
1447
|
-
return record;
|
|
1448
|
-
}
|
|
1449
|
-
async function readPauseRecord(pauseId) {
|
|
1450
|
-
assertSafeName("pause", pauseId);
|
|
1451
|
-
const record = await readJSON(pausePath(pauseId));
|
|
1452
|
-
return record === null ? null : validatePauseRecord(record, pauseId);
|
|
1453
|
-
}
|
|
1454
|
-
async function writePauseRecord(record) {
|
|
1455
|
-
assertSafeName("pause", record.pauseId);
|
|
1456
|
-
validatePauseRecord(record, record.pauseId);
|
|
1457
|
-
await atomicWriteJSON(pausePath(record.pauseId), record);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// src/pause/manager.ts
|
|
1751
|
+
import { z as z8 } from "zod";
|
|
1461
1752
|
var AGENT_LOOP_STATE_SCHEMA_VERSION = 2;
|
|
1462
|
-
var SUPPORTED_AGENT_LOOP_STATE_VERSIONS =
|
|
1463
|
-
var RunningAgentLoopStateSchema =
|
|
1753
|
+
var SUPPORTED_AGENT_LOOP_STATE_VERSIONS = z8.union([z8.literal(1), z8.literal(2)]);
|
|
1754
|
+
var RunningAgentLoopStateSchema = z8.object({
|
|
1464
1755
|
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1465
|
-
phase:
|
|
1466
|
-
iteration:
|
|
1467
|
-
completedIterations:
|
|
1468
|
-
lastSessionId:
|
|
1756
|
+
phase: z8.literal("running"),
|
|
1757
|
+
iteration: z8.number().int().nonnegative(),
|
|
1758
|
+
completedIterations: z8.number().int().nonnegative(),
|
|
1759
|
+
lastSessionId: z8.string(),
|
|
1469
1760
|
lastStatus: AgentStatusSchema.nullable(),
|
|
1470
|
-
lastResponseText:
|
|
1471
|
-
lastResponseValidationError:
|
|
1472
|
-
iterationsWithoutStatus:
|
|
1473
|
-
blockerStreak:
|
|
1474
|
-
pendingSteerPause:
|
|
1761
|
+
lastResponseText: z8.string(),
|
|
1762
|
+
lastResponseValidationError: z8.string(),
|
|
1763
|
+
iterationsWithoutStatus: z8.number().int().nonnegative(),
|
|
1764
|
+
blockerStreak: z8.object({ key: z8.string(), count: z8.number().int().nonnegative() }).nullable(),
|
|
1765
|
+
pendingSteerPause: z8.object({ reason: z8.string(), correlationKey: z8.string().nullable(), at: z8.number().int(), payload: z8.record(z8.string(), z8.unknown()).optional() }).nullable().default(null)
|
|
1475
1766
|
});
|
|
1476
|
-
var SettledAgentLoopStateSchema =
|
|
1767
|
+
var SettledAgentLoopStateSchema = z8.object({
|
|
1477
1768
|
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1478
|
-
phase:
|
|
1479
|
-
result:
|
|
1480
|
-
sessionId:
|
|
1769
|
+
phase: z8.literal("settled"),
|
|
1770
|
+
result: z8.object({
|
|
1771
|
+
sessionId: z8.string(),
|
|
1481
1772
|
lastStatus: AgentStatusSchema.nullable(),
|
|
1482
|
-
iterations:
|
|
1483
|
-
response:
|
|
1773
|
+
iterations: z8.number().int().nonnegative(),
|
|
1774
|
+
response: z8.unknown().optional()
|
|
1484
1775
|
})
|
|
1485
1776
|
});
|
|
1486
|
-
var AgentLoopStateSchema =
|
|
1777
|
+
var AgentLoopStateSchema = z8.discriminatedUnion("phase", [
|
|
1487
1778
|
RunningAgentLoopStateSchema,
|
|
1488
1779
|
SettledAgentLoopStateSchema
|
|
1489
1780
|
]);
|
|
@@ -1528,287 +1819,48 @@ class PauseManager {
|
|
|
1528
1819
|
const runtimeBlob = await readRuntimeCheckpoint(this.agentId);
|
|
1529
1820
|
if (runtimeBlob !== null && !runtime.restoreCheckpoint) {
|
|
1530
1821
|
throw new Error(`runtime checkpoint exists for resumed agentId=${this.agentId}, but runtime cannot restore it`);
|
|
1531
|
-
}
|
|
1532
|
-
if (runtime.restoreCheckpoint && runtimeBlob === null) {
|
|
1533
|
-
throw new Error(`missing runtime checkpoint for resumed agentId=${this.agentId}`);
|
|
1534
|
-
}
|
|
1535
|
-
if (runtimeBlob !== null)
|
|
1536
|
-
runtime.restoreCheckpoint?.(runtimeBlob);
|
|
1537
|
-
return { kind: "running", state: toProgressState(state) };
|
|
1538
|
-
}
|
|
1539
|
-
async saveRunningAgentLoop(state, runtime) {
|
|
1540
|
-
await writeAgentState(this.agentId, {
|
|
1541
|
-
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1542
|
-
phase: "running",
|
|
1543
|
-
...state
|
|
1544
|
-
});
|
|
1545
|
-
if (runtime.captureCheckpoint) {
|
|
1546
|
-
const blob = runtime.captureCheckpoint();
|
|
1547
|
-
if (blob !== undefined)
|
|
1548
|
-
await writeRuntimeCheckpoint(this.agentId, blob);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
async saveSettledAgentLoop(result) {
|
|
1552
|
-
await writeAgentState(this.agentId, {
|
|
1553
|
-
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1554
|
-
phase: "settled",
|
|
1555
|
-
result: {
|
|
1556
|
-
sessionId: result.sessionId,
|
|
1557
|
-
lastStatus: result.lastStatus,
|
|
1558
|
-
iterations: result.iterations,
|
|
1559
|
-
...result.response !== undefined ? { response: result.response } : {}
|
|
1560
|
-
}
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
// src/pause/pause-core.ts
|
|
1566
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1567
|
-
|
|
1568
|
-
// src/step-invocation/types.ts
|
|
1569
|
-
import { z as z7 } from "zod";
|
|
1570
|
-
var StepPauseRequestSchema = z7.object({
|
|
1571
|
-
reason: z7.string().min(1),
|
|
1572
|
-
kind: z7.enum(["decision", "sleep", "event", "custom"]),
|
|
1573
|
-
payload: z7.record(z7.string(), z7.unknown()).optional(),
|
|
1574
|
-
ttlMs: z7.number().int().positive().optional(),
|
|
1575
|
-
correlationKey: z7.string().min(1).optional(),
|
|
1576
|
-
snapshot: z7.boolean().optional()
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
class StepExecutionError extends Error {
|
|
1580
|
-
kind;
|
|
1581
|
-
exitCode;
|
|
1582
|
-
constructor(error, stepLabel) {
|
|
1583
|
-
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
1584
|
-
this.name = "StepExecutionError";
|
|
1585
|
-
this.kind = error.kind;
|
|
1586
|
-
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// src/active-step.ts
|
|
1591
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1592
|
-
var activeStepStorage = new AsyncLocalStorage;
|
|
1593
|
-
var fallbackActive = null;
|
|
1594
|
-
var BRIDGE_KEY = Symbol.for("agent-compose.activeStep.bridge");
|
|
1595
|
-
function bridgeSlot() {
|
|
1596
|
-
const g = globalThis;
|
|
1597
|
-
return g[BRIDGE_KEY] ??= { state: null };
|
|
1598
|
-
}
|
|
1599
|
-
function makeState(step) {
|
|
1600
|
-
return {
|
|
1601
|
-
stepIndex: step.stepIndex,
|
|
1602
|
-
nextAgentCallIndex: 0,
|
|
1603
|
-
pauseOrdinalByScope: new Map
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
function currentState() {
|
|
1607
|
-
return activeStepStorage.getStore() ?? bridgeSlot().state ?? fallbackActive;
|
|
1608
|
-
}
|
|
1609
|
-
async function runWithActiveStep(step, fn) {
|
|
1610
|
-
return activeStepStorage.run(makeState(step), fn);
|
|
1611
|
-
}
|
|
1612
|
-
function setActiveStepBridge(step) {
|
|
1613
|
-
const slot = bridgeSlot();
|
|
1614
|
-
const prev = { state: slot.state };
|
|
1615
|
-
slot.state = step ? makeState(step) : null;
|
|
1616
|
-
return prev;
|
|
1617
|
-
}
|
|
1618
|
-
function restoreActiveStepBridge(prev) {
|
|
1619
|
-
bridgeSlot().state = prev.state;
|
|
1620
|
-
}
|
|
1621
|
-
function getActiveStep() {
|
|
1622
|
-
const state = currentState();
|
|
1623
|
-
return state ? { stepIndex: state.stepIndex } : null;
|
|
1624
|
-
}
|
|
1625
|
-
function nextAgentCallInActiveStep() {
|
|
1626
|
-
const state = currentState();
|
|
1627
|
-
if (!state)
|
|
1628
|
-
return null;
|
|
1629
|
-
const callIndex = state.nextAgentCallIndex;
|
|
1630
|
-
state.nextAgentCallIndex += 1;
|
|
1631
|
-
return { stepIndex: state.stepIndex, callIndex };
|
|
1632
|
-
}
|
|
1633
|
-
function nextPauseOrdinalInActiveStep(scope) {
|
|
1634
|
-
const state = currentState();
|
|
1635
|
-
if (!state)
|
|
1636
|
-
return null;
|
|
1637
|
-
const next = state.pauseOrdinalByScope.get(scope) ?? 0;
|
|
1638
|
-
state.pauseOrdinalByScope.set(scope, next + 1);
|
|
1639
|
-
return next;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// src/pause/errors.ts
|
|
1643
|
-
class PauseError extends Error {
|
|
1644
|
-
constructor(message) {
|
|
1645
|
-
super(message);
|
|
1646
|
-
this.name = new.target.name;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
class PauseExpiredError extends PauseError {
|
|
1651
|
-
code = "pause_expired";
|
|
1652
|
-
constructor(message = "pause expired before it was resolved") {
|
|
1653
|
-
super(message);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
class PauseSchemaError extends PauseError {
|
|
1658
|
-
code = "pause_schema";
|
|
1659
|
-
issues;
|
|
1660
|
-
constructor(message, issues) {
|
|
1661
|
-
super(message);
|
|
1662
|
-
this.issues = issues;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
class PauseRequestError extends PauseError {
|
|
1667
|
-
code = "pause_request";
|
|
1668
|
-
constructor(message = "pause request is invalid") {
|
|
1669
|
-
super(message);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
// src/pause/pause-core.ts
|
|
1674
|
-
var PAUSE_SIGNAL_BRAND = Symbol.for("agent-compose.PauseSignal");
|
|
1675
|
-
|
|
1676
|
-
class PauseSignal {
|
|
1677
|
-
pauseId;
|
|
1678
|
-
pauseRequest;
|
|
1679
|
-
observability;
|
|
1680
|
-
[PAUSE_SIGNAL_BRAND] = true;
|
|
1681
|
-
constructor(pauseId, pauseRequest, observability) {
|
|
1682
|
-
this.pauseId = pauseId;
|
|
1683
|
-
this.pauseRequest = pauseRequest;
|
|
1684
|
-
this.observability = observability;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
function isPauseSignal(err) {
|
|
1688
|
-
return typeof err === "object" && err !== null && err[PAUSE_SIGNAL_BRAND] === true;
|
|
1689
|
-
}
|
|
1690
|
-
var PAUSE_NAMESPACE = "5f3e2b8a-0c41-4e9d-9a7b-6d2c1f8e3a40";
|
|
1691
|
-
function uuidToBytes(uuid) {
|
|
1692
|
-
return Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
1693
|
-
}
|
|
1694
|
-
function bytesToUuid(b) {
|
|
1695
|
-
const h = b.toString("hex");
|
|
1696
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
1697
|
-
}
|
|
1698
|
-
function uuidv5(name, namespace) {
|
|
1699
|
-
const hash = createHash2("sha1").update(uuidToBytes(namespace)).update(Buffer.from(name, "utf8")).digest();
|
|
1700
|
-
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
1701
|
-
bytes[6] = bytes[6] & 15 | 80;
|
|
1702
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
1703
|
-
return bytesToUuid(bytes);
|
|
1704
|
-
}
|
|
1705
|
-
function scopeOf(coord) {
|
|
1706
|
-
return coord.agentScope ? `${coord.runId}:${coord.stepIndex}:${coord.agentScope.agentId}:${coord.agentScope.iteration}` : `${coord.runId}:${coord.stepIndex}:bare`;
|
|
1707
|
-
}
|
|
1708
|
-
var pauseOrdinalByScope = new Map;
|
|
1709
|
-
function nextPauseOrdinal(scope) {
|
|
1710
|
-
const activeOrdinal = nextPauseOrdinalInActiveStep(scope);
|
|
1711
|
-
if (activeOrdinal !== null)
|
|
1712
|
-
return activeOrdinal;
|
|
1713
|
-
const next = pauseOrdinalByScope.get(scope) ?? 0;
|
|
1714
|
-
pauseOrdinalByScope.set(scope, next + 1);
|
|
1715
|
-
return next;
|
|
1716
|
-
}
|
|
1717
|
-
function nextPauseId(coord) {
|
|
1718
|
-
const scope = scopeOf(coord);
|
|
1719
|
-
const ordinal = nextPauseOrdinal(scope);
|
|
1720
|
-
return uuidv5(`${scope}:${ordinal}`, PAUSE_NAMESPACE);
|
|
1721
|
-
}
|
|
1722
|
-
function nonJsonPath(value, path = "$", seen = new Set) {
|
|
1723
|
-
if (value === null)
|
|
1724
|
-
return null;
|
|
1725
|
-
const kind = typeof value;
|
|
1726
|
-
if (kind === "string" || kind === "boolean")
|
|
1727
|
-
return null;
|
|
1728
|
-
if (kind === "number")
|
|
1729
|
-
return Number.isFinite(value) ? null : path;
|
|
1730
|
-
if (kind === "undefined" || kind === "bigint" || kind === "function" || kind === "symbol")
|
|
1731
|
-
return path;
|
|
1732
|
-
if (kind !== "object")
|
|
1733
|
-
return path;
|
|
1734
|
-
const obj = value;
|
|
1735
|
-
if (seen.has(obj))
|
|
1736
|
-
return path;
|
|
1737
|
-
seen.add(obj);
|
|
1738
|
-
if (Array.isArray(value)) {
|
|
1739
|
-
for (let i = 0;i < value.length; i++) {
|
|
1740
|
-
const child = nonJsonPath(value[i], `${path}[${i}]`, seen);
|
|
1741
|
-
if (child)
|
|
1742
|
-
return child;
|
|
1743
|
-
}
|
|
1744
|
-
seen.delete(obj);
|
|
1745
|
-
return null;
|
|
1746
|
-
}
|
|
1747
|
-
const proto = Object.getPrototypeOf(obj);
|
|
1748
|
-
if (proto !== Object.prototype && proto !== null)
|
|
1749
|
-
return path;
|
|
1750
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
1751
|
-
if (typeof key !== "string")
|
|
1752
|
-
return `${path}.${String(key)}`;
|
|
1753
|
-
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
1754
|
-
if (!desc?.enumerable)
|
|
1755
|
-
return `${path}.${key}`;
|
|
1756
|
-
const child = nonJsonPath(value[key], `${path}.${key}`, seen);
|
|
1757
|
-
if (child)
|
|
1758
|
-
return child;
|
|
1759
|
-
}
|
|
1760
|
-
seen.delete(obj);
|
|
1761
|
-
return null;
|
|
1762
|
-
}
|
|
1763
|
-
function toWireRequest(req, kind) {
|
|
1764
|
-
const wire = {
|
|
1765
|
-
reason: req.reason,
|
|
1766
|
-
kind,
|
|
1767
|
-
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
1768
|
-
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {},
|
|
1769
|
-
...req.correlationKey !== undefined ? { correlationKey: req.correlationKey } : {},
|
|
1770
|
-
...req.snapshot !== undefined ? { snapshot: req.snapshot } : {}
|
|
1771
|
-
};
|
|
1772
|
-
const parsed = StepPauseRequestSchema.safeParse(wire);
|
|
1773
|
-
if (!parsed.success) {
|
|
1774
|
-
throw new PauseRequestError(`pause request failed validation: ${parsed.error.message}`);
|
|
1775
|
-
}
|
|
1776
|
-
const badPath = nonJsonPath(parsed.data);
|
|
1777
|
-
if (badPath) {
|
|
1778
|
-
throw new PauseRequestError(`pause request must be JSON-serializable (invalid value at ${badPath})`);
|
|
1779
|
-
}
|
|
1780
|
-
return parsed.data;
|
|
1781
|
-
}
|
|
1782
|
-
async function corePause(req, coord, kind = "custom") {
|
|
1783
|
-
const wire = toWireRequest(req, kind);
|
|
1784
|
-
const pauseId = nextPauseId(coord);
|
|
1785
|
-
const record = await readPauseRecord(pauseId);
|
|
1786
|
-
if (record === null || record.resolved !== true) {
|
|
1787
|
-
await writePauseRecord({ pauseId, request: wire });
|
|
1788
|
-
throw new PauseSignal(pauseId, wire);
|
|
1789
|
-
}
|
|
1790
|
-
if (record.expired === true) {
|
|
1791
|
-
const onExpiry = req.onExpiry ?? { mode: "throw" };
|
|
1792
|
-
if (onExpiry.mode === "resolve")
|
|
1793
|
-
return onExpiry.value;
|
|
1794
|
-
throw new PauseExpiredError(`pause "${req.reason}" expired before it was resolved`);
|
|
1822
|
+
}
|
|
1823
|
+
if (runtime.restoreCheckpoint && runtimeBlob === null) {
|
|
1824
|
+
throw new Error(`missing runtime checkpoint for resumed agentId=${this.agentId}`);
|
|
1825
|
+
}
|
|
1826
|
+
if (runtimeBlob !== null)
|
|
1827
|
+
runtime.restoreCheckpoint?.(runtimeBlob);
|
|
1828
|
+
return { kind: "running", state: toProgressState(state) };
|
|
1795
1829
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1830
|
+
async saveRunningAgentLoop(state, runtime) {
|
|
1831
|
+
await writeAgentState(this.agentId, {
|
|
1832
|
+
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1833
|
+
phase: "running",
|
|
1834
|
+
...state
|
|
1835
|
+
});
|
|
1836
|
+
if (runtime.captureCheckpoint) {
|
|
1837
|
+
const blob = runtime.captureCheckpoint();
|
|
1838
|
+
if (blob !== undefined)
|
|
1839
|
+
await writeRuntimeCheckpoint(this.agentId, blob);
|
|
1801
1840
|
}
|
|
1802
|
-
return parsed.data;
|
|
1803
1841
|
}
|
|
1804
|
-
|
|
1842
|
+
async saveSettledAgentLoop(result) {
|
|
1843
|
+
await writeAgentState(this.agentId, {
|
|
1844
|
+
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1845
|
+
phase: "settled",
|
|
1846
|
+
result: {
|
|
1847
|
+
sessionId: result.sessionId,
|
|
1848
|
+
lastStatus: result.lastStatus,
|
|
1849
|
+
iterations: result.iterations,
|
|
1850
|
+
...result.response !== undefined ? { response: result.response } : {}
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1805
1854
|
}
|
|
1806
1855
|
|
|
1807
1856
|
// src/agent/steer-control.ts
|
|
1808
|
-
import { z as
|
|
1809
|
-
var SteerDecisionSchema =
|
|
1810
|
-
message:
|
|
1811
|
-
|
|
1857
|
+
import { z as z9 } from "zod";
|
|
1858
|
+
var SteerDecisionSchema = z9.object({
|
|
1859
|
+
message: z9.string().min(1).optional(),
|
|
1860
|
+
decision: z9.string().min(1).optional(),
|
|
1861
|
+
actor: z9.string().nullish()
|
|
1862
|
+
}).transform((d) => ({ message: (d.message ?? d.decision ?? "").trim(), actor: d.actor ?? null })).refine((d) => d.message.length > 0, {
|
|
1863
|
+
message: "a steer/resume answer needs a non-empty `message` or `decision`"
|
|
1812
1864
|
});
|
|
1813
1865
|
var steerPending = new Map;
|
|
1814
1866
|
function signalSteerPending(agentId, payload) {
|
|
@@ -1921,6 +1973,8 @@ function summarizeAgentMessage(msg) {
|
|
|
1921
1973
|
return { type: "done", sessionId: msg.sessionId };
|
|
1922
1974
|
case "error":
|
|
1923
1975
|
return { type: "error", text: truncate(msg.text) };
|
|
1976
|
+
case "plan":
|
|
1977
|
+
return { type: "plan", entries: msg.entries };
|
|
1924
1978
|
}
|
|
1925
1979
|
}
|
|
1926
1980
|
async function agentLoop(opts) {
|
|
@@ -1946,7 +2000,8 @@ async function agentLoop(opts) {
|
|
|
1946
2000
|
abortSignal: loopAbort.signal,
|
|
1947
2001
|
retryCount: 0,
|
|
1948
2002
|
agentId,
|
|
1949
|
-
iteration
|
|
2003
|
+
iteration,
|
|
2004
|
+
pause: boundProcessorPause(opts.pause, { agentId, iteration })
|
|
1950
2005
|
});
|
|
1951
2006
|
if (!opts.runtime)
|
|
1952
2007
|
throw new Error("agentLoop: opts.runtime is required");
|
|
@@ -1958,7 +2013,8 @@ async function agentLoop(opts) {
|
|
|
1958
2013
|
processors,
|
|
1959
2014
|
requestContext,
|
|
1960
2015
|
agentId,
|
|
1961
|
-
...opts.
|
|
2016
|
+
...opts.pause ? { pause: opts.pause } : {},
|
|
2017
|
+
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: z10.toJSONSchema(opts.responseSchema) } } : {}
|
|
1962
2018
|
});
|
|
1963
2019
|
if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
|
|
1964
2020
|
process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
|
|
@@ -2052,6 +2108,7 @@ async function agentLoop(opts) {
|
|
|
2052
2108
|
steerDecision = await opts.pause({
|
|
2053
2109
|
reason: pendingSteerPause.reason,
|
|
2054
2110
|
...pendingSteerPause.correlationKey !== null ? { correlationKey: pendingSteerPause.correlationKey } : {},
|
|
2111
|
+
...pendingSteerPause.payload !== undefined ? { payload: pendingSteerPause.payload } : {},
|
|
2055
2112
|
schema: SteerDecisionSchema
|
|
2056
2113
|
}, { agentId, iteration });
|
|
2057
2114
|
pendingSteerPause = null;
|
|
@@ -2088,6 +2145,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2088
2145
|
prompt,
|
|
2089
2146
|
sessionId: lastSessionId ?? undefined,
|
|
2090
2147
|
iteration: iteration + 1,
|
|
2148
|
+
signal: loopAbort.signal,
|
|
2091
2149
|
...opts.inbox ? { inboxStream: opts.inbox } : {}
|
|
2092
2150
|
})) {
|
|
2093
2151
|
const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
|
|
@@ -2120,6 +2178,19 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2120
2178
|
}
|
|
2121
2179
|
lastResponseText = responseText;
|
|
2122
2180
|
let status = parseAgentStatus(responseText);
|
|
2181
|
+
if (opts.pause && pendingSteerPause === null) {
|
|
2182
|
+
const localPause = opts.consumeLocalPauseRequest?.() ?? null;
|
|
2183
|
+
if (localPause) {
|
|
2184
|
+
pendingSteerPause = {
|
|
2185
|
+
reason: localPause.reason,
|
|
2186
|
+
correlationKey: null,
|
|
2187
|
+
...localPause.options && localPause.options.length > 0 ? { payload: { options: localPause.options } } : {},
|
|
2188
|
+
at: Date.now()
|
|
2189
|
+
};
|
|
2190
|
+
blockerStreak = null;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2123
2194
|
const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
|
|
2124
2195
|
const validateResponse = (target) => {
|
|
2125
2196
|
const parsed = opts.responseSchema.safeParse(target);
|
|
@@ -2405,12 +2476,15 @@ class ClaudeRunner {
|
|
|
2405
2476
|
toolInput: pre.tool_input ?? {},
|
|
2406
2477
|
toolUseId: toolUseId ?? `${pre.tool_name}-${Date.now()}`
|
|
2407
2478
|
};
|
|
2479
|
+
const gateAgentId = this.options.agentId ?? "agent";
|
|
2480
|
+
const gateIteration = opts.iteration ?? 1;
|
|
2408
2481
|
const gate = await this.gateToolCall(call, {
|
|
2409
2482
|
requestContext,
|
|
2410
2483
|
abortSignal: signal ?? opts.signal ?? new AbortController().signal,
|
|
2411
2484
|
retryCount: 0,
|
|
2412
|
-
agentId:
|
|
2413
|
-
iteration:
|
|
2485
|
+
agentId: gateAgentId,
|
|
2486
|
+
iteration: gateIteration,
|
|
2487
|
+
pause: boundProcessorPause(this.options.pause, { agentId: gateAgentId, iteration: gateIteration })
|
|
2414
2488
|
});
|
|
2415
2489
|
if (gate.kind === "allow")
|
|
2416
2490
|
return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "allow", updatedInput: gate.call.toolInput } };
|
|
@@ -2441,6 +2515,14 @@ class ClaudeRunner {
|
|
|
2441
2515
|
})();
|
|
2442
2516
|
}
|
|
2443
2517
|
const promptForQuery = inboxQueue ?? opts.prompt;
|
|
2518
|
+
const queryAbort = new AbortController;
|
|
2519
|
+
const onLoopAbort = () => queryAbort.abort();
|
|
2520
|
+
if (opts.signal) {
|
|
2521
|
+
if (opts.signal.aborted)
|
|
2522
|
+
queryAbort.abort();
|
|
2523
|
+
else
|
|
2524
|
+
opts.signal.addEventListener("abort", onLoopAbort, { once: true });
|
|
2525
|
+
}
|
|
2444
2526
|
try {
|
|
2445
2527
|
let emittedAssistantText = false;
|
|
2446
2528
|
for await (const message of query({
|
|
@@ -2454,7 +2536,8 @@ class ClaudeRunner {
|
|
|
2454
2536
|
thinking: resolveThinking(this.model, this.config.thinking),
|
|
2455
2537
|
effort: this.config.effort,
|
|
2456
2538
|
cwd: this.options.cwd,
|
|
2457
|
-
|
|
2539
|
+
abortController: queryAbort,
|
|
2540
|
+
env: { ...process.env, ...this.config.env ?? {}, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} },
|
|
2458
2541
|
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2459
2542
|
...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
|
|
2460
2543
|
skills: this.config.skills ?? "all",
|
|
@@ -2485,6 +2568,7 @@ class ClaudeRunner {
|
|
|
2485
2568
|
}
|
|
2486
2569
|
} finally {
|
|
2487
2570
|
inboxQueue?.close();
|
|
2571
|
+
opts.signal?.removeEventListener("abort", onLoopAbort);
|
|
2488
2572
|
}
|
|
2489
2573
|
}
|
|
2490
2574
|
}
|
|
@@ -2495,12 +2579,295 @@ function createClaudeRuntime(config = {}) {
|
|
|
2495
2579
|
}
|
|
2496
2580
|
var claude_default = createClaudeRuntime();
|
|
2497
2581
|
// src/runtimes/_cli-agent.ts
|
|
2582
|
+
import { ndJsonStream } from "@agentclientprotocol/sdk";
|
|
2583
|
+
|
|
2584
|
+
// src/runtimes/_acp-client.ts
|
|
2585
|
+
import {
|
|
2586
|
+
ClientSideConnection,
|
|
2587
|
+
RequestError
|
|
2588
|
+
} from "@agentclientprotocol/sdk";
|
|
2498
2589
|
function now3() {
|
|
2499
2590
|
return new Date().toISOString();
|
|
2500
2591
|
}
|
|
2592
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
2593
|
+
var OPTION_KIND_FOR_VERDICT = {
|
|
2594
|
+
allow: ["allow_once", "allow_always"],
|
|
2595
|
+
deny: ["reject_once", "reject_always"]
|
|
2596
|
+
};
|
|
2597
|
+
function contentBlockText(block) {
|
|
2598
|
+
if (!block)
|
|
2599
|
+
return "";
|
|
2600
|
+
return block.type === "text" ? block.text : "";
|
|
2601
|
+
}
|
|
2602
|
+
function renderToolContent(content) {
|
|
2603
|
+
const parts = [];
|
|
2604
|
+
const diffs = [];
|
|
2605
|
+
for (const block of content ?? []) {
|
|
2606
|
+
if (block.type === "content") {
|
|
2607
|
+
parts.push(contentBlockText(block.content));
|
|
2608
|
+
} else if (block.type === "diff") {
|
|
2609
|
+
diffs.push({ path: block.path, oldText: block.oldText ?? null, newText: block.newText });
|
|
2610
|
+
const header = block.oldText == null ? `--- (new file)
|
|
2611
|
+
+++ ${block.path}` : `--- ${block.path}
|
|
2612
|
+
+++ ${block.path}`;
|
|
2613
|
+
parts.push(`${header}
|
|
2614
|
+
${block.newText}`);
|
|
2615
|
+
} else if (block.type === "terminal") {
|
|
2616
|
+
parts.push(`[terminal ${block.terminalId}]`);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
return { output: parts.filter((p) => p !== "").join(`
|
|
2620
|
+
`), diffs };
|
|
2621
|
+
}
|
|
2622
|
+
function mapLocations(locations) {
|
|
2623
|
+
if (!locations || locations.length === 0)
|
|
2624
|
+
return;
|
|
2625
|
+
return locations.map((l) => l.line != null ? { path: l.path, line: l.line } : { path: l.path });
|
|
2626
|
+
}
|
|
2627
|
+
function terminalToolResult(args) {
|
|
2628
|
+
const { output, diffs } = renderToolContent(args.content);
|
|
2629
|
+
const locations = mapLocations(args.locations);
|
|
2630
|
+
return {
|
|
2631
|
+
type: "tool_result",
|
|
2632
|
+
toolUseId: args.toolCallId,
|
|
2633
|
+
output,
|
|
2634
|
+
isError: args.status === "failed",
|
|
2635
|
+
timestamp: args.timestamp,
|
|
2636
|
+
...diffs.length > 0 ? { diffs } : {},
|
|
2637
|
+
...locations ? { locations } : {}
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
function normalizeSessionUpdate(update) {
|
|
2641
|
+
const ts = now3();
|
|
2642
|
+
switch (update.sessionUpdate) {
|
|
2643
|
+
case "agent_message_chunk":
|
|
2644
|
+
return [{ type: "text", text: contentBlockText(update.content), timestamp: ts }];
|
|
2645
|
+
case "agent_thought_chunk":
|
|
2646
|
+
return [{ type: "thinking", text: contentBlockText(update.content), timestamp: ts }];
|
|
2647
|
+
case "tool_call": {
|
|
2648
|
+
const messages = [{
|
|
2649
|
+
type: "tool_use",
|
|
2650
|
+
toolName: update.title || update.kind || "tool",
|
|
2651
|
+
toolInput: update.rawInput ?? {},
|
|
2652
|
+
toolUseId: update.toolCallId,
|
|
2653
|
+
timestamp: ts
|
|
2654
|
+
}];
|
|
2655
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
2656
|
+
messages.push(terminalToolResult({
|
|
2657
|
+
toolCallId: update.toolCallId,
|
|
2658
|
+
status: update.status,
|
|
2659
|
+
content: update.content,
|
|
2660
|
+
locations: update.locations,
|
|
2661
|
+
timestamp: ts
|
|
2662
|
+
}));
|
|
2663
|
+
}
|
|
2664
|
+
return messages;
|
|
2665
|
+
}
|
|
2666
|
+
case "tool_call_update": {
|
|
2667
|
+
if (update.status !== "completed" && update.status !== "failed")
|
|
2668
|
+
return [];
|
|
2669
|
+
return [terminalToolResult({
|
|
2670
|
+
toolCallId: update.toolCallId,
|
|
2671
|
+
status: update.status,
|
|
2672
|
+
content: update.content,
|
|
2673
|
+
locations: update.locations,
|
|
2674
|
+
timestamp: ts
|
|
2675
|
+
})];
|
|
2676
|
+
}
|
|
2677
|
+
case "plan":
|
|
2678
|
+
return [{
|
|
2679
|
+
type: "plan",
|
|
2680
|
+
entries: update.entries.map((e) => ({ content: e.content, priority: e.priority, status: e.status })),
|
|
2681
|
+
timestamp: ts
|
|
2682
|
+
}];
|
|
2683
|
+
case "user_message_chunk":
|
|
2684
|
+
case "available_commands_update":
|
|
2685
|
+
case "current_mode_update":
|
|
2686
|
+
case "usage_update":
|
|
2687
|
+
case "plan_update":
|
|
2688
|
+
case "plan_removed":
|
|
2689
|
+
case "config_option_update":
|
|
2690
|
+
case "session_info_update":
|
|
2691
|
+
return [];
|
|
2692
|
+
default:
|
|
2693
|
+
return [];
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
class AcpClientPeer {
|
|
2698
|
+
deps;
|
|
2699
|
+
conn;
|
|
2700
|
+
updates = new AsyncQueue;
|
|
2701
|
+
sessionId;
|
|
2702
|
+
pendingPause = null;
|
|
2703
|
+
cancelled = false;
|
|
2704
|
+
abortPrompt = null;
|
|
2705
|
+
constructor(deps) {
|
|
2706
|
+
this.deps = deps;
|
|
2707
|
+
const handler = {
|
|
2708
|
+
sessionUpdate: (params) => {
|
|
2709
|
+
for (const msg of normalizeSessionUpdate(params.update))
|
|
2710
|
+
this.updates.push(msg);
|
|
2711
|
+
},
|
|
2712
|
+
requestPermission: (params) => this.onRequestPermission(params)
|
|
2713
|
+
};
|
|
2714
|
+
this.conn = new ClientSideConnection((_agent) => handler, this.deps.stream);
|
|
2715
|
+
if (this.deps.signal) {
|
|
2716
|
+
if (this.deps.signal.aborted)
|
|
2717
|
+
this.cancel();
|
|
2718
|
+
else
|
|
2719
|
+
this.deps.signal.addEventListener("abort", () => this.cancel(), { once: true });
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
async initialize() {
|
|
2723
|
+
const res = await this.conn.initialize({
|
|
2724
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2725
|
+
clientCapabilities: {
|
|
2726
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
2727
|
+
terminal: false
|
|
2728
|
+
}
|
|
2729
|
+
});
|
|
2730
|
+
return { protocolVersion: res.protocolVersion };
|
|
2731
|
+
}
|
|
2732
|
+
async startSession(cwd) {
|
|
2733
|
+
const res = await this.conn.newSession({ cwd, mcpServers: [] });
|
|
2734
|
+
this.sessionId = res.sessionId;
|
|
2735
|
+
return res.sessionId;
|
|
2736
|
+
}
|
|
2737
|
+
async* prompt(text) {
|
|
2738
|
+
if (!this.sessionId)
|
|
2739
|
+
throw new Error("AcpClientPeer.prompt called before startSession");
|
|
2740
|
+
const updates = new AsyncQueue;
|
|
2741
|
+
this.updates = updates;
|
|
2742
|
+
const aborted = new Promise((resolve) => {
|
|
2743
|
+
this.abortPrompt = () => {
|
|
2744
|
+
resolve();
|
|
2745
|
+
updates.close();
|
|
2746
|
+
};
|
|
2747
|
+
});
|
|
2748
|
+
if (this.cancelled)
|
|
2749
|
+
this.abortPrompt();
|
|
2750
|
+
const promptPromise = this.conn.prompt({ sessionId: this.sessionId, prompt: [{ type: "text", text }] }).then((res) => {
|
|
2751
|
+
updates.close();
|
|
2752
|
+
return { ok: true, res };
|
|
2753
|
+
}, (err) => {
|
|
2754
|
+
updates.close();
|
|
2755
|
+
return { ok: false, err };
|
|
2756
|
+
});
|
|
2757
|
+
for await (const msg of updates) {
|
|
2758
|
+
yield msg;
|
|
2759
|
+
}
|
|
2760
|
+
if (this.pendingPause !== null) {
|
|
2761
|
+
const pause = this.pendingPause;
|
|
2762
|
+
this.pendingPause = null;
|
|
2763
|
+
this.abortPrompt = null;
|
|
2764
|
+
throw pause;
|
|
2765
|
+
}
|
|
2766
|
+
const settled = await Promise.race([
|
|
2767
|
+
promptPromise.then((o) => ({ kind: "prompt", o })),
|
|
2768
|
+
aborted.then(() => ({ kind: "aborted" }))
|
|
2769
|
+
]);
|
|
2770
|
+
this.abortPrompt = null;
|
|
2771
|
+
if (settled.kind === "aborted")
|
|
2772
|
+
return;
|
|
2773
|
+
const outcome = settled.o;
|
|
2774
|
+
if (!outcome.ok) {
|
|
2775
|
+
yield { type: "error", text: formatError(outcome.err), timestamp: now3() };
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
const stopReason = outcome.res.stopReason;
|
|
2779
|
+
if (stopReason === "refusal") {
|
|
2780
|
+
yield { type: "error", text: "agent refused the request", timestamp: now3() };
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
const usage = outcome.res.usage;
|
|
2784
|
+
if (usage) {
|
|
2785
|
+
yield {
|
|
2786
|
+
type: "usage",
|
|
2787
|
+
inputTokens: usage.inputTokens,
|
|
2788
|
+
outputTokens: usage.outputTokens,
|
|
2789
|
+
cacheReadTokens: usage.cachedReadTokens ?? 0,
|
|
2790
|
+
cacheCreationTokens: usage.cachedWriteTokens ?? 0,
|
|
2791
|
+
durationMs: 0,
|
|
2792
|
+
numTurns: 1,
|
|
2793
|
+
timestamp: now3()
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
get currentSessionId() {
|
|
2798
|
+
return this.sessionId;
|
|
2799
|
+
}
|
|
2800
|
+
cancel() {
|
|
2801
|
+
if (this.cancelled) {
|
|
2802
|
+
this.abortPrompt?.();
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
this.cancelled = true;
|
|
2806
|
+
if (this.sessionId) {
|
|
2807
|
+
this.conn.cancel({ sessionId: this.sessionId }).catch(() => {});
|
|
2808
|
+
}
|
|
2809
|
+
this.abortPrompt?.();
|
|
2810
|
+
}
|
|
2811
|
+
close() {
|
|
2812
|
+
this.abortPrompt?.();
|
|
2813
|
+
this.updates.close();
|
|
2814
|
+
}
|
|
2815
|
+
async onRequestPermission(params) {
|
|
2816
|
+
if (this.cancelled || this.deps.signal?.aborted) {
|
|
2817
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2818
|
+
}
|
|
2819
|
+
const tc = params.toolCall;
|
|
2820
|
+
const call = {
|
|
2821
|
+
toolName: tc.title || tc.kind || "tool",
|
|
2822
|
+
toolInput: tc.rawInput ?? {},
|
|
2823
|
+
toolUseId: tc.toolCallId
|
|
2824
|
+
};
|
|
2825
|
+
let verdict;
|
|
2826
|
+
try {
|
|
2827
|
+
verdict = await this.deps.gateToolCall(call, this.deps.procCtx());
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
if (isPauseSignal(err)) {
|
|
2830
|
+
this.pendingPause = err;
|
|
2831
|
+
this.cancel();
|
|
2832
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2833
|
+
}
|
|
2834
|
+
throw err;
|
|
2835
|
+
}
|
|
2836
|
+
if (verdict.kind === "abort") {
|
|
2837
|
+
this.cancel();
|
|
2838
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2839
|
+
}
|
|
2840
|
+
const wantedKinds = verdict.kind === "allow" ? OPTION_KIND_FOR_VERDICT.allow : OPTION_KIND_FOR_VERDICT.deny;
|
|
2841
|
+
const option = params.options.find((o) => wantedKinds.includes(o.kind)) ?? params.options[0];
|
|
2842
|
+
if (!option) {
|
|
2843
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2844
|
+
}
|
|
2845
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/runtimes/_cli-agent.ts
|
|
2850
|
+
function now4() {
|
|
2851
|
+
return new Date().toISOString();
|
|
2852
|
+
}
|
|
2501
2853
|
function shellQuote(value) {
|
|
2502
2854
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2503
2855
|
}
|
|
2856
|
+
var ACP_FALLBACK = Symbol("acp-fallback");
|
|
2857
|
+
var ACP_HANDSHAKE_TIMEOUT_MS = Number(process.env.AC_ACP_HANDSHAKE_TIMEOUT_MS) || 1e4;
|
|
2858
|
+
var ACP_TRANSPORT_READY = true;
|
|
2859
|
+
async function withHandshakeTimeout(p, ms) {
|
|
2860
|
+
let timer;
|
|
2861
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
2862
|
+
timer = setTimeout(() => reject(new Error(`ACP handshake timed out after ${ms}ms`)), ms);
|
|
2863
|
+
});
|
|
2864
|
+
try {
|
|
2865
|
+
return await Promise.race([p, timeout]);
|
|
2866
|
+
} finally {
|
|
2867
|
+
if (timer)
|
|
2868
|
+
clearTimeout(timer);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2504
2871
|
|
|
2505
2872
|
class CliAgentRunner {
|
|
2506
2873
|
sandbox;
|
|
@@ -2508,6 +2875,16 @@ class CliAgentRunner {
|
|
|
2508
2875
|
spec;
|
|
2509
2876
|
configModel;
|
|
2510
2877
|
kind;
|
|
2878
|
+
get acpCapable() {
|
|
2879
|
+
return this.spec.acp !== undefined && this.acpTransportReady && typeof this.sandbox.commands.spawnDuplex === "function";
|
|
2880
|
+
}
|
|
2881
|
+
get supportsToolCallProcessor() {
|
|
2882
|
+
return this.acpCapable;
|
|
2883
|
+
}
|
|
2884
|
+
acpDecision;
|
|
2885
|
+
acpSession;
|
|
2886
|
+
currentAcpProcCtx;
|
|
2887
|
+
acpTransportReady = ACP_TRANSPORT_READY;
|
|
2511
2888
|
constructor(sandbox, options, spec, configModel) {
|
|
2512
2889
|
this.sandbox = sandbox;
|
|
2513
2890
|
this.options = options;
|
|
@@ -2515,9 +2892,37 @@ class CliAgentRunner {
|
|
|
2515
2892
|
this.configModel = configModel;
|
|
2516
2893
|
this.kind = spec.kind;
|
|
2517
2894
|
}
|
|
2895
|
+
enableAcpForTesting() {
|
|
2896
|
+
this.acpTransportReady = true;
|
|
2897
|
+
}
|
|
2518
2898
|
get model() {
|
|
2519
2899
|
return this.configModel ?? this.options.model ?? this.spec.defaultModel;
|
|
2520
2900
|
}
|
|
2901
|
+
async gateToolCall(call, ctx) {
|
|
2902
|
+
const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
|
|
2903
|
+
if (verdict.kind === "continue")
|
|
2904
|
+
return { kind: "allow", call: verdict.value };
|
|
2905
|
+
return verdict;
|
|
2906
|
+
}
|
|
2907
|
+
buildProcCtx(signal, iteration) {
|
|
2908
|
+
const agentId = this.options.agentId ?? "agent";
|
|
2909
|
+
const iter = iteration ?? 1;
|
|
2910
|
+
return {
|
|
2911
|
+
requestContext: this.options.requestContext ?? RequestContext.fromReserved({
|
|
2912
|
+
teamId: "",
|
|
2913
|
+
runId: "",
|
|
2914
|
+
workflowId: "",
|
|
2915
|
+
factoryId: null,
|
|
2916
|
+
apiKeyScopes: [],
|
|
2917
|
+
parentRunId: null
|
|
2918
|
+
}),
|
|
2919
|
+
abortSignal: signal ?? new AbortController().signal,
|
|
2920
|
+
retryCount: 0,
|
|
2921
|
+
agentId,
|
|
2922
|
+
iteration: iter,
|
|
2923
|
+
pause: boundProcessorPause(this.options.pause, { agentId, iteration: iter })
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2521
2926
|
installed = false;
|
|
2522
2927
|
async ensureInstalled() {
|
|
2523
2928
|
if (this.installed)
|
|
@@ -2534,7 +2939,133 @@ class CliAgentRunner {
|
|
|
2534
2939
|
this.installed = true;
|
|
2535
2940
|
}
|
|
2536
2941
|
async* sendMessage(opts) {
|
|
2537
|
-
yield { type: "init", sessionId: opts.sessionId ?? "", timestamp:
|
|
2942
|
+
yield { type: "init", sessionId: opts.sessionId ?? "", timestamp: now4() };
|
|
2943
|
+
if (this.acpCapable && this.acpDecision !== "jsonl") {
|
|
2944
|
+
let fellBack = false;
|
|
2945
|
+
try {
|
|
2946
|
+
for await (const ev of this.sendMessageAcp(opts)) {
|
|
2947
|
+
if (ev === ACP_FALLBACK) {
|
|
2948
|
+
fellBack = true;
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
yield ev;
|
|
2952
|
+
}
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
if (isPauseSignal(err))
|
|
2955
|
+
throw err;
|
|
2956
|
+
yield { type: "error", text: formatError(err), timestamp: now4() };
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (!fellBack) {
|
|
2960
|
+
this.acpDecision = "acp";
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
this.acpDecision = "jsonl";
|
|
2964
|
+
}
|
|
2965
|
+
yield* this.sendMessageJsonl(opts);
|
|
2966
|
+
}
|
|
2967
|
+
async teardownAcp(proc, peer) {
|
|
2968
|
+
peer.close();
|
|
2969
|
+
try {
|
|
2970
|
+
await proc.stream.writable.close();
|
|
2971
|
+
} catch {}
|
|
2972
|
+
proc.kill();
|
|
2973
|
+
proc.exited.catch(() => {});
|
|
2974
|
+
}
|
|
2975
|
+
async ensureAcpSession(opts) {
|
|
2976
|
+
if (this.acpSession)
|
|
2977
|
+
return this.acpSession;
|
|
2978
|
+
await this.ensureInstalled();
|
|
2979
|
+
const acp = this.spec.acp;
|
|
2980
|
+
const proc = this.spawnAcpProcess(acp, opts.signal);
|
|
2981
|
+
const peer = new AcpClientPeer({
|
|
2982
|
+
stream: proc.stream,
|
|
2983
|
+
gateToolCall: (call, ctx) => this.gateToolCall(call, ctx),
|
|
2984
|
+
procCtx: () => this.currentAcpProcCtx ?? this.buildProcCtx(opts.signal, opts.iteration),
|
|
2985
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
2986
|
+
});
|
|
2987
|
+
let negotiated;
|
|
2988
|
+
try {
|
|
2989
|
+
({ protocolVersion: negotiated } = await withHandshakeTimeout(peer.initialize(), ACP_HANDSHAKE_TIMEOUT_MS));
|
|
2990
|
+
} catch {
|
|
2991
|
+
await this.teardownAcp(proc, peer);
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
if (negotiated !== ACP_PROTOCOL_VERSION) {
|
|
2995
|
+
await this.teardownAcp(proc, peer);
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
let sessionId;
|
|
2999
|
+
try {
|
|
3000
|
+
sessionId = await withHandshakeTimeout(peer.startSession(this.options.cwd ?? "/"), ACP_HANDSHAKE_TIMEOUT_MS);
|
|
3001
|
+
} catch {
|
|
3002
|
+
await this.teardownAcp(proc, peer);
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
this.acpSession = { proc, peer, sessionId };
|
|
3006
|
+
return this.acpSession;
|
|
3007
|
+
}
|
|
3008
|
+
async* sendMessageAcp(opts) {
|
|
3009
|
+
this.currentAcpProcCtx = this.buildProcCtx(opts.signal, opts.iteration);
|
|
3010
|
+
const session = await this.ensureAcpSession(opts);
|
|
3011
|
+
if (!session) {
|
|
3012
|
+
yield ACP_FALLBACK;
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const { proc, peer, sessionId } = session;
|
|
3016
|
+
let sawError = false;
|
|
3017
|
+
let stalled = false;
|
|
3018
|
+
let watchdog;
|
|
3019
|
+
const armWatchdog = () => {
|
|
3020
|
+
if (watchdog)
|
|
3021
|
+
clearTimeout(watchdog);
|
|
3022
|
+
watchdog = setTimeout(() => {
|
|
3023
|
+
stalled = true;
|
|
3024
|
+
peer.cancel();
|
|
3025
|
+
}, ACP_HANDSHAKE_TIMEOUT_MS);
|
|
3026
|
+
};
|
|
3027
|
+
try {
|
|
3028
|
+
armWatchdog();
|
|
3029
|
+
for await (const msg of peer.prompt(opts.prompt)) {
|
|
3030
|
+
armWatchdog();
|
|
3031
|
+
if (msg.type === "error")
|
|
3032
|
+
sawError = true;
|
|
3033
|
+
yield msg;
|
|
3034
|
+
}
|
|
3035
|
+
} finally {
|
|
3036
|
+
if (watchdog)
|
|
3037
|
+
clearTimeout(watchdog);
|
|
3038
|
+
}
|
|
3039
|
+
if (stalled && !sawError) {
|
|
3040
|
+
await this.teardownAcp(proc, peer);
|
|
3041
|
+
this.acpSession = undefined;
|
|
3042
|
+
yield { type: "error", text: `${this.spec.kind} prompt turn stalled (no activity for ${ACP_HANDSHAKE_TIMEOUT_MS}ms)`, timestamp: now4() };
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
if (!sawError)
|
|
3046
|
+
yield { type: "done", sessionId: peer.currentSessionId ?? sessionId, timestamp: now4() };
|
|
3047
|
+
}
|
|
3048
|
+
spawnAcpProcess(acp, signal) {
|
|
3049
|
+
const spawnDuplex = this.sandbox.commands.spawnDuplex;
|
|
3050
|
+
if (!spawnDuplex) {
|
|
3051
|
+
throw new Error("spawnAcpProcess called on a provider without commands.spawnDuplex");
|
|
3052
|
+
}
|
|
3053
|
+
const cmd = `${acp.command} ${acp.args.map(shellQuote).join(" ")}`.trim();
|
|
3054
|
+
const agentEnv = { ...acp.env, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} };
|
|
3055
|
+
const proc = spawnDuplex.call(this.sandbox.commands, cmd, {
|
|
3056
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
3057
|
+
...Object.keys(agentEnv).length > 0 ? { envs: agentEnv } : {}
|
|
3058
|
+
});
|
|
3059
|
+
const kill = () => proc.kill();
|
|
3060
|
+
if (signal) {
|
|
3061
|
+
if (signal.aborted)
|
|
3062
|
+
kill();
|
|
3063
|
+
else
|
|
3064
|
+
signal.addEventListener("abort", kill, { once: true });
|
|
3065
|
+
}
|
|
3066
|
+
return { stream: ndJsonStream(proc.stdin, proc.stdout), exited: proc.exited, kill };
|
|
3067
|
+
}
|
|
3068
|
+
async* sendMessageJsonl(opts) {
|
|
2538
3069
|
let sessionId = opts.sessionId;
|
|
2539
3070
|
let sawError = false;
|
|
2540
3071
|
try {
|
|
@@ -2592,13 +3123,13 @@ class CliAgentRunner {
|
|
|
2592
3123
|
const res = await runPromise;
|
|
2593
3124
|
if (res.exitCode !== 0 && !sawError) {
|
|
2594
3125
|
const tail = (res.stderr ?? "").slice(-2000);
|
|
2595
|
-
yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp:
|
|
3126
|
+
yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now4() };
|
|
2596
3127
|
return;
|
|
2597
3128
|
}
|
|
2598
3129
|
if (!sawError)
|
|
2599
|
-
yield { type: "done", sessionId: sessionId ?? "", timestamp:
|
|
3130
|
+
yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
|
|
2600
3131
|
} catch (err) {
|
|
2601
|
-
yield { type: "error", text: formatError(err), timestamp:
|
|
3132
|
+
yield { type: "error", text: formatError(err), timestamp: now4() };
|
|
2602
3133
|
}
|
|
2603
3134
|
}
|
|
2604
3135
|
}
|
|
@@ -2609,13 +3140,19 @@ function createCliAgentRuntime(spec, configModel) {
|
|
|
2609
3140
|
}
|
|
2610
3141
|
|
|
2611
3142
|
// src/runtimes/codex.ts
|
|
2612
|
-
function
|
|
3143
|
+
function now5() {
|
|
2613
3144
|
return new Date().toISOString();
|
|
2614
3145
|
}
|
|
3146
|
+
var CODEX_ACP_ADAPTER = "@agentclientprotocol/codex-acp@0.1.0";
|
|
2615
3147
|
var codexSpec = {
|
|
2616
3148
|
kind: "codex",
|
|
2617
3149
|
authEnv: "CODEX_API_KEY",
|
|
2618
3150
|
bin: "codex",
|
|
3151
|
+
acp: {
|
|
3152
|
+
command: "npx",
|
|
3153
|
+
args: ["-y", CODEX_ACP_ADAPTER],
|
|
3154
|
+
...process.env.CODEX_PATH ? { env: { CODEX_PATH: process.env.CODEX_PATH } } : {}
|
|
3155
|
+
},
|
|
2619
3156
|
install: 'sudo npm install -g @openai/codex && (command -v codex >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/codex" /usr/local/bin/codex)',
|
|
2620
3157
|
promptPayload: (prompt) => prompt,
|
|
2621
3158
|
buildCommand: ({ promptPath, sessionId, model, cwd }) => {
|
|
@@ -2631,7 +3168,7 @@ var codexSpec = {
|
|
|
2631
3168
|
},
|
|
2632
3169
|
extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
|
|
2633
3170
|
mapEvent: (p) => {
|
|
2634
|
-
const ts =
|
|
3171
|
+
const ts = now5();
|
|
2635
3172
|
switch (p.type) {
|
|
2636
3173
|
case "item.started":
|
|
2637
3174
|
case "item.completed": {
|
|
@@ -2686,7 +3223,7 @@ function createCodexRuntime(config = {}) {
|
|
|
2686
3223
|
}
|
|
2687
3224
|
var codex_default = createCodexRuntime();
|
|
2688
3225
|
// src/runtimes/amp.ts
|
|
2689
|
-
function
|
|
3226
|
+
function now6() {
|
|
2690
3227
|
return new Date().toISOString();
|
|
2691
3228
|
}
|
|
2692
3229
|
function blocks(msg) {
|
|
@@ -2706,7 +3243,7 @@ var ampSpec = {
|
|
|
2706
3243
|
},
|
|
2707
3244
|
extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
|
|
2708
3245
|
mapEvent: (p) => {
|
|
2709
|
-
const ts =
|
|
3246
|
+
const ts = now6();
|
|
2710
3247
|
if (p.type === "assistant") {
|
|
2711
3248
|
return blocks(p).flatMap((b) => {
|
|
2712
3249
|
if (b.type === "text")
|
|
@@ -2751,6 +3288,7 @@ var amp_default = createAmpRuntime();
|
|
|
2751
3288
|
import { promises as fs2 } from "node:fs";
|
|
2752
3289
|
import { dirname as dirname2 } from "node:path";
|
|
2753
3290
|
import { spawn } from "node:child_process";
|
|
3291
|
+
import { Readable, Writable } from "node:stream";
|
|
2754
3292
|
import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
|
|
2755
3293
|
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
2756
3294
|
import pRetry from "p-retry";
|
|
@@ -2779,6 +3317,26 @@ var SANDBOX_VCPUS = {
|
|
|
2779
3317
|
"32vcpu-64gb": 32
|
|
2780
3318
|
};
|
|
2781
3319
|
var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
|
|
3320
|
+
var E2B_TEMPLATE_SIZES = [
|
|
3321
|
+
"2vcpu-4gb",
|
|
3322
|
+
"4vcpu-8gb"
|
|
3323
|
+
];
|
|
3324
|
+
function isE2bSupportedSize(size) {
|
|
3325
|
+
return E2B_TEMPLATE_SIZES.includes(size);
|
|
3326
|
+
}
|
|
3327
|
+
function e2bMachineSpec(size) {
|
|
3328
|
+
const cpuCount = SANDBOX_VCPUS[size];
|
|
3329
|
+
return { cpuCount, memoryMB: cpuCount * 2048 };
|
|
3330
|
+
}
|
|
3331
|
+
function e2bBaseTemplate(size) {
|
|
3332
|
+
return `agent-compose-base-${size}`;
|
|
3333
|
+
}
|
|
3334
|
+
function e2bAgentEnvTemplate(size) {
|
|
3335
|
+
return `agent-env-${size}`;
|
|
3336
|
+
}
|
|
3337
|
+
function isPlatformE2bTemplateAlias(id) {
|
|
3338
|
+
return E2B_TEMPLATE_SIZES.some((size) => id === e2bBaseTemplate(size) || id === e2bAgentEnvTemplate(size));
|
|
3339
|
+
}
|
|
2782
3340
|
function makeSandboxProvider(sb) {
|
|
2783
3341
|
const commands = sb.commands;
|
|
2784
3342
|
return {
|
|
@@ -2786,18 +3344,33 @@ function makeSandboxProvider(sb) {
|
|
|
2786
3344
|
commands: {
|
|
2787
3345
|
async run(cmd, opts) {
|
|
2788
3346
|
let stderr = "";
|
|
2789
|
-
const
|
|
2790
|
-
|
|
3347
|
+
const { sudo, ...rest } = opts ?? {};
|
|
3348
|
+
const runOpts = {
|
|
3349
|
+
...rest,
|
|
3350
|
+
...sudo ? { user: "root" } : {},
|
|
2791
3351
|
onStderr: (chunk) => {
|
|
2792
3352
|
stderr += chunk;
|
|
2793
3353
|
opts?.onStderr?.(chunk);
|
|
2794
3354
|
}
|
|
2795
|
-
});
|
|
2796
|
-
return {
|
|
2797
|
-
exitCode: result.exitCode,
|
|
2798
|
-
stdout: result.stdout,
|
|
2799
|
-
stderr: typeof result.stderr === "string" ? result.stderr : stderr
|
|
2800
3355
|
};
|
|
3356
|
+
try {
|
|
3357
|
+
const result = await commands.run(cmd, runOpts);
|
|
3358
|
+
return {
|
|
3359
|
+
exitCode: result.exitCode,
|
|
3360
|
+
stdout: result.stdout,
|
|
3361
|
+
stderr: typeof result.stderr === "string" ? result.stderr : stderr
|
|
3362
|
+
};
|
|
3363
|
+
} catch (e) {
|
|
3364
|
+
const ce = e;
|
|
3365
|
+
if (typeof ce.exitCode === "number") {
|
|
3366
|
+
return {
|
|
3367
|
+
exitCode: ce.exitCode,
|
|
3368
|
+
stdout: typeof ce.stdout === "string" ? ce.stdout : "",
|
|
3369
|
+
stderr: typeof ce.stderr === "string" ? ce.stderr : stderr
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
throw e;
|
|
3373
|
+
}
|
|
2801
3374
|
}
|
|
2802
3375
|
},
|
|
2803
3376
|
files: {
|
|
@@ -2805,7 +3378,9 @@ function makeSandboxProvider(sb) {
|
|
|
2805
3378
|
await sb.files.write(path, content);
|
|
2806
3379
|
}
|
|
2807
3380
|
},
|
|
2808
|
-
kill
|
|
3381
|
+
async kill() {
|
|
3382
|
+
await sb.kill();
|
|
3383
|
+
}
|
|
2809
3384
|
};
|
|
2810
3385
|
}
|
|
2811
3386
|
function e2bMaxSandboxMs() {
|
|
@@ -2832,6 +3407,9 @@ function makeE2bSandboxProvider(sb) {
|
|
|
2832
3407
|
async snapshot() {
|
|
2833
3408
|
const { snapshotId } = await sb.createSnapshot();
|
|
2834
3409
|
return { snapshotId };
|
|
3410
|
+
},
|
|
3411
|
+
async updateNetworkPolicy(policy) {
|
|
3412
|
+
await sb.updateNetwork(toE2bNetwork(policy));
|
|
2835
3413
|
}
|
|
2836
3414
|
};
|
|
2837
3415
|
}
|
|
@@ -2923,6 +3501,31 @@ function toVercelNetworkPolicy(policy) {
|
|
|
2923
3501
|
}
|
|
2924
3502
|
return { ...policy, allow };
|
|
2925
3503
|
}
|
|
3504
|
+
function toE2bNetwork(policy) {
|
|
3505
|
+
if (policy === "allow-all")
|
|
3506
|
+
return {};
|
|
3507
|
+
if (policy === "deny-all")
|
|
3508
|
+
return { denyOut: ({ allTraffic }) => [allTraffic] };
|
|
3509
|
+
const allowHosts = Array.isArray(policy.allow) ? [...policy.allow] : Object.keys(policy.allow ?? {});
|
|
3510
|
+
const subnetAllow = policy.subnets?.allow ?? [];
|
|
3511
|
+
const allowOut = [...new Set([...allowHosts, ...subnetAllow])];
|
|
3512
|
+
const network = {
|
|
3513
|
+
denyOut: ({ allTraffic }) => [allTraffic]
|
|
3514
|
+
};
|
|
3515
|
+
if (allowOut.length > 0)
|
|
3516
|
+
network.allowOut = allowOut;
|
|
3517
|
+
if (!Array.isArray(policy.allow) && policy.allow) {
|
|
3518
|
+
const rules = {};
|
|
3519
|
+
for (const [host, hostRules] of Object.entries(policy.allow)) {
|
|
3520
|
+
const hostTransforms = hostRules.flatMap((rule) => (rule.transform ?? []).filter((t) => t.headers && Object.keys(t.headers).length > 0).map((t) => ({ transform: { headers: t.headers } })));
|
|
3521
|
+
if (hostTransforms.length > 0)
|
|
3522
|
+
rules[host] = hostTransforms;
|
|
3523
|
+
}
|
|
3524
|
+
if (Object.keys(rules).length > 0)
|
|
3525
|
+
network.rules = rules;
|
|
3526
|
+
}
|
|
3527
|
+
return network;
|
|
3528
|
+
}
|
|
2926
3529
|
var VERCEL_MAX_TAGS = 5;
|
|
2927
3530
|
function buildVercelTags(metadata) {
|
|
2928
3531
|
const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
|
|
@@ -3031,6 +3634,38 @@ function makeLocalSandboxProvider() {
|
|
|
3031
3634
|
proc.on("error", reject);
|
|
3032
3635
|
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
|
|
3033
3636
|
});
|
|
3637
|
+
},
|
|
3638
|
+
spawnDuplex(cmd, opts) {
|
|
3639
|
+
const proc = spawn("sh", ["-c", cmd], {
|
|
3640
|
+
...opts?.cwd ? { cwd: opts.cwd } : {},
|
|
3641
|
+
env: { ...process.env, ...opts?.envs ?? {} },
|
|
3642
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3643
|
+
detached: true
|
|
3644
|
+
});
|
|
3645
|
+
let stderr = "";
|
|
3646
|
+
proc.stderr.setEncoding("utf8");
|
|
3647
|
+
proc.stderr.on("data", (chunk) => {
|
|
3648
|
+
stderr += chunk;
|
|
3649
|
+
});
|
|
3650
|
+
const exited = new Promise((resolve, reject) => {
|
|
3651
|
+
proc.on("error", reject);
|
|
3652
|
+
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stderr }));
|
|
3653
|
+
});
|
|
3654
|
+
return {
|
|
3655
|
+
stdin: Writable.toWeb(proc.stdin),
|
|
3656
|
+
stdout: Readable.toWeb(proc.stdout),
|
|
3657
|
+
exited,
|
|
3658
|
+
kill() {
|
|
3659
|
+
if (proc.pid === undefined)
|
|
3660
|
+
return;
|
|
3661
|
+
try {
|
|
3662
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
3663
|
+
} catch (err) {
|
|
3664
|
+
if (err.code !== "ESRCH")
|
|
3665
|
+
throw err;
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3034
3669
|
}
|
|
3035
3670
|
},
|
|
3036
3671
|
files: {
|
|
@@ -3098,17 +3733,36 @@ var SANDBOX_PROVIDERS = {
|
|
|
3098
3733
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3099
3734
|
const snap = await Snapshot.get({ snapshotId, ...creds });
|
|
3100
3735
|
await snap.delete();
|
|
3736
|
+
},
|
|
3737
|
+
snapshotExists: async (snapshotId, env) => {
|
|
3738
|
+
const { Snapshot } = await import("@vercel/sandbox");
|
|
3739
|
+
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3740
|
+
try {
|
|
3741
|
+
await Snapshot.get({ snapshotId, ...creds });
|
|
3742
|
+
return true;
|
|
3743
|
+
} catch (err) {
|
|
3744
|
+
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
3745
|
+
const status = err?.status ?? err?.statusCode;
|
|
3746
|
+
if (status === 404 || /\b404\b|not[\s_-]?found|no such snapshot/i.test(message))
|
|
3747
|
+
return false;
|
|
3748
|
+
throw err;
|
|
3749
|
+
}
|
|
3101
3750
|
}
|
|
3102
3751
|
},
|
|
3103
3752
|
e2b: {
|
|
3104
3753
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3105
|
-
create: async ({ template, timeoutMs, networkPolicy
|
|
3754
|
+
create: async ({ template, timeoutMs, networkPolicy, size, ...rest }) => {
|
|
3106
3755
|
const maxMs = e2bMaxSandboxMs();
|
|
3107
3756
|
if (timeoutMs > maxMs) {
|
|
3108
3757
|
console.warn(`[sandbox] e2b create timeout clamped: requested ${timeoutMs}ms exceeds the plan cap ${maxMs}ms — ` + `the sandbox dies at the cap, not the requested deadline. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
|
|
3109
3758
|
}
|
|
3110
|
-
const sandboxOpts = {
|
|
3111
|
-
|
|
3759
|
+
const sandboxOpts = {
|
|
3760
|
+
...rest,
|
|
3761
|
+
timeoutMs: Math.min(timeoutMs, maxMs),
|
|
3762
|
+
...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
|
|
3763
|
+
};
|
|
3764
|
+
const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
|
|
3765
|
+
const tmpl = template ?? (isE2bSupportedSize(resolvedSize) ? e2bBaseTemplate(resolvedSize) : undefined) ?? process.env.E2B_DEFAULT_TEMPLATE;
|
|
3112
3766
|
return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
|
|
3113
3767
|
},
|
|
3114
3768
|
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
|
|
@@ -3144,10 +3798,16 @@ var SANDBOX_PROVIDERS = {
|
|
|
3144
3798
|
},
|
|
3145
3799
|
"e2b-desktop": {
|
|
3146
3800
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3147
|
-
create: async ({ template, timeoutMs, networkPolicy
|
|
3801
|
+
create: async ({ template, timeoutMs, networkPolicy, size: _size, ...rest }) => {
|
|
3148
3802
|
if (!template)
|
|
3149
3803
|
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
3150
|
-
|
|
3804
|
+
if (networkPolicy && networkPolicy !== "allow-all") {
|
|
3805
|
+
throw new Error("E2B Desktop provider cannot enforce an egress network policy — `@e2b/desktop` bundles an e2b version that predates the native firewall. " + "e2b-desktop is registry-only and must not run with a confined policy.");
|
|
3806
|
+
}
|
|
3807
|
+
return makeDesktopSandboxProvider(await Desktop.create(template, {
|
|
3808
|
+
...rest,
|
|
3809
|
+
timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs())
|
|
3810
|
+
}));
|
|
3151
3811
|
}
|
|
3152
3812
|
}
|
|
3153
3813
|
};
|
|
@@ -3180,6 +3840,17 @@ ${missing.join(`
|
|
|
3180
3840
|
`)}`);
|
|
3181
3841
|
return def.deleteSnapshot(snapshotId, Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]])));
|
|
3182
3842
|
}
|
|
3843
|
+
async function snapshotResolves(provider, snapshotId) {
|
|
3844
|
+
const def = SANDBOX_PROVIDERS[provider];
|
|
3845
|
+
if (!def?.snapshotExists)
|
|
3846
|
+
throw new Error(`Provider "${provider}" does not support snapshotExists`);
|
|
3847
|
+
const missing = Object.entries(def.requiredEnv).filter(([k]) => !process.env[k]).map(([k, desc]) => ` ${k} — ${desc}`);
|
|
3848
|
+
if (missing.length > 0)
|
|
3849
|
+
throw new Error(`Sandbox provider "${provider}" requires env vars:
|
|
3850
|
+
${missing.join(`
|
|
3851
|
+
`)}`);
|
|
3852
|
+
return def.snapshotExists(snapshotId, Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]])));
|
|
3853
|
+
}
|
|
3183
3854
|
async function getSandboxQuotas() {
|
|
3184
3855
|
const out = {};
|
|
3185
3856
|
await Promise.all(Object.entries(SANDBOX_PROVIDERS).map(async ([name, def]) => {
|
|
@@ -3235,17 +3906,34 @@ class StepObservabilityCollector {
|
|
|
3235
3906
|
events = [];
|
|
3236
3907
|
subSteps = [];
|
|
3237
3908
|
liveEmitter;
|
|
3909
|
+
memoize;
|
|
3910
|
+
usedStepNames = new Set;
|
|
3238
3911
|
ackedSeqs = new Set;
|
|
3239
3912
|
inFlight = new Set;
|
|
3240
3913
|
constructor(opts = {}) {
|
|
3241
3914
|
this.liveEmitter = opts.liveEmitter;
|
|
3915
|
+
this.memoize = opts.memoize;
|
|
3242
3916
|
}
|
|
3243
3917
|
setMetadata = async (data) => {
|
|
3244
3918
|
Object.assign(this.metadata, data);
|
|
3245
3919
|
};
|
|
3246
3920
|
step = async (name, fn) => {
|
|
3921
|
+
if (this.usedStepNames.has(name)) {
|
|
3922
|
+
throw new Error(`ctx.step("${name}", …) reuses a step name already used in this step body. ` + `Durable step names must be unique — they key the resume-time memoise file.`);
|
|
3923
|
+
}
|
|
3924
|
+
this.usedStepNames.add(name);
|
|
3247
3925
|
const startedAt = Date.now();
|
|
3248
3926
|
try {
|
|
3927
|
+
if (this.memoize) {
|
|
3928
|
+
const { value, restored } = await this.memoize(name, fn);
|
|
3929
|
+
this.subSteps.push({
|
|
3930
|
+
name,
|
|
3931
|
+
startedAt,
|
|
3932
|
+
durationMs: restored ? 0 : Date.now() - startedAt,
|
|
3933
|
+
status: restored ? "restored" : "completed"
|
|
3934
|
+
});
|
|
3935
|
+
return value;
|
|
3936
|
+
}
|
|
3249
3937
|
const result = await fn();
|
|
3250
3938
|
this.subSteps.push({
|
|
3251
3939
|
name,
|
|
@@ -3309,19 +3997,19 @@ class StepObservabilityCollector {
|
|
|
3309
3997
|
}
|
|
3310
3998
|
|
|
3311
3999
|
// src/pause/checkpoint.ts
|
|
3312
|
-
async function
|
|
4000
|
+
async function memoize(name, fn) {
|
|
3313
4001
|
const existing = await readCheckpoint(name);
|
|
3314
4002
|
if (existing.found)
|
|
3315
|
-
return existing.value;
|
|
4003
|
+
return { value: existing.value, restored: true };
|
|
3316
4004
|
const value = await fn();
|
|
3317
4005
|
await writeCheckpoint(name, value);
|
|
3318
|
-
return value;
|
|
4006
|
+
return { value, restored: false };
|
|
3319
4007
|
}
|
|
3320
|
-
function
|
|
4008
|
+
function scopedMemoize(scope) {
|
|
3321
4009
|
assertSafeCheckpointName(scope);
|
|
3322
4010
|
return async (name, fn) => {
|
|
3323
4011
|
assertSafeCheckpointName(name);
|
|
3324
|
-
return
|
|
4012
|
+
return memoize(`${scope}.${name}`, fn);
|
|
3325
4013
|
};
|
|
3326
4014
|
}
|
|
3327
4015
|
|
|
@@ -3389,7 +4077,10 @@ async function runWorkflowSingleStep(opts) {
|
|
|
3389
4077
|
const parsedInput = step.input.safeParse(opts.input);
|
|
3390
4078
|
if (!parsedInput.success)
|
|
3391
4079
|
throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
3392
|
-
const collector = new StepObservabilityCollector({
|
|
4080
|
+
const collector = new StepObservabilityCollector({
|
|
4081
|
+
liveEmitter: opts.liveAgentEventEmitter,
|
|
4082
|
+
memoize: scopedMemoize(`step${opts.stepIndex}`)
|
|
4083
|
+
});
|
|
3393
4084
|
const coord = { runId: opts.run.id, stepIndex: opts.stepIndex };
|
|
3394
4085
|
const pauseWithKind = function(req, kind) {
|
|
3395
4086
|
return corePause(req, coord, kind);
|
|
@@ -3410,7 +4101,6 @@ async function runWorkflowSingleStep(opts) {
|
|
|
3410
4101
|
setMetadata: collector.setMetadata,
|
|
3411
4102
|
step: collector.step,
|
|
3412
4103
|
agentEvents: collector.agentEvents,
|
|
3413
|
-
checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
|
|
3414
4104
|
pause,
|
|
3415
4105
|
...buildPauseWrappers(pauseWithKind)
|
|
3416
4106
|
};
|
|
@@ -3466,7 +4156,10 @@ async function runWorkflowSteps(opts) {
|
|
|
3466
4156
|
await opts.onStepFailed?.(i, step.name, err, 0);
|
|
3467
4157
|
throw err;
|
|
3468
4158
|
}
|
|
3469
|
-
const collector = new StepObservabilityCollector({
|
|
4159
|
+
const collector = new StepObservabilityCollector({
|
|
4160
|
+
liveEmitter: opts.liveAgentEventEmitter,
|
|
4161
|
+
memoize: scopedMemoize(`step${i}`)
|
|
4162
|
+
});
|
|
3470
4163
|
const coord = { runId: run.id, stepIndex: i };
|
|
3471
4164
|
const pauseWithKind = function(req, kind) {
|
|
3472
4165
|
return corePause(req, coord, kind);
|
|
@@ -3485,7 +4178,6 @@ async function runWorkflowSteps(opts) {
|
|
|
3485
4178
|
setMetadata: collector.setMetadata,
|
|
3486
4179
|
step: collector.step,
|
|
3487
4180
|
agentEvents: collector.agentEvents,
|
|
3488
|
-
checkpoint: scopedCheckpoint(`step${i}`),
|
|
3489
4181
|
pause,
|
|
3490
4182
|
...buildPauseWrappers(pauseWithKind)
|
|
3491
4183
|
};
|
|
@@ -3682,32 +4374,32 @@ function stepResultFilePath(token) {
|
|
|
3682
4374
|
}
|
|
3683
4375
|
// src/step-invocation/invoker.ts
|
|
3684
4376
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3685
|
-
import { z as
|
|
3686
|
-
var StepObservabilitySchema =
|
|
3687
|
-
metadata:
|
|
3688
|
-
events:
|
|
3689
|
-
subSteps:
|
|
3690
|
-
name:
|
|
3691
|
-
startedAt:
|
|
3692
|
-
durationMs:
|
|
3693
|
-
status:
|
|
3694
|
-
error:
|
|
4377
|
+
import { z as z11 } from "zod";
|
|
4378
|
+
var StepObservabilitySchema = z11.object({
|
|
4379
|
+
metadata: z11.record(z11.string(), z11.unknown()).optional(),
|
|
4380
|
+
events: z11.array(z11.unknown()).optional(),
|
|
4381
|
+
subSteps: z11.array(z11.object({
|
|
4382
|
+
name: z11.string(),
|
|
4383
|
+
startedAt: z11.number(),
|
|
4384
|
+
durationMs: z11.number(),
|
|
4385
|
+
status: z11.enum(["completed", "failed"]),
|
|
4386
|
+
error: z11.string().optional()
|
|
3695
4387
|
})).optional()
|
|
3696
4388
|
}).passthrough();
|
|
3697
|
-
var StepResultEnvelopeSchema =
|
|
3698
|
-
|
|
3699
|
-
ok:
|
|
3700
|
-
output:
|
|
4389
|
+
var StepResultEnvelopeSchema = z11.discriminatedUnion("ok", [
|
|
4390
|
+
z11.object({
|
|
4391
|
+
ok: z11.literal(true),
|
|
4392
|
+
output: z11.unknown().optional(),
|
|
3701
4393
|
observability: StepObservabilitySchema.optional()
|
|
3702
4394
|
}),
|
|
3703
|
-
|
|
3704
|
-
ok:
|
|
3705
|
-
kind:
|
|
3706
|
-
error:
|
|
4395
|
+
z11.object({
|
|
4396
|
+
ok: z11.literal(false),
|
|
4397
|
+
kind: z11.enum(["protocol", "user-step"]),
|
|
4398
|
+
error: z11.string().optional()
|
|
3707
4399
|
})
|
|
3708
4400
|
]);
|
|
3709
|
-
var StepPauseEnvelopeSchema =
|
|
3710
|
-
pauseId:
|
|
4401
|
+
var StepPauseEnvelopeSchema = z11.object({
|
|
4402
|
+
pauseId: z11.string().min(1),
|
|
3711
4403
|
pauseRequest: StepPauseRequestSchema,
|
|
3712
4404
|
observability: StepObservabilitySchema.optional()
|
|
3713
4405
|
});
|
|
@@ -4000,8 +4692,43 @@ async function serveStep(handler) {
|
|
|
4000
4692
|
await emitResult(token, payload);
|
|
4001
4693
|
process.exit(exitCode);
|
|
4002
4694
|
}
|
|
4695
|
+
// src/agent/local-pause-request.ts
|
|
4696
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4697
|
+
import { join as join3 } from "node:path";
|
|
4698
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4699
|
+
var UNSCOPED = "_unscoped";
|
|
4700
|
+
var requestsDir = () => join3(getStateDir(), "pause-requests");
|
|
4701
|
+
var markerPath = (agentId) => join3(requestsDir(), `${slug(agentId) || UNSCOPED}.json`);
|
|
4702
|
+
function slug(agentId) {
|
|
4703
|
+
return (agentId ?? "").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
4704
|
+
}
|
|
4705
|
+
function writeLocalPauseRequest(agentId, req) {
|
|
4706
|
+
mkdirSync2(requestsDir(), { recursive: true });
|
|
4707
|
+
const final = markerPath(agentId);
|
|
4708
|
+
const tmp = `${final}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
4709
|
+
writeFileSync2(tmp, JSON.stringify(req), "utf8");
|
|
4710
|
+
renameSync(tmp, final);
|
|
4711
|
+
}
|
|
4712
|
+
function consumeLocalPauseRequest(agentId) {
|
|
4713
|
+
const paths = [...new Set([markerPath(agentId), markerPath(null)])];
|
|
4714
|
+
for (const path of paths) {
|
|
4715
|
+
if (!existsSync(path))
|
|
4716
|
+
continue;
|
|
4717
|
+
try {
|
|
4718
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
4719
|
+
rmSync(path, { force: true });
|
|
4720
|
+
if (raw && typeof raw === "object" && typeof raw.reason === "string" && raw.reason.trim().length > 0) {
|
|
4721
|
+
const r = raw;
|
|
4722
|
+
return { reason: r.reason.trim(), ...Array.isArray(r.options) && r.options.length > 0 ? { options: r.options } : {} };
|
|
4723
|
+
}
|
|
4724
|
+
} catch {
|
|
4725
|
+
rmSync(path, { force: true });
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
return null;
|
|
4729
|
+
}
|
|
4003
4730
|
// src/agent/run-agent.ts
|
|
4004
|
-
import { z as
|
|
4731
|
+
import { z as z12 } from "zod";
|
|
4005
4732
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4006
4733
|
|
|
4007
4734
|
// src/agent/agent-context.ts
|
|
@@ -4065,11 +4792,19 @@ Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
|
|
|
4065
4792
|
|
|
4066
4793
|
## Pausing to ask the human — \`agentc pause\`
|
|
4067
4794
|
|
|
4068
|
-
When you can't or shouldn't proceed without a human, run \`agentc pause
|
|
4069
|
-
|
|
4795
|
+
When you can't or shouldn't proceed without a human, run \`agentc pause\`, then
|
|
4796
|
+
**END YOUR TURN**:
|
|
4797
|
+
|
|
4798
|
+
agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
|
|
4799
|
+
--option retry --option skip
|
|
4070
4800
|
|
|
4071
|
-
|
|
4072
|
-
|
|
4801
|
+
\`agentc pause\` does NOT block and does NOT print the answer. It records your
|
|
4802
|
+
question and returns immediately. The moment you end your turn, the run pauses
|
|
4803
|
+
(your sandbox is snapshotted and compute stops while the human decides) and the
|
|
4804
|
+
human's answer is delivered to you as your **next message** — you pick up
|
|
4805
|
+
exactly where you left off, with the answer in hand. So: ask, end your turn,
|
|
4806
|
+
and wait. Do NOT keep working, do NOT call more tools, and do NOT mark the task
|
|
4807
|
+
complete after pausing.
|
|
4073
4808
|
|
|
4074
4809
|
Reach for it the moment you hit — or foresee — any of these:
|
|
4075
4810
|
- **A wall only a human can clear:** a 401/403, a missing credential, an
|
|
@@ -4083,8 +4818,8 @@ Reach for it the moment you hit — or foresee — any of these:
|
|
|
4083
4818
|
several valid paths, a conflict with existing state, missing input only they have.
|
|
4084
4819
|
|
|
4085
4820
|
You compose the \`--reason\` (the ask) yourself; pass \`--option\` choices when
|
|
4086
|
-
there are clear ones, omit them for a free-form answer.
|
|
4087
|
-
|
|
4821
|
+
there are clear ones, omit them for a free-form answer. Each agent pauses
|
|
4822
|
+
independently — pausing doesn't stop the others.
|
|
4088
4823
|
|
|
4089
4824
|
## Credentials
|
|
4090
4825
|
|
|
@@ -4206,29 +4941,29 @@ function stripFrontmatter(content) {
|
|
|
4206
4941
|
return end === -1 ? content : content.slice(end + 4).trimStart();
|
|
4207
4942
|
}
|
|
4208
4943
|
function zodTypeName(schema) {
|
|
4209
|
-
if (schema instanceof
|
|
4944
|
+
if (schema instanceof z12.ZodString)
|
|
4210
4945
|
return "string";
|
|
4211
|
-
if (schema instanceof
|
|
4946
|
+
if (schema instanceof z12.ZodNumber)
|
|
4212
4947
|
return "number";
|
|
4213
|
-
if (schema instanceof
|
|
4948
|
+
if (schema instanceof z12.ZodBoolean)
|
|
4214
4949
|
return "boolean";
|
|
4215
|
-
if (schema instanceof
|
|
4950
|
+
if (schema instanceof z12.ZodArray)
|
|
4216
4951
|
return `${zodTypeName(schema.element)}[]`;
|
|
4217
|
-
if (schema instanceof
|
|
4952
|
+
if (schema instanceof z12.ZodEnum)
|
|
4218
4953
|
return schema.options.map((o) => `"${o}"`).join(" | ");
|
|
4219
|
-
if (schema instanceof
|
|
4954
|
+
if (schema instanceof z12.ZodOptional)
|
|
4220
4955
|
return `${zodTypeName(schema.unwrap())} (optional)`;
|
|
4221
|
-
if (schema instanceof
|
|
4956
|
+
if (schema instanceof z12.ZodNullable)
|
|
4222
4957
|
return `${zodTypeName(schema.unwrap())} | null`;
|
|
4223
|
-
if (schema instanceof
|
|
4958
|
+
if (schema instanceof z12.ZodLiteral)
|
|
4224
4959
|
return `"${String(schema.value)}"`;
|
|
4225
|
-
if (schema instanceof
|
|
4960
|
+
if (schema instanceof z12.ZodObject)
|
|
4226
4961
|
return "object";
|
|
4227
4962
|
return "any";
|
|
4228
4963
|
}
|
|
4229
4964
|
function buildResponseFormatAppendix(schema) {
|
|
4230
4965
|
const header = ["## Response Format", "", "When you emit `exit_signal: true` in your `<status>` block, also include a `<response>` block containing JSON immediately after `<status>`:", ""];
|
|
4231
|
-
const shape = schema instanceof
|
|
4966
|
+
const shape = schema instanceof z12.ZodObject ? schema.shape : null;
|
|
4232
4967
|
const body = shape ? (() => {
|
|
4233
4968
|
const entries = Object.entries(shape);
|
|
4234
4969
|
const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
|
|
@@ -4358,6 +5093,7 @@ async function agent(opts) {
|
|
|
4358
5093
|
...inbox ? { inbox } : {},
|
|
4359
5094
|
mode,
|
|
4360
5095
|
consumeSteerPending: () => consumeSteerPending(agentId),
|
|
5096
|
+
consumeLocalPauseRequest: () => consumeLocalPauseRequest(agentId),
|
|
4361
5097
|
...steerPause ? { pause: steerPause } : {}
|
|
4362
5098
|
});
|
|
4363
5099
|
} finally {
|