@agent-compose/sdk 0.5.7 → 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/__tests__/run-agent-liveness.test.d.ts +17 -0
- package/dist/agent/agent-context.d.ts +67 -0
- package/dist/agent/agent-loop.d.ts +23 -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 +76 -2
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2409 -1457
- 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/pause/wrappers.d.ts +7 -11
- 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 +2394 -1457
- package/dist/runtimes/vercel.js +389 -2
- package/dist/sandbox.d.ts +132 -14
- package/dist/step-invocation/types.d.ts +1 -1
- 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 +1 -11
- 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 +47 -6
- package/dist/types/workflow.d.ts +27 -4
- package/dist/utils/bundler.d.ts +7 -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 +5 -1
- package/package.json +3 -2
- package/src/agent/agent-context.ts +220 -0
- package/src/agent/agent-loop.ts +90 -22
- package/src/agent/local-pause-request.ts +90 -0
- package/src/agent/run-agent.ts +43 -3
- package/src/agent/steer-control.ts +21 -7
- package/src/client.ts +123 -2
- package/src/index.ts +16 -4
- 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/pause/wrappers.ts +7 -21
- 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 +429 -67
- package/src/step-invocation/types.ts +1 -1
- package/src/types/execution-context.ts +1 -11
- 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 +51 -6
- package/src/types/workflow.ts +27 -6
- package/src/utils/bundler.ts +9 -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 +5 -1
- package/src/workflows/invoke-child.ts +7 -1
|
@@ -330,1467 +330,1537 @@ class RequestContext {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
// src/
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
}
|
|
336
356
|
}
|
|
337
357
|
|
|
338
|
-
// src/
|
|
339
|
-
|
|
340
|
-
|
|
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 };
|
|
341
366
|
}
|
|
342
|
-
function
|
|
343
|
-
return
|
|
367
|
+
function makeState(step) {
|
|
368
|
+
return {
|
|
369
|
+
stepIndex: step.stepIndex,
|
|
370
|
+
nextAgentCallIndex: 0,
|
|
371
|
+
pauseOrdinalByScope: new Map
|
|
372
|
+
};
|
|
344
373
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return [{ type: "error", text: formatError(part.error), timestamp: ts }];
|
|
380
|
-
default:
|
|
381
|
-
return [];
|
|
382
|
-
}
|
|
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;
|
|
383
408
|
}
|
|
384
409
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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)})`);
|
|
401
427
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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");
|
|
407
442
|
}
|
|
408
|
-
|
|
409
|
-
|
|
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();
|
|
410
449
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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`);
|
|
414
510
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
throw new Error("Vercel runtime checkpoint is invalid: expected messages[]");
|
|
511
|
+
if (!hasOwn(record, "resumePayload") || record.resumePayload === undefined) {
|
|
512
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require resumePayload`);
|
|
418
513
|
}
|
|
419
|
-
|
|
420
|
-
|
|
514
|
+
return {
|
|
515
|
+
pauseId: expectedPauseId,
|
|
516
|
+
request: hasOwn(record, "request") ? record.request : null,
|
|
517
|
+
...record
|
|
518
|
+
};
|
|
421
519
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for (const t of this.tools) {
|
|
425
|
-
if (this.options.allowedTools && !this.options.allowedTools.includes(t.name))
|
|
426
|
-
continue;
|
|
427
|
-
set[t.name] = tool({
|
|
428
|
-
description: t.description,
|
|
429
|
-
inputSchema: t.inputSchema,
|
|
430
|
-
execute: async (input, execOpts) => {
|
|
431
|
-
const call = {
|
|
432
|
-
toolName: t.name,
|
|
433
|
-
toolInput: input,
|
|
434
|
-
toolUseId: execOpts.toolCallId ?? `${t.name}-${Date.now()}`
|
|
435
|
-
};
|
|
436
|
-
const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
|
|
437
|
-
teamId: "",
|
|
438
|
-
runId: "",
|
|
439
|
-
workflowId: "",
|
|
440
|
-
factoryId: null,
|
|
441
|
-
apiKeyScopes: [],
|
|
442
|
-
parentRunId: null
|
|
443
|
-
});
|
|
444
|
-
const gate = await this.gateToolCall(call, {
|
|
445
|
-
requestContext,
|
|
446
|
-
abortSignal: execOpts.abortSignal ?? signal ?? new AbortController().signal,
|
|
447
|
-
retryCount: 0,
|
|
448
|
-
agentId: this.options.agentId ?? "agent",
|
|
449
|
-
iteration
|
|
450
|
-
});
|
|
451
|
-
if (gate.kind === "deny")
|
|
452
|
-
return gate.reason;
|
|
453
|
-
if (gate.kind === "abort")
|
|
454
|
-
throw new Error(gate.reason);
|
|
455
|
-
return t.execute(gate.call.toolInput, {
|
|
456
|
-
sandbox: this.sandbox,
|
|
457
|
-
cwd: this.options.cwd,
|
|
458
|
-
abortSignal: execOpts.abortSignal ?? signal
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
return set;
|
|
520
|
+
if (record.pauseId !== expectedPauseId) {
|
|
521
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require matching pauseId`);
|
|
464
522
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
tools: this.buildTools(opts.iteration ?? 1, opts.signal),
|
|
474
|
-
stopWhen: stepCountIs(this.options.maxTurns ?? 40),
|
|
475
|
-
abortSignal: opts.signal
|
|
476
|
-
});
|
|
477
|
-
try {
|
|
478
|
-
for await (const part of result.fullStream) {
|
|
479
|
-
for (const msg of toAgentMessages(part))
|
|
480
|
-
yield msg;
|
|
481
|
-
}
|
|
482
|
-
const response = await result.response;
|
|
483
|
-
const responseMessages = response.messages;
|
|
484
|
-
if (responseMessages) {
|
|
485
|
-
this.messages.length = 0;
|
|
486
|
-
this.messages.push(...responseMessages);
|
|
487
|
-
}
|
|
488
|
-
let usage;
|
|
489
|
-
try {
|
|
490
|
-
usage = await result.usage;
|
|
491
|
-
} catch {
|
|
492
|
-
usage = undefined;
|
|
493
|
-
}
|
|
494
|
-
if (usage) {
|
|
495
|
-
yield {
|
|
496
|
-
type: "usage",
|
|
497
|
-
inputTokens: usage.inputTokens ?? 0,
|
|
498
|
-
outputTokens: usage.outputTokens ?? 0,
|
|
499
|
-
cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
500
|
-
cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
501
|
-
durationMs: 0,
|
|
502
|
-
numTurns: opts.iteration ?? 1,
|
|
503
|
-
timestamp: now()
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
} catch (err) {
|
|
507
|
-
yield { type: "error", text: formatError(err), timestamp: now() };
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
yield { type: "done", sessionId, timestamp: now() };
|
|
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`);
|
|
511
531
|
}
|
|
532
|
+
return record;
|
|
512
533
|
}
|
|
513
|
-
function
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
534
|
+
async function readPauseRecord(pauseId) {
|
|
535
|
+
assertSafeName("pause", pauseId);
|
|
536
|
+
const record = await readJSON(pausePath(pauseId));
|
|
537
|
+
return record === null ? null : validatePauseRecord(record, pauseId);
|
|
517
538
|
}
|
|
518
|
-
async function
|
|
519
|
-
|
|
520
|
-
|
|
539
|
+
async function writePauseRecord(record) {
|
|
540
|
+
assertSafeName("pause", record.pauseId);
|
|
541
|
+
validatePauseRecord(record, record.pauseId);
|
|
542
|
+
await atomicWriteJSON(pausePath(record.pauseId), record);
|
|
521
543
|
}
|
|
522
|
-
// src/types/workflow.ts
|
|
523
|
-
import { z as z3 } from "zod";
|
|
524
544
|
|
|
525
|
-
// src/
|
|
526
|
-
|
|
545
|
+
// src/pause/errors.ts
|
|
546
|
+
class PauseError extends Error {
|
|
547
|
+
constructor(message) {
|
|
548
|
+
super(message);
|
|
549
|
+
this.name = new.target.name;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
527
552
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (source.networkPolicy !== undefined)
|
|
534
|
-
out.networkPolicy = freezeMetadataValue(source.networkPolicy);
|
|
535
|
-
if (source.placeholders !== undefined)
|
|
536
|
-
out.placeholders = Object.freeze({ ...source.placeholders });
|
|
537
|
-
if (source.inputSchema !== undefined)
|
|
538
|
-
out.inputSchema = freezeMetadataValue(source.inputSchema);
|
|
539
|
-
if (source.outputSchema !== undefined)
|
|
540
|
-
out.outputSchema = freezeMetadataValue(source.outputSchema);
|
|
541
|
-
if (source.snapshots !== undefined)
|
|
542
|
-
out.snapshots = Object.freeze({ ...source.snapshots });
|
|
543
|
-
if (source.processors !== undefined)
|
|
544
|
-
out.processors = Object.freeze([...source.processors]);
|
|
545
|
-
if (source.connectors !== undefined)
|
|
546
|
-
out.connectors = freezeMetadataValue(source.connectors);
|
|
547
|
-
if (source.connectorOperation !== undefined)
|
|
548
|
-
out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
549
|
-
if (source.invokePolicy !== undefined)
|
|
550
|
-
out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
551
|
-
return Object.freeze(out);
|
|
553
|
+
class PauseExpiredError extends PauseError {
|
|
554
|
+
code = "pause_expired";
|
|
555
|
+
constructor(message = "pause expired before it was resolved") {
|
|
556
|
+
super(message);
|
|
557
|
+
}
|
|
552
558
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
|
|
560
|
+
class PauseSchemaError extends PauseError {
|
|
561
|
+
code = "pause_schema";
|
|
562
|
+
issues;
|
|
563
|
+
constructor(message, issues) {
|
|
564
|
+
super(message);
|
|
565
|
+
this.issues = issues;
|
|
560
566
|
}
|
|
561
|
-
|
|
562
|
-
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
class PauseRequestError extends PauseError {
|
|
570
|
+
code = "pause_request";
|
|
571
|
+
constructor(message = "pause request is invalid") {
|
|
572
|
+
super(message);
|
|
563
573
|
}
|
|
564
|
-
return Object.freeze(value);
|
|
565
574
|
}
|
|
566
575
|
|
|
567
|
-
// src/
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (finalStep.output !== opts.output) {
|
|
581
|
-
throw new Error(`defineWorkflow("${opts.id}").build: final step "${finalStep.name}" output schema is not the same Zod schema instance as the workflow's declared output schema. ` + `Pass the same schema reference to both the final step and defineWorkflow.`);
|
|
582
|
-
}
|
|
583
|
-
const workflow = {
|
|
584
|
-
id: opts.id,
|
|
585
|
-
input: opts.input,
|
|
586
|
-
output: opts.output,
|
|
587
|
-
steps: Object.freeze([...steps]),
|
|
588
|
-
metadata: extractMetadata(opts)
|
|
589
|
-
};
|
|
590
|
-
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
591
|
-
return Object.freeze(workflow);
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
return build([]);
|
|
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
|
+
}
|
|
595
589
|
}
|
|
596
|
-
function
|
|
597
|
-
return typeof
|
|
590
|
+
function isPauseSignal(err) {
|
|
591
|
+
return typeof err === "object" && err !== null && err[PAUSE_SIGNAL_BRAND] === true;
|
|
598
592
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const inputSchema = def.input ?? unknownSchema;
|
|
604
|
-
const outputSchema = def.output ?? unknownSchema;
|
|
605
|
-
const step = {
|
|
606
|
-
name: "run",
|
|
607
|
-
input: inputSchema,
|
|
608
|
-
output: outputSchema,
|
|
609
|
-
run: async (stepCtx) => {
|
|
610
|
-
const workflowCtx = {
|
|
611
|
-
input: stepCtx.input,
|
|
612
|
-
run: stepCtx.run,
|
|
613
|
-
requestContext: stepCtx.requestContext,
|
|
614
|
-
invokeChild: stepCtx.invokeChild,
|
|
615
|
-
setMetadata: stepCtx.setMetadata,
|
|
616
|
-
step: stepCtx.step,
|
|
617
|
-
agentEvents: stepCtx.agentEvents,
|
|
618
|
-
checkpoint: stepCtx.checkpoint,
|
|
619
|
-
pause: stepCtx.pause,
|
|
620
|
-
requestDecision: stepCtx.requestDecision,
|
|
621
|
-
sleep: stepCtx.sleep,
|
|
622
|
-
waitForEvent: stepCtx.waitForEvent,
|
|
623
|
-
processors: metadata.processors ?? []
|
|
624
|
-
};
|
|
625
|
-
const sandbox = stepCtx.sandbox;
|
|
626
|
-
if (!sandbox)
|
|
627
|
-
throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
|
628
|
-
return def.run(workflowCtx, sandbox);
|
|
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)"));
|
|
629
597
|
}
|
|
598
|
+
return boundary(req, scope);
|
|
630
599
|
};
|
|
631
|
-
const workflow = {
|
|
632
|
-
id: "@run-form",
|
|
633
|
-
input: inputSchema,
|
|
634
|
-
output: outputSchema,
|
|
635
|
-
steps: Object.freeze([step]),
|
|
636
|
-
metadata
|
|
637
|
-
};
|
|
638
|
-
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
639
|
-
return Object.freeze(workflow);
|
|
640
600
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return createStepWorkflow(def);
|
|
601
|
+
var PAUSE_NAMESPACE = "5f3e2b8a-0c41-4e9d-9a7b-6d2c1f8e3a40";
|
|
602
|
+
function uuidToBytes(uuid) {
|
|
603
|
+
return Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
645
604
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
|
|
650
|
-
}
|
|
651
|
-
return defineWorkflow({
|
|
652
|
-
...env.description !== undefined ? { description: env.description } : {},
|
|
653
|
-
snapshots: env.snapshots ?? { saveLatest: true },
|
|
654
|
-
run: async (_ctx, sandbox) => env.setup(sandbox)
|
|
655
|
-
});
|
|
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)}`;
|
|
656
608
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// src/processors/builtins.ts
|
|
664
|
-
function denyTools(names) {
|
|
665
|
-
const banned = new Set(names);
|
|
666
|
-
return {
|
|
667
|
-
name: "denyTools",
|
|
668
|
-
processToolCall: (call) => banned.has(call.toolName) ? Verdict.deny(`tool "${call.toolName}" is denied by policy`) : Verdict.continue(call)
|
|
669
|
-
};
|
|
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);
|
|
670
615
|
}
|
|
671
|
-
function
|
|
672
|
-
return {
|
|
673
|
-
name: "requireScope",
|
|
674
|
-
processInput: (prompt, ctx) => {
|
|
675
|
-
const missing = required.filter((s) => !ctx.requestContext.hasScope(s));
|
|
676
|
-
return missing.length === 0 ? Verdict.continue(prompt) : Verdict.abort(`API key is missing required scopes: ${missing.join(", ")}`);
|
|
677
|
-
}
|
|
678
|
-
};
|
|
616
|
+
function scopeOf(coord) {
|
|
617
|
+
return coord.agentScope ? `${coord.runId}:${coord.stepIndex}:${coord.agentScope.agentId}:${coord.agentScope.iteration}` : `${coord.runId}:${coord.stepIndex}:bare`;
|
|
679
618
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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;
|
|
691
654
|
}
|
|
692
|
-
|
|
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;
|
|
693
673
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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})`);
|
|
704
690
|
}
|
|
691
|
+
return parsed.data;
|
|
705
692
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (line.startsWith("id:")) {
|
|
726
|
-
seq = Number(line.slice(3).trim());
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
if (line.startsWith("event:")) {
|
|
730
|
-
event = line.slice(6).trim();
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
if (line.startsWith("data:")) {
|
|
734
|
-
dataLines.push(line.slice(5).trim());
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
if (line === "" && dataLines.length > 0) {
|
|
738
|
-
let data;
|
|
739
|
-
try {
|
|
740
|
-
data = JSON.parse(dataLines.join(`
|
|
741
|
-
`));
|
|
742
|
-
} catch (err) {
|
|
743
|
-
throw new Error(`Malformed SSE JSON payload for event "${event || "message"}": ${err instanceof Error ? err.message : String(err)}`);
|
|
744
|
-
}
|
|
745
|
-
yield { id: seq, event, data };
|
|
746
|
-
event = "";
|
|
747
|
-
seq = 0;
|
|
748
|
-
dataLines = [];
|
|
749
|
-
}
|
|
750
|
-
}
|
|
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);
|
|
751
712
|
}
|
|
752
|
-
|
|
753
|
-
reader.releaseLock();
|
|
713
|
+
return parsed.data;
|
|
754
714
|
}
|
|
715
|
+
return payload;
|
|
755
716
|
}
|
|
756
717
|
|
|
757
|
-
// src/
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
function detectAmbientParentRunId() {
|
|
761
|
-
const envRunId = typeof process !== "undefined" ? process.env?.RUN_ID : undefined;
|
|
762
|
-
return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
|
|
718
|
+
// src/utils/errors.ts
|
|
719
|
+
function formatError(err) {
|
|
720
|
+
return err instanceof Error ? err.message : String(err);
|
|
763
721
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
722
|
+
|
|
723
|
+
// src/runtimes/vercel.ts
|
|
724
|
+
function now() {
|
|
725
|
+
return new Date().toISOString();
|
|
767
726
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (typeof message !== "string" || message.trim().length === 0) {
|
|
771
|
-
throw new Error("answerSteer: `message` must be a non-empty string");
|
|
772
|
-
}
|
|
773
|
-
return { message, ...actor !== undefined ? { actor } : {} };
|
|
727
|
+
function serialiseOutput(value) {
|
|
728
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
774
729
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
|
|
813
|
-
...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
|
|
814
|
-
...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
|
|
815
|
-
...parentRunId ? { parentRunId } : {},
|
|
816
|
-
...opts?.agentId ? { agentId: opts.agentId } : {}
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
async invokeAndWait(name, input, opts) {
|
|
821
|
-
const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
|
|
822
|
-
const pollMs = opts?.pollIntervalMs ?? 1000;
|
|
823
|
-
const { id: runId } = await this.invoke(name, input, opts);
|
|
824
|
-
const deadline = Date.now() + timeoutMs;
|
|
825
|
-
while (Date.now() < deadline) {
|
|
826
|
-
const status = await this.getStatus(runId);
|
|
827
|
-
if (status.status === "success" || status.status === "failed" || status.status === "abandoned") {
|
|
828
|
-
return status;
|
|
829
|
-
}
|
|
830
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
831
|
-
}
|
|
832
|
-
throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
|
|
833
|
-
}
|
|
834
|
-
async listSnapshots(opts) {
|
|
835
|
-
const body = await this.listSnapshotsPage(opts);
|
|
836
|
-
return body.data;
|
|
837
|
-
}
|
|
838
|
-
async listSnapshotsPage(opts) {
|
|
839
|
-
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
840
|
-
const q2 = new URLSearchParams;
|
|
841
|
-
if (opts?.workflow)
|
|
842
|
-
q2.set("workflow", opts.workflow);
|
|
843
|
-
if (opts?.limit != null)
|
|
844
|
-
q2.set("limit", String(opts.limit));
|
|
845
|
-
if (opts?.before)
|
|
846
|
-
q2.set("before", opts.before);
|
|
847
|
-
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/snapshots${q2.toString() ? `?${q2}` : ""}`);
|
|
848
|
-
}
|
|
849
|
-
deleteSnapshot(runId) {
|
|
850
|
-
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshot`, { method: "DELETE" });
|
|
851
|
-
}
|
|
852
|
-
async listRunSnapshots(runId) {
|
|
853
|
-
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshots`);
|
|
854
|
-
return body.data;
|
|
855
|
-
}
|
|
856
|
-
deleteRunSnapshot(runId, snapshotId) {
|
|
857
|
-
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshots/${encodeURIComponent(snapshotId)}`, { method: "DELETE" });
|
|
858
|
-
}
|
|
859
|
-
getStatus(runId) {
|
|
860
|
-
return this.fetch(`/api/v1/workflows/${runId}/status`);
|
|
730
|
+
function toAgentMessages(part) {
|
|
731
|
+
const ts = now();
|
|
732
|
+
switch (part.type) {
|
|
733
|
+
case "text-delta":
|
|
734
|
+
case "text":
|
|
735
|
+
return [{ type: "text", text: String(part.text ?? ""), timestamp: ts }];
|
|
736
|
+
case "reasoning-delta":
|
|
737
|
+
case "reasoning":
|
|
738
|
+
return [{ type: "thinking", text: String(part.text ?? ""), timestamp: ts }];
|
|
739
|
+
case "tool-call":
|
|
740
|
+
return [{
|
|
741
|
+
type: "tool_use",
|
|
742
|
+
toolName: String(part.toolName ?? ""),
|
|
743
|
+
toolInput: part.input ?? {},
|
|
744
|
+
toolUseId: String(part.toolCallId ?? ""),
|
|
745
|
+
timestamp: ts
|
|
746
|
+
}];
|
|
747
|
+
case "tool-result":
|
|
748
|
+
return [{
|
|
749
|
+
type: "tool_result",
|
|
750
|
+
toolUseId: String(part.toolCallId ?? ""),
|
|
751
|
+
output: serialiseOutput(part.output),
|
|
752
|
+
isError: false,
|
|
753
|
+
timestamp: ts
|
|
754
|
+
}];
|
|
755
|
+
case "tool-error":
|
|
756
|
+
return [{
|
|
757
|
+
type: "tool_result",
|
|
758
|
+
toolUseId: String(part.toolCallId ?? ""),
|
|
759
|
+
output: formatError(part.error),
|
|
760
|
+
isError: true,
|
|
761
|
+
timestamp: ts
|
|
762
|
+
}];
|
|
763
|
+
case "error":
|
|
764
|
+
return [{ type: "error", text: formatError(part.error), timestamp: ts }];
|
|
765
|
+
default:
|
|
766
|
+
return [];
|
|
861
767
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
class VercelRunner {
|
|
771
|
+
sandbox;
|
|
772
|
+
options;
|
|
773
|
+
config;
|
|
774
|
+
supportsToolCallProcessor = true;
|
|
775
|
+
kind;
|
|
776
|
+
model;
|
|
777
|
+
tools;
|
|
778
|
+
messages = [];
|
|
779
|
+
constructor(sandbox, options, config) {
|
|
780
|
+
this.sandbox = sandbox;
|
|
781
|
+
this.options = options;
|
|
782
|
+
this.config = config;
|
|
783
|
+
this.tools = config.tools ?? codingTools;
|
|
784
|
+
this.kind = config.kind;
|
|
785
|
+
this.model = config.modelId;
|
|
872
786
|
}
|
|
873
|
-
async
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
return
|
|
878
|
-
method: "POST",
|
|
879
|
-
body: { resumePayload: payload },
|
|
880
|
-
...Object.keys(headers).length ? { headers } : {},
|
|
881
|
-
...opts?.signal ? { signal: opts.signal } : {}
|
|
882
|
-
});
|
|
787
|
+
async gateToolCall(call, ctx) {
|
|
788
|
+
const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
|
|
789
|
+
if (verdict.kind === "continue")
|
|
790
|
+
return { kind: "allow", call: verdict.value };
|
|
791
|
+
return verdict;
|
|
883
792
|
}
|
|
884
|
-
|
|
885
|
-
return this.
|
|
793
|
+
captureCheckpoint() {
|
|
794
|
+
return { messages: [...this.messages] };
|
|
886
795
|
}
|
|
887
|
-
|
|
888
|
-
|
|
796
|
+
restoreCheckpoint(blob) {
|
|
797
|
+
if (!blob || typeof blob !== "object") {
|
|
798
|
+
throw new Error("Vercel runtime checkpoint is invalid: expected an object with messages[]");
|
|
799
|
+
}
|
|
800
|
+
const incoming = blob.messages;
|
|
801
|
+
if (!Array.isArray(incoming)) {
|
|
802
|
+
throw new Error("Vercel runtime checkpoint is invalid: expected messages[]");
|
|
803
|
+
}
|
|
804
|
+
this.messages.length = 0;
|
|
805
|
+
this.messages.push(...incoming);
|
|
889
806
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
807
|
+
buildTools(iteration, signal) {
|
|
808
|
+
const set = {};
|
|
809
|
+
for (const t of this.tools) {
|
|
810
|
+
if (this.options.allowedTools && !this.options.allowedTools.includes(t.name))
|
|
811
|
+
continue;
|
|
812
|
+
set[t.name] = tool({
|
|
813
|
+
description: t.description,
|
|
814
|
+
inputSchema: t.inputSchema,
|
|
815
|
+
execute: async (input, execOpts) => {
|
|
816
|
+
const call = {
|
|
817
|
+
toolName: t.name,
|
|
818
|
+
toolInput: input,
|
|
819
|
+
toolUseId: execOpts.toolCallId ?? `${t.name}-${Date.now()}`
|
|
820
|
+
};
|
|
821
|
+
const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
|
|
822
|
+
teamId: "",
|
|
823
|
+
runId: "",
|
|
824
|
+
workflowId: "",
|
|
825
|
+
factoryId: null,
|
|
826
|
+
apiKeyScopes: [],
|
|
827
|
+
parentRunId: null
|
|
828
|
+
});
|
|
829
|
+
const gateAgentId = this.options.agentId ?? "agent";
|
|
830
|
+
const gate = await this.gateToolCall(call, {
|
|
831
|
+
requestContext,
|
|
832
|
+
abortSignal: execOpts.abortSignal ?? signal ?? new AbortController().signal,
|
|
833
|
+
retryCount: 0,
|
|
834
|
+
agentId: gateAgentId,
|
|
835
|
+
iteration,
|
|
836
|
+
pause: boundProcessorPause(this.options.pause, { agentId: gateAgentId, iteration })
|
|
837
|
+
});
|
|
838
|
+
if (gate.kind === "deny")
|
|
839
|
+
return gate.reason;
|
|
840
|
+
if (gate.kind === "abort")
|
|
841
|
+
throw new Error(gate.reason);
|
|
842
|
+
return t.execute(gate.call.toolInput, {
|
|
843
|
+
sandbox: this.sandbox,
|
|
844
|
+
cwd: this.options.cwd,
|
|
845
|
+
abortSignal: execOpts.abortSignal ?? signal
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return set;
|
|
901
851
|
}
|
|
902
|
-
async
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
852
|
+
async* sendMessage(opts) {
|
|
853
|
+
const sessionId = opts.sessionId ?? `vercel-${Date.now()}`;
|
|
854
|
+
yield { type: "init", sessionId, timestamp: now() };
|
|
855
|
+
const inputMessages = [...this.messages, { role: "user", content: opts.prompt }];
|
|
856
|
+
const result = streamText({
|
|
857
|
+
model: this.config.model,
|
|
858
|
+
...this.config.system ? { system: this.config.system } : {},
|
|
859
|
+
messages: inputMessages,
|
|
860
|
+
tools: this.buildTools(opts.iteration ?? 1, opts.signal),
|
|
861
|
+
stopWhen: stepCountIs(this.options.maxTurns ?? 40),
|
|
862
|
+
abortSignal: opts.signal
|
|
910
863
|
});
|
|
864
|
+
try {
|
|
865
|
+
for await (const part of result.fullStream) {
|
|
866
|
+
for (const msg of toAgentMessages(part))
|
|
867
|
+
yield msg;
|
|
868
|
+
}
|
|
869
|
+
const response = await result.response;
|
|
870
|
+
const responseMessages = response.messages;
|
|
871
|
+
if (responseMessages) {
|
|
872
|
+
this.messages.length = 0;
|
|
873
|
+
this.messages.push(...responseMessages);
|
|
874
|
+
}
|
|
875
|
+
let usage;
|
|
876
|
+
try {
|
|
877
|
+
usage = await result.usage;
|
|
878
|
+
} catch {
|
|
879
|
+
usage = undefined;
|
|
880
|
+
}
|
|
881
|
+
if (usage) {
|
|
882
|
+
yield {
|
|
883
|
+
type: "usage",
|
|
884
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
885
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
886
|
+
cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
887
|
+
cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
888
|
+
durationMs: 0,
|
|
889
|
+
numTurns: opts.iteration ?? 1,
|
|
890
|
+
timestamp: now()
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
} catch (err) {
|
|
894
|
+
yield { type: "error", text: formatError(err), timestamp: now() };
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
yield { type: "done", sessionId, timestamp: now() };
|
|
911
898
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
899
|
+
}
|
|
900
|
+
function createVercelRuntime(config) {
|
|
901
|
+
return defineRuntime({
|
|
902
|
+
create: (sandbox, opts) => new VercelRunner(sandbox, opts, config)
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
async function listVercelRuntimeModels() {
|
|
906
|
+
const { models } = await gateway.getAvailableModels();
|
|
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));
|
|
908
|
+
}
|
|
909
|
+
// src/types/workflow.ts
|
|
910
|
+
import { z as z4 } from "zod";
|
|
911
|
+
|
|
912
|
+
// src/workflow-steps/types.ts
|
|
913
|
+
var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
|
|
914
|
+
|
|
915
|
+
// src/types/workflow-metadata.ts
|
|
916
|
+
function extractMetadata(source) {
|
|
917
|
+
const out = {};
|
|
918
|
+
if (source.description !== undefined)
|
|
919
|
+
out.description = source.description;
|
|
920
|
+
if (source.networkPolicy !== undefined)
|
|
921
|
+
out.networkPolicy = freezeMetadataValue(source.networkPolicy);
|
|
922
|
+
if (source.placeholders !== undefined)
|
|
923
|
+
out.placeholders = Object.freeze({ ...source.placeholders });
|
|
924
|
+
if (source.inputSchema !== undefined)
|
|
925
|
+
out.inputSchema = freezeMetadataValue(source.inputSchema);
|
|
926
|
+
if (source.outputSchema !== undefined)
|
|
927
|
+
out.outputSchema = freezeMetadataValue(source.outputSchema);
|
|
928
|
+
if (source.snapshots !== undefined)
|
|
929
|
+
out.snapshots = Object.freeze({ ...source.snapshots });
|
|
930
|
+
if (source.resources !== undefined)
|
|
931
|
+
out.resources = Object.freeze({ ...source.resources });
|
|
932
|
+
if (source.processors !== undefined)
|
|
933
|
+
out.processors = Object.freeze([...source.processors]);
|
|
934
|
+
if (source.connectors !== undefined)
|
|
935
|
+
out.connectors = freezeMetadataValue(source.connectors);
|
|
936
|
+
if (source.connectorOperation !== undefined)
|
|
937
|
+
out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
938
|
+
if (source.invokePolicy !== undefined)
|
|
939
|
+
out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
940
|
+
if (source.environmentBuild !== undefined)
|
|
941
|
+
out.environmentBuild = source.environmentBuild;
|
|
942
|
+
return Object.freeze(out);
|
|
943
|
+
}
|
|
944
|
+
function freezeMetadataValue(value) {
|
|
945
|
+
if (value === null || typeof value !== "object")
|
|
946
|
+
return value;
|
|
947
|
+
if (Array.isArray(value)) {
|
|
948
|
+
for (const v of value)
|
|
949
|
+
freezeMetadataValue(v);
|
|
950
|
+
return Object.freeze(value);
|
|
938
951
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
return body.artifacts.map((a) => ({
|
|
942
|
-
path: a.path,
|
|
943
|
-
factorySlug: a.factory_slug,
|
|
944
|
-
sizeBytes: a.size_bytes,
|
|
945
|
-
lastWriteAt: a.last_write_at,
|
|
946
|
-
preview: a.preview
|
|
947
|
-
}));
|
|
952
|
+
for (const v of Object.values(value)) {
|
|
953
|
+
freezeMetadataValue(v);
|
|
948
954
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
955
|
+
return Object.freeze(value);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/workflow-steps/workflow.ts
|
|
959
|
+
function createStepWorkflow(opts) {
|
|
960
|
+
if (!opts.id)
|
|
961
|
+
throw new Error("defineWorkflow: 'id' is required for step-based workflows");
|
|
962
|
+
const build = (steps) => ({
|
|
963
|
+
step(step) {
|
|
964
|
+
return build([...steps, step]);
|
|
965
|
+
},
|
|
966
|
+
build() {
|
|
967
|
+
if (steps.length === 0) {
|
|
968
|
+
throw new Error(`defineWorkflow("${opts.id}").build: no steps registered — add at least one with .step(step)`);
|
|
958
969
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
970
|
+
const finalStep = steps[steps.length - 1];
|
|
971
|
+
if (finalStep.output !== opts.output) {
|
|
972
|
+
throw new Error(`defineWorkflow("${opts.id}").build: final step "${finalStep.name}" output schema is not the same Zod schema instance as the workflow's declared output schema. ` + `Pass the same schema reference to both the final step and defineWorkflow.`);
|
|
973
|
+
}
|
|
974
|
+
const workflow = {
|
|
975
|
+
id: opts.id,
|
|
976
|
+
input: opts.input,
|
|
977
|
+
output: opts.output,
|
|
978
|
+
steps: Object.freeze([...steps]),
|
|
979
|
+
metadata: extractMetadata(opts)
|
|
980
|
+
};
|
|
981
|
+
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
982
|
+
return Object.freeze(workflow);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
return build([]);
|
|
986
|
+
}
|
|
987
|
+
function isWorkflow(value) {
|
|
988
|
+
return typeof value === "object" && value !== null && value[WORKFLOW_BRAND] === true;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/types/workflow.ts
|
|
992
|
+
function compileRunForm(def, metadata) {
|
|
993
|
+
const unknownSchema = z4.unknown();
|
|
994
|
+
const inputSchema = def.input ?? unknownSchema;
|
|
995
|
+
const outputSchema = def.output ?? unknownSchema;
|
|
996
|
+
const step = {
|
|
997
|
+
name: "run",
|
|
998
|
+
input: inputSchema,
|
|
999
|
+
output: outputSchema,
|
|
1000
|
+
run: async (stepCtx) => {
|
|
1001
|
+
const workflowCtx = {
|
|
1002
|
+
input: stepCtx.input,
|
|
1003
|
+
run: stepCtx.run,
|
|
1004
|
+
requestContext: stepCtx.requestContext,
|
|
1005
|
+
invokeChild: stepCtx.invokeChild,
|
|
1006
|
+
setMetadata: stepCtx.setMetadata,
|
|
1007
|
+
step: stepCtx.step,
|
|
1008
|
+
agentEvents: stepCtx.agentEvents,
|
|
1009
|
+
pause: stepCtx.pause,
|
|
1010
|
+
sleep: stepCtx.sleep,
|
|
1011
|
+
waitForEvent: stepCtx.waitForEvent,
|
|
1012
|
+
processors: metadata.processors ?? []
|
|
1013
|
+
};
|
|
1014
|
+
const sandbox = stepCtx.sandbox;
|
|
1015
|
+
if (!sandbox)
|
|
1016
|
+
throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
|
1017
|
+
return def.run(workflowCtx, sandbox);
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
const workflow = {
|
|
1021
|
+
id: "@run-form",
|
|
1022
|
+
input: inputSchema,
|
|
1023
|
+
output: outputSchema,
|
|
1024
|
+
steps: Object.freeze([step]),
|
|
1025
|
+
metadata
|
|
1026
|
+
};
|
|
1027
|
+
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
1028
|
+
return Object.freeze(workflow);
|
|
1029
|
+
}
|
|
1030
|
+
function defineWorkflow(def) {
|
|
1031
|
+
if ("run" in def)
|
|
1032
|
+
return compileRunForm(def, extractMetadata(def));
|
|
1033
|
+
return createStepWorkflow(def);
|
|
1034
|
+
}
|
|
1035
|
+
// src/types/sandbox-environment.ts
|
|
1036
|
+
function defineSandboxEnvironment(env) {
|
|
1037
|
+
if (typeof env.setup !== "function") {
|
|
1038
|
+
throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
|
|
1012
1039
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1040
|
+
return defineWorkflow({
|
|
1041
|
+
...env.description !== undefined ? { description: env.description } : {},
|
|
1042
|
+
...env.resources !== undefined ? { resources: env.resources } : {},
|
|
1043
|
+
snapshots: env.snapshots ?? { saveLatest: true },
|
|
1044
|
+
environmentBuild: true,
|
|
1045
|
+
run: async (_ctx, sandbox) => env.setup(sandbox)
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
// src/processors/processor.ts
|
|
1049
|
+
var Verdict = {
|
|
1050
|
+
continue: (value) => ({ kind: "continue", value }),
|
|
1051
|
+
deny: (reason) => ({ kind: "deny", reason }),
|
|
1052
|
+
abort: (reason) => ({ kind: "abort", reason })
|
|
1053
|
+
};
|
|
1054
|
+
// src/processors/builtins.ts
|
|
1055
|
+
import { z as z5 } from "zod";
|
|
1056
|
+
function denyTools(names) {
|
|
1057
|
+
const banned = new Set(names);
|
|
1058
|
+
return {
|
|
1059
|
+
name: "denyTools",
|
|
1060
|
+
processToolCall: (call) => banned.has(call.toolName) ? Verdict.deny(`tool "${call.toolName}" is denied by policy`) : Verdict.continue(call)
|
|
1061
|
+
};
|
|
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
|
+
}
|
|
1083
|
+
function requireScope(required) {
|
|
1084
|
+
return {
|
|
1085
|
+
name: "requireScope",
|
|
1086
|
+
processInput: (prompt, ctx) => {
|
|
1087
|
+
const missing = required.filter((s) => !ctx.requestContext.hasScope(s));
|
|
1088
|
+
return missing.length === 0 ? Verdict.continue(prompt) : Verdict.abort(`API key is missing required scopes: ${missing.join(", ")}`);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function redactPattern(pattern, replacement = "[REDACTED]") {
|
|
1093
|
+
return {
|
|
1094
|
+
name: "redactPattern",
|
|
1095
|
+
processOutput: (message) => {
|
|
1096
|
+
if (message.type === "text" || message.type === "thinking") {
|
|
1097
|
+
const redacted = message.text.replace(pattern, replacement);
|
|
1098
|
+
if (redacted !== message.text) {
|
|
1099
|
+
return Verdict.continue({ ...message, text: redacted });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return Verdict.continue(message);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// src/client.ts
|
|
1107
|
+
import { ofetch } from "ofetch";
|
|
1108
|
+
|
|
1109
|
+
// src/errors.ts
|
|
1110
|
+
class AgentComposeError extends Error {
|
|
1111
|
+
status;
|
|
1112
|
+
constructor(status, message) {
|
|
1113
|
+
super(message);
|
|
1114
|
+
this.status = status;
|
|
1115
|
+
this.name = "AgentComposeError";
|
|
1015
1116
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/sse.ts
|
|
1120
|
+
async function* parseSseStream(body) {
|
|
1121
|
+
const reader = body.getReader();
|
|
1122
|
+
const decoder = new TextDecoder;
|
|
1123
|
+
let buffer = "";
|
|
1124
|
+
let event = "";
|
|
1125
|
+
let seq = 0;
|
|
1126
|
+
let dataLines = [];
|
|
1127
|
+
try {
|
|
1128
|
+
while (true) {
|
|
1129
|
+
const { done, value } = await reader.read();
|
|
1130
|
+
if (done)
|
|
1131
|
+
break;
|
|
1132
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1133
|
+
const lines = buffer.split(`
|
|
1134
|
+
`);
|
|
1135
|
+
buffer = lines.pop() ?? "";
|
|
1136
|
+
for (const line of lines) {
|
|
1137
|
+
if (line.startsWith("id:")) {
|
|
1138
|
+
seq = Number(line.slice(3).trim());
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (line.startsWith("event:")) {
|
|
1142
|
+
event = line.slice(6).trim();
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (line.startsWith("data:")) {
|
|
1146
|
+
dataLines.push(line.slice(5).trim());
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (line === "" && dataLines.length > 0) {
|
|
1150
|
+
let data;
|
|
1151
|
+
try {
|
|
1152
|
+
data = JSON.parse(dataLines.join(`
|
|
1153
|
+
`));
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
throw new Error(`Malformed SSE JSON payload for event "${event || "message"}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1156
|
+
}
|
|
1157
|
+
yield { id: seq, event, data };
|
|
1158
|
+
event = "";
|
|
1159
|
+
seq = 0;
|
|
1160
|
+
dataLines = [];
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
} finally {
|
|
1165
|
+
reader.releaseLock();
|
|
1018
1166
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/client.ts
|
|
1170
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1171
|
+
var DEFAULT_FACTORY = "default";
|
|
1172
|
+
function detectAmbientParentRunId() {
|
|
1173
|
+
const envRunId = typeof process !== "undefined" ? process.env?.RUN_ID : undefined;
|
|
1174
|
+
return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
|
|
1175
|
+
}
|
|
1176
|
+
function templatePath(factorySlug, ...rest) {
|
|
1177
|
+
const tail = rest.length > 0 ? "/" + rest.map(encodeURIComponent).join("/") : "";
|
|
1178
|
+
return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
|
|
1179
|
+
}
|
|
1180
|
+
var PUBLIC_API_HOST = "https://api.agentcompose.ai";
|
|
1181
|
+
function steerAnswerPayload(message, actor) {
|
|
1182
|
+
if (typeof message !== "string" || message.trim().length === 0) {
|
|
1183
|
+
throw new Error("answerSteer: `message` must be a non-empty string");
|
|
1022
1184
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1185
|
+
return { message, ...actor !== undefined ? { actor } : {} };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
class AgentComposeClient {
|
|
1189
|
+
fetch;
|
|
1190
|
+
baseUrl;
|
|
1191
|
+
apiKey;
|
|
1192
|
+
constructor(options = {}) {
|
|
1193
|
+
const apiKey = options.apiKey ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_API_KEY : undefined) ?? "";
|
|
1194
|
+
if (!apiKey) {
|
|
1195
|
+
throw new Error("AgentComposeClient: `apiKey` is required. Pass it as `{ apiKey }` " + "or set the AGENT_COMPOSE_API_KEY env var.");
|
|
1196
|
+
}
|
|
1197
|
+
const baseUrl = options.baseUrl ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_URL : undefined) ?? PUBLIC_API_HOST;
|
|
1198
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1199
|
+
this.apiKey = apiKey;
|
|
1200
|
+
this.fetch = ofetch.create({
|
|
1201
|
+
baseURL: this.baseUrl,
|
|
1202
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1203
|
+
async onResponseError({ response }) {
|
|
1204
|
+
const body = response._data;
|
|
1205
|
+
throw new AgentComposeError(response.status, body?.error ?? response.statusText);
|
|
1206
|
+
}
|
|
1028
1207
|
});
|
|
1029
1208
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1209
|
+
register(payload) {
|
|
1210
|
+
const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
|
|
1211
|
+
return this.fetch(templatePath(factorySlug), { method: "POST", body });
|
|
1032
1212
|
}
|
|
1033
|
-
|
|
1213
|
+
invoke(name, input, opts) {
|
|
1214
|
+
const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
|
|
1034
1215
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1035
|
-
|
|
1216
|
+
const headers = {};
|
|
1217
|
+
if (opts?.idempotencyKey)
|
|
1218
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
1219
|
+
return this.fetch(templatePath(factorySlug, name, "invoke"), {
|
|
1036
1220
|
method: "POST",
|
|
1037
|
-
|
|
1221
|
+
...opts?.idempotencyKey ? { headers } : {},
|
|
1222
|
+
body: {
|
|
1223
|
+
input,
|
|
1224
|
+
...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
|
|
1225
|
+
...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
|
|
1226
|
+
...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
|
|
1227
|
+
...opts?.size !== undefined ? { size: opts.size } : {},
|
|
1228
|
+
...parentRunId ? { parentRunId } : {},
|
|
1229
|
+
...opts?.agentId ? { agentId: opts.agentId } : {}
|
|
1230
|
+
}
|
|
1038
1231
|
});
|
|
1039
1232
|
}
|
|
1040
|
-
async
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1233
|
+
async invokeAndWait(name, input, opts) {
|
|
1234
|
+
const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
|
|
1235
|
+
const pollMs = opts?.pollIntervalMs ?? 1000;
|
|
1236
|
+
const { id: runId } = await this.invoke(name, input, opts);
|
|
1237
|
+
const deadline = Date.now() + timeoutMs;
|
|
1238
|
+
while (Date.now() < deadline) {
|
|
1239
|
+
const status = await this.getStatus(runId);
|
|
1240
|
+
if (status.status === "success" || status.status === "failed" || status.status === "abandoned") {
|
|
1241
|
+
return status;
|
|
1242
|
+
}
|
|
1243
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1244
|
+
}
|
|
1245
|
+
throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
|
|
1044
1246
|
}
|
|
1045
|
-
|
|
1247
|
+
async listSnapshots(opts) {
|
|
1248
|
+
const body = await this.listSnapshotsPage(opts);
|
|
1249
|
+
return body.data;
|
|
1250
|
+
}
|
|
1251
|
+
async listSnapshotsPage(opts) {
|
|
1046
1252
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1047
|
-
|
|
1253
|
+
const q2 = new URLSearchParams;
|
|
1254
|
+
if (opts?.workflow)
|
|
1255
|
+
q2.set("workflow", opts.workflow);
|
|
1256
|
+
if (opts?.limit != null)
|
|
1257
|
+
q2.set("limit", String(opts.limit));
|
|
1258
|
+
if (opts?.before)
|
|
1259
|
+
q2.set("before", opts.before);
|
|
1260
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/snapshots${q2.toString() ? `?${q2}` : ""}`);
|
|
1048
1261
|
}
|
|
1049
|
-
|
|
1050
|
-
return this.fetch(
|
|
1262
|
+
deleteSnapshot(runId) {
|
|
1263
|
+
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshot`, { method: "DELETE" });
|
|
1051
1264
|
}
|
|
1052
|
-
async
|
|
1053
|
-
const body = await this.fetch(
|
|
1265
|
+
async listRunSnapshots(runId) {
|
|
1266
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshots`);
|
|
1054
1267
|
return body.data;
|
|
1055
1268
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
return this.fetch(`/api/v1/usage${qs}`);
|
|
1269
|
+
deleteRunSnapshot(runId, snapshotId) {
|
|
1270
|
+
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshots/${encodeURIComponent(snapshotId)}`, { method: "DELETE" });
|
|
1059
1271
|
}
|
|
1060
|
-
|
|
1061
|
-
return this.fetch(`/api/v1/workflows/${
|
|
1272
|
+
getStatus(runId) {
|
|
1273
|
+
return this.fetch(`/api/v1/workflows/${runId}/status`);
|
|
1062
1274
|
}
|
|
1063
|
-
async
|
|
1064
|
-
const headers = {
|
|
1065
|
-
if (opts?.
|
|
1066
|
-
headers["
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1275
|
+
async resumePause(runId, pauseId, payload, opts) {
|
|
1276
|
+
const headers = {};
|
|
1277
|
+
if (opts?.idempotencyKey)
|
|
1278
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
1279
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/pauses/${encodeURIComponent(pauseId)}/resume`, {
|
|
1280
|
+
method: "POST",
|
|
1281
|
+
body: { resumePayload: payload },
|
|
1282
|
+
...Object.keys(headers).length ? { headers } : {},
|
|
1070
1283
|
...opts?.signal ? { signal: opts.signal } : {}
|
|
1071
1284
|
});
|
|
1072
|
-
if (!res.ok || !res.body) {
|
|
1073
|
-
let message = res.statusText;
|
|
1074
|
-
try {
|
|
1075
|
-
const body = await res.json();
|
|
1076
|
-
if (body.error)
|
|
1077
|
-
message = body.error;
|
|
1078
|
-
} catch {}
|
|
1079
|
-
throw new AgentComposeError(res.status, message);
|
|
1080
|
-
}
|
|
1081
|
-
for await (const ev of parseSseStream(res.body)) {
|
|
1082
|
-
yield ev.data;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
// src/utils/bundler.ts
|
|
1087
|
-
import { createHash } from "node:crypto";
|
|
1088
|
-
import { parse as babelParse } from "@babel/parser";
|
|
1089
|
-
|
|
1090
|
-
// src/utils/source-loader.ts
|
|
1091
|
-
import { dirname } from "path";
|
|
1092
|
-
import { writeFile, rm, mkdir } from "node:fs/promises";
|
|
1093
|
-
var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
1094
|
-
var LATEST_VERSION = "$LATEST";
|
|
1095
|
-
async function importSourceModule(source, tmpPath) {
|
|
1096
|
-
await mkdir(dirname(tmpPath), { recursive: true });
|
|
1097
|
-
await writeFile(tmpPath, source, "utf8");
|
|
1098
|
-
const mod = await import(tmpPath);
|
|
1099
|
-
const cleanup = () => rm(tmpPath, { force: true });
|
|
1100
|
-
return { mod, cleanup };
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// src/types/workflow-plan.ts
|
|
1104
|
-
function workflowPlan(steps) {
|
|
1105
|
-
return { steps };
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// src/utils/bundler.ts
|
|
1109
|
-
var BUNDLER_VERSION = 2;
|
|
1110
|
-
|
|
1111
|
-
class WorkflowSourceValidationError extends Error {
|
|
1112
|
-
constructor(message) {
|
|
1113
|
-
super(message);
|
|
1114
|
-
this.name = "WorkflowSourceValidationError";
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
async function bundle(path, label) {
|
|
1118
|
-
const bun = globalThis;
|
|
1119
|
-
if (!bun.Bun?.build)
|
|
1120
|
-
throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
1121
|
-
const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
|
|
1122
|
-
if (!result.success) {
|
|
1123
|
-
throw new Error(`Failed to bundle ${label}:
|
|
1124
|
-
${result.logs.map((l) => l.message).join(`
|
|
1125
|
-
`)}`);
|
|
1126
|
-
}
|
|
1127
|
-
return result.outputs[0].text();
|
|
1128
|
-
}
|
|
1129
|
-
async function extractFromBundle(source, label, pick) {
|
|
1130
|
-
const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
|
|
1131
|
-
try {
|
|
1132
|
-
const loaded = await importSourceModule(source, tmpPath);
|
|
1133
|
-
try {
|
|
1134
|
-
return pick(loaded.mod.default);
|
|
1135
|
-
} finally {
|
|
1136
|
-
await loaded.cleanup();
|
|
1137
|
-
}
|
|
1138
|
-
} catch (err) {
|
|
1139
|
-
throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
|
|
1143
|
-
function isDefineWorkflowCall(node) {
|
|
1144
|
-
if (node.callee.type === "Identifier") {
|
|
1145
|
-
return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
|
|
1146
|
-
}
|
|
1147
|
-
if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
|
|
1148
|
-
return isDefineWorkflowCall(node.callee.object);
|
|
1149
1285
|
}
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1286
|
+
async resumePauseByKey(runId, correlationKey, payload, opts) {
|
|
1287
|
+
const headers = {};
|
|
1288
|
+
if (opts?.idempotencyKey)
|
|
1289
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
1290
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/pauses/by-key/${encodeURIComponent(correlationKey)}/resume`, {
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
body: { resumePayload: payload },
|
|
1293
|
+
...Object.keys(headers).length ? { headers } : {},
|
|
1294
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
1159
1295
|
});
|
|
1160
|
-
} catch (err) {
|
|
1161
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1162
|
-
throw new WorkflowSourceValidationError(`Bundled workflow ${label} failed to parse as JavaScript: ${msg}. ` + `If you registered raw .ts source, switch to \`agentc register <file.ts>\` so the source is bundled first.`);
|
|
1163
1296
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
throw new WorkflowSourceValidationError(`Bundled workflow ${label} has no default export. ` + `Use \`export default defineWorkflow({ run: ... })\`.`);
|
|
1297
|
+
async answerSteerByKey(runId, correlationKey, message, opts) {
|
|
1298
|
+
return this.resumePauseByKey(runId, correlationKey, steerAnswerPayload(message, opts?.actor), opts);
|
|
1167
1299
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1300
|
+
async answerSteer(runId, pauseId, message, opts) {
|
|
1301
|
+
return this.resumePause(runId, pauseId, steerAnswerPayload(message, opts?.actor), opts);
|
|
1170
1302
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1303
|
+
async requestAgentPause(runId, agentId, opts) {
|
|
1304
|
+
const body = {};
|
|
1305
|
+
if (opts?.reason !== undefined)
|
|
1306
|
+
body.reason = opts.reason;
|
|
1307
|
+
if (opts?.correlationKey !== undefined)
|
|
1308
|
+
body.correlationKey = opts.correlationKey;
|
|
1309
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/agents/${encodeURIComponent(agentId)}/pause`, {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
body,
|
|
1312
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
1313
|
+
});
|
|
1176
1314
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
continue;
|
|
1187
|
-
for (const decl of stmt.declarations) {
|
|
1188
|
-
if (decl.id.type === "Identifier" && decl.id.name === localName && decl.init && decl.init.type !== "TSSatisfiesExpression") {
|
|
1189
|
-
return decl.init;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1315
|
+
async sendAgentMessage(runId, agentId, message, opts) {
|
|
1316
|
+
const body = { message };
|
|
1317
|
+
if (opts?.senderName !== undefined)
|
|
1318
|
+
body.senderName = opts.senderName;
|
|
1319
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/agents/${encodeURIComponent(agentId)}/messages`, {
|
|
1320
|
+
method: "POST",
|
|
1321
|
+
body,
|
|
1322
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
1323
|
+
});
|
|
1192
1324
|
}
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
function sha256(text) {
|
|
1196
|
-
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
1197
|
-
}
|
|
1198
|
-
async function bundleWorkflow(workflowPath, overrides) {
|
|
1199
|
-
const label = `"${workflowPath}"`;
|
|
1200
|
-
const source = await bundle(workflowPath, `workflow ${label}`);
|
|
1201
|
-
assertDefaultExportIsDefineWorkflow(source, label);
|
|
1202
|
-
const workflow = await extractFromBundle(source, "workflow", (wf) => isWorkflow(wf) ? wf : undefined);
|
|
1203
|
-
if (!workflow) {
|
|
1204
|
-
throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export is not a Workflow. ` + `Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` + `raw \`export default async function\` is no longer supported.`);
|
|
1325
|
+
getRun(runId) {
|
|
1326
|
+
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}`);
|
|
1205
1327
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const outputSchema = extractIOSchema(workflow.output);
|
|
1215
|
-
return {
|
|
1216
|
-
source,
|
|
1217
|
-
manifest,
|
|
1218
|
-
workflowPlan: plan,
|
|
1219
|
-
networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
|
|
1220
|
-
placeholders: overrides?.placeholders ?? metadata.placeholders,
|
|
1221
|
-
...metadata.description !== undefined ? { description: metadata.description } : {},
|
|
1222
|
-
...inputSchema !== undefined ? { inputSchema } : {},
|
|
1223
|
-
...outputSchema !== undefined ? { outputSchema } : {},
|
|
1224
|
-
...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
|
|
1225
|
-
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1226
|
-
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1227
|
-
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
function extractIOSchema(zodSchema) {
|
|
1231
|
-
let json;
|
|
1232
|
-
try {
|
|
1233
|
-
const z4 = __require("zod");
|
|
1234
|
-
if (!z4.toJSONSchema)
|
|
1235
|
-
return;
|
|
1236
|
-
json = z4.toJSONSchema(zodSchema);
|
|
1237
|
-
} catch {
|
|
1238
|
-
return;
|
|
1328
|
+
async getRunTimeline(runId, opts) {
|
|
1329
|
+
const q2 = new URLSearchParams;
|
|
1330
|
+
if (opts?.limit !== undefined)
|
|
1331
|
+
q2.set("limit", String(opts.limit));
|
|
1332
|
+
if (opts?.offset !== undefined)
|
|
1333
|
+
q2.set("offset", String(opts.offset));
|
|
1334
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/timeline${q2.toString() ? `?${q2}` : ""}`);
|
|
1335
|
+
return body.events;
|
|
1239
1336
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const props = {};
|
|
1251
|
-
for (const [key, raw] of Object.entries(json.properties)) {
|
|
1252
|
-
if (raw && typeof raw === "object") {
|
|
1253
|
-
const r = raw;
|
|
1254
|
-
props[key] = {};
|
|
1255
|
-
if (r.type !== undefined)
|
|
1256
|
-
props[key].type = r.type;
|
|
1257
|
-
if (r.description !== undefined)
|
|
1258
|
-
props[key].description = String(r.description);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (Object.keys(props).length > 0)
|
|
1262
|
-
out.properties = props;
|
|
1337
|
+
async reportEvent(runId, input) {
|
|
1338
|
+
const body = {
|
|
1339
|
+
...input,
|
|
1340
|
+
...input.timestamp instanceof Date ? { timestamp: input.timestamp.toISOString() } : {}
|
|
1341
|
+
};
|
|
1342
|
+
const response = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`, {
|
|
1343
|
+
method: "POST",
|
|
1344
|
+
body
|
|
1345
|
+
});
|
|
1346
|
+
return response.event;
|
|
1263
1347
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
// src/utils/schemas.ts
|
|
1269
|
-
import { z as z4 } from "zod";
|
|
1270
|
-
var AgentStatusSchema = z4.object({
|
|
1271
|
-
summary: z4.string(),
|
|
1272
|
-
completed: z4.array(z4.string()),
|
|
1273
|
-
blockers: z4.array(z4.string()),
|
|
1274
|
-
exit_signal: z4.boolean(),
|
|
1275
|
-
needs_input: z4.boolean().optional(),
|
|
1276
|
-
question: z4.string().optional()
|
|
1277
|
-
});
|
|
1278
|
-
// src/runtimes/claude.ts
|
|
1279
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1280
|
-
|
|
1281
|
-
// src/agent/agent-loop.ts
|
|
1282
|
-
import { z as z9 } from "zod";
|
|
1283
|
-
|
|
1284
|
-
// src/agent/protocol.ts
|
|
1285
|
-
import { z as z5 } from "zod";
|
|
1286
|
-
var AgentMessageSchema = z5.object({
|
|
1287
|
-
type: z5.enum(["init", "text", "thinking", "tool_use", "tool_result", "done", "error", "usage"]),
|
|
1288
|
-
timestamp: z5.string()
|
|
1289
|
-
}).passthrough();
|
|
1290
|
-
function parseAgentResponse(text) {
|
|
1291
|
-
const match = text.match(/<response>([\s\S]*?)<\/response>/);
|
|
1292
|
-
if (!match)
|
|
1293
|
-
return null;
|
|
1294
|
-
try {
|
|
1295
|
-
return JSON.parse(match[1].trim());
|
|
1296
|
-
} catch {
|
|
1297
|
-
return null;
|
|
1348
|
+
async listRunEvents(runId) {
|
|
1349
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`);
|
|
1350
|
+
return body.events;
|
|
1298
1351
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
import { promises as fs } from "node:fs";
|
|
1309
|
-
import { join as join2 } from "node:path";
|
|
1310
|
-
import { randomBytes } from "node:crypto";
|
|
1311
|
-
function getStateDir() {
|
|
1312
|
-
return process.env.AGENT_COMPOSE_STATE_DIR ?? "/tmp/wf/state";
|
|
1313
|
-
}
|
|
1314
|
-
var checkpointsDir = () => join2(getStateDir(), "checkpoints");
|
|
1315
|
-
var pausesDir = () => join2(getStateDir(), "pauses");
|
|
1316
|
-
var agentStatePath = (agentInstanceId) => join2(getStateDir(), `agent-${agentInstanceId}.json`);
|
|
1317
|
-
var runtimeStatePath = (agentInstanceId) => join2(getStateDir(), `runtime-${agentInstanceId}.json`);
|
|
1318
|
-
var checkpointPath = (name) => join2(checkpointsDir(), `${name}.json`);
|
|
1319
|
-
var pausePath = (pauseId) => join2(pausesDir(), `${pauseId}.json`);
|
|
1320
|
-
var NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
|
|
1321
|
-
function assertSafeName(label, name) {
|
|
1322
|
-
if (!NAME_RE.test(name)) {
|
|
1323
|
-
throw new Error(`${label} name must match ${NAME_RE} (got ${JSON.stringify(name)})`);
|
|
1352
|
+
async listRunArtifacts(runId) {
|
|
1353
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/artifacts`);
|
|
1354
|
+
return body.artifacts.map((a) => ({
|
|
1355
|
+
path: a.path,
|
|
1356
|
+
factorySlug: a.factory_slug,
|
|
1357
|
+
sizeBytes: a.size_bytes,
|
|
1358
|
+
lastWriteAt: a.last_write_at,
|
|
1359
|
+
preview: a.preview
|
|
1360
|
+
}));
|
|
1324
1361
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1362
|
+
async putFactoryFile(path, content, opts) {
|
|
1363
|
+
const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
1364
|
+
const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
|
|
1365
|
+
const wire = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?path=${encodeURIComponent(path)}`, {
|
|
1366
|
+
method: "PUT",
|
|
1367
|
+
body: content,
|
|
1368
|
+
headers: {
|
|
1369
|
+
"content-type": opts?.contentType ?? "text/plain; charset=utf-8",
|
|
1370
|
+
...runToken ? { "x-run-token": runToken } : {}
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
return {
|
|
1374
|
+
path: wire.path,
|
|
1375
|
+
contentHash: wire.content_hash,
|
|
1376
|
+
sizeBytes: wire.size_bytes,
|
|
1377
|
+
created: wire.created
|
|
1378
|
+
};
|
|
1339
1379
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1380
|
+
async getFactoryFile(path, opts) {
|
|
1381
|
+
const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
1382
|
+
const q2 = new URLSearchParams({ path });
|
|
1383
|
+
if (opts?.revision !== undefined)
|
|
1384
|
+
q2.set("revision", String(opts.revision));
|
|
1385
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
|
|
1346
1386
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
try {
|
|
1351
|
-
const raw = await fs.readFile(path, "utf8");
|
|
1352
|
-
return JSON.parse(raw);
|
|
1353
|
-
} catch (err) {
|
|
1354
|
-
if (err.code === "ENOENT")
|
|
1355
|
-
return null;
|
|
1356
|
-
throw err;
|
|
1387
|
+
async listMembers() {
|
|
1388
|
+
const body = await this.fetch("/api/v1/team/members");
|
|
1389
|
+
return body.members;
|
|
1357
1390
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1391
|
+
async createMentions(input) {
|
|
1392
|
+
const factorySlug = input.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
1393
|
+
const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
|
|
1394
|
+
const { factorySlug: _omit, ...payload } = input;
|
|
1395
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/mentions`, {
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
body: payload,
|
|
1398
|
+
...runToken ? { headers: { "x-run-token": runToken } } : {}
|
|
1399
|
+
});
|
|
1400
|
+
return body.mentions;
|
|
1367
1401
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
async function writeRuntimeCheckpoint(agentInstanceId, blob) {
|
|
1382
|
-
assertSafeName("runtime", agentInstanceId);
|
|
1383
|
-
await atomicWriteJSON(runtimeStatePath(agentInstanceId), blob);
|
|
1384
|
-
}
|
|
1385
|
-
async function readCheckpoint(name) {
|
|
1386
|
-
assertSafeCheckpointName(name);
|
|
1387
|
-
return readJSONWithPresence(checkpointPath(name));
|
|
1388
|
-
}
|
|
1389
|
-
async function writeCheckpoint(name, value) {
|
|
1390
|
-
assertSafeCheckpointName(name);
|
|
1391
|
-
await atomicWriteJSON(checkpointPath(name), value);
|
|
1392
|
-
}
|
|
1393
|
-
function hasOwn(obj, key) {
|
|
1394
|
-
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1395
|
-
}
|
|
1396
|
-
function validatePauseRecord(value, expectedPauseId) {
|
|
1397
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1398
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: expected object`);
|
|
1402
|
+
async listFactoryEvents(opts) {
|
|
1403
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1404
|
+
const q2 = new URLSearchParams;
|
|
1405
|
+
if (opts?.limit != null)
|
|
1406
|
+
q2.set("limit", String(opts.limit));
|
|
1407
|
+
if (opts?.name)
|
|
1408
|
+
q2.set("name", opts.name);
|
|
1409
|
+
if (opts?.from)
|
|
1410
|
+
q2.set("from", opts.from);
|
|
1411
|
+
if (opts?.before)
|
|
1412
|
+
q2.set("before", opts.before);
|
|
1413
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/events${q2.toString() ? `?${q2}` : ""}`);
|
|
1399
1414
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1415
|
+
async listRunLogs(runId, opts) {
|
|
1416
|
+
const q2 = new URLSearchParams;
|
|
1417
|
+
if (opts?.afterId !== undefined)
|
|
1418
|
+
q2.set("afterId", String(opts.afterId));
|
|
1419
|
+
if (opts?.limit !== undefined)
|
|
1420
|
+
q2.set("limit", String(opts.limit));
|
|
1421
|
+
if (opts?.direction !== undefined)
|
|
1422
|
+
q2.set("direction", opts.direction);
|
|
1423
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/logs${q2.toString() ? `?${q2}` : ""}`);
|
|
1424
|
+
return body.logs;
|
|
1425
|
+
}
|
|
1426
|
+
async listTemplates(opts) {
|
|
1427
|
+
const path = opts?.factorySlug ? templatePath(opts.factorySlug) : "/api/v1/templates";
|
|
1428
|
+
const body = await this.fetch(path);
|
|
1429
|
+
return body.templates;
|
|
1403
1430
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1408
|
-
if (!hasOwn(record, "resumePayload") || record.resumePayload === undefined) {
|
|
1409
|
-
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require resumePayload`);
|
|
1410
|
-
}
|
|
1411
|
-
return {
|
|
1412
|
-
pauseId: expectedPauseId,
|
|
1413
|
-
request: hasOwn(record, "request") ? record.request : null,
|
|
1414
|
-
...record
|
|
1415
|
-
};
|
|
1431
|
+
async listFactories() {
|
|
1432
|
+
const body = await this.fetch("/api/v1/factories");
|
|
1433
|
+
return body.factories;
|
|
1416
1434
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1435
|
+
createFactory(payload) {
|
|
1436
|
+
return this.fetch("/api/v1/factories", { method: "POST", body: payload });
|
|
1419
1437
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1438
|
+
getFactory(slug) {
|
|
1439
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`);
|
|
1422
1440
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1441
|
+
updateFactory(slug, updates) {
|
|
1442
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "PATCH", body: updates });
|
|
1425
1443
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1444
|
+
deleteFactory(slug) {
|
|
1445
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "DELETE" });
|
|
1428
1446
|
}
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
assertSafeName("pause", pauseId);
|
|
1433
|
-
const record = await readJSON(pausePath(pauseId));
|
|
1434
|
-
return record === null ? null : validatePauseRecord(record, pauseId);
|
|
1435
|
-
}
|
|
1436
|
-
async function writePauseRecord(record) {
|
|
1437
|
-
assertSafeName("pause", record.pauseId);
|
|
1438
|
-
validatePauseRecord(record, record.pauseId);
|
|
1439
|
-
await atomicWriteJSON(pausePath(record.pauseId), record);
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// src/pause/manager.ts
|
|
1443
|
-
var AGENT_LOOP_STATE_SCHEMA_VERSION = 2;
|
|
1444
|
-
var SUPPORTED_AGENT_LOOP_STATE_VERSIONS = z6.union([z6.literal(1), z6.literal(2)]);
|
|
1445
|
-
var RunningAgentLoopStateSchema = z6.object({
|
|
1446
|
-
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1447
|
-
phase: z6.literal("running"),
|
|
1448
|
-
iteration: z6.number().int().nonnegative(),
|
|
1449
|
-
completedIterations: z6.number().int().nonnegative(),
|
|
1450
|
-
lastSessionId: z6.string(),
|
|
1451
|
-
lastStatus: AgentStatusSchema.nullable(),
|
|
1452
|
-
lastResponseText: z6.string(),
|
|
1453
|
-
lastResponseValidationError: z6.string(),
|
|
1454
|
-
iterationsWithoutStatus: z6.number().int().nonnegative(),
|
|
1455
|
-
blockerStreak: z6.object({ key: z6.string(), count: z6.number().int().nonnegative() }).nullable(),
|
|
1456
|
-
pendingSteerPause: z6.object({ reason: z6.string(), correlationKey: z6.string().nullable(), at: z6.number().int() }).nullable().default(null)
|
|
1457
|
-
});
|
|
1458
|
-
var SettledAgentLoopStateSchema = z6.object({
|
|
1459
|
-
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1460
|
-
phase: z6.literal("settled"),
|
|
1461
|
-
result: z6.object({
|
|
1462
|
-
sessionId: z6.string(),
|
|
1463
|
-
lastStatus: AgentStatusSchema.nullable(),
|
|
1464
|
-
iterations: z6.number().int().nonnegative(),
|
|
1465
|
-
response: z6.unknown().optional()
|
|
1466
|
-
})
|
|
1467
|
-
});
|
|
1468
|
-
var AgentLoopStateSchema = z6.discriminatedUnion("phase", [
|
|
1469
|
-
RunningAgentLoopStateSchema,
|
|
1470
|
-
SettledAgentLoopStateSchema
|
|
1471
|
-
]);
|
|
1472
|
-
function toProgressState(state) {
|
|
1473
|
-
return {
|
|
1474
|
-
iteration: state.iteration,
|
|
1475
|
-
completedIterations: state.completedIterations,
|
|
1476
|
-
lastSessionId: state.lastSessionId,
|
|
1477
|
-
lastStatus: state.lastStatus,
|
|
1478
|
-
lastResponseText: state.lastResponseText,
|
|
1479
|
-
lastResponseValidationError: state.lastResponseValidationError,
|
|
1480
|
-
iterationsWithoutStatus: state.iterationsWithoutStatus,
|
|
1481
|
-
blockerStreak: state.blockerStreak,
|
|
1482
|
-
pendingSteerPause: state.pendingSteerPause
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
class PauseManager {
|
|
1487
|
-
agentId;
|
|
1488
|
-
constructor(agentId) {
|
|
1489
|
-
this.agentId = agentId;
|
|
1447
|
+
async listSchedules(factorySlug = "default") {
|
|
1448
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/schedules`);
|
|
1449
|
+
return body.schedules;
|
|
1490
1450
|
}
|
|
1491
|
-
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
return { kind: "invalid", error: parsed.error.message };
|
|
1498
|
-
const state = parsed.data;
|
|
1499
|
-
if (state.phase === "settled") {
|
|
1500
|
-
return {
|
|
1501
|
-
kind: "settled",
|
|
1502
|
-
result: {
|
|
1503
|
-
sessionId: state.result.sessionId,
|
|
1504
|
-
lastStatus: state.result.lastStatus,
|
|
1505
|
-
iterations: state.result.iterations,
|
|
1506
|
-
...Object.prototype.hasOwnProperty.call(state.result, "response") ? { response: state.result.response } : {}
|
|
1507
|
-
}
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
const runtimeBlob = await readRuntimeCheckpoint(this.agentId);
|
|
1511
|
-
if (runtimeBlob !== null && !runtime.restoreCheckpoint) {
|
|
1512
|
-
throw new Error(`runtime checkpoint exists for resumed agentId=${this.agentId}, but runtime cannot restore it`);
|
|
1513
|
-
}
|
|
1514
|
-
if (runtime.restoreCheckpoint && runtimeBlob === null) {
|
|
1515
|
-
throw new Error(`missing runtime checkpoint for resumed agentId=${this.agentId}`);
|
|
1516
|
-
}
|
|
1517
|
-
if (runtimeBlob !== null)
|
|
1518
|
-
runtime.restoreCheckpoint?.(runtimeBlob);
|
|
1519
|
-
return { kind: "running", state: toProgressState(state) };
|
|
1451
|
+
createSchedule(payload) {
|
|
1452
|
+
const factorySlug = payload.factorySlug ?? "default";
|
|
1453
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/schedules`, {
|
|
1454
|
+
method: "POST",
|
|
1455
|
+
body: { name: payload.name, workflow: payload.workflow, cron: payload.cron }
|
|
1456
|
+
});
|
|
1520
1457
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1458
|
+
deleteSchedule(id, factorySlug = "default") {
|
|
1459
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1460
|
+
}
|
|
1461
|
+
setSecret(workflowName, key, value, opts) {
|
|
1462
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1463
|
+
return this.fetch(templatePath(factorySlug, workflowName, "secrets"), {
|
|
1464
|
+
method: "POST",
|
|
1465
|
+
body: { key, value }
|
|
1526
1466
|
});
|
|
1527
|
-
if (runtime.captureCheckpoint) {
|
|
1528
|
-
const blob = runtime.captureCheckpoint();
|
|
1529
|
-
if (blob !== undefined)
|
|
1530
|
-
await writeRuntimeCheckpoint(this.agentId, blob);
|
|
1531
|
-
}
|
|
1532
1467
|
}
|
|
1533
|
-
async
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1468
|
+
async listSecrets(workflowName, opts) {
|
|
1469
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1470
|
+
const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
|
|
1471
|
+
return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
1472
|
+
}
|
|
1473
|
+
deleteSecret(workflowName, key, opts) {
|
|
1474
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1475
|
+
return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
|
|
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 }
|
|
1543
1482
|
});
|
|
1544
1483
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
// src/step-invocation/types.ts
|
|
1551
|
-
import { z as z7 } from "zod";
|
|
1552
|
-
var StepPauseRequestSchema = z7.object({
|
|
1553
|
-
reason: z7.string().min(1),
|
|
1554
|
-
kind: z7.enum(["decision", "sleep", "event", "custom"]),
|
|
1555
|
-
payload: z7.record(z7.string(), z7.unknown()).optional(),
|
|
1556
|
-
ttlMs: z7.number().int().positive().optional(),
|
|
1557
|
-
correlationKey: z7.string().min(1).optional(),
|
|
1558
|
-
snapshot: z7.boolean().optional()
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
class StepExecutionError extends Error {
|
|
1562
|
-
kind;
|
|
1563
|
-
exitCode;
|
|
1564
|
-
constructor(error, stepLabel) {
|
|
1565
|
-
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
1566
|
-
this.name = "StepExecutionError";
|
|
1567
|
-
this.kind = error.kind;
|
|
1568
|
-
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
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 }));
|
|
1569
1488
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (!state)
|
|
1610
|
-
return null;
|
|
1611
|
-
const callIndex = state.nextAgentCallIndex;
|
|
1612
|
-
state.nextAgentCallIndex += 1;
|
|
1613
|
-
return { stepIndex: state.stepIndex, callIndex };
|
|
1614
|
-
}
|
|
1615
|
-
function nextPauseOrdinalInActiveStep(scope) {
|
|
1616
|
-
const state = currentState();
|
|
1617
|
-
if (!state)
|
|
1618
|
-
return null;
|
|
1619
|
-
const next = state.pauseOrdinalByScope.get(scope) ?? 0;
|
|
1620
|
-
state.pauseOrdinalByScope.set(scope, next + 1);
|
|
1621
|
-
return next;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// src/pause/errors.ts
|
|
1625
|
-
class PauseError extends Error {
|
|
1626
|
-
constructor(message) {
|
|
1627
|
-
super(message);
|
|
1628
|
-
this.name = new.target.name;
|
|
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
|
+
}
|
|
1493
|
+
createApiKey(input) {
|
|
1494
|
+
return this.fetch("/api-keys", { method: "POST", body: input });
|
|
1495
|
+
}
|
|
1496
|
+
async listApiKeys() {
|
|
1497
|
+
const body = await this.fetch("/api-keys");
|
|
1498
|
+
return body.data;
|
|
1499
|
+
}
|
|
1500
|
+
getUsage(from, to) {
|
|
1501
|
+
const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
|
|
1502
|
+
return this.fetch(`/api/v1/usage${qs}`);
|
|
1503
|
+
}
|
|
1504
|
+
cancelRun(runId) {
|
|
1505
|
+
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
|
|
1506
|
+
}
|
|
1507
|
+
async* streamRunLogs(runId, opts) {
|
|
1508
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
1509
|
+
if (opts?.lastEventId && opts.lastEventId > 0) {
|
|
1510
|
+
headers["Last-Event-ID"] = String(opts.lastEventId);
|
|
1511
|
+
}
|
|
1512
|
+
const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
|
|
1513
|
+
headers,
|
|
1514
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
1515
|
+
});
|
|
1516
|
+
if (!res.ok || !res.body) {
|
|
1517
|
+
let message = res.statusText;
|
|
1518
|
+
try {
|
|
1519
|
+
const body = await res.json();
|
|
1520
|
+
if (body.error)
|
|
1521
|
+
message = body.error;
|
|
1522
|
+
} catch {}
|
|
1523
|
+
throw new AgentComposeError(res.status, message);
|
|
1524
|
+
}
|
|
1525
|
+
for await (const ev of parseSseStream(res.body)) {
|
|
1526
|
+
yield ev.data;
|
|
1527
|
+
}
|
|
1629
1528
|
}
|
|
1630
1529
|
}
|
|
1530
|
+
// src/utils/bundler.ts
|
|
1531
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1532
|
+
import { parse as babelParse } from "@babel/parser";
|
|
1631
1533
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1534
|
+
// src/utils/source-loader.ts
|
|
1535
|
+
import { dirname } from "path";
|
|
1536
|
+
import { writeFile, rm, mkdir } from "node:fs/promises";
|
|
1537
|
+
var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
1538
|
+
var LATEST_VERSION = "$LATEST";
|
|
1539
|
+
async function importSourceModule(source, tmpPath) {
|
|
1540
|
+
await mkdir(dirname(tmpPath), { recursive: true });
|
|
1541
|
+
await writeFile(tmpPath, source, "utf8");
|
|
1542
|
+
const mod = await import(tmpPath);
|
|
1543
|
+
const cleanup = () => rm(tmpPath, { force: true });
|
|
1544
|
+
return { mod, cleanup };
|
|
1637
1545
|
}
|
|
1638
1546
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
constructor(message, issues) {
|
|
1643
|
-
super(message);
|
|
1644
|
-
this.issues = issues;
|
|
1645
|
-
}
|
|
1547
|
+
// src/types/workflow-plan.ts
|
|
1548
|
+
function workflowPlan(steps) {
|
|
1549
|
+
return { steps };
|
|
1646
1550
|
}
|
|
1647
1551
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1552
|
+
// src/utils/bundler.ts
|
|
1553
|
+
var BUNDLER_VERSION = 2;
|
|
1554
|
+
|
|
1555
|
+
class WorkflowSourceValidationError extends Error {
|
|
1556
|
+
constructor(message) {
|
|
1651
1557
|
super(message);
|
|
1558
|
+
this.name = "WorkflowSourceValidationError";
|
|
1652
1559
|
}
|
|
1653
1560
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
constructor(pauseId, pauseRequest, observability) {
|
|
1664
|
-
this.pauseId = pauseId;
|
|
1665
|
-
this.pauseRequest = pauseRequest;
|
|
1666
|
-
this.observability = observability;
|
|
1561
|
+
async function bundle(path, label) {
|
|
1562
|
+
const bun = globalThis;
|
|
1563
|
+
if (!bun.Bun?.build)
|
|
1564
|
+
throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
1565
|
+
const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
|
|
1566
|
+
if (!result.success) {
|
|
1567
|
+
throw new Error(`Failed to bundle ${label}:
|
|
1568
|
+
${result.logs.map((l) => l.message).join(`
|
|
1569
|
+
`)}`);
|
|
1667
1570
|
}
|
|
1571
|
+
return result.outputs[0].text();
|
|
1668
1572
|
}
|
|
1669
|
-
function
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1573
|
+
async function extractFromBundle(source, label, pick) {
|
|
1574
|
+
const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
|
|
1575
|
+
try {
|
|
1576
|
+
const loaded = await importSourceModule(source, tmpPath);
|
|
1577
|
+
try {
|
|
1578
|
+
return pick(loaded.mod.default);
|
|
1579
|
+
} finally {
|
|
1580
|
+
await loaded.cleanup();
|
|
1581
|
+
}
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1584
|
+
}
|
|
1675
1585
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1586
|
+
var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
|
|
1587
|
+
function isDefineWorkflowCall(node) {
|
|
1588
|
+
if (node.callee.type === "Identifier") {
|
|
1589
|
+
return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
|
|
1590
|
+
}
|
|
1591
|
+
if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
|
|
1592
|
+
return isDefineWorkflowCall(node.callee.object);
|
|
1593
|
+
}
|
|
1594
|
+
return false;
|
|
1679
1595
|
}
|
|
1680
|
-
function
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1596
|
+
function assertDefaultExportIsDefineWorkflow(source, label) {
|
|
1597
|
+
let ast;
|
|
1598
|
+
try {
|
|
1599
|
+
ast = babelParse(source, {
|
|
1600
|
+
sourceType: "module",
|
|
1601
|
+
plugins: [],
|
|
1602
|
+
errorRecovery: false
|
|
1603
|
+
});
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1606
|
+
throw new WorkflowSourceValidationError(`Bundled workflow ${label} failed to parse as JavaScript: ${msg}. ` + `If you registered raw .ts source, switch to \`agentc register <file.ts>\` so the source is bundled first.`);
|
|
1607
|
+
}
|
|
1608
|
+
const defaultExpr = findDefaultExportExpression(ast.program.body);
|
|
1609
|
+
if (!defaultExpr) {
|
|
1610
|
+
throw new WorkflowSourceValidationError(`Bundled workflow ${label} has no default export. ` + `Use \`export default defineWorkflow({ run: ... })\`.`);
|
|
1611
|
+
}
|
|
1612
|
+
if (defaultExpr.type !== "CallExpression" || !isDefineWorkflowCall(defaultExpr)) {
|
|
1613
|
+
throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export must be a \`defineWorkflow({...})\` call ` + `(or a \`defineWorkflow(...).step(...).build()\` chain). ` + `Got: ${defaultExpr.type}. Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk".`);
|
|
1614
|
+
}
|
|
1686
1615
|
}
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1616
|
+
function findDefaultExportExpression(body) {
|
|
1617
|
+
const defaultDecl = body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
1618
|
+
if (defaultDecl) {
|
|
1619
|
+
return defaultDecl.declaration;
|
|
1620
|
+
}
|
|
1621
|
+
const exportNamed = body.find((node) => node.type === "ExportNamedDeclaration" && node.specifiers.some((spec2) => spec2.type === "ExportSpecifier" && spec2.exported.type === "Identifier" && spec2.exported.name === "default"));
|
|
1622
|
+
if (!exportNamed || exportNamed.type !== "ExportNamedDeclaration")
|
|
1623
|
+
return null;
|
|
1624
|
+
const spec = exportNamed.specifiers.find((s) => s.type === "ExportSpecifier" && s.exported.type === "Identifier" && s.exported.name === "default");
|
|
1625
|
+
if (!spec || spec.type !== "ExportSpecifier" || spec.local.type !== "Identifier")
|
|
1626
|
+
return null;
|
|
1627
|
+
const localName = spec.local.name;
|
|
1628
|
+
for (const stmt of body) {
|
|
1629
|
+
if (stmt.type !== "VariableDeclaration")
|
|
1630
|
+
continue;
|
|
1631
|
+
for (const decl of stmt.declarations) {
|
|
1632
|
+
if (decl.id.type === "Identifier" && decl.id.name === localName && decl.init && decl.init.type !== "TSSatisfiesExpression") {
|
|
1633
|
+
return decl.init;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return null;
|
|
1689
1638
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
const activeOrdinal = nextPauseOrdinalInActiveStep(scope);
|
|
1693
|
-
if (activeOrdinal !== null)
|
|
1694
|
-
return activeOrdinal;
|
|
1695
|
-
const next = pauseOrdinalByScope.get(scope) ?? 0;
|
|
1696
|
-
pauseOrdinalByScope.set(scope, next + 1);
|
|
1697
|
-
return next;
|
|
1639
|
+
function sha256(text) {
|
|
1640
|
+
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
1698
1641
|
}
|
|
1699
|
-
function
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1642
|
+
async function bundleWorkflow(workflowPath, overrides) {
|
|
1643
|
+
const label = `"${workflowPath}"`;
|
|
1644
|
+
const source = await bundle(workflowPath, `workflow ${label}`);
|
|
1645
|
+
assertDefaultExportIsDefineWorkflow(source, label);
|
|
1646
|
+
const workflow = await extractFromBundle(source, "workflow", (wf) => isWorkflow(wf) ? wf : undefined);
|
|
1647
|
+
if (!workflow) {
|
|
1648
|
+
throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export is not a Workflow. ` + `Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` + `raw \`export default async function\` is no longer supported.`);
|
|
1649
|
+
}
|
|
1650
|
+
const { metadata } = workflow;
|
|
1651
|
+
const plan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
|
|
1652
|
+
const manifest = {
|
|
1653
|
+
definitionBrand: true,
|
|
1654
|
+
sourceHash: sha256(source),
|
|
1655
|
+
bundlerVersion: BUNDLER_VERSION
|
|
1656
|
+
};
|
|
1657
|
+
const inputSchema = extractIOSchema(workflow.input);
|
|
1658
|
+
const outputSchema = extractIOSchema(workflow.output);
|
|
1659
|
+
return {
|
|
1660
|
+
source,
|
|
1661
|
+
manifest,
|
|
1662
|
+
workflowPlan: plan,
|
|
1663
|
+
networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
|
|
1664
|
+
placeholders: overrides?.placeholders ?? metadata.placeholders,
|
|
1665
|
+
...metadata.description !== undefined ? { description: metadata.description } : {},
|
|
1666
|
+
...inputSchema !== undefined ? { inputSchema } : {},
|
|
1667
|
+
...outputSchema !== undefined ? { outputSchema } : {},
|
|
1668
|
+
...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
|
|
1669
|
+
...metadata.resources !== undefined ? { resources: metadata.resources } : {},
|
|
1670
|
+
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1671
|
+
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1672
|
+
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {},
|
|
1673
|
+
...metadata.environmentBuild !== undefined ? { environmentBuild: metadata.environmentBuild } : {}
|
|
1674
|
+
};
|
|
1703
1675
|
}
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if (
|
|
1715
|
-
return
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1676
|
+
function extractIOSchema(zodSchema) {
|
|
1677
|
+
let json;
|
|
1678
|
+
try {
|
|
1679
|
+
const z6 = __require("zod");
|
|
1680
|
+
if (!z6.toJSONSchema)
|
|
1681
|
+
return;
|
|
1682
|
+
json = z6.toJSONSchema(zodSchema);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (!json || typeof json !== "object")
|
|
1687
|
+
return;
|
|
1688
|
+
if (Object.keys(json).filter((k) => k !== "$schema").length === 0)
|
|
1689
|
+
return;
|
|
1690
|
+
const out = {};
|
|
1691
|
+
if (json.type !== undefined)
|
|
1692
|
+
out.type = json.type;
|
|
1693
|
+
if (json.description !== undefined)
|
|
1694
|
+
out.description = String(json.description);
|
|
1695
|
+
if (json.properties && typeof json.properties === "object") {
|
|
1696
|
+
const props = {};
|
|
1697
|
+
for (const [key, raw] of Object.entries(json.properties)) {
|
|
1698
|
+
if (raw && typeof raw === "object") {
|
|
1699
|
+
const r = raw;
|
|
1700
|
+
props[key] = {};
|
|
1701
|
+
if (r.type !== undefined)
|
|
1702
|
+
props[key].type = r.type;
|
|
1703
|
+
if (r.description !== undefined)
|
|
1704
|
+
props[key].description = String(r.description);
|
|
1705
|
+
}
|
|
1725
1706
|
}
|
|
1726
|
-
|
|
1707
|
+
if (Object.keys(props).length > 0)
|
|
1708
|
+
out.properties = props;
|
|
1709
|
+
}
|
|
1710
|
+
if (Array.isArray(json.required))
|
|
1711
|
+
out.required = json.required.map(String);
|
|
1712
|
+
return out;
|
|
1713
|
+
}
|
|
1714
|
+
// src/utils/schemas.ts
|
|
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()
|
|
1723
|
+
});
|
|
1724
|
+
// src/runtimes/claude.ts
|
|
1725
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1726
|
+
|
|
1727
|
+
// src/agent/agent-loop.ts
|
|
1728
|
+
import { z as z10 } from "zod";
|
|
1729
|
+
|
|
1730
|
+
// src/agent/protocol.ts
|
|
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()
|
|
1735
|
+
}).passthrough();
|
|
1736
|
+
function parseAgentResponse(text) {
|
|
1737
|
+
const match = text.match(/<response>([\s\S]*?)<\/response>/);
|
|
1738
|
+
if (!match)
|
|
1739
|
+
return null;
|
|
1740
|
+
try {
|
|
1741
|
+
return JSON.parse(match[1].trim());
|
|
1742
|
+
} catch {
|
|
1727
1743
|
return null;
|
|
1728
1744
|
}
|
|
1729
|
-
const proto = Object.getPrototypeOf(obj);
|
|
1730
|
-
if (proto !== Object.prototype && proto !== null)
|
|
1731
|
-
return path;
|
|
1732
|
-
for (const key of Reflect.ownKeys(obj)) {
|
|
1733
|
-
if (typeof key !== "string")
|
|
1734
|
-
return `${path}.${String(key)}`;
|
|
1735
|
-
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
1736
|
-
if (!desc?.enumerable)
|
|
1737
|
-
return `${path}.${key}`;
|
|
1738
|
-
const child = nonJsonPath(value[key], `${path}.${key}`, seen);
|
|
1739
|
-
if (child)
|
|
1740
|
-
return child;
|
|
1741
|
-
}
|
|
1742
|
-
seen.delete(obj);
|
|
1743
|
-
return null;
|
|
1744
1745
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1746
|
+
|
|
1747
|
+
// src/agent/agent-loop.ts
|
|
1748
|
+
import { randomUUID } from "node:crypto";
|
|
1749
|
+
|
|
1750
|
+
// src/pause/manager.ts
|
|
1751
|
+
import { z as z8 } from "zod";
|
|
1752
|
+
var AGENT_LOOP_STATE_SCHEMA_VERSION = 2;
|
|
1753
|
+
var SUPPORTED_AGENT_LOOP_STATE_VERSIONS = z8.union([z8.literal(1), z8.literal(2)]);
|
|
1754
|
+
var RunningAgentLoopStateSchema = z8.object({
|
|
1755
|
+
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1756
|
+
phase: z8.literal("running"),
|
|
1757
|
+
iteration: z8.number().int().nonnegative(),
|
|
1758
|
+
completedIterations: z8.number().int().nonnegative(),
|
|
1759
|
+
lastSessionId: z8.string(),
|
|
1760
|
+
lastStatus: AgentStatusSchema.nullable(),
|
|
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)
|
|
1766
|
+
});
|
|
1767
|
+
var SettledAgentLoopStateSchema = z8.object({
|
|
1768
|
+
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1769
|
+
phase: z8.literal("settled"),
|
|
1770
|
+
result: z8.object({
|
|
1771
|
+
sessionId: z8.string(),
|
|
1772
|
+
lastStatus: AgentStatusSchema.nullable(),
|
|
1773
|
+
iterations: z8.number().int().nonnegative(),
|
|
1774
|
+
response: z8.unknown().optional()
|
|
1775
|
+
})
|
|
1776
|
+
});
|
|
1777
|
+
var AgentLoopStateSchema = z8.discriminatedUnion("phase", [
|
|
1778
|
+
RunningAgentLoopStateSchema,
|
|
1779
|
+
SettledAgentLoopStateSchema
|
|
1780
|
+
]);
|
|
1781
|
+
function toProgressState(state) {
|
|
1782
|
+
return {
|
|
1783
|
+
iteration: state.iteration,
|
|
1784
|
+
completedIterations: state.completedIterations,
|
|
1785
|
+
lastSessionId: state.lastSessionId,
|
|
1786
|
+
lastStatus: state.lastStatus,
|
|
1787
|
+
lastResponseText: state.lastResponseText,
|
|
1788
|
+
lastResponseValidationError: state.lastResponseValidationError,
|
|
1789
|
+
iterationsWithoutStatus: state.iterationsWithoutStatus,
|
|
1790
|
+
blockerStreak: state.blockerStreak,
|
|
1791
|
+
pendingSteerPause: state.pendingSteerPause
|
|
1753
1792
|
};
|
|
1754
|
-
const parsed = StepPauseRequestSchema.safeParse(wire);
|
|
1755
|
-
if (!parsed.success) {
|
|
1756
|
-
throw new PauseRequestError(`pause request failed validation: ${parsed.error.message}`);
|
|
1757
|
-
}
|
|
1758
|
-
const badPath = nonJsonPath(parsed.data);
|
|
1759
|
-
if (badPath) {
|
|
1760
|
-
throw new PauseRequestError(`pause request must be JSON-serializable (invalid value at ${badPath})`);
|
|
1761
|
-
}
|
|
1762
|
-
return parsed.data;
|
|
1763
1793
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
await writePauseRecord({ pauseId, request: wire });
|
|
1770
|
-
throw new PauseSignal(pauseId, wire);
|
|
1794
|
+
|
|
1795
|
+
class PauseManager {
|
|
1796
|
+
agentId;
|
|
1797
|
+
constructor(agentId) {
|
|
1798
|
+
this.agentId = agentId;
|
|
1771
1799
|
}
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
if (
|
|
1775
|
-
return
|
|
1776
|
-
|
|
1800
|
+
async restoreAgentLoop(runtime) {
|
|
1801
|
+
const priorState = await readAgentState(this.agentId);
|
|
1802
|
+
if (priorState === null)
|
|
1803
|
+
return { kind: "fresh" };
|
|
1804
|
+
const parsed = AgentLoopStateSchema.safeParse(priorState);
|
|
1805
|
+
if (!parsed.success)
|
|
1806
|
+
return { kind: "invalid", error: parsed.error.message };
|
|
1807
|
+
const state = parsed.data;
|
|
1808
|
+
if (state.phase === "settled") {
|
|
1809
|
+
return {
|
|
1810
|
+
kind: "settled",
|
|
1811
|
+
result: {
|
|
1812
|
+
sessionId: state.result.sessionId,
|
|
1813
|
+
lastStatus: state.result.lastStatus,
|
|
1814
|
+
iterations: state.result.iterations,
|
|
1815
|
+
...Object.prototype.hasOwnProperty.call(state.result, "response") ? { response: state.result.response } : {}
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
const runtimeBlob = await readRuntimeCheckpoint(this.agentId);
|
|
1820
|
+
if (runtimeBlob !== null && !runtime.restoreCheckpoint) {
|
|
1821
|
+
throw new Error(`runtime checkpoint exists for resumed agentId=${this.agentId}, but runtime cannot restore it`);
|
|
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) };
|
|
1777
1829
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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);
|
|
1783
1840
|
}
|
|
1784
|
-
return parsed.data;
|
|
1785
1841
|
}
|
|
1786
|
-
|
|
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
|
+
}
|
|
1787
1854
|
}
|
|
1788
1855
|
|
|
1789
1856
|
// src/agent/steer-control.ts
|
|
1790
|
-
import { z as
|
|
1791
|
-
var SteerDecisionSchema =
|
|
1792
|
-
message:
|
|
1793
|
-
|
|
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`"
|
|
1794
1864
|
});
|
|
1795
1865
|
var steerPending = new Map;
|
|
1796
1866
|
function signalSteerPending(agentId, payload) {
|
|
@@ -1853,7 +1923,7 @@ async function runControlPoller(opts) {
|
|
|
1853
1923
|
// src/agent/agent-loop.ts
|
|
1854
1924
|
var DEFAULT_CLAUDE_MODEL = "claude-fable-5";
|
|
1855
1925
|
var SAME_BLOCKER_ITERATIONS = 3;
|
|
1856
|
-
var STALL_ITERATIONS =
|
|
1926
|
+
var STALL_ITERATIONS = 6;
|
|
1857
1927
|
var MESSAGE_PREVIEW_CHARS = 400;
|
|
1858
1928
|
function parseAgentStatus(text) {
|
|
1859
1929
|
const match = text.match(/<status>([\s\S]*?)<\/status>/);
|
|
@@ -1903,6 +1973,8 @@ function summarizeAgentMessage(msg) {
|
|
|
1903
1973
|
return { type: "done", sessionId: msg.sessionId };
|
|
1904
1974
|
case "error":
|
|
1905
1975
|
return { type: "error", text: truncate(msg.text) };
|
|
1976
|
+
case "plan":
|
|
1977
|
+
return { type: "plan", entries: msg.entries };
|
|
1906
1978
|
}
|
|
1907
1979
|
}
|
|
1908
1980
|
async function agentLoop(opts) {
|
|
@@ -1928,7 +2000,8 @@ async function agentLoop(opts) {
|
|
|
1928
2000
|
abortSignal: loopAbort.signal,
|
|
1929
2001
|
retryCount: 0,
|
|
1930
2002
|
agentId,
|
|
1931
|
-
iteration
|
|
2003
|
+
iteration,
|
|
2004
|
+
pause: boundProcessorPause(opts.pause, { agentId, iteration })
|
|
1932
2005
|
});
|
|
1933
2006
|
if (!opts.runtime)
|
|
1934
2007
|
throw new Error("agentLoop: opts.runtime is required");
|
|
@@ -1940,7 +2013,8 @@ async function agentLoop(opts) {
|
|
|
1940
2013
|
processors,
|
|
1941
2014
|
requestContext,
|
|
1942
2015
|
agentId,
|
|
1943
|
-
...opts.
|
|
2016
|
+
...opts.pause ? { pause: opts.pause } : {},
|
|
2017
|
+
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: z10.toJSONSchema(opts.responseSchema) } } : {}
|
|
1944
2018
|
});
|
|
1945
2019
|
if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
|
|
1946
2020
|
process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
|
|
@@ -2034,6 +2108,7 @@ async function agentLoop(opts) {
|
|
|
2034
2108
|
steerDecision = await opts.pause({
|
|
2035
2109
|
reason: pendingSteerPause.reason,
|
|
2036
2110
|
...pendingSteerPause.correlationKey !== null ? { correlationKey: pendingSteerPause.correlationKey } : {},
|
|
2111
|
+
...pendingSteerPause.payload !== undefined ? { payload: pendingSteerPause.payload } : {},
|
|
2037
2112
|
schema: SteerDecisionSchema
|
|
2038
2113
|
}, { agentId, iteration });
|
|
2039
2114
|
pendingSteerPause = null;
|
|
@@ -2070,6 +2145,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2070
2145
|
prompt,
|
|
2071
2146
|
sessionId: lastSessionId ?? undefined,
|
|
2072
2147
|
iteration: iteration + 1,
|
|
2148
|
+
signal: loopAbort.signal,
|
|
2073
2149
|
...opts.inbox ? { inboxStream: opts.inbox } : {}
|
|
2074
2150
|
})) {
|
|
2075
2151
|
const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
|
|
@@ -2084,7 +2160,15 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2084
2160
|
}
|
|
2085
2161
|
const msg = outputVerdict.value;
|
|
2086
2162
|
opts.onAgentEvent?.(iteration, msg);
|
|
2087
|
-
|
|
2163
|
+
const summary = summarizeAgentMessage(msg);
|
|
2164
|
+
opts.onAgentLifecycleEvent?.({
|
|
2165
|
+
event: "agent.message",
|
|
2166
|
+
at: Date.now(),
|
|
2167
|
+
agentId,
|
|
2168
|
+
label,
|
|
2169
|
+
iteration: iteration + 1,
|
|
2170
|
+
message: summary.type === "usage" && client.model != null ? { ...summary, model: client.model } : summary
|
|
2171
|
+
});
|
|
2088
2172
|
if (msg.type === "init")
|
|
2089
2173
|
lastSessionId = msg.sessionId;
|
|
2090
2174
|
if (msg.type === "text")
|
|
@@ -2094,6 +2178,19 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2094
2178
|
}
|
|
2095
2179
|
lastResponseText = responseText;
|
|
2096
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
|
+
}
|
|
2097
2194
|
const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
|
|
2098
2195
|
const validateResponse = (target) => {
|
|
2099
2196
|
const parsed = opts.responseSchema.safeParse(target);
|
|
@@ -2163,9 +2260,9 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
2163
2260
|
if (!status && rawResponse === null) {
|
|
2164
2261
|
if (++iterationsWithoutStatus >= STALL_ITERATIONS)
|
|
2165
2262
|
throw new Error(`${logLabel} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
|
|
2166
|
-
if (
|
|
2167
|
-
lastResponseValidationError = "no <response> block
|
|
2168
|
-
process.stdout.write(`${logLabel} no <status
|
|
2263
|
+
if (schemaRetriesLeft-- > 0) {
|
|
2264
|
+
lastResponseValidationError = "Your turn ended with no <status> block" + (opts.responseSchema ? " (and no <response> block)" : "") + ". " + "If you launched a background job (`… &`) and are waiting to be notified when it finishes — STOP: nothing will notify you here. " + "Poll it NOW (read its output / wait for it synchronously to completion), then emit your <status>" + (opts.responseSchema ? " and the complete <response> JSON." : ".");
|
|
2265
|
+
process.stdout.write(`${logLabel} empty turn (no <status>) — corrective re-prompt (${schemaRetriesLeft} retries left)
|
|
2169
2266
|
`);
|
|
2170
2267
|
iteration--;
|
|
2171
2268
|
continue;
|
|
@@ -2379,12 +2476,15 @@ class ClaudeRunner {
|
|
|
2379
2476
|
toolInput: pre.tool_input ?? {},
|
|
2380
2477
|
toolUseId: toolUseId ?? `${pre.tool_name}-${Date.now()}`
|
|
2381
2478
|
};
|
|
2479
|
+
const gateAgentId = this.options.agentId ?? "agent";
|
|
2480
|
+
const gateIteration = opts.iteration ?? 1;
|
|
2382
2481
|
const gate = await this.gateToolCall(call, {
|
|
2383
2482
|
requestContext,
|
|
2384
2483
|
abortSignal: signal ?? opts.signal ?? new AbortController().signal,
|
|
2385
2484
|
retryCount: 0,
|
|
2386
|
-
agentId:
|
|
2387
|
-
iteration:
|
|
2485
|
+
agentId: gateAgentId,
|
|
2486
|
+
iteration: gateIteration,
|
|
2487
|
+
pause: boundProcessorPause(this.options.pause, { agentId: gateAgentId, iteration: gateIteration })
|
|
2388
2488
|
});
|
|
2389
2489
|
if (gate.kind === "allow")
|
|
2390
2490
|
return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "allow", updatedInput: gate.call.toolInput } };
|
|
@@ -2414,67 +2514,360 @@ class ClaudeRunner {
|
|
|
2414
2514
|
} catch {}
|
|
2415
2515
|
})();
|
|
2416
2516
|
}
|
|
2417
|
-
const promptForQuery = inboxQueue ?? opts.prompt;
|
|
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
|
+
}
|
|
2526
|
+
try {
|
|
2527
|
+
let emittedAssistantText = false;
|
|
2528
|
+
for await (const message of query({
|
|
2529
|
+
prompt: promptForQuery,
|
|
2530
|
+
options: {
|
|
2531
|
+
tools: this.options.allowedTools,
|
|
2532
|
+
allowedTools: this.options.allowedTools,
|
|
2533
|
+
maxTurns: this.options.maxTurns,
|
|
2534
|
+
model: this.model,
|
|
2535
|
+
outputFormat: this.options.outputFormat,
|
|
2536
|
+
thinking: resolveThinking(this.model, this.config.thinking),
|
|
2537
|
+
effort: this.config.effort,
|
|
2538
|
+
cwd: this.options.cwd,
|
|
2539
|
+
abortController: queryAbort,
|
|
2540
|
+
env: { ...process.env, ...this.config.env ?? {}, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} },
|
|
2541
|
+
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2542
|
+
...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
|
|
2543
|
+
skills: this.config.skills ?? "all",
|
|
2544
|
+
resume: opts.sessionId,
|
|
2545
|
+
mcpServers: this.config.mcpServers,
|
|
2546
|
+
permissionMode: "acceptEdits",
|
|
2547
|
+
hooks: { PreToolUse: [{ hooks: [preToolHook] }] }
|
|
2548
|
+
}
|
|
2549
|
+
})) {
|
|
2550
|
+
const raw = message;
|
|
2551
|
+
const translated = translateMessage(raw);
|
|
2552
|
+
for (const msg of translated) {
|
|
2553
|
+
if (raw.type === "result" && msg.type === "text" && emittedAssistantText)
|
|
2554
|
+
continue;
|
|
2555
|
+
if (msg.type === "text" && raw.type === "assistant")
|
|
2556
|
+
emittedAssistantText = true;
|
|
2557
|
+
yield msg;
|
|
2558
|
+
}
|
|
2559
|
+
if (raw.type === "result" && inboxQueue)
|
|
2560
|
+
inboxQueue.close();
|
|
2561
|
+
}
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
const text = formatError(err);
|
|
2564
|
+
if (/maximum number of turns/i.test(text)) {
|
|
2565
|
+
yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now2() };
|
|
2566
|
+
} else {
|
|
2567
|
+
yield { type: "error", text, timestamp: now2() };
|
|
2568
|
+
}
|
|
2569
|
+
} finally {
|
|
2570
|
+
inboxQueue?.close();
|
|
2571
|
+
opts.signal?.removeEventListener("abort", onLoopAbort);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function createClaudeRuntime(config = {}) {
|
|
2576
|
+
return defineRuntime({
|
|
2577
|
+
create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config)
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
var claude_default = createClaudeRuntime();
|
|
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";
|
|
2589
|
+
function now3() {
|
|
2590
|
+
return new Date().toISOString();
|
|
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;
|
|
2418
2826
|
try {
|
|
2419
|
-
|
|
2420
|
-
for await (const message of query({
|
|
2421
|
-
prompt: promptForQuery,
|
|
2422
|
-
options: {
|
|
2423
|
-
tools: this.options.allowedTools,
|
|
2424
|
-
allowedTools: this.options.allowedTools,
|
|
2425
|
-
maxTurns: this.options.maxTurns,
|
|
2426
|
-
model: this.model,
|
|
2427
|
-
outputFormat: this.options.outputFormat,
|
|
2428
|
-
thinking: resolveThinking(this.model, this.config.thinking),
|
|
2429
|
-
effort: this.config.effort,
|
|
2430
|
-
cwd: this.options.cwd,
|
|
2431
|
-
env: { ...process.env, ...this.config.env ?? {} },
|
|
2432
|
-
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2433
|
-
...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
|
|
2434
|
-
skills: this.config.skills ?? "all",
|
|
2435
|
-
resume: opts.sessionId,
|
|
2436
|
-
mcpServers: this.config.mcpServers,
|
|
2437
|
-
permissionMode: "acceptEdits",
|
|
2438
|
-
hooks: { PreToolUse: [{ hooks: [preToolHook] }] }
|
|
2439
|
-
}
|
|
2440
|
-
})) {
|
|
2441
|
-
const raw = message;
|
|
2442
|
-
const translated = translateMessage(raw);
|
|
2443
|
-
for (const msg of translated) {
|
|
2444
|
-
if (raw.type === "result" && msg.type === "text" && emittedAssistantText)
|
|
2445
|
-
continue;
|
|
2446
|
-
if (msg.type === "text" && raw.type === "assistant")
|
|
2447
|
-
emittedAssistantText = true;
|
|
2448
|
-
yield msg;
|
|
2449
|
-
}
|
|
2450
|
-
if (raw.type === "result" && inboxQueue)
|
|
2451
|
-
inboxQueue.close();
|
|
2452
|
-
}
|
|
2827
|
+
verdict = await this.deps.gateToolCall(call, this.deps.procCtx());
|
|
2453
2828
|
} catch (err) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
yield { type: "error", text, timestamp: now2() };
|
|
2829
|
+
if (isPauseSignal(err)) {
|
|
2830
|
+
this.pendingPause = err;
|
|
2831
|
+
this.cancel();
|
|
2832
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2459
2833
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
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" } };
|
|
2462
2844
|
}
|
|
2845
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
2463
2846
|
}
|
|
2464
2847
|
}
|
|
2465
|
-
|
|
2466
|
-
return defineRuntime({
|
|
2467
|
-
create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config)
|
|
2468
|
-
});
|
|
2469
|
-
}
|
|
2470
|
-
var claude_default = createClaudeRuntime();
|
|
2848
|
+
|
|
2471
2849
|
// src/runtimes/_cli-agent.ts
|
|
2472
|
-
function
|
|
2850
|
+
function now4() {
|
|
2473
2851
|
return new Date().toISOString();
|
|
2474
2852
|
}
|
|
2475
2853
|
function shellQuote(value) {
|
|
2476
2854
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2477
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
|
+
}
|
|
2478
2871
|
|
|
2479
2872
|
class CliAgentRunner {
|
|
2480
2873
|
sandbox;
|
|
@@ -2482,6 +2875,16 @@ class CliAgentRunner {
|
|
|
2482
2875
|
spec;
|
|
2483
2876
|
configModel;
|
|
2484
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;
|
|
2485
2888
|
constructor(sandbox, options, spec, configModel) {
|
|
2486
2889
|
this.sandbox = sandbox;
|
|
2487
2890
|
this.options = options;
|
|
@@ -2489,9 +2892,37 @@ class CliAgentRunner {
|
|
|
2489
2892
|
this.configModel = configModel;
|
|
2490
2893
|
this.kind = spec.kind;
|
|
2491
2894
|
}
|
|
2895
|
+
enableAcpForTesting() {
|
|
2896
|
+
this.acpTransportReady = true;
|
|
2897
|
+
}
|
|
2492
2898
|
get model() {
|
|
2493
2899
|
return this.configModel ?? this.options.model ?? this.spec.defaultModel;
|
|
2494
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
|
+
}
|
|
2495
2926
|
installed = false;
|
|
2496
2927
|
async ensureInstalled() {
|
|
2497
2928
|
if (this.installed)
|
|
@@ -2508,7 +2939,133 @@ class CliAgentRunner {
|
|
|
2508
2939
|
this.installed = true;
|
|
2509
2940
|
}
|
|
2510
2941
|
async* sendMessage(opts) {
|
|
2511
|
-
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) {
|
|
2512
3069
|
let sessionId = opts.sessionId;
|
|
2513
3070
|
let sawError = false;
|
|
2514
3071
|
try {
|
|
@@ -2566,13 +3123,13 @@ class CliAgentRunner {
|
|
|
2566
3123
|
const res = await runPromise;
|
|
2567
3124
|
if (res.exitCode !== 0 && !sawError) {
|
|
2568
3125
|
const tail = (res.stderr ?? "").slice(-2000);
|
|
2569
|
-
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() };
|
|
2570
3127
|
return;
|
|
2571
3128
|
}
|
|
2572
3129
|
if (!sawError)
|
|
2573
|
-
yield { type: "done", sessionId: sessionId ?? "", timestamp:
|
|
3130
|
+
yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
|
|
2574
3131
|
} catch (err) {
|
|
2575
|
-
yield { type: "error", text: formatError(err), timestamp:
|
|
3132
|
+
yield { type: "error", text: formatError(err), timestamp: now4() };
|
|
2576
3133
|
}
|
|
2577
3134
|
}
|
|
2578
3135
|
}
|
|
@@ -2583,13 +3140,19 @@ function createCliAgentRuntime(spec, configModel) {
|
|
|
2583
3140
|
}
|
|
2584
3141
|
|
|
2585
3142
|
// src/runtimes/codex.ts
|
|
2586
|
-
function
|
|
3143
|
+
function now5() {
|
|
2587
3144
|
return new Date().toISOString();
|
|
2588
3145
|
}
|
|
3146
|
+
var CODEX_ACP_ADAPTER = "@agentclientprotocol/codex-acp@0.1.0";
|
|
2589
3147
|
var codexSpec = {
|
|
2590
3148
|
kind: "codex",
|
|
2591
3149
|
authEnv: "CODEX_API_KEY",
|
|
2592
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
|
+
},
|
|
2593
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)',
|
|
2594
3157
|
promptPayload: (prompt) => prompt,
|
|
2595
3158
|
buildCommand: ({ promptPath, sessionId, model, cwd }) => {
|
|
@@ -2605,7 +3168,7 @@ var codexSpec = {
|
|
|
2605
3168
|
},
|
|
2606
3169
|
extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
|
|
2607
3170
|
mapEvent: (p) => {
|
|
2608
|
-
const ts =
|
|
3171
|
+
const ts = now5();
|
|
2609
3172
|
switch (p.type) {
|
|
2610
3173
|
case "item.started":
|
|
2611
3174
|
case "item.completed": {
|
|
@@ -2660,7 +3223,7 @@ function createCodexRuntime(config = {}) {
|
|
|
2660
3223
|
}
|
|
2661
3224
|
var codex_default = createCodexRuntime();
|
|
2662
3225
|
// src/runtimes/amp.ts
|
|
2663
|
-
function
|
|
3226
|
+
function now6() {
|
|
2664
3227
|
return new Date().toISOString();
|
|
2665
3228
|
}
|
|
2666
3229
|
function blocks(msg) {
|
|
@@ -2680,7 +3243,7 @@ var ampSpec = {
|
|
|
2680
3243
|
},
|
|
2681
3244
|
extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
|
|
2682
3245
|
mapEvent: (p) => {
|
|
2683
|
-
const ts =
|
|
3246
|
+
const ts = now6();
|
|
2684
3247
|
if (p.type === "assistant") {
|
|
2685
3248
|
return blocks(p).flatMap((b) => {
|
|
2686
3249
|
if (b.type === "text")
|
|
@@ -2725,6 +3288,7 @@ var amp_default = createAmpRuntime();
|
|
|
2725
3288
|
import { promises as fs2 } from "node:fs";
|
|
2726
3289
|
import { dirname as dirname2 } from "node:path";
|
|
2727
3290
|
import { spawn } from "node:child_process";
|
|
3291
|
+
import { Readable, Writable } from "node:stream";
|
|
2728
3292
|
import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
|
|
2729
3293
|
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
2730
3294
|
import pRetry from "p-retry";
|
|
@@ -2746,6 +3310,33 @@ class SandboxUnavailableError extends Error {
|
|
|
2746
3310
|
// src/sandbox.ts
|
|
2747
3311
|
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
2748
3312
|
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
3313
|
+
var SANDBOX_VCPUS = {
|
|
3314
|
+
"2vcpu-4gb": 2,
|
|
3315
|
+
"4vcpu-8gb": 4,
|
|
3316
|
+
"8vcpu-16gb": 8,
|
|
3317
|
+
"32vcpu-64gb": 32
|
|
3318
|
+
};
|
|
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
|
+
}
|
|
2749
3340
|
function makeSandboxProvider(sb) {
|
|
2750
3341
|
const commands = sb.commands;
|
|
2751
3342
|
return {
|
|
@@ -2753,18 +3344,33 @@ function makeSandboxProvider(sb) {
|
|
|
2753
3344
|
commands: {
|
|
2754
3345
|
async run(cmd, opts) {
|
|
2755
3346
|
let stderr = "";
|
|
2756
|
-
const
|
|
2757
|
-
|
|
3347
|
+
const { sudo, ...rest } = opts ?? {};
|
|
3348
|
+
const runOpts = {
|
|
3349
|
+
...rest,
|
|
3350
|
+
...sudo ? { user: "root" } : {},
|
|
2758
3351
|
onStderr: (chunk) => {
|
|
2759
3352
|
stderr += chunk;
|
|
2760
3353
|
opts?.onStderr?.(chunk);
|
|
2761
3354
|
}
|
|
2762
|
-
});
|
|
2763
|
-
return {
|
|
2764
|
-
exitCode: result.exitCode,
|
|
2765
|
-
stdout: result.stdout,
|
|
2766
|
-
stderr: typeof result.stderr === "string" ? result.stderr : stderr
|
|
2767
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
|
+
}
|
|
2768
3374
|
}
|
|
2769
3375
|
},
|
|
2770
3376
|
files: {
|
|
@@ -2772,7 +3378,9 @@ function makeSandboxProvider(sb) {
|
|
|
2772
3378
|
await sb.files.write(path, content);
|
|
2773
3379
|
}
|
|
2774
3380
|
},
|
|
2775
|
-
kill
|
|
3381
|
+
async kill() {
|
|
3382
|
+
await sb.kill();
|
|
3383
|
+
}
|
|
2776
3384
|
};
|
|
2777
3385
|
}
|
|
2778
3386
|
function e2bMaxSandboxMs() {
|
|
@@ -2799,6 +3407,9 @@ function makeE2bSandboxProvider(sb) {
|
|
|
2799
3407
|
async snapshot() {
|
|
2800
3408
|
const { snapshotId } = await sb.createSnapshot();
|
|
2801
3409
|
return { snapshotId };
|
|
3410
|
+
},
|
|
3411
|
+
async updateNetworkPolicy(policy) {
|
|
3412
|
+
await sb.updateNetwork(toE2bNetwork(policy));
|
|
2802
3413
|
}
|
|
2803
3414
|
};
|
|
2804
3415
|
}
|
|
@@ -2890,6 +3501,31 @@ function toVercelNetworkPolicy(policy) {
|
|
|
2890
3501
|
}
|
|
2891
3502
|
return { ...policy, allow };
|
|
2892
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
|
+
}
|
|
2893
3529
|
var VERCEL_MAX_TAGS = 5;
|
|
2894
3530
|
function buildVercelTags(metadata) {
|
|
2895
3531
|
const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
|
|
@@ -2911,7 +3547,7 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2911
3547
|
let stdout = "";
|
|
2912
3548
|
let stderr = "";
|
|
2913
3549
|
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} }).catch((err) => asUnavailable(err, true));
|
|
2914
|
-
|
|
3550
|
+
const collect = async () => {
|
|
2915
3551
|
await pRetry(async (attempt) => {
|
|
2916
3552
|
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
2917
3553
|
if (attempt > 1) {
|
|
@@ -2930,11 +3566,22 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2930
3566
|
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
2931
3567
|
const finished = await handle.wait();
|
|
2932
3568
|
return { exitCode: finished.exitCode, stdout, stderr };
|
|
3569
|
+
};
|
|
3570
|
+
let timer;
|
|
3571
|
+
const deadline = opts?.timeoutMs ? new Promise((_, reject) => {
|
|
3572
|
+
timer = setTimeout(() => reject(new Error(`command timed out after ${opts.timeoutMs}ms: ${cmd.slice(0, 200)}`)), opts.timeoutMs);
|
|
3573
|
+
}) : undefined;
|
|
3574
|
+
try {
|
|
3575
|
+
return await (deadline ? Promise.race([collect(), deadline]) : collect());
|
|
2933
3576
|
} catch (err) {
|
|
2934
|
-
if (
|
|
3577
|
+
if (err instanceof Error && err.message.startsWith("command timed out after"))
|
|
3578
|
+
throw err;
|
|
3579
|
+
if (signal?.aborted)
|
|
2935
3580
|
throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
|
|
2936
|
-
}
|
|
2937
3581
|
return asUnavailable(err, false);
|
|
3582
|
+
} finally {
|
|
3583
|
+
if (timer)
|
|
3584
|
+
clearTimeout(timer);
|
|
2938
3585
|
}
|
|
2939
3586
|
}
|
|
2940
3587
|
},
|
|
@@ -2987,6 +3634,38 @@ function makeLocalSandboxProvider() {
|
|
|
2987
3634
|
proc.on("error", reject);
|
|
2988
3635
|
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
|
|
2989
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
|
+
};
|
|
2990
3669
|
}
|
|
2991
3670
|
},
|
|
2992
3671
|
files: {
|
|
@@ -3011,7 +3690,8 @@ var SANDBOX_PROVIDERS = {
|
|
|
3011
3690
|
const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
|
|
3012
3691
|
const tags = buildVercelTags(opts.metadata);
|
|
3013
3692
|
const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
|
|
3014
|
-
const
|
|
3693
|
+
const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
|
|
3694
|
+
const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds });
|
|
3015
3695
|
return makeVercelSandboxProvider(sb, opts.envs);
|
|
3016
3696
|
},
|
|
3017
3697
|
reconnect: async (sandboxId) => {
|
|
@@ -3053,17 +3733,36 @@ var SANDBOX_PROVIDERS = {
|
|
|
3053
3733
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3054
3734
|
const snap = await Snapshot.get({ snapshotId, ...creds });
|
|
3055
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
|
+
}
|
|
3056
3750
|
}
|
|
3057
3751
|
},
|
|
3058
3752
|
e2b: {
|
|
3059
3753
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3060
|
-
create: async ({ template, timeoutMs, networkPolicy
|
|
3754
|
+
create: async ({ template, timeoutMs, networkPolicy, size, ...rest }) => {
|
|
3061
3755
|
const maxMs = e2bMaxSandboxMs();
|
|
3062
3756
|
if (timeoutMs > maxMs) {
|
|
3063
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.`);
|
|
3064
3758
|
}
|
|
3065
|
-
const sandboxOpts = {
|
|
3066
|
-
|
|
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;
|
|
3067
3766
|
return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
|
|
3068
3767
|
},
|
|
3069
3768
|
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
|
|
@@ -3099,10 +3798,16 @@ var SANDBOX_PROVIDERS = {
|
|
|
3099
3798
|
},
|
|
3100
3799
|
"e2b-desktop": {
|
|
3101
3800
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3102
|
-
create: async ({ template, timeoutMs, networkPolicy:
|
|
3801
|
+
create: async ({ template, timeoutMs, networkPolicy, size: _size, ...rest }) => {
|
|
3103
3802
|
if (!template)
|
|
3104
3803
|
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
3105
|
-
|
|
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
|
+
}));
|
|
3106
3811
|
}
|
|
3107
3812
|
}
|
|
3108
3813
|
};
|
|
@@ -3135,6 +3840,17 @@ ${missing.join(`
|
|
|
3135
3840
|
`)}`);
|
|
3136
3841
|
return def.deleteSnapshot(snapshotId, Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]])));
|
|
3137
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
|
+
}
|
|
3138
3854
|
async function getSandboxQuotas() {
|
|
3139
3855
|
const out = {};
|
|
3140
3856
|
await Promise.all(Object.entries(SANDBOX_PROVIDERS).map(async ([name, def]) => {
|
|
@@ -3190,17 +3906,34 @@ class StepObservabilityCollector {
|
|
|
3190
3906
|
events = [];
|
|
3191
3907
|
subSteps = [];
|
|
3192
3908
|
liveEmitter;
|
|
3909
|
+
memoize;
|
|
3910
|
+
usedStepNames = new Set;
|
|
3193
3911
|
ackedSeqs = new Set;
|
|
3194
3912
|
inFlight = new Set;
|
|
3195
3913
|
constructor(opts = {}) {
|
|
3196
3914
|
this.liveEmitter = opts.liveEmitter;
|
|
3915
|
+
this.memoize = opts.memoize;
|
|
3197
3916
|
}
|
|
3198
3917
|
setMetadata = async (data) => {
|
|
3199
3918
|
Object.assign(this.metadata, data);
|
|
3200
3919
|
};
|
|
3201
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);
|
|
3202
3925
|
const startedAt = Date.now();
|
|
3203
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
|
+
}
|
|
3204
3937
|
const result = await fn();
|
|
3205
3938
|
this.subSteps.push({
|
|
3206
3939
|
name,
|
|
@@ -3264,33 +3997,25 @@ class StepObservabilityCollector {
|
|
|
3264
3997
|
}
|
|
3265
3998
|
|
|
3266
3999
|
// src/pause/checkpoint.ts
|
|
3267
|
-
async function
|
|
4000
|
+
async function memoize(name, fn) {
|
|
3268
4001
|
const existing = await readCheckpoint(name);
|
|
3269
4002
|
if (existing.found)
|
|
3270
|
-
return existing.value;
|
|
4003
|
+
return { value: existing.value, restored: true };
|
|
3271
4004
|
const value = await fn();
|
|
3272
4005
|
await writeCheckpoint(name, value);
|
|
3273
|
-
return value;
|
|
4006
|
+
return { value, restored: false };
|
|
3274
4007
|
}
|
|
3275
|
-
function
|
|
4008
|
+
function scopedMemoize(scope) {
|
|
3276
4009
|
assertSafeCheckpointName(scope);
|
|
3277
4010
|
return async (name, fn) => {
|
|
3278
4011
|
assertSafeCheckpointName(name);
|
|
3279
|
-
return
|
|
4012
|
+
return memoize(`${scope}.${name}`, fn);
|
|
3280
4013
|
};
|
|
3281
4014
|
}
|
|
3282
4015
|
|
|
3283
4016
|
// src/pause/wrappers.ts
|
|
3284
4017
|
function buildPauseWrappers(pause) {
|
|
3285
4018
|
return {
|
|
3286
|
-
requestDecision(req) {
|
|
3287
|
-
return pause({
|
|
3288
|
-
reason: req.reason,
|
|
3289
|
-
schema: req.schema,
|
|
3290
|
-
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
3291
|
-
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
|
|
3292
|
-
}, "decision");
|
|
3293
|
-
},
|
|
3294
4019
|
sleep(durationMs) {
|
|
3295
4020
|
return pause({
|
|
3296
4021
|
reason: `sleep ${durationMs}ms`,
|
|
@@ -3352,7 +4077,10 @@ async function runWorkflowSingleStep(opts) {
|
|
|
3352
4077
|
const parsedInput = step.input.safeParse(opts.input);
|
|
3353
4078
|
if (!parsedInput.success)
|
|
3354
4079
|
throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
3355
|
-
const collector = new StepObservabilityCollector({
|
|
4080
|
+
const collector = new StepObservabilityCollector({
|
|
4081
|
+
liveEmitter: opts.liveAgentEventEmitter,
|
|
4082
|
+
memoize: scopedMemoize(`step${opts.stepIndex}`)
|
|
4083
|
+
});
|
|
3356
4084
|
const coord = { runId: opts.run.id, stepIndex: opts.stepIndex };
|
|
3357
4085
|
const pauseWithKind = function(req, kind) {
|
|
3358
4086
|
return corePause(req, coord, kind);
|
|
@@ -3373,7 +4101,6 @@ async function runWorkflowSingleStep(opts) {
|
|
|
3373
4101
|
setMetadata: collector.setMetadata,
|
|
3374
4102
|
step: collector.step,
|
|
3375
4103
|
agentEvents: collector.agentEvents,
|
|
3376
|
-
checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
|
|
3377
4104
|
pause,
|
|
3378
4105
|
...buildPauseWrappers(pauseWithKind)
|
|
3379
4106
|
};
|
|
@@ -3429,7 +4156,10 @@ async function runWorkflowSteps(opts) {
|
|
|
3429
4156
|
await opts.onStepFailed?.(i, step.name, err, 0);
|
|
3430
4157
|
throw err;
|
|
3431
4158
|
}
|
|
3432
|
-
const collector = new StepObservabilityCollector({
|
|
4159
|
+
const collector = new StepObservabilityCollector({
|
|
4160
|
+
liveEmitter: opts.liveAgentEventEmitter,
|
|
4161
|
+
memoize: scopedMemoize(`step${i}`)
|
|
4162
|
+
});
|
|
3433
4163
|
const coord = { runId: run.id, stepIndex: i };
|
|
3434
4164
|
const pauseWithKind = function(req, kind) {
|
|
3435
4165
|
return corePause(req, coord, kind);
|
|
@@ -3448,7 +4178,6 @@ async function runWorkflowSteps(opts) {
|
|
|
3448
4178
|
setMetadata: collector.setMetadata,
|
|
3449
4179
|
step: collector.step,
|
|
3450
4180
|
agentEvents: collector.agentEvents,
|
|
3451
|
-
checkpoint: scopedCheckpoint(`step${i}`),
|
|
3452
4181
|
pause,
|
|
3453
4182
|
...buildPauseWrappers(pauseWithKind)
|
|
3454
4183
|
};
|
|
@@ -3600,7 +4329,7 @@ function buildInvokeChild(runId, opts = {}) {
|
|
|
3600
4329
|
return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
|
|
3601
4330
|
...childOpts,
|
|
3602
4331
|
parentRunId: runId,
|
|
3603
|
-
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
|
|
4332
|
+
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
|
|
3604
4333
|
});
|
|
3605
4334
|
}
|
|
3606
4335
|
// src/workflow-steps/step.ts
|
|
@@ -3645,32 +4374,32 @@ function stepResultFilePath(token) {
|
|
|
3645
4374
|
}
|
|
3646
4375
|
// src/step-invocation/invoker.ts
|
|
3647
4376
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3648
|
-
import { z as
|
|
3649
|
-
var StepObservabilitySchema =
|
|
3650
|
-
metadata:
|
|
3651
|
-
events:
|
|
3652
|
-
subSteps:
|
|
3653
|
-
name:
|
|
3654
|
-
startedAt:
|
|
3655
|
-
durationMs:
|
|
3656
|
-
status:
|
|
3657
|
-
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()
|
|
3658
4387
|
})).optional()
|
|
3659
4388
|
}).passthrough();
|
|
3660
|
-
var StepResultEnvelopeSchema =
|
|
3661
|
-
|
|
3662
|
-
ok:
|
|
3663
|
-
output:
|
|
4389
|
+
var StepResultEnvelopeSchema = z11.discriminatedUnion("ok", [
|
|
4390
|
+
z11.object({
|
|
4391
|
+
ok: z11.literal(true),
|
|
4392
|
+
output: z11.unknown().optional(),
|
|
3664
4393
|
observability: StepObservabilitySchema.optional()
|
|
3665
4394
|
}),
|
|
3666
|
-
|
|
3667
|
-
ok:
|
|
3668
|
-
kind:
|
|
3669
|
-
error:
|
|
4395
|
+
z11.object({
|
|
4396
|
+
ok: z11.literal(false),
|
|
4397
|
+
kind: z11.enum(["protocol", "user-step"]),
|
|
4398
|
+
error: z11.string().optional()
|
|
3670
4399
|
})
|
|
3671
4400
|
]);
|
|
3672
|
-
var StepPauseEnvelopeSchema =
|
|
3673
|
-
pauseId:
|
|
4401
|
+
var StepPauseEnvelopeSchema = z11.object({
|
|
4402
|
+
pauseId: z11.string().min(1),
|
|
3674
4403
|
pauseRequest: StepPauseRequestSchema,
|
|
3675
4404
|
observability: StepObservabilitySchema.optional()
|
|
3676
4405
|
});
|
|
@@ -3963,10 +4692,196 @@ async function serveStep(handler) {
|
|
|
3963
4692
|
await emitResult(token, payload);
|
|
3964
4693
|
process.exit(exitCode);
|
|
3965
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
|
+
}
|
|
3966
4730
|
// src/agent/run-agent.ts
|
|
3967
|
-
import { z as
|
|
4731
|
+
import { z as z12 } from "zod";
|
|
3968
4732
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3969
4733
|
|
|
4734
|
+
// src/agent/agent-context.ts
|
|
4735
|
+
var AGENT_COMPOSE_MANUAL = `# Working inside an Agent Compose sandbox
|
|
4736
|
+
|
|
4737
|
+
You are an agent running in a per-run sandbox on the Agent Compose platform.
|
|
4738
|
+
Use the **\`agentc\` CLI** and the **\`@agent-compose/sdk\`** for everything below —
|
|
4739
|
+
do NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on
|
|
4740
|
+
your PATH and already authenticated from the environment
|
|
4741
|
+
(\`AGENT_COMPOSE_URL\` / \`AGENT_COMPOSE_API_KEY\` / \`AGENT_COMPOSE_FACTORY\` are
|
|
4742
|
+
injected for this run), so commands just work — no login, no keys to manage.
|
|
4743
|
+
|
|
4744
|
+
The \`/ac:*\` skills are installed as Claude Code slash commands (\`/ac:invoke\`,
|
|
4745
|
+
\`/ac:events\`, \`/ac:logs\`, \`/ac:register\`, …) — reach for them too.
|
|
4746
|
+
|
|
4747
|
+
## Files — your outputs persist by default
|
|
4748
|
+
|
|
4749
|
+
Your working directory defaults to **\`$AGENT_COMPOSE_RUN_DIR\`** — a per-run
|
|
4750
|
+
directory on the shared factory drive
|
|
4751
|
+
(\`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/\`) the platform
|
|
4752
|
+
creates and attributes to this run. **Files you write here persist by
|
|
4753
|
+
default** — they show up in the dashboard's Files tab and the run's Artifacts
|
|
4754
|
+
card, with no API calls to save them. The dir already exists and is writable.
|
|
4755
|
+
|
|
4756
|
+
Need throwaway scratch — heavy build output, package caches, temp files?
|
|
4757
|
+
\`cd /tmp\` (or any path outside \`/factory\`): anything off the factory drive is
|
|
4758
|
+
ephemeral and discarded when the sandbox ends. In short: **stay in your working
|
|
4759
|
+
dir to keep something, \`cd\` out to throw it away.**
|
|
4760
|
+
|
|
4761
|
+
The whole shared drive is POSIX-mounted at \`/factory\`; the dashboard-visible
|
|
4762
|
+
root is \`$AGENT_COMPOSE_FACTORY_DIR\` (\`/factory/files\`). Earlier versions and
|
|
4763
|
+
runs live in sibling dirs under
|
|
4764
|
+
\`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/\` — read them for prior
|
|
4765
|
+
context. Other workflows' dirs are present but not your concern.
|
|
4766
|
+
|
|
4767
|
+
## Events — the factory timeline
|
|
4768
|
+
|
|
4769
|
+
Record something on the run/factory timeline (the dashboard renders these)
|
|
4770
|
+
with the CLI — your run id is \`$RUN_ID\`:
|
|
4771
|
+
|
|
4772
|
+
agentc events send "$RUN_ID" <name> --summary "<one line>" [--body '<json>']
|
|
4773
|
+
|
|
4774
|
+
Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
|
|
4775
|
+
\`agentc events list\` reads them back. \`/ac:events\` is the skill equivalent.
|
|
4776
|
+
|
|
4777
|
+
## Runs
|
|
4778
|
+
|
|
4779
|
+
agentc list # registered workflows (/ac:list)
|
|
4780
|
+
agentc logs "$RUN_ID" # a run's logs (/ac:logs)
|
|
4781
|
+
agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)
|
|
4782
|
+
|
|
4783
|
+
## Writing workflow / agent code — the SDK
|
|
4784
|
+
|
|
4785
|
+
\`@agent-compose/sdk\` is installed in \`/workspace\` — import it from any script
|
|
4786
|
+
you write there:
|
|
4787
|
+
|
|
4788
|
+
import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
|
|
4789
|
+
|
|
4790
|
+
Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
|
|
4791
|
+
\`agentc register <file.ts>\` (or \`/ac:register\`).
|
|
4792
|
+
|
|
4793
|
+
## Pausing to ask the human — \`agentc pause\`
|
|
4794
|
+
|
|
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
|
|
4800
|
+
|
|
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.
|
|
4808
|
+
|
|
4809
|
+
Reach for it the moment you hit — or foresee — any of these:
|
|
4810
|
+
- **A wall only a human can clear:** a 401/403, a missing credential, an
|
|
4811
|
+
unconnected provider, a host the network refuses. Do NOT retry blindly or try
|
|
4812
|
+
to work around it — pause and say what needs enabling.
|
|
4813
|
+
- **A durable or outward-facing action that needs sign-off:** registering a
|
|
4814
|
+
workflow, deploying, sending email/messages, deleting or overwriting shared
|
|
4815
|
+
data, spending money. Prepare everything, then pause for approval BEFORE you
|
|
4816
|
+
commit it.
|
|
4817
|
+
- **A judgment call only the human can settle:** an under-specified request,
|
|
4818
|
+
several valid paths, a conflict with existing state, missing input only they have.
|
|
4819
|
+
|
|
4820
|
+
You compose the \`--reason\` (the ask) yourself; pass \`--option\` choices when
|
|
4821
|
+
there are clear ones, omit them for a free-form answer. Each agent pauses
|
|
4822
|
+
independently — pausing doesn't stop the others.
|
|
4823
|
+
|
|
4824
|
+
## Credentials
|
|
4825
|
+
|
|
4826
|
+
Connector credentials (Google, GitHub, …) are NEVER in your environment.
|
|
4827
|
+
They're injected at the network layer when you call an allowed host — make the
|
|
4828
|
+
request **without** an Authorization header and the platform adds it. Don't try
|
|
4829
|
+
to read or exfiltrate tokens; they aren't here. The "Connectors & access"
|
|
4830
|
+
section below (when present) lists exactly which providers this run can reach.
|
|
4831
|
+
|
|
4832
|
+
## Tools in this environment
|
|
4833
|
+
|
|
4834
|
+
- \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
|
|
4835
|
+
- \`@agent-compose/sdk\` — installed in /workspace for writing workflows
|
|
4836
|
+
- \`/ac:*\` Claude Code skills — slash commands for the above
|
|
4837
|
+
- \`archil\` (factory drive), \`rtk\`, \`bun\`
|
|
4838
|
+
- A world-writable \`/workspace\` working directory`;
|
|
4839
|
+
function renderConnectorsSection(connectors) {
|
|
4840
|
+
if (connectors.length === 0)
|
|
4841
|
+
return "";
|
|
4842
|
+
const rows = connectors.map((c) => {
|
|
4843
|
+
const host = c.hosts?.length ? c.hosts.join(", ") : "(host set by the platform)";
|
|
4844
|
+
const verbs = c.methods?.length ? c.methods.join("/") : "any method";
|
|
4845
|
+
const paths = c.pathPrefixes?.length ? ` under ${c.pathPrefixes.join(", ")}` : "";
|
|
4846
|
+
const repo = c.repository ? ` — repo \`${c.repository}\` (${c.access ?? "read"})` : "";
|
|
4847
|
+
const why = c.scopes?.length ? `
|
|
4848
|
+
_scopes: ${c.scopes.join(", ")}_` : "";
|
|
4849
|
+
return `- **${c.name ?? c.provider}** → \`${host}\` — ${verbs}${paths}${repo}${why}`;
|
|
4850
|
+
});
|
|
4851
|
+
return `
|
|
4852
|
+
|
|
4853
|
+
## Connectors & access — what this run can reach
|
|
4854
|
+
|
|
4855
|
+
These providers are connected for this run. Call their APIs with plain
|
|
4856
|
+
fetch/SDKs and **no Authorization header** — the platform injects the
|
|
4857
|
+
credential at the network layer. Requests outside the listed method/path are
|
|
4858
|
+
refused (403) and the token withheld. Anything NOT listed is unreachable; if
|
|
4859
|
+
you need it, \`agentc pause\` and ask for it to be connected.
|
|
4860
|
+
|
|
4861
|
+
${rows.join(`
|
|
4862
|
+
`)}
|
|
4863
|
+
`;
|
|
4864
|
+
}
|
|
4865
|
+
function buildAgentContextDoc(env) {
|
|
4866
|
+
let connectors = [];
|
|
4867
|
+
const raw = env.AGENT_COMPOSE_CONNECTORS;
|
|
4868
|
+
if (raw) {
|
|
4869
|
+
try {
|
|
4870
|
+
const parsed = JSON.parse(raw);
|
|
4871
|
+
if (Array.isArray(parsed))
|
|
4872
|
+
connectors = parsed;
|
|
4873
|
+
} catch {}
|
|
4874
|
+
}
|
|
4875
|
+
return AGENT_COMPOSE_MANUAL + renderConnectorsSection(connectors);
|
|
4876
|
+
}
|
|
4877
|
+
async function writeAgentContext(args) {
|
|
4878
|
+
const doc = buildAgentContextDoc(args.env);
|
|
4879
|
+
const dir = args.cwd.replace(/\/+$/, "") || "/workspace";
|
|
4880
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
|
|
4881
|
+
await args.sandbox.files.write(`${dir}/${name}`, doc);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
|
|
3970
4885
|
// src/agent/protocol-suffix.md
|
|
3971
4886
|
var protocol_suffix_default = '## Status Signal\n\nWhen you have finished your work or are blocked, emit a `<status>` block at the end of your response:\n\n```json\n<status>\n{\n "summary": "one sentence describing what was done or what is blocking",\n "completed": ["each acceptance criterion that is now fully met"],\n "blockers": [],\n "changed_files": ["relative/path/to/file"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Field semantics:**\n- `summary`: one sentence — what was accomplished or what is blocking\n- `completed`: acceptance criteria items that are fully done — be specific\n- `blockers`: non-empty when `exit_signal: false` — describe the exact obstacle\n- `changed_files`: relative paths of files you created or modified\n- `tests_run`: `true` if you ran any test suite (pass or fail); `false` if no tests exist or you skipped them\n- `exit_signal: true` — set when ALL acceptance criteria are met and no blockers remain\n- `exit_signal: false` — set when blocked or unfinished; `blockers` must be non-empty\n\n**If you are still actively working** and have not reached a natural stopping point, do NOT emit a `<status>` block — just keep working.\n\n**Example (done):**\n\n```json\n<status>\n{\n "summary": "Added input validation middleware to /api/tasks with tests",\n "completed": ["POST /api/tasks validates required fields", "Returns 400 with details on invalid input", "Unit tests passing"],\n "blockers": [],\n "changed_files": ["src/middleware/validate.ts", "src/routes/tasks.ts", "tests/validate.test.ts"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Example (blocked):**\n\n```json\n<status>\n{\n "summary": "Implemented middleware but tests are failing due to module resolution",\n "completed": ["Middleware created and wired into route"],\n "blockers": ["Tests fail: cannot resolve import \'./validate\' — module resolution config unclear"],\n "changed_files": ["src/middleware/validate.ts"],\n "tests_run": true,\n "exit_signal": false\n}\n</status>\n```\n';
|
|
3972
4887
|
|
|
@@ -4026,29 +4941,29 @@ function stripFrontmatter(content) {
|
|
|
4026
4941
|
return end === -1 ? content : content.slice(end + 4).trimStart();
|
|
4027
4942
|
}
|
|
4028
4943
|
function zodTypeName(schema) {
|
|
4029
|
-
if (schema instanceof
|
|
4944
|
+
if (schema instanceof z12.ZodString)
|
|
4030
4945
|
return "string";
|
|
4031
|
-
if (schema instanceof
|
|
4946
|
+
if (schema instanceof z12.ZodNumber)
|
|
4032
4947
|
return "number";
|
|
4033
|
-
if (schema instanceof
|
|
4948
|
+
if (schema instanceof z12.ZodBoolean)
|
|
4034
4949
|
return "boolean";
|
|
4035
|
-
if (schema instanceof
|
|
4950
|
+
if (schema instanceof z12.ZodArray)
|
|
4036
4951
|
return `${zodTypeName(schema.element)}[]`;
|
|
4037
|
-
if (schema instanceof
|
|
4952
|
+
if (schema instanceof z12.ZodEnum)
|
|
4038
4953
|
return schema.options.map((o) => `"${o}"`).join(" | ");
|
|
4039
|
-
if (schema instanceof
|
|
4954
|
+
if (schema instanceof z12.ZodOptional)
|
|
4040
4955
|
return `${zodTypeName(schema.unwrap())} (optional)`;
|
|
4041
|
-
if (schema instanceof
|
|
4956
|
+
if (schema instanceof z12.ZodNullable)
|
|
4042
4957
|
return `${zodTypeName(schema.unwrap())} | null`;
|
|
4043
|
-
if (schema instanceof
|
|
4958
|
+
if (schema instanceof z12.ZodLiteral)
|
|
4044
4959
|
return `"${String(schema.value)}"`;
|
|
4045
|
-
if (schema instanceof
|
|
4960
|
+
if (schema instanceof z12.ZodObject)
|
|
4046
4961
|
return "object";
|
|
4047
4962
|
return "any";
|
|
4048
4963
|
}
|
|
4049
4964
|
function buildResponseFormatAppendix(schema) {
|
|
4050
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>`:", ""];
|
|
4051
|
-
const shape = schema instanceof
|
|
4966
|
+
const shape = schema instanceof z12.ZodObject ? schema.shape : null;
|
|
4052
4967
|
const body = shape ? (() => {
|
|
4053
4968
|
const entries = Object.entries(shape);
|
|
4054
4969
|
const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
|
|
@@ -4082,7 +4997,28 @@ function resolveAgentId(explicitId) {
|
|
|
4082
4997
|
return `step${activeCall.stepIndex}-agent-${activeCall.callIndex}`;
|
|
4083
4998
|
}
|
|
4084
4999
|
async function agent(opts) {
|
|
4085
|
-
|
|
5000
|
+
let workingDir = opts.workingDir || process.env.AGENT_COMPOSE_RUN_DIR || "/workspace";
|
|
5001
|
+
const CONTEXT_WRITE_DEADLINE_MS = 15000;
|
|
5002
|
+
const tryWriteContext = (cwd) => {
|
|
5003
|
+
let timer;
|
|
5004
|
+
return Promise.race([
|
|
5005
|
+
writeAgentContext({ sandbox: opts.sandbox, cwd, env: process.env }).then(() => true),
|
|
5006
|
+
new Promise((resolve) => {
|
|
5007
|
+
timer = setTimeout(() => resolve(false), CONTEXT_WRITE_DEADLINE_MS);
|
|
5008
|
+
})
|
|
5009
|
+
]).catch((err) => {
|
|
5010
|
+
console.error(`[agent] writeAgentContext(${cwd}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5011
|
+
return false;
|
|
5012
|
+
}).finally(() => {
|
|
5013
|
+
if (timer)
|
|
5014
|
+
clearTimeout(timer);
|
|
5015
|
+
});
|
|
5016
|
+
};
|
|
5017
|
+
if (!await tryWriteContext(workingDir) && workingDir !== "/workspace") {
|
|
5018
|
+
console.error(`[agent] working dir ${workingDir} not writable within ${CONTEXT_WRITE_DEADLINE_MS}ms ` + `(factory drive degraded?) — falling back to /workspace so the agent can run`);
|
|
5019
|
+
workingDir = "/workspace";
|
|
5020
|
+
await tryWriteContext(workingDir);
|
|
5021
|
+
}
|
|
4086
5022
|
const agentId = resolveAgentId(opts.agentId);
|
|
4087
5023
|
const callbackUrl = process.env.AGENT_COMPOSE_URL;
|
|
4088
5024
|
const callbackToken = process.env.AGENT_COMPOSE_RUN_TOKEN;
|
|
@@ -4157,6 +5093,7 @@ async function agent(opts) {
|
|
|
4157
5093
|
...inbox ? { inbox } : {},
|
|
4158
5094
|
mode,
|
|
4159
5095
|
consumeSteerPending: () => consumeSteerPending(agentId),
|
|
5096
|
+
consumeLocalPauseRequest: () => consumeLocalPauseRequest(agentId),
|
|
4160
5097
|
...steerPause ? { pause: steerPause } : {}
|
|
4161
5098
|
});
|
|
4162
5099
|
} finally {
|