@agent-compose/sdk 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/active-step.d.ts +60 -0
- package/dist/agent/agent-loop-steer.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +46 -0
- package/dist/agent/async-queue.d.ts +29 -0
- package/dist/agent/protocol.d.ts +9 -1
- package/dist/agent/resolve-agent-id.test.d.ts +1 -0
- package/dist/agent/run-agent.d.ts +16 -3
- package/dist/agent/steer-control.d.ts +57 -0
- package/dist/agent/steer-control.test.d.ts +1 -0
- package/dist/client.d.ts +161 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1341 -157
- package/dist/pause/__tests__/agent-loop-checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/errors.test.d.ts +1 -0
- package/dist/pause/__tests__/manager.test.d.ts +1 -0
- package/dist/pause/__tests__/pause-core.test.d.ts +1 -0
- package/dist/pause/__tests__/state-dir.test.d.ts +1 -0
- package/dist/pause/__tests__/wrappers.test.d.ts +1 -0
- package/dist/pause/checkpoint.d.ts +28 -0
- package/dist/pause/errors.d.ts +52 -0
- package/dist/pause/manager.d.ts +63 -0
- package/dist/pause/pause-core.d.ts +101 -0
- package/dist/pause/state-dir.d.ts +80 -0
- package/dist/pause/wrappers.d.ts +41 -0
- package/dist/request-context/request-context.d.ts +12 -0
- package/dist/runtimes/claude.d.ts +6 -0
- package/dist/runtimes/openai-desktop.d.ts +2 -0
- package/dist/runtimes/openai-desktop.js +1338 -156
- package/dist/runtimes/vercel.d.ts +12 -0
- package/dist/runtimes/vercel.js +50 -7
- package/dist/runtimes/vercel.test.d.ts +1 -0
- package/dist/sse.d.ts +2 -3
- package/dist/step-invocation/index.d.ts +2 -2
- package/dist/step-invocation/invoker.d.ts +3 -0
- package/dist/step-invocation/protocol.d.ts +12 -0
- package/dist/step-invocation/server.d.ts +1 -0
- package/dist/step-invocation/types.d.ts +40 -5
- package/dist/types/events.d.ts +9 -0
- package/dist/types/execution-context.d.ts +25 -0
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/runtime.d.ts +55 -0
- package/dist/types/sandbox.d.ts +6 -1
- package/dist/utils/schemas.d.ts +2 -0
- package/dist/workflow-steps/__tests__/pause-wiring.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +2 -0
- package/dist/workflow-steps/observability.d.ts +43 -11
- package/dist/workflow-steps/run-callback.d.ts +39 -0
- package/dist/workflow-steps/runner.d.ts +8 -0
- package/package.json +1 -1
- package/src/active-step.ts +124 -0
- package/src/agent/agent-loop.ts +253 -19
- package/src/agent/async-queue.ts +61 -0
- package/src/agent/protocol.ts +12 -2
- package/src/agent/run-agent.ts +184 -8
- package/src/agent/steer-control.ts +125 -0
- package/src/client.ts +277 -0
- package/src/index.ts +18 -2
- package/src/pause/checkpoint.ts +44 -0
- package/src/pause/errors.ts +70 -0
- package/src/pause/manager.ts +177 -0
- package/src/pause/pause-core.ts +267 -0
- package/src/pause/state-dir.ts +262 -0
- package/src/pause/wrappers.ts +79 -0
- package/src/request-context/request-context.ts +17 -2
- package/src/runtimes/claude.ts +101 -6
- package/src/runtimes/openai-desktop.ts +11 -0
- package/src/runtimes/vercel.ts +26 -0
- package/src/sandbox.ts +45 -17
- package/src/sse.ts +8 -6
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +107 -29
- package/src/step-invocation/protocol.ts +16 -0
- package/src/step-invocation/server.ts +45 -12
- package/src/step-invocation/types.ts +43 -7
- package/src/tools/coding.ts +16 -5
- package/src/types/events.ts +9 -0
- package/src/types/execution-context.ts +25 -0
- package/src/types/protocol.ts +8 -0
- package/src/types/runtime.ts +52 -0
- package/src/types/sandbox.ts +10 -1
- package/src/types/workflow.ts +6 -1
- package/src/utils/bundler.ts +8 -3
- package/src/utils/schemas.ts +2 -0
- package/src/workflow-steps/index.ts +3 -0
- package/src/workflow-steps/observability.ts +84 -13
- package/src/workflow-steps/run-callback.ts +72 -0
- package/src/workflow-steps/runner.ts +70 -8
- package/dist/utils/discovery.d.ts +0 -2
- package/src/utils/discovery.ts +0 -4
|
@@ -36,6 +36,17 @@ function q(value) {
|
|
|
36
36
|
function resolvePath(path, cwd) {
|
|
37
37
|
return cwd && !isAbsolute(path) ? join(cwd, path) : path;
|
|
38
38
|
}
|
|
39
|
+
function formatCommandFailure(command, result) {
|
|
40
|
+
const parts = [`Command failed with exit code ${result.exitCode}: ${command}`];
|
|
41
|
+
if (result.stderr.trim())
|
|
42
|
+
parts.push(`stderr:
|
|
43
|
+
${result.stderr}`);
|
|
44
|
+
if (result.stdout.trim())
|
|
45
|
+
parts.push(`stdout:
|
|
46
|
+
${result.stdout}`);
|
|
47
|
+
return parts.join(`
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
39
50
|
async function readFile(sandbox, path, opts) {
|
|
40
51
|
const script = `
|
|
41
52
|
const fs = require("fs");
|
|
@@ -49,13 +60,19 @@ const end = Math.min(lines.length, start + Math.max(0, limit) - 1);
|
|
|
49
60
|
for (let i = start; i <= end; i++) console.log(String(i) + ": " + lines[i - 1]);
|
|
50
61
|
`;
|
|
51
62
|
const args = [q(path), opts?.offset !== undefined ? String(opts.offset) : "", opts?.limit !== undefined ? String(opts.limit) : ""].filter(Boolean).map(q).join(" ");
|
|
52
|
-
const
|
|
53
|
-
|
|
63
|
+
const command = `node -e ${q(script)} ${args}`;
|
|
64
|
+
const result = await sandbox.commands.run(command, { cwd: opts?.cwd, timeoutMs: 30000 });
|
|
65
|
+
if (result.exitCode !== 0)
|
|
66
|
+
throw new Error(formatCommandFailure(command, result));
|
|
67
|
+
return result.stdout;
|
|
54
68
|
}
|
|
55
69
|
async function readRawFile(sandbox, path, cwd) {
|
|
56
70
|
const script = `const fs = require("fs"); process.stdout.write(fs.readFileSync(process.argv[1], "utf8"));`;
|
|
57
|
-
const
|
|
58
|
-
|
|
71
|
+
const command = `node -e ${q(script)} ${q(path)}`;
|
|
72
|
+
const result = await sandbox.commands.run(command, { cwd, timeoutMs: 30000 });
|
|
73
|
+
if (result.exitCode !== 0)
|
|
74
|
+
throw new Error(formatCommandFailure(command, result));
|
|
75
|
+
return result.stdout;
|
|
59
76
|
}
|
|
60
77
|
var readTool = {
|
|
61
78
|
name: "Read",
|
|
@@ -116,8 +133,7 @@ var bashTool = {
|
|
|
116
133
|
timeoutMs: timeoutMs ?? 120000
|
|
117
134
|
});
|
|
118
135
|
if (result.exitCode !== 0)
|
|
119
|
-
throw new Error(
|
|
120
|
-
${result.stdout}` : ""}`);
|
|
136
|
+
throw new Error(formatCommandFailure(command, result));
|
|
121
137
|
return result.stdout;
|
|
122
138
|
}
|
|
123
139
|
};
|
|
@@ -138,6 +154,7 @@ async function runProcessorChain(processors, selectHook, initial, ctx) {
|
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
// src/request-context/request-context.ts
|
|
157
|
+
import { z as z2 } from "zod";
|
|
141
158
|
var AC_RESERVED_PREFIX = "ac__";
|
|
142
159
|
var AC_TEAM_ID = "ac__teamId";
|
|
143
160
|
var AC_RUN_ID = "ac__runId";
|
|
@@ -163,6 +180,17 @@ var SERIALISABLE_RESERVED_KEYS = new Set([
|
|
|
163
180
|
AC_API_KEY_SCOPES,
|
|
164
181
|
AC_PARENT_RUN_ID
|
|
165
182
|
]);
|
|
183
|
+
var RequestContextWireSchema = z2.object({
|
|
184
|
+
reserved: z2.object({
|
|
185
|
+
teamId: z2.string(),
|
|
186
|
+
runId: z2.string(),
|
|
187
|
+
workflowId: z2.string(),
|
|
188
|
+
factoryId: z2.string().nullable(),
|
|
189
|
+
apiKeyScopes: z2.array(z2.string()).readonly(),
|
|
190
|
+
parentRunId: z2.string().nullable()
|
|
191
|
+
}),
|
|
192
|
+
user: z2.record(z2.string(), z2.unknown())
|
|
193
|
+
});
|
|
166
194
|
|
|
167
195
|
class ReservedKeyError extends Error {
|
|
168
196
|
key;
|
|
@@ -224,7 +252,8 @@ class RequestContext {
|
|
|
224
252
|
return new RequestContext(reserved, new Map);
|
|
225
253
|
}
|
|
226
254
|
static deserialise(wire) {
|
|
227
|
-
const
|
|
255
|
+
const parsed = RequestContextWireSchema.parse(wire);
|
|
256
|
+
const ctx = new RequestContext({ ...parsed.reserved, abortSignal: undefined }, new Map(Object.entries(parsed.user)));
|
|
228
257
|
return ctx;
|
|
229
258
|
}
|
|
230
259
|
withAbortSignal(signal) {
|
|
@@ -372,6 +401,20 @@ class VercelRunner {
|
|
|
372
401
|
return { kind: "allow", call: verdict.value };
|
|
373
402
|
return verdict;
|
|
374
403
|
}
|
|
404
|
+
captureCheckpoint() {
|
|
405
|
+
return { messages: [...this.messages] };
|
|
406
|
+
}
|
|
407
|
+
restoreCheckpoint(blob) {
|
|
408
|
+
if (!blob || typeof blob !== "object") {
|
|
409
|
+
throw new Error("Vercel runtime checkpoint is invalid: expected an object with messages[]");
|
|
410
|
+
}
|
|
411
|
+
const incoming = blob.messages;
|
|
412
|
+
if (!Array.isArray(incoming)) {
|
|
413
|
+
throw new Error("Vercel runtime checkpoint is invalid: expected messages[]");
|
|
414
|
+
}
|
|
415
|
+
this.messages.length = 0;
|
|
416
|
+
this.messages.push(...incoming);
|
|
417
|
+
}
|
|
375
418
|
buildTools(iteration, signal) {
|
|
376
419
|
const set = {};
|
|
377
420
|
for (const t of this.tools) {
|
|
@@ -469,7 +512,7 @@ function createVercelRuntime(config) {
|
|
|
469
512
|
});
|
|
470
513
|
}
|
|
471
514
|
// src/types/workflow.ts
|
|
472
|
-
import { z as
|
|
515
|
+
import { z as z3 } from "zod";
|
|
473
516
|
|
|
474
517
|
// src/workflow-steps/types.ts
|
|
475
518
|
var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
|
|
@@ -546,7 +589,7 @@ function isWorkflow(value) {
|
|
|
546
589
|
|
|
547
590
|
// src/types/workflow.ts
|
|
548
591
|
function compileRunForm(def, metadata) {
|
|
549
|
-
const unknownSchema =
|
|
592
|
+
const unknownSchema = z3.unknown();
|
|
550
593
|
const inputSchema = def.input ?? unknownSchema;
|
|
551
594
|
const outputSchema = def.output ?? unknownSchema;
|
|
552
595
|
const step = {
|
|
@@ -562,6 +605,11 @@ function compileRunForm(def, metadata) {
|
|
|
562
605
|
setMetadata: stepCtx.setMetadata,
|
|
563
606
|
step: stepCtx.step,
|
|
564
607
|
agentEvents: stepCtx.agentEvents,
|
|
608
|
+
checkpoint: stepCtx.checkpoint,
|
|
609
|
+
pause: stepCtx.pause,
|
|
610
|
+
requestDecision: stepCtx.requestDecision,
|
|
611
|
+
sleep: stepCtx.sleep,
|
|
612
|
+
waitForEvent: stepCtx.waitForEvent,
|
|
565
613
|
processors: metadata.processors ?? []
|
|
566
614
|
};
|
|
567
615
|
const sandbox = stepCtx.sandbox;
|
|
@@ -678,11 +726,14 @@ async function* parseSseStream(body) {
|
|
|
678
726
|
continue;
|
|
679
727
|
}
|
|
680
728
|
if (line === "" && dataLines.length > 0) {
|
|
729
|
+
let data;
|
|
681
730
|
try {
|
|
682
|
-
|
|
731
|
+
data = JSON.parse(dataLines.join(`
|
|
683
732
|
`));
|
|
684
|
-
|
|
685
|
-
|
|
733
|
+
} catch (err) {
|
|
734
|
+
throw new Error(`Malformed SSE JSON payload for event "${event || "message"}": ${err instanceof Error ? err.message : String(err)}`);
|
|
735
|
+
}
|
|
736
|
+
yield { id: seq, event, data };
|
|
686
737
|
event = "";
|
|
687
738
|
seq = 0;
|
|
688
739
|
dataLines = [];
|
|
@@ -706,6 +757,12 @@ function templatePath(factorySlug, ...rest) {
|
|
|
706
757
|
return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
|
|
707
758
|
}
|
|
708
759
|
var PUBLIC_API_HOST = "https://api.agentcompose.ai";
|
|
760
|
+
function steerAnswerPayload(message, actor) {
|
|
761
|
+
if (typeof message !== "string" || message.trim().length === 0) {
|
|
762
|
+
throw new Error("answerSteer: `message` must be a non-empty string");
|
|
763
|
+
}
|
|
764
|
+
return { message, ...actor !== undefined ? { actor } : {} };
|
|
765
|
+
}
|
|
709
766
|
|
|
710
767
|
class AgentComposeClient {
|
|
711
768
|
fetch;
|
|
@@ -791,6 +848,56 @@ class AgentComposeClient {
|
|
|
791
848
|
getStatus(runId) {
|
|
792
849
|
return this.fetch(`/api/v1/workflows/${runId}/status`);
|
|
793
850
|
}
|
|
851
|
+
async resumePause(runId, pauseId, payload, opts) {
|
|
852
|
+
const headers = {};
|
|
853
|
+
if (opts?.idempotencyKey)
|
|
854
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
855
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/pauses/${encodeURIComponent(pauseId)}/resume`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
body: { resumePayload: payload },
|
|
858
|
+
...Object.keys(headers).length ? { headers } : {},
|
|
859
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
async resumePauseByKey(runId, correlationKey, payload, opts) {
|
|
863
|
+
const headers = {};
|
|
864
|
+
if (opts?.idempotencyKey)
|
|
865
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
866
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/pauses/by-key/${encodeURIComponent(correlationKey)}/resume`, {
|
|
867
|
+
method: "POST",
|
|
868
|
+
body: { resumePayload: payload },
|
|
869
|
+
...Object.keys(headers).length ? { headers } : {},
|
|
870
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
async answerSteerByKey(runId, correlationKey, message, opts) {
|
|
874
|
+
return this.resumePauseByKey(runId, correlationKey, steerAnswerPayload(message, opts?.actor), opts);
|
|
875
|
+
}
|
|
876
|
+
async answerSteer(runId, pauseId, message, opts) {
|
|
877
|
+
return this.resumePause(runId, pauseId, steerAnswerPayload(message, opts?.actor), opts);
|
|
878
|
+
}
|
|
879
|
+
async requestAgentPause(runId, agentId, opts) {
|
|
880
|
+
const body = {};
|
|
881
|
+
if (opts?.reason !== undefined)
|
|
882
|
+
body.reason = opts.reason;
|
|
883
|
+
if (opts?.correlationKey !== undefined)
|
|
884
|
+
body.correlationKey = opts.correlationKey;
|
|
885
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/agents/${encodeURIComponent(agentId)}/pause`, {
|
|
886
|
+
method: "POST",
|
|
887
|
+
body,
|
|
888
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
async sendAgentMessage(runId, agentId, message, opts) {
|
|
892
|
+
const body = { message };
|
|
893
|
+
if (opts?.senderName !== undefined)
|
|
894
|
+
body.senderName = opts.senderName;
|
|
895
|
+
return this.fetch(`/api/v1/runs/${encodeURIComponent(runId)}/agents/${encodeURIComponent(agentId)}/messages`, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
body,
|
|
898
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
794
901
|
getRun(runId) {
|
|
795
902
|
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}`);
|
|
796
903
|
}
|
|
@@ -930,10 +1037,6 @@ class AgentComposeClient {
|
|
|
930
1037
|
}
|
|
931
1038
|
}
|
|
932
1039
|
}
|
|
933
|
-
// src/utils/discovery.ts
|
|
934
|
-
function discoverRuntimeName(source) {
|
|
935
|
-
return source.match(/runtime:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
936
|
-
}
|
|
937
1040
|
// src/utils/bundler.ts
|
|
938
1041
|
import { createHash } from "node:crypto";
|
|
939
1042
|
import { parse as babelParse } from "@babel/parser";
|
|
@@ -986,8 +1089,8 @@ async function extractFromBundle(source, label, pick) {
|
|
|
986
1089
|
} finally {
|
|
987
1090
|
await loaded.cleanup();
|
|
988
1091
|
}
|
|
989
|
-
} catch {
|
|
990
|
-
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
|
|
991
1094
|
}
|
|
992
1095
|
}
|
|
993
1096
|
var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
|
|
@@ -1080,10 +1183,10 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1080
1183
|
function extractIOSchema(zodSchema) {
|
|
1081
1184
|
let json;
|
|
1082
1185
|
try {
|
|
1083
|
-
const
|
|
1084
|
-
if (!
|
|
1186
|
+
const z4 = __require("zod");
|
|
1187
|
+
if (!z4.toJSONSchema)
|
|
1085
1188
|
return;
|
|
1086
|
-
json =
|
|
1189
|
+
json = z4.toJSONSchema(zodSchema);
|
|
1087
1190
|
} catch {
|
|
1088
1191
|
return;
|
|
1089
1192
|
}
|
|
@@ -1116,24 +1219,26 @@ function extractIOSchema(zodSchema) {
|
|
|
1116
1219
|
return out;
|
|
1117
1220
|
}
|
|
1118
1221
|
// src/utils/schemas.ts
|
|
1119
|
-
import { z as
|
|
1120
|
-
var AgentStatusSchema =
|
|
1121
|
-
summary:
|
|
1122
|
-
completed:
|
|
1123
|
-
blockers:
|
|
1124
|
-
exit_signal:
|
|
1222
|
+
import { z as z4 } from "zod";
|
|
1223
|
+
var AgentStatusSchema = z4.object({
|
|
1224
|
+
summary: z4.string(),
|
|
1225
|
+
completed: z4.array(z4.string()),
|
|
1226
|
+
blockers: z4.array(z4.string()),
|
|
1227
|
+
exit_signal: z4.boolean(),
|
|
1228
|
+
needs_input: z4.boolean().optional(),
|
|
1229
|
+
question: z4.string().optional()
|
|
1125
1230
|
});
|
|
1126
1231
|
// src/runtimes/claude.ts
|
|
1127
1232
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1128
1233
|
|
|
1129
1234
|
// src/agent/agent-loop.ts
|
|
1130
|
-
import { z as
|
|
1235
|
+
import { z as z9 } from "zod";
|
|
1131
1236
|
|
|
1132
1237
|
// src/agent/protocol.ts
|
|
1133
|
-
import { z as
|
|
1134
|
-
var AgentMessageSchema =
|
|
1135
|
-
type:
|
|
1136
|
-
timestamp:
|
|
1238
|
+
import { z as z5 } from "zod";
|
|
1239
|
+
var AgentMessageSchema = z5.object({
|
|
1240
|
+
type: z5.enum(["init", "text", "thinking", "tool_use", "tool_result", "done", "error", "usage"]),
|
|
1241
|
+
timestamp: z5.string()
|
|
1137
1242
|
}).passthrough();
|
|
1138
1243
|
function parseAgentResponse(text) {
|
|
1139
1244
|
const match = text.match(/<response>([\s\S]*?)<\/response>/);
|
|
@@ -1148,6 +1253,557 @@ function parseAgentResponse(text) {
|
|
|
1148
1253
|
|
|
1149
1254
|
// src/agent/agent-loop.ts
|
|
1150
1255
|
import { randomUUID } from "node:crypto";
|
|
1256
|
+
|
|
1257
|
+
// src/pause/manager.ts
|
|
1258
|
+
import { z as z6 } from "zod";
|
|
1259
|
+
|
|
1260
|
+
// src/pause/state-dir.ts
|
|
1261
|
+
import { promises as fs } from "node:fs";
|
|
1262
|
+
import { join as join2 } from "node:path";
|
|
1263
|
+
import { randomBytes } from "node:crypto";
|
|
1264
|
+
function getStateDir() {
|
|
1265
|
+
return process.env.AGENT_COMPOSE_STATE_DIR ?? "/tmp/wf/state";
|
|
1266
|
+
}
|
|
1267
|
+
var checkpointsDir = () => join2(getStateDir(), "checkpoints");
|
|
1268
|
+
var pausesDir = () => join2(getStateDir(), "pauses");
|
|
1269
|
+
var agentStatePath = (agentInstanceId) => join2(getStateDir(), `agent-${agentInstanceId}.json`);
|
|
1270
|
+
var runtimeStatePath = (agentInstanceId) => join2(getStateDir(), `runtime-${agentInstanceId}.json`);
|
|
1271
|
+
var checkpointPath = (name) => join2(checkpointsDir(), `${name}.json`);
|
|
1272
|
+
var pausePath = (pauseId) => join2(pausesDir(), `${pauseId}.json`);
|
|
1273
|
+
var NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
|
|
1274
|
+
function assertSafeName(label, name) {
|
|
1275
|
+
if (!NAME_RE.test(name)) {
|
|
1276
|
+
throw new Error(`${label} name must match ${NAME_RE} (got ${JSON.stringify(name)})`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function assertSafeCheckpointName(name) {
|
|
1280
|
+
assertSafeName("checkpoint", name);
|
|
1281
|
+
}
|
|
1282
|
+
async function ensureDirs() {
|
|
1283
|
+
await fs.mkdir(checkpointsDir(), { recursive: true });
|
|
1284
|
+
await fs.mkdir(pausesDir(), { recursive: true });
|
|
1285
|
+
}
|
|
1286
|
+
async function atomicWriteJSON(path, value) {
|
|
1287
|
+
await ensureDirs();
|
|
1288
|
+
const tmp = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
1289
|
+
const payload = JSON.stringify(value);
|
|
1290
|
+
if (payload === undefined) {
|
|
1291
|
+
throw new Error("pause state values must be JSON-serialisable");
|
|
1292
|
+
}
|
|
1293
|
+
const handle = await fs.open(tmp, "w");
|
|
1294
|
+
try {
|
|
1295
|
+
await handle.writeFile(payload, { encoding: "utf8" });
|
|
1296
|
+
await handle.sync();
|
|
1297
|
+
} finally {
|
|
1298
|
+
await handle.close();
|
|
1299
|
+
}
|
|
1300
|
+
await fs.rename(tmp, path);
|
|
1301
|
+
}
|
|
1302
|
+
async function readJSON(path) {
|
|
1303
|
+
try {
|
|
1304
|
+
const raw = await fs.readFile(path, "utf8");
|
|
1305
|
+
return JSON.parse(raw);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
if (err.code === "ENOENT")
|
|
1308
|
+
return null;
|
|
1309
|
+
throw err;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async function readJSONWithPresence(path) {
|
|
1313
|
+
try {
|
|
1314
|
+
const raw = await fs.readFile(path, "utf8");
|
|
1315
|
+
return { found: true, value: JSON.parse(raw) };
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
if (err.code === "ENOENT")
|
|
1318
|
+
return { found: false };
|
|
1319
|
+
throw err;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function readAgentState(agentInstanceId) {
|
|
1323
|
+
assertSafeName("agent", agentInstanceId);
|
|
1324
|
+
return readJSON(agentStatePath(agentInstanceId));
|
|
1325
|
+
}
|
|
1326
|
+
async function writeAgentState(agentInstanceId, state) {
|
|
1327
|
+
assertSafeName("agent", agentInstanceId);
|
|
1328
|
+
await atomicWriteJSON(agentStatePath(agentInstanceId), state);
|
|
1329
|
+
}
|
|
1330
|
+
async function readRuntimeCheckpoint(agentInstanceId) {
|
|
1331
|
+
assertSafeName("runtime", agentInstanceId);
|
|
1332
|
+
return readJSON(runtimeStatePath(agentInstanceId));
|
|
1333
|
+
}
|
|
1334
|
+
async function writeRuntimeCheckpoint(agentInstanceId, blob) {
|
|
1335
|
+
assertSafeName("runtime", agentInstanceId);
|
|
1336
|
+
await atomicWriteJSON(runtimeStatePath(agentInstanceId), blob);
|
|
1337
|
+
}
|
|
1338
|
+
async function readCheckpoint(name) {
|
|
1339
|
+
assertSafeCheckpointName(name);
|
|
1340
|
+
return readJSONWithPresence(checkpointPath(name));
|
|
1341
|
+
}
|
|
1342
|
+
async function writeCheckpoint(name, value) {
|
|
1343
|
+
assertSafeCheckpointName(name);
|
|
1344
|
+
await atomicWriteJSON(checkpointPath(name), value);
|
|
1345
|
+
}
|
|
1346
|
+
function hasOwn(obj, key) {
|
|
1347
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1348
|
+
}
|
|
1349
|
+
function validatePauseRecord(value, expectedPauseId) {
|
|
1350
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1351
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: expected object`);
|
|
1352
|
+
}
|
|
1353
|
+
const record = value;
|
|
1354
|
+
if (record.pauseId !== undefined && record.pauseId !== expectedPauseId) {
|
|
1355
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: pauseId mismatch`);
|
|
1356
|
+
}
|
|
1357
|
+
if (record.resolved === true) {
|
|
1358
|
+
if (typeof record.expired !== "boolean") {
|
|
1359
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require boolean expired`);
|
|
1360
|
+
}
|
|
1361
|
+
if (!hasOwn(record, "resumePayload") || record.resumePayload === undefined) {
|
|
1362
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved records require resumePayload`);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
pauseId: expectedPauseId,
|
|
1366
|
+
request: hasOwn(record, "request") ? record.request : null,
|
|
1367
|
+
...record
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (record.pauseId !== expectedPauseId) {
|
|
1371
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require matching pauseId`);
|
|
1372
|
+
}
|
|
1373
|
+
if (!hasOwn(record, "request") || record.request === undefined) {
|
|
1374
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records require request`);
|
|
1375
|
+
}
|
|
1376
|
+
if (record.resolved !== undefined && record.resolved !== false) {
|
|
1377
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: resolved must be true, false, or absent`);
|
|
1378
|
+
}
|
|
1379
|
+
if (hasOwn(record, "expired") || hasOwn(record, "resumePayload")) {
|
|
1380
|
+
throw new Error(`pause record ${expectedPauseId} is malformed: unresolved records must not include resolution fields`);
|
|
1381
|
+
}
|
|
1382
|
+
return record;
|
|
1383
|
+
}
|
|
1384
|
+
async function readPauseRecord(pauseId) {
|
|
1385
|
+
assertSafeName("pause", pauseId);
|
|
1386
|
+
const record = await readJSON(pausePath(pauseId));
|
|
1387
|
+
return record === null ? null : validatePauseRecord(record, pauseId);
|
|
1388
|
+
}
|
|
1389
|
+
async function writePauseRecord(record) {
|
|
1390
|
+
assertSafeName("pause", record.pauseId);
|
|
1391
|
+
validatePauseRecord(record, record.pauseId);
|
|
1392
|
+
await atomicWriteJSON(pausePath(record.pauseId), record);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/pause/manager.ts
|
|
1396
|
+
var AGENT_LOOP_STATE_SCHEMA_VERSION = 2;
|
|
1397
|
+
var SUPPORTED_AGENT_LOOP_STATE_VERSIONS = z6.union([z6.literal(1), z6.literal(2)]);
|
|
1398
|
+
var RunningAgentLoopStateSchema = z6.object({
|
|
1399
|
+
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1400
|
+
phase: z6.literal("running"),
|
|
1401
|
+
iteration: z6.number().int().nonnegative(),
|
|
1402
|
+
completedIterations: z6.number().int().nonnegative(),
|
|
1403
|
+
lastSessionId: z6.string(),
|
|
1404
|
+
lastStatus: AgentStatusSchema.nullable(),
|
|
1405
|
+
lastResponseText: z6.string(),
|
|
1406
|
+
lastResponseValidationError: z6.string(),
|
|
1407
|
+
iterationsWithoutStatus: z6.number().int().nonnegative(),
|
|
1408
|
+
blockerStreak: z6.object({ key: z6.string(), count: z6.number().int().nonnegative() }).nullable(),
|
|
1409
|
+
pendingSteerPause: z6.object({ reason: z6.string(), correlationKey: z6.string().nullable(), at: z6.number().int() }).nullable().default(null)
|
|
1410
|
+
});
|
|
1411
|
+
var SettledAgentLoopStateSchema = z6.object({
|
|
1412
|
+
schemaVersion: SUPPORTED_AGENT_LOOP_STATE_VERSIONS,
|
|
1413
|
+
phase: z6.literal("settled"),
|
|
1414
|
+
result: z6.object({
|
|
1415
|
+
sessionId: z6.string(),
|
|
1416
|
+
lastStatus: AgentStatusSchema.nullable(),
|
|
1417
|
+
iterations: z6.number().int().nonnegative(),
|
|
1418
|
+
response: z6.unknown().optional()
|
|
1419
|
+
})
|
|
1420
|
+
});
|
|
1421
|
+
var AgentLoopStateSchema = z6.discriminatedUnion("phase", [
|
|
1422
|
+
RunningAgentLoopStateSchema,
|
|
1423
|
+
SettledAgentLoopStateSchema
|
|
1424
|
+
]);
|
|
1425
|
+
function toProgressState(state) {
|
|
1426
|
+
return {
|
|
1427
|
+
iteration: state.iteration,
|
|
1428
|
+
completedIterations: state.completedIterations,
|
|
1429
|
+
lastSessionId: state.lastSessionId,
|
|
1430
|
+
lastStatus: state.lastStatus,
|
|
1431
|
+
lastResponseText: state.lastResponseText,
|
|
1432
|
+
lastResponseValidationError: state.lastResponseValidationError,
|
|
1433
|
+
iterationsWithoutStatus: state.iterationsWithoutStatus,
|
|
1434
|
+
blockerStreak: state.blockerStreak,
|
|
1435
|
+
pendingSteerPause: state.pendingSteerPause
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
class PauseManager {
|
|
1440
|
+
agentId;
|
|
1441
|
+
constructor(agentId) {
|
|
1442
|
+
this.agentId = agentId;
|
|
1443
|
+
}
|
|
1444
|
+
async restoreAgentLoop(runtime) {
|
|
1445
|
+
const priorState = await readAgentState(this.agentId);
|
|
1446
|
+
if (priorState === null)
|
|
1447
|
+
return { kind: "fresh" };
|
|
1448
|
+
const parsed = AgentLoopStateSchema.safeParse(priorState);
|
|
1449
|
+
if (!parsed.success)
|
|
1450
|
+
return { kind: "invalid", error: parsed.error.message };
|
|
1451
|
+
const state = parsed.data;
|
|
1452
|
+
if (state.phase === "settled") {
|
|
1453
|
+
return {
|
|
1454
|
+
kind: "settled",
|
|
1455
|
+
result: {
|
|
1456
|
+
sessionId: state.result.sessionId,
|
|
1457
|
+
lastStatus: state.result.lastStatus,
|
|
1458
|
+
iterations: state.result.iterations,
|
|
1459
|
+
...Object.prototype.hasOwnProperty.call(state.result, "response") ? { response: state.result.response } : {}
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
const runtimeBlob = await readRuntimeCheckpoint(this.agentId);
|
|
1464
|
+
if (runtimeBlob !== null && !runtime.restoreCheckpoint) {
|
|
1465
|
+
throw new Error(`runtime checkpoint exists for resumed agentId=${this.agentId}, but runtime cannot restore it`);
|
|
1466
|
+
}
|
|
1467
|
+
if (runtime.restoreCheckpoint && runtimeBlob === null) {
|
|
1468
|
+
throw new Error(`missing runtime checkpoint for resumed agentId=${this.agentId}`);
|
|
1469
|
+
}
|
|
1470
|
+
if (runtimeBlob !== null)
|
|
1471
|
+
runtime.restoreCheckpoint?.(runtimeBlob);
|
|
1472
|
+
return { kind: "running", state: toProgressState(state) };
|
|
1473
|
+
}
|
|
1474
|
+
async saveRunningAgentLoop(state, runtime) {
|
|
1475
|
+
await writeAgentState(this.agentId, {
|
|
1476
|
+
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1477
|
+
phase: "running",
|
|
1478
|
+
...state
|
|
1479
|
+
});
|
|
1480
|
+
if (runtime.captureCheckpoint) {
|
|
1481
|
+
const blob = runtime.captureCheckpoint();
|
|
1482
|
+
if (blob !== undefined)
|
|
1483
|
+
await writeRuntimeCheckpoint(this.agentId, blob);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
async saveSettledAgentLoop(result) {
|
|
1487
|
+
await writeAgentState(this.agentId, {
|
|
1488
|
+
schemaVersion: AGENT_LOOP_STATE_SCHEMA_VERSION,
|
|
1489
|
+
phase: "settled",
|
|
1490
|
+
result: {
|
|
1491
|
+
sessionId: result.sessionId,
|
|
1492
|
+
lastStatus: result.lastStatus,
|
|
1493
|
+
iterations: result.iterations,
|
|
1494
|
+
...result.response !== undefined ? { response: result.response } : {}
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/pause/pause-core.ts
|
|
1501
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1502
|
+
|
|
1503
|
+
// src/step-invocation/types.ts
|
|
1504
|
+
import { z as z7 } from "zod";
|
|
1505
|
+
var StepPauseRequestSchema = z7.object({
|
|
1506
|
+
reason: z7.string().min(1),
|
|
1507
|
+
kind: z7.enum(["decision", "sleep", "event", "custom"]),
|
|
1508
|
+
payload: z7.record(z7.string(), z7.unknown()).optional(),
|
|
1509
|
+
ttlMs: z7.number().int().positive().optional(),
|
|
1510
|
+
correlationKey: z7.string().min(1).optional(),
|
|
1511
|
+
snapshot: z7.boolean().optional()
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
class StepExecutionError extends Error {
|
|
1515
|
+
kind;
|
|
1516
|
+
exitCode;
|
|
1517
|
+
constructor(error, stepLabel) {
|
|
1518
|
+
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
1519
|
+
this.name = "StepExecutionError";
|
|
1520
|
+
this.kind = error.kind;
|
|
1521
|
+
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/active-step.ts
|
|
1526
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1527
|
+
var activeStepStorage = new AsyncLocalStorage;
|
|
1528
|
+
var fallbackActive = null;
|
|
1529
|
+
var BRIDGE_KEY = Symbol.for("agent-compose.activeStep.bridge");
|
|
1530
|
+
function bridgeSlot() {
|
|
1531
|
+
const g = globalThis;
|
|
1532
|
+
return g[BRIDGE_KEY] ??= { state: null };
|
|
1533
|
+
}
|
|
1534
|
+
function makeState(step) {
|
|
1535
|
+
return {
|
|
1536
|
+
stepIndex: step.stepIndex,
|
|
1537
|
+
nextAgentCallIndex: 0,
|
|
1538
|
+
pauseOrdinalByScope: new Map
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function currentState() {
|
|
1542
|
+
return activeStepStorage.getStore() ?? bridgeSlot().state ?? fallbackActive;
|
|
1543
|
+
}
|
|
1544
|
+
async function runWithActiveStep(step, fn) {
|
|
1545
|
+
return activeStepStorage.run(makeState(step), fn);
|
|
1546
|
+
}
|
|
1547
|
+
function setActiveStepBridge(step) {
|
|
1548
|
+
const slot = bridgeSlot();
|
|
1549
|
+
const prev = { state: slot.state };
|
|
1550
|
+
slot.state = step ? makeState(step) : null;
|
|
1551
|
+
return prev;
|
|
1552
|
+
}
|
|
1553
|
+
function restoreActiveStepBridge(prev) {
|
|
1554
|
+
bridgeSlot().state = prev.state;
|
|
1555
|
+
}
|
|
1556
|
+
function getActiveStep() {
|
|
1557
|
+
const state = currentState();
|
|
1558
|
+
return state ? { stepIndex: state.stepIndex } : null;
|
|
1559
|
+
}
|
|
1560
|
+
function nextAgentCallInActiveStep() {
|
|
1561
|
+
const state = currentState();
|
|
1562
|
+
if (!state)
|
|
1563
|
+
return null;
|
|
1564
|
+
const callIndex = state.nextAgentCallIndex;
|
|
1565
|
+
state.nextAgentCallIndex += 1;
|
|
1566
|
+
return { stepIndex: state.stepIndex, callIndex };
|
|
1567
|
+
}
|
|
1568
|
+
function nextPauseOrdinalInActiveStep(scope) {
|
|
1569
|
+
const state = currentState();
|
|
1570
|
+
if (!state)
|
|
1571
|
+
return null;
|
|
1572
|
+
const next = state.pauseOrdinalByScope.get(scope) ?? 0;
|
|
1573
|
+
state.pauseOrdinalByScope.set(scope, next + 1);
|
|
1574
|
+
return next;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/pause/errors.ts
|
|
1578
|
+
class PauseError extends Error {
|
|
1579
|
+
constructor(message) {
|
|
1580
|
+
super(message);
|
|
1581
|
+
this.name = new.target.name;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
class PauseExpiredError extends PauseError {
|
|
1586
|
+
code = "pause_expired";
|
|
1587
|
+
constructor(message = "pause expired before it was resolved") {
|
|
1588
|
+
super(message);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
class PauseSchemaError extends PauseError {
|
|
1593
|
+
code = "pause_schema";
|
|
1594
|
+
issues;
|
|
1595
|
+
constructor(message, issues) {
|
|
1596
|
+
super(message);
|
|
1597
|
+
this.issues = issues;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
class PauseRequestError extends PauseError {
|
|
1602
|
+
code = "pause_request";
|
|
1603
|
+
constructor(message = "pause request is invalid") {
|
|
1604
|
+
super(message);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/pause/pause-core.ts
|
|
1609
|
+
var PAUSE_SIGNAL_BRAND = Symbol.for("agent-compose.PauseSignal");
|
|
1610
|
+
|
|
1611
|
+
class PauseSignal {
|
|
1612
|
+
pauseId;
|
|
1613
|
+
pauseRequest;
|
|
1614
|
+
observability;
|
|
1615
|
+
[PAUSE_SIGNAL_BRAND] = true;
|
|
1616
|
+
constructor(pauseId, pauseRequest, observability) {
|
|
1617
|
+
this.pauseId = pauseId;
|
|
1618
|
+
this.pauseRequest = pauseRequest;
|
|
1619
|
+
this.observability = observability;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function isPauseSignal(err) {
|
|
1623
|
+
return typeof err === "object" && err !== null && err[PAUSE_SIGNAL_BRAND] === true;
|
|
1624
|
+
}
|
|
1625
|
+
var PAUSE_NAMESPACE = "5f3e2b8a-0c41-4e9d-9a7b-6d2c1f8e3a40";
|
|
1626
|
+
function uuidToBytes(uuid) {
|
|
1627
|
+
return Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
1628
|
+
}
|
|
1629
|
+
function bytesToUuid(b) {
|
|
1630
|
+
const h = b.toString("hex");
|
|
1631
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
1632
|
+
}
|
|
1633
|
+
function uuidv5(name, namespace) {
|
|
1634
|
+
const hash = createHash2("sha1").update(uuidToBytes(namespace)).update(Buffer.from(name, "utf8")).digest();
|
|
1635
|
+
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
1636
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
1637
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1638
|
+
return bytesToUuid(bytes);
|
|
1639
|
+
}
|
|
1640
|
+
function scopeOf(coord) {
|
|
1641
|
+
return coord.agentScope ? `${coord.runId}:${coord.stepIndex}:${coord.agentScope.agentId}:${coord.agentScope.iteration}` : `${coord.runId}:${coord.stepIndex}:bare`;
|
|
1642
|
+
}
|
|
1643
|
+
var pauseOrdinalByScope = new Map;
|
|
1644
|
+
function nextPauseOrdinal(scope) {
|
|
1645
|
+
const activeOrdinal = nextPauseOrdinalInActiveStep(scope);
|
|
1646
|
+
if (activeOrdinal !== null)
|
|
1647
|
+
return activeOrdinal;
|
|
1648
|
+
const next = pauseOrdinalByScope.get(scope) ?? 0;
|
|
1649
|
+
pauseOrdinalByScope.set(scope, next + 1);
|
|
1650
|
+
return next;
|
|
1651
|
+
}
|
|
1652
|
+
function nextPauseId(coord) {
|
|
1653
|
+
const scope = scopeOf(coord);
|
|
1654
|
+
const ordinal = nextPauseOrdinal(scope);
|
|
1655
|
+
return uuidv5(`${scope}:${ordinal}`, PAUSE_NAMESPACE);
|
|
1656
|
+
}
|
|
1657
|
+
function nonJsonPath(value, path = "$", seen = new Set) {
|
|
1658
|
+
if (value === null)
|
|
1659
|
+
return null;
|
|
1660
|
+
const kind = typeof value;
|
|
1661
|
+
if (kind === "string" || kind === "boolean")
|
|
1662
|
+
return null;
|
|
1663
|
+
if (kind === "number")
|
|
1664
|
+
return Number.isFinite(value) ? null : path;
|
|
1665
|
+
if (kind === "undefined" || kind === "bigint" || kind === "function" || kind === "symbol")
|
|
1666
|
+
return path;
|
|
1667
|
+
if (kind !== "object")
|
|
1668
|
+
return path;
|
|
1669
|
+
const obj = value;
|
|
1670
|
+
if (seen.has(obj))
|
|
1671
|
+
return path;
|
|
1672
|
+
seen.add(obj);
|
|
1673
|
+
if (Array.isArray(value)) {
|
|
1674
|
+
for (let i = 0;i < value.length; i++) {
|
|
1675
|
+
const child = nonJsonPath(value[i], `${path}[${i}]`, seen);
|
|
1676
|
+
if (child)
|
|
1677
|
+
return child;
|
|
1678
|
+
}
|
|
1679
|
+
seen.delete(obj);
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
const proto = Object.getPrototypeOf(obj);
|
|
1683
|
+
if (proto !== Object.prototype && proto !== null)
|
|
1684
|
+
return path;
|
|
1685
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
1686
|
+
if (typeof key !== "string")
|
|
1687
|
+
return `${path}.${String(key)}`;
|
|
1688
|
+
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
1689
|
+
if (!desc?.enumerable)
|
|
1690
|
+
return `${path}.${key}`;
|
|
1691
|
+
const child = nonJsonPath(value[key], `${path}.${key}`, seen);
|
|
1692
|
+
if (child)
|
|
1693
|
+
return child;
|
|
1694
|
+
}
|
|
1695
|
+
seen.delete(obj);
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
function toWireRequest(req, kind) {
|
|
1699
|
+
const wire = {
|
|
1700
|
+
reason: req.reason,
|
|
1701
|
+
kind,
|
|
1702
|
+
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
1703
|
+
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {},
|
|
1704
|
+
...req.correlationKey !== undefined ? { correlationKey: req.correlationKey } : {},
|
|
1705
|
+
...req.snapshot !== undefined ? { snapshot: req.snapshot } : {}
|
|
1706
|
+
};
|
|
1707
|
+
const parsed = StepPauseRequestSchema.safeParse(wire);
|
|
1708
|
+
if (!parsed.success) {
|
|
1709
|
+
throw new PauseRequestError(`pause request failed validation: ${parsed.error.message}`);
|
|
1710
|
+
}
|
|
1711
|
+
const badPath = nonJsonPath(parsed.data);
|
|
1712
|
+
if (badPath) {
|
|
1713
|
+
throw new PauseRequestError(`pause request must be JSON-serializable (invalid value at ${badPath})`);
|
|
1714
|
+
}
|
|
1715
|
+
return parsed.data;
|
|
1716
|
+
}
|
|
1717
|
+
async function corePause(req, coord, kind = "custom") {
|
|
1718
|
+
const wire = toWireRequest(req, kind);
|
|
1719
|
+
const pauseId = nextPauseId(coord);
|
|
1720
|
+
const record = await readPauseRecord(pauseId);
|
|
1721
|
+
if (record === null || record.resolved !== true) {
|
|
1722
|
+
await writePauseRecord({ pauseId, request: wire });
|
|
1723
|
+
throw new PauseSignal(pauseId, wire);
|
|
1724
|
+
}
|
|
1725
|
+
if (record.expired === true) {
|
|
1726
|
+
const onExpiry = req.onExpiry ?? { mode: "throw" };
|
|
1727
|
+
if (onExpiry.mode === "resolve")
|
|
1728
|
+
return onExpiry.value;
|
|
1729
|
+
throw new PauseExpiredError(`pause "${req.reason}" expired before it was resolved`);
|
|
1730
|
+
}
|
|
1731
|
+
const payload = record.resumePayload ?? null;
|
|
1732
|
+
if (req.schema) {
|
|
1733
|
+
const parsed = req.schema.safeParse(payload);
|
|
1734
|
+
if (!parsed.success) {
|
|
1735
|
+
throw new PauseSchemaError(`resume payload for pause "${req.reason}" failed schema validation: ${parsed.error.message}`, parsed.error.issues);
|
|
1736
|
+
}
|
|
1737
|
+
return parsed.data;
|
|
1738
|
+
}
|
|
1739
|
+
return payload;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/agent/steer-control.ts
|
|
1743
|
+
import { z as z8 } from "zod";
|
|
1744
|
+
var SteerDecisionSchema = z8.object({
|
|
1745
|
+
message: z8.string().min(1),
|
|
1746
|
+
actor: z8.string().nullish()
|
|
1747
|
+
});
|
|
1748
|
+
var steerPending = new Map;
|
|
1749
|
+
function signalSteerPending(agentId, payload) {
|
|
1750
|
+
const queue = steerPending.get(agentId) ?? [];
|
|
1751
|
+
const existing = queue.findIndex((p) => p.correlationKey === payload.correlationKey);
|
|
1752
|
+
if (existing >= 0)
|
|
1753
|
+
queue[existing] = payload;
|
|
1754
|
+
else
|
|
1755
|
+
queue.push(payload);
|
|
1756
|
+
steerPending.set(agentId, queue);
|
|
1757
|
+
}
|
|
1758
|
+
function consumeSteerPending(agentId) {
|
|
1759
|
+
const queue = steerPending.get(agentId);
|
|
1760
|
+
if (queue === undefined || queue.length === 0)
|
|
1761
|
+
return null;
|
|
1762
|
+
const next = queue.shift();
|
|
1763
|
+
if (queue.length === 0)
|
|
1764
|
+
steerPending.delete(agentId);
|
|
1765
|
+
return next;
|
|
1766
|
+
}
|
|
1767
|
+
async function runControlPoller(opts) {
|
|
1768
|
+
const headers = { authorization: `Bearer ${opts.token}`, accept: "application/json" };
|
|
1769
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}/api/v1/internal/runs/${opts.runId}/control`;
|
|
1770
|
+
let backoffMs = 0;
|
|
1771
|
+
while (!opts.signal.aborted) {
|
|
1772
|
+
if (backoffMs > 0) {
|
|
1773
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
1774
|
+
if (opts.signal.aborted)
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
const res = await fetch(url, { method: "GET", headers, signal: opts.signal });
|
|
1779
|
+
if (!res.ok) {
|
|
1780
|
+
if (res.status === 401 || res.status === 403)
|
|
1781
|
+
return;
|
|
1782
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
backoffMs = 0;
|
|
1786
|
+
const body = await res.json();
|
|
1787
|
+
const ctrl = body.control;
|
|
1788
|
+
if (ctrl && ctrl.kind === "steer_pause" && (ctrl.agentId == null || ctrl.agentId === opts.agentId)) {
|
|
1789
|
+
signalSteerPending(opts.agentId, {
|
|
1790
|
+
reason: ctrl.reason ?? null,
|
|
1791
|
+
correlationKey: ctrl.correlationKey ?? null
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
if (opts.signal.aborted)
|
|
1796
|
+
return;
|
|
1797
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
1798
|
+
if (backoffMs >= 4000) {
|
|
1799
|
+
process.stderr.write(`[control-poller] poll failed, backing off ${backoffMs}ms: ${err instanceof Error ? err.message : String(err)}
|
|
1800
|
+
`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/agent/agent-loop.ts
|
|
1151
1807
|
var DEFAULT_CLAUDE_MODEL = "claude-opus-4-7";
|
|
1152
1808
|
var SAME_BLOCKER_ITERATIONS = 3;
|
|
1153
1809
|
var STALL_ITERATIONS = 3;
|
|
@@ -1183,7 +1839,7 @@ function summarizeAgentMessage(msg) {
|
|
|
1183
1839
|
case "thinking":
|
|
1184
1840
|
return { type: "thinking", text: msg.text };
|
|
1185
1841
|
case "tool_use":
|
|
1186
|
-
return { type: "tool_use", toolName: msg.toolName, toolUseId: msg.toolUseId, toolInputPreview: preview(msg.toolInput) };
|
|
1842
|
+
return { type: "tool_use", toolName: msg.toolName, toolUseId: msg.toolUseId, toolInput: msg.toolInput, toolInputPreview: preview(msg.toolInput) };
|
|
1187
1843
|
case "tool_result":
|
|
1188
1844
|
return { type: "tool_result", toolUseId: msg.toolUseId, output: truncate(msg.output), isError: msg.isError };
|
|
1189
1845
|
case "usage":
|
|
@@ -1236,7 +1892,7 @@ async function agentLoop(opts) {
|
|
|
1236
1892
|
processors,
|
|
1237
1893
|
requestContext,
|
|
1238
1894
|
agentId,
|
|
1239
|
-
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema:
|
|
1895
|
+
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: z9.toJSONSchema(opts.responseSchema) } } : {}
|
|
1240
1896
|
});
|
|
1241
1897
|
if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
|
|
1242
1898
|
process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
|
|
@@ -1249,11 +1905,101 @@ async function agentLoop(opts) {
|
|
|
1249
1905
|
let completedIterations = 0;
|
|
1250
1906
|
let lastResponseText = "";
|
|
1251
1907
|
let lastResponseValidationError = "";
|
|
1252
|
-
|
|
1908
|
+
let startIteration = 0;
|
|
1909
|
+
let resumed = false;
|
|
1910
|
+
let pendingSteerPause = null;
|
|
1911
|
+
const pauseManager = new PauseManager(agentId);
|
|
1912
|
+
const restore = await pauseManager.restoreAgentLoop(client);
|
|
1913
|
+
if (restore.kind === "settled") {
|
|
1914
|
+
process.stdout.write(`${logLabel} restored settled agent result from checkpoint after ${restore.result.iterations} iterations
|
|
1915
|
+
`);
|
|
1916
|
+
return { agentId, label, ...restore.result };
|
|
1917
|
+
}
|
|
1918
|
+
if (restore.kind === "running") {
|
|
1919
|
+
const s = restore.state;
|
|
1920
|
+
startIteration = s.iteration;
|
|
1921
|
+
completedIterations = s.completedIterations;
|
|
1922
|
+
lastSessionId = s.lastSessionId;
|
|
1923
|
+
lastStatus = s.lastStatus;
|
|
1924
|
+
lastResponseText = s.lastResponseText;
|
|
1925
|
+
lastResponseValidationError = s.lastResponseValidationError;
|
|
1926
|
+
iterationsWithoutStatus = s.iterationsWithoutStatus;
|
|
1927
|
+
blockerStreak = s.blockerStreak;
|
|
1928
|
+
pendingSteerPause = s.pendingSteerPause ?? null;
|
|
1929
|
+
resumed = true;
|
|
1930
|
+
process.stdout.write(`${logLabel} resumed from agent-state checkpoint at iteration ${startIteration}/${maxIterations}
|
|
1931
|
+
`);
|
|
1932
|
+
}
|
|
1933
|
+
if (restore.kind === "invalid") {
|
|
1934
|
+
process.stderr.write(`${logLabel} agent-state checkpoint at agentId=${agentId} is unrecognised — starting fresh. ` + `Schema version mismatch or corrupt file: ${restore.error}
|
|
1935
|
+
`);
|
|
1936
|
+
}
|
|
1937
|
+
if (!resumed) {
|
|
1938
|
+
opts.onAgentLifecycleEvent?.({
|
|
1939
|
+
event: "agent.spawned",
|
|
1940
|
+
at: startedAt,
|
|
1941
|
+
agentId,
|
|
1942
|
+
label,
|
|
1943
|
+
allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
|
|
1944
|
+
...client.model != null ? { model: client.model } : {},
|
|
1945
|
+
...client.kind != null ? { runtimeKind: client.kind } : {}
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const flushState = async (currentIteration) => {
|
|
1949
|
+
await pauseManager.saveRunningAgentLoop({
|
|
1950
|
+
iteration: currentIteration,
|
|
1951
|
+
completedIterations,
|
|
1952
|
+
lastSessionId,
|
|
1953
|
+
lastStatus,
|
|
1954
|
+
lastResponseText,
|
|
1955
|
+
lastResponseValidationError,
|
|
1956
|
+
iterationsWithoutStatus,
|
|
1957
|
+
blockerStreak,
|
|
1958
|
+
pendingSteerPause
|
|
1959
|
+
}, client);
|
|
1960
|
+
};
|
|
1961
|
+
const writeSettledState = async (result) => {
|
|
1962
|
+
await pauseManager.saveSettledAgentLoop({
|
|
1963
|
+
sessionId: result.sessionId,
|
|
1964
|
+
lastStatus: result.lastStatus,
|
|
1965
|
+
iterations: result.iterations,
|
|
1966
|
+
...result.response !== undefined ? { response: result.response } : {}
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1253
1969
|
try {
|
|
1254
|
-
for (let iteration =
|
|
1970
|
+
for (let iteration = startIteration;iteration < maxIterations || !!opts.pause && pendingSteerPause !== null; iteration++) {
|
|
1971
|
+
await flushState(iteration);
|
|
1972
|
+
let steerDecision = null;
|
|
1973
|
+
if (opts.pause) {
|
|
1974
|
+
if (pendingSteerPause === null) {
|
|
1975
|
+
const steer = opts.consumeSteerPending?.() ?? null;
|
|
1976
|
+
if (steer) {
|
|
1977
|
+
pendingSteerPause = {
|
|
1978
|
+
reason: steer.reason ?? "human steer",
|
|
1979
|
+
correlationKey: steer.correlationKey,
|
|
1980
|
+
at: Date.now()
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
if (pendingSteerPause !== null) {
|
|
1985
|
+
await flushState(iteration);
|
|
1986
|
+
steerDecision = await opts.pause({
|
|
1987
|
+
reason: pendingSteerPause.reason,
|
|
1988
|
+
...pendingSteerPause.correlationKey !== null ? { correlationKey: pendingSteerPause.correlationKey } : {},
|
|
1989
|
+
schema: SteerDecisionSchema
|
|
1990
|
+
}, { agentId, iteration });
|
|
1991
|
+
pendingSteerPause = null;
|
|
1992
|
+
await flushState(iteration);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1255
1995
|
const procCtx = buildProcCtx(iteration + 1);
|
|
1256
|
-
|
|
1996
|
+
let initialPrompt = opts.buildPrompt(lastStatus, iteration);
|
|
1997
|
+
if (steerDecision) {
|
|
1998
|
+
const who = steerDecision.actor ? ` from ${steerDecision.actor}` : "";
|
|
1999
|
+
initialPrompt = `${initialPrompt}
|
|
2000
|
+
|
|
2001
|
+
[human steer${who}]: ${steerDecision.message}`;
|
|
2002
|
+
}
|
|
1257
2003
|
const inputVerdict = await runProcessorChain(processors, (p) => p.processInput, initialPrompt, procCtx);
|
|
1258
2004
|
if (inputVerdict.kind !== "continue") {
|
|
1259
2005
|
loopAbort.abort();
|
|
@@ -1263,7 +2009,12 @@ async function agentLoop(opts) {
|
|
|
1263
2009
|
process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration} turns
|
|
1264
2010
|
`);
|
|
1265
2011
|
let responseText = "";
|
|
1266
|
-
for await (const rawMsg of client.sendMessage({
|
|
2012
|
+
for await (const rawMsg of client.sendMessage({
|
|
2013
|
+
prompt,
|
|
2014
|
+
sessionId: iteration > 0 ? lastSessionId : undefined,
|
|
2015
|
+
iteration: iteration + 1,
|
|
2016
|
+
...opts.inbox ? { inboxStream: opts.inbox } : {}
|
|
2017
|
+
})) {
|
|
1267
2018
|
const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
|
|
1268
2019
|
if (outputVerdict.kind === "abort") {
|
|
1269
2020
|
loopAbort.abort();
|
|
@@ -1287,16 +2038,23 @@ async function agentLoop(opts) {
|
|
|
1287
2038
|
lastResponseText = responseText;
|
|
1288
2039
|
let status = parseAgentStatus(responseText);
|
|
1289
2040
|
const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
|
|
2041
|
+
const validateResponse = (target) => {
|
|
2042
|
+
const parsed = opts.responseSchema.safeParse(target);
|
|
2043
|
+
if (!parsed.success)
|
|
2044
|
+
lastResponseValidationError = parsed.error.message;
|
|
2045
|
+
return parsed;
|
|
2046
|
+
};
|
|
1290
2047
|
if (opts.responseSchema && rawResponse !== null) {
|
|
1291
|
-
const parsed =
|
|
2048
|
+
const parsed = validateResponse(rawResponse);
|
|
1292
2049
|
if (parsed.success) {
|
|
1293
2050
|
const successStatus = status ?? { summary: "structured response completed", completed: [], blockers: [], exit_signal: true };
|
|
1294
2051
|
process.stdout.write(`${logLabel} structured response validated after ${iteration + 1}/${maxIterations} iterations
|
|
1295
2052
|
`);
|
|
2053
|
+
const result2 = { agentId, label, sessionId: lastSessionId, lastStatus: successStatus, iterations: iteration + 1, response: parsed.data };
|
|
2054
|
+
await writeSettledState(result2);
|
|
1296
2055
|
opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: iteration + 1, durationMs: Date.now() - startedAt });
|
|
1297
|
-
return
|
|
2056
|
+
return result2;
|
|
1298
2057
|
}
|
|
1299
|
-
lastResponseValidationError = parsed.error.message;
|
|
1300
2058
|
}
|
|
1301
2059
|
completedIterations = iteration + 1;
|
|
1302
2060
|
process.stdout.write(`${logLabel} iteration ${iteration + 1} status: exit_signal=${status?.exit_signal ?? "(no status)"} blockers=${JSON.stringify(status?.blockers ?? [])}
|
|
@@ -1314,9 +2072,8 @@ async function agentLoop(opts) {
|
|
|
1314
2072
|
opts.onIteration?.(iteration + 1, status);
|
|
1315
2073
|
continue;
|
|
1316
2074
|
}
|
|
1317
|
-
const parsed =
|
|
2075
|
+
const parsed = validateResponse({ ...status, ...rawResponse });
|
|
1318
2076
|
if (!parsed.success) {
|
|
1319
|
-
lastResponseValidationError = parsed.error.message;
|
|
1320
2077
|
process.stderr.write(`${logLabel} <response> SCHEMA FAILED: ${parsed.error.message}
|
|
1321
2078
|
raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
1322
2079
|
`);
|
|
@@ -1328,8 +2085,19 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
1328
2085
|
}
|
|
1329
2086
|
process.stdout.write(`${logLabel} done after ${iteration + 1}/${maxIterations} iterations
|
|
1330
2087
|
`);
|
|
2088
|
+
const result2 = { agentId, label, sessionId: lastSessionId, lastStatus: status, iterations: iteration + 1, response };
|
|
2089
|
+
await writeSettledState(result2);
|
|
1331
2090
|
opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: iteration + 1, durationMs: Date.now() - startedAt });
|
|
1332
|
-
return
|
|
2091
|
+
return result2;
|
|
2092
|
+
}
|
|
2093
|
+
if (opts.pause && opts.mode === "hitl" && status?.needs_input && pendingSteerPause === null) {
|
|
2094
|
+
pendingSteerPause = {
|
|
2095
|
+
reason: status.question?.trim() || "the agent requested human input",
|
|
2096
|
+
correlationKey: null,
|
|
2097
|
+
at: Date.now()
|
|
2098
|
+
};
|
|
2099
|
+
blockerStreak = null;
|
|
2100
|
+
continue;
|
|
1333
2101
|
}
|
|
1334
2102
|
if (!status) {
|
|
1335
2103
|
if (++iterationsWithoutStatus >= STALL_ITERATIONS)
|
|
@@ -1356,9 +2124,13 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
1356
2124
|
throw new Error(`${logLabel} did not produce a valid <response> after ${maxIterations} iterations${lastResponseValidationError ? `: ${lastResponseValidationError}` : ""}. Response tail: ${lastResponseText.slice(-600)}`);
|
|
1357
2125
|
process.stderr.write(`${logLabel} exhausted ${maxIterations} iterations, proceeding with available work
|
|
1358
2126
|
`);
|
|
2127
|
+
const result = { agentId, label, sessionId: lastSessionId, lastStatus, iterations: maxIterations };
|
|
2128
|
+
await writeSettledState(result);
|
|
1359
2129
|
opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: maxIterations, durationMs: Date.now() - startedAt });
|
|
1360
|
-
return
|
|
2130
|
+
return result;
|
|
1361
2131
|
} catch (err) {
|
|
2132
|
+
if (isPauseSignal(err))
|
|
2133
|
+
throw err;
|
|
1362
2134
|
opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "failed", iterations: completedIterations, durationMs: Date.now() - startedAt, failureReason: err instanceof Error ? err.message : String(err) });
|
|
1363
2135
|
throw err;
|
|
1364
2136
|
}
|
|
@@ -1374,6 +2146,50 @@ function parseRawJsonResponse(text) {
|
|
|
1374
2146
|
}
|
|
1375
2147
|
}
|
|
1376
2148
|
|
|
2149
|
+
// src/agent/async-queue.ts
|
|
2150
|
+
class AsyncQueue {
|
|
2151
|
+
buffer = [];
|
|
2152
|
+
resolvers = [];
|
|
2153
|
+
done = false;
|
|
2154
|
+
push(value) {
|
|
2155
|
+
if (this.done)
|
|
2156
|
+
return;
|
|
2157
|
+
const resolve = this.resolvers.shift();
|
|
2158
|
+
if (resolve) {
|
|
2159
|
+
resolve({ value, done: false });
|
|
2160
|
+
} else {
|
|
2161
|
+
this.buffer.push(value);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
close() {
|
|
2165
|
+
if (this.done)
|
|
2166
|
+
return;
|
|
2167
|
+
this.done = true;
|
|
2168
|
+
while (this.resolvers.length > 0) {
|
|
2169
|
+
const resolve = this.resolvers.shift();
|
|
2170
|
+
resolve({ value: undefined, done: true });
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
[Symbol.asyncIterator]() {
|
|
2174
|
+
return {
|
|
2175
|
+
next: () => {
|
|
2176
|
+
const buffered = this.buffer.shift();
|
|
2177
|
+
if (buffered !== undefined)
|
|
2178
|
+
return Promise.resolve({ value: buffered, done: false });
|
|
2179
|
+
if (this.done)
|
|
2180
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
2181
|
+
return new Promise((resolve) => {
|
|
2182
|
+
this.resolvers.push(resolve);
|
|
2183
|
+
});
|
|
2184
|
+
},
|
|
2185
|
+
return: () => {
|
|
2186
|
+
this.close();
|
|
2187
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1377
2193
|
// src/runtimes/claude.ts
|
|
1378
2194
|
function now2() {
|
|
1379
2195
|
return new Date().toISOString();
|
|
@@ -1393,6 +2209,8 @@ function translateMessage(message) {
|
|
|
1393
2209
|
msgs.push({ type: "text", text: String(b.text ?? ""), timestamp: ts });
|
|
1394
2210
|
if (b.type === "thinking")
|
|
1395
2211
|
msgs.push({ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts });
|
|
2212
|
+
if (b.type === "redacted_thinking")
|
|
2213
|
+
msgs.push({ type: "thinking", text: "(redacted)", timestamp: ts });
|
|
1396
2214
|
if (b.type === "tool_use")
|
|
1397
2215
|
msgs.push({ type: "tool_use", toolName: String(b.name ?? ""), toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts });
|
|
1398
2216
|
}
|
|
@@ -1431,15 +2249,20 @@ function translateMessage(message) {
|
|
|
1431
2249
|
}
|
|
1432
2250
|
return msgs;
|
|
1433
2251
|
}
|
|
2252
|
+
var DEFAULT_CLAUDE_PATH = "/usr/local/bin/claude";
|
|
1434
2253
|
|
|
1435
2254
|
class ClaudeRunner {
|
|
1436
2255
|
options;
|
|
1437
2256
|
config;
|
|
1438
2257
|
supportsToolCallProcessor = true;
|
|
2258
|
+
kind = "claude";
|
|
1439
2259
|
constructor(_sandbox, options = {}, config = {}) {
|
|
1440
2260
|
this.options = options;
|
|
1441
2261
|
this.config = config;
|
|
1442
2262
|
}
|
|
2263
|
+
get model() {
|
|
2264
|
+
return this.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL;
|
|
2265
|
+
}
|
|
1443
2266
|
async gateToolCall(call, ctx) {
|
|
1444
2267
|
const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
|
|
1445
2268
|
if (verdict.kind === "continue")
|
|
@@ -1475,10 +2298,33 @@ class ClaudeRunner {
|
|
|
1475
2298
|
return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "deny", permissionDecisionReason: gate.reason } };
|
|
1476
2299
|
return { continue: false, systemMessage: gate.reason };
|
|
1477
2300
|
};
|
|
2301
|
+
const inboxQueue = opts.inboxStream ? new AsyncQueue : null;
|
|
2302
|
+
if (inboxQueue) {
|
|
2303
|
+
inboxQueue.push({
|
|
2304
|
+
type: "user",
|
|
2305
|
+
message: { role: "user", content: opts.prompt },
|
|
2306
|
+
parent_tool_use_id: null
|
|
2307
|
+
});
|
|
2308
|
+
(async () => {
|
|
2309
|
+
try {
|
|
2310
|
+
for await (const ev of opts.inboxStream) {
|
|
2311
|
+
const tag = ev.senderName ? `[message from ${ev.senderName}]
|
|
2312
|
+
` : `[user message]
|
|
2313
|
+
`;
|
|
2314
|
+
inboxQueue.push({
|
|
2315
|
+
type: "user",
|
|
2316
|
+
message: { role: "user", content: tag + ev.text },
|
|
2317
|
+
parent_tool_use_id: null
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
} catch {}
|
|
2321
|
+
})();
|
|
2322
|
+
}
|
|
2323
|
+
const promptForQuery = inboxQueue ?? opts.prompt;
|
|
1478
2324
|
try {
|
|
1479
2325
|
let emittedAssistantText = false;
|
|
1480
2326
|
for await (const message of query({
|
|
1481
|
-
prompt:
|
|
2327
|
+
prompt: promptForQuery,
|
|
1482
2328
|
options: {
|
|
1483
2329
|
tools: this.options.allowedTools,
|
|
1484
2330
|
allowedTools: this.options.allowedTools,
|
|
@@ -1489,7 +2335,7 @@ class ClaudeRunner {
|
|
|
1489
2335
|
effort: this.config.effort,
|
|
1490
2336
|
cwd: this.options.cwd,
|
|
1491
2337
|
env: { ...process.env, ...this.config.env ?? {} },
|
|
1492
|
-
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ??
|
|
2338
|
+
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
1493
2339
|
...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
|
|
1494
2340
|
resume: opts.sessionId,
|
|
1495
2341
|
mcpServers: this.config.mcpServers,
|
|
@@ -1506,9 +2352,13 @@ class ClaudeRunner {
|
|
|
1506
2352
|
emittedAssistantText = true;
|
|
1507
2353
|
yield msg;
|
|
1508
2354
|
}
|
|
2355
|
+
if (raw.type === "result" && inboxQueue)
|
|
2356
|
+
inboxQueue.close();
|
|
1509
2357
|
}
|
|
1510
2358
|
} catch (err) {
|
|
1511
2359
|
yield { type: "error", text: formatError(err), timestamp: now2() };
|
|
2360
|
+
} finally {
|
|
2361
|
+
inboxQueue?.close();
|
|
1512
2362
|
}
|
|
1513
2363
|
}
|
|
1514
2364
|
}
|
|
@@ -1519,7 +2369,7 @@ function createClaudeRuntime(config = {}) {
|
|
|
1519
2369
|
}
|
|
1520
2370
|
var claude_default = createClaudeRuntime();
|
|
1521
2371
|
// src/sandbox.ts
|
|
1522
|
-
import { promises as
|
|
2372
|
+
import { promises as fs2 } from "node:fs";
|
|
1523
2373
|
import { dirname as dirname2 } from "node:path";
|
|
1524
2374
|
import { spawn } from "node:child_process";
|
|
1525
2375
|
import { Sandbox } from "e2b";
|
|
@@ -1528,9 +2378,26 @@ import pRetry from "p-retry";
|
|
|
1528
2378
|
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
1529
2379
|
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
1530
2380
|
function makeSandboxProvider(sb) {
|
|
2381
|
+
const commands = sb.commands;
|
|
1531
2382
|
return {
|
|
1532
2383
|
sandboxId: sb.sandboxId,
|
|
1533
|
-
commands:
|
|
2384
|
+
commands: {
|
|
2385
|
+
async run(cmd, opts) {
|
|
2386
|
+
let stderr = "";
|
|
2387
|
+
const result = await commands.run(cmd, {
|
|
2388
|
+
...opts,
|
|
2389
|
+
onStderr: (chunk) => {
|
|
2390
|
+
stderr += chunk;
|
|
2391
|
+
opts?.onStderr?.(chunk);
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
return {
|
|
2395
|
+
exitCode: result.exitCode,
|
|
2396
|
+
stdout: result.stdout,
|
|
2397
|
+
stderr: typeof result.stderr === "string" ? result.stderr : stderr
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
},
|
|
1534
2401
|
files: {
|
|
1535
2402
|
async write(path, content) {
|
|
1536
2403
|
await sb.files.write(path, content);
|
|
@@ -1555,7 +2422,7 @@ function makeDesktopSandboxProvider(sb) {
|
|
|
1555
2422
|
};
|
|
1556
2423
|
}
|
|
1557
2424
|
async function parseSseExecStream(body, opts) {
|
|
1558
|
-
let stdout = "", exitCode = 0, exited = false;
|
|
2425
|
+
let stdout = "", stderr = "", exitCode = 0, exited = false;
|
|
1559
2426
|
const reader = body.getReader(), decoder = new TextDecoder;
|
|
1560
2427
|
let buf = "";
|
|
1561
2428
|
while (true) {
|
|
@@ -1573,13 +2440,14 @@ async function parseSseExecStream(body, opts) {
|
|
|
1573
2440
|
let event;
|
|
1574
2441
|
try {
|
|
1575
2442
|
event = JSON.parse(line.slice(6));
|
|
1576
|
-
} catch {
|
|
1577
|
-
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
throw new Error(`Malformed sandbox exec event: ${err instanceof Error ? err.message : String(err)}`);
|
|
1578
2445
|
}
|
|
1579
2446
|
if (event.type === "stdout") {
|
|
1580
2447
|
stdout += event.data ?? "";
|
|
1581
2448
|
opts?.onStdout?.(event.data ?? "");
|
|
1582
2449
|
} else if (event.type === "stderr") {
|
|
2450
|
+
stderr += event.data ?? "";
|
|
1583
2451
|
opts?.onStderr?.(event.data ?? "");
|
|
1584
2452
|
} else if (event.type === "exit") {
|
|
1585
2453
|
exitCode = event.exitCode ?? 0;
|
|
@@ -1591,7 +2459,9 @@ async function parseSseExecStream(body, opts) {
|
|
|
1591
2459
|
if (exited)
|
|
1592
2460
|
break;
|
|
1593
2461
|
}
|
|
1594
|
-
|
|
2462
|
+
if (!exited)
|
|
2463
|
+
throw new Error("Sandbox exec stream ended without an exit event");
|
|
2464
|
+
return { exitCode, stdout, stderr };
|
|
1595
2465
|
}
|
|
1596
2466
|
function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
1597
2467
|
const mergeEnvs = (cmdEnvs) => globalEnvs ? { ...globalEnvs, ...cmdEnvs } : cmdEnvs;
|
|
@@ -1599,26 +2469,28 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
1599
2469
|
sandboxId: sb.sandboxId,
|
|
1600
2470
|
commands: {
|
|
1601
2471
|
async run(cmd, opts) {
|
|
1602
|
-
if (opts?.background) {
|
|
1603
|
-
sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts.cwd, env: mergeEnvs(opts.envs), detached: true }).catch((err) => console.error(`[sandbox] background command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1604
|
-
return { exitCode: 0, stdout: "" };
|
|
1605
|
-
}
|
|
1606
2472
|
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
1607
2473
|
let stdout = "";
|
|
1608
|
-
|
|
2474
|
+
let stderr = "";
|
|
2475
|
+
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} });
|
|
1609
2476
|
await pRetry(async (attempt) => {
|
|
1610
2477
|
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
2478
|
+
if (attempt > 1) {
|
|
2479
|
+
stdout = "";
|
|
2480
|
+
stderr = "";
|
|
2481
|
+
}
|
|
1611
2482
|
for await (const log of h.logs()) {
|
|
1612
2483
|
if (log.stream === "stdout") {
|
|
1613
2484
|
stdout += log.data;
|
|
1614
2485
|
opts?.onStdout?.(log.data);
|
|
1615
2486
|
} else {
|
|
2487
|
+
stderr += log.data;
|
|
1616
2488
|
opts?.onStderr?.(log.data);
|
|
1617
2489
|
}
|
|
1618
2490
|
}
|
|
1619
2491
|
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
1620
2492
|
const finished = await handle.wait();
|
|
1621
|
-
return { exitCode: finished.exitCode, stdout };
|
|
2493
|
+
return { exitCode: finished.exitCode, stdout, stderr };
|
|
1622
2494
|
}
|
|
1623
2495
|
},
|
|
1624
2496
|
files: {
|
|
@@ -1644,13 +2516,15 @@ function makeLocalSandboxProvider() {
|
|
|
1644
2516
|
sandboxId: "local",
|
|
1645
2517
|
commands: {
|
|
1646
2518
|
run(cmd, opts) {
|
|
2519
|
+
const finalCmd = opts?.sudo ? `sudo ${cmd}` : cmd;
|
|
1647
2520
|
return new Promise((resolve, reject) => {
|
|
1648
|
-
const proc = spawn("sh", ["-c",
|
|
2521
|
+
const proc = spawn("sh", ["-c", finalCmd], {
|
|
1649
2522
|
...opts?.cwd ? { cwd: opts.cwd } : {},
|
|
1650
2523
|
env: { ...process.env, ...opts?.envs ?? {} },
|
|
1651
2524
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1652
2525
|
});
|
|
1653
2526
|
let stdout = "";
|
|
2527
|
+
let stderr = "";
|
|
1654
2528
|
proc.stdout?.setEncoding("utf8");
|
|
1655
2529
|
proc.stderr?.setEncoding("utf8");
|
|
1656
2530
|
proc.stdout?.on("data", (chunk) => {
|
|
@@ -1658,17 +2532,18 @@ function makeLocalSandboxProvider() {
|
|
|
1658
2532
|
opts?.onStdout?.(chunk);
|
|
1659
2533
|
});
|
|
1660
2534
|
proc.stderr?.on("data", (chunk) => {
|
|
2535
|
+
stderr += chunk;
|
|
1661
2536
|
opts?.onStderr?.(chunk);
|
|
1662
2537
|
});
|
|
1663
2538
|
proc.on("error", reject);
|
|
1664
|
-
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout }));
|
|
2539
|
+
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
|
|
1665
2540
|
});
|
|
1666
2541
|
}
|
|
1667
2542
|
},
|
|
1668
2543
|
files: {
|
|
1669
2544
|
async write(path, content) {
|
|
1670
|
-
await
|
|
1671
|
-
await
|
|
2545
|
+
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
2546
|
+
await fs2.writeFile(path, content);
|
|
1672
2547
|
}
|
|
1673
2548
|
},
|
|
1674
2549
|
async kill() {}
|
|
@@ -1843,10 +2718,18 @@ async function killAllSandboxes(onError) {
|
|
|
1843
2718
|
}));
|
|
1844
2719
|
}
|
|
1845
2720
|
// src/workflow-steps/observability.ts
|
|
2721
|
+
var LIVE_EMIT_DRAIN_TIMEOUT_MS = 1500;
|
|
2722
|
+
|
|
1846
2723
|
class StepObservabilityCollector {
|
|
1847
2724
|
metadata = {};
|
|
1848
2725
|
events = [];
|
|
1849
2726
|
subSteps = [];
|
|
2727
|
+
liveEmitter;
|
|
2728
|
+
ackedSeqs = new Set;
|
|
2729
|
+
inFlight = new Set;
|
|
2730
|
+
constructor(opts = {}) {
|
|
2731
|
+
this.liveEmitter = opts.liveEmitter;
|
|
2732
|
+
}
|
|
1850
2733
|
setMetadata = async (data) => {
|
|
1851
2734
|
Object.assign(this.metadata, data);
|
|
1852
2735
|
};
|
|
@@ -1862,6 +2745,8 @@ class StepObservabilityCollector {
|
|
|
1862
2745
|
});
|
|
1863
2746
|
return result;
|
|
1864
2747
|
} catch (err) {
|
|
2748
|
+
if (isPauseSignal(err))
|
|
2749
|
+
throw err;
|
|
1865
2750
|
this.subSteps.push({
|
|
1866
2751
|
name,
|
|
1867
2752
|
startedAt,
|
|
@@ -1874,26 +2759,92 @@ class StepObservabilityCollector {
|
|
|
1874
2759
|
};
|
|
1875
2760
|
agentEvents = {
|
|
1876
2761
|
emit: (event) => {
|
|
2762
|
+
const seq = this.events.length;
|
|
1877
2763
|
this.events.push(event);
|
|
2764
|
+
if (this.liveEmitter) {
|
|
2765
|
+
const tracked = this.liveEmitter(event, seq).then((ok) => {
|
|
2766
|
+
if (ok)
|
|
2767
|
+
this.ackedSeqs.add(seq);
|
|
2768
|
+
}).catch(() => {});
|
|
2769
|
+
this.inFlight.add(tracked);
|
|
2770
|
+
tracked.finally(() => this.inFlight.delete(tracked));
|
|
2771
|
+
}
|
|
1878
2772
|
}
|
|
1879
2773
|
};
|
|
1880
|
-
|
|
2774
|
+
async drainInFlight(timeoutMs = LIVE_EMIT_DRAIN_TIMEOUT_MS) {
|
|
2775
|
+
if (this.inFlight.size === 0)
|
|
2776
|
+
return;
|
|
2777
|
+
await Promise.race([
|
|
2778
|
+
Promise.allSettled([...this.inFlight]),
|
|
2779
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
2780
|
+
]);
|
|
2781
|
+
}
|
|
2782
|
+
async snapshot() {
|
|
2783
|
+
await this.drainInFlight();
|
|
1881
2784
|
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
1882
|
-
const hasEvents = this.events.length > 0;
|
|
1883
2785
|
const hasSubSteps = this.subSteps.length > 0;
|
|
2786
|
+
const remainingEvents = this.events.filter((_, idx) => !this.ackedSeqs.has(idx));
|
|
2787
|
+
const hasEvents = remainingEvents.length > 0;
|
|
1884
2788
|
if (!hasMetadata && !hasEvents && !hasSubSteps)
|
|
1885
2789
|
return;
|
|
1886
2790
|
const result = {};
|
|
1887
2791
|
if (hasMetadata)
|
|
1888
2792
|
result.metadata = { ...this.metadata };
|
|
1889
2793
|
if (hasEvents)
|
|
1890
|
-
result.events =
|
|
2794
|
+
result.events = remainingEvents;
|
|
1891
2795
|
if (hasSubSteps)
|
|
1892
2796
|
result.subSteps = [...this.subSteps];
|
|
1893
2797
|
return result;
|
|
1894
2798
|
}
|
|
1895
2799
|
}
|
|
1896
2800
|
|
|
2801
|
+
// src/pause/checkpoint.ts
|
|
2802
|
+
async function checkpoint(name, fn) {
|
|
2803
|
+
const existing = await readCheckpoint(name);
|
|
2804
|
+
if (existing.found)
|
|
2805
|
+
return existing.value;
|
|
2806
|
+
const value = await fn();
|
|
2807
|
+
await writeCheckpoint(name, value);
|
|
2808
|
+
return value;
|
|
2809
|
+
}
|
|
2810
|
+
function scopedCheckpoint(scope) {
|
|
2811
|
+
assertSafeCheckpointName(scope);
|
|
2812
|
+
return async (name, fn) => {
|
|
2813
|
+
assertSafeCheckpointName(name);
|
|
2814
|
+
return checkpoint(`${scope}.${name}`, fn);
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/pause/wrappers.ts
|
|
2819
|
+
function buildPauseWrappers(pause) {
|
|
2820
|
+
return {
|
|
2821
|
+
requestDecision(req) {
|
|
2822
|
+
return pause({
|
|
2823
|
+
reason: req.reason,
|
|
2824
|
+
schema: req.schema,
|
|
2825
|
+
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
2826
|
+
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
|
|
2827
|
+
}, "decision");
|
|
2828
|
+
},
|
|
2829
|
+
sleep(durationMs) {
|
|
2830
|
+
return pause({
|
|
2831
|
+
reason: `sleep ${durationMs}ms`,
|
|
2832
|
+
ttlMs: durationMs,
|
|
2833
|
+
snapshot: false,
|
|
2834
|
+
onExpiry: { mode: "resolve", value: undefined }
|
|
2835
|
+
}, "sleep");
|
|
2836
|
+
},
|
|
2837
|
+
waitForEvent(req) {
|
|
2838
|
+
return pause({
|
|
2839
|
+
reason: req.reason,
|
|
2840
|
+
correlationKey: req.correlationKey,
|
|
2841
|
+
...req.schema !== undefined ? { schema: req.schema } : {},
|
|
2842
|
+
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
|
|
2843
|
+
}, "event");
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
|
|
1897
2848
|
// src/workflow-steps/runner.ts
|
|
1898
2849
|
class StepValidationError extends Error {
|
|
1899
2850
|
stepName;
|
|
@@ -1936,7 +2887,14 @@ async function runWorkflowSingleStep(opts) {
|
|
|
1936
2887
|
const parsedInput = step.input.safeParse(opts.input);
|
|
1937
2888
|
if (!parsedInput.success)
|
|
1938
2889
|
throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
1939
|
-
const collector = new StepObservabilityCollector;
|
|
2890
|
+
const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
|
|
2891
|
+
const coord = { runId: opts.run.id, stepIndex: opts.stepIndex };
|
|
2892
|
+
const pauseWithKind = function(req, kind) {
|
|
2893
|
+
return corePause(req, coord, kind);
|
|
2894
|
+
};
|
|
2895
|
+
const pause = function(req) {
|
|
2896
|
+
return corePause(req, coord);
|
|
2897
|
+
};
|
|
1940
2898
|
const stepCtx = {
|
|
1941
2899
|
input: parsedInput.data,
|
|
1942
2900
|
requestContext: opts.requestContext,
|
|
@@ -1949,13 +2907,30 @@ async function runWorkflowSingleStep(opts) {
|
|
|
1949
2907
|
}),
|
|
1950
2908
|
setMetadata: collector.setMetadata,
|
|
1951
2909
|
step: collector.step,
|
|
1952
|
-
agentEvents: collector.agentEvents
|
|
2910
|
+
agentEvents: collector.agentEvents,
|
|
2911
|
+
checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
|
|
2912
|
+
pause,
|
|
2913
|
+
...buildPauseWrappers(pauseWithKind)
|
|
1953
2914
|
};
|
|
1954
|
-
|
|
2915
|
+
let output;
|
|
2916
|
+
try {
|
|
2917
|
+
const bridged = setActiveStepBridge({ stepIndex: opts.stepIndex });
|
|
2918
|
+
try {
|
|
2919
|
+
output = await runWithActiveStep({ stepIndex: opts.stepIndex }, () => Promise.resolve(step.run(stepCtx)));
|
|
2920
|
+
} finally {
|
|
2921
|
+
restoreActiveStepBridge(bridged);
|
|
2922
|
+
}
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
if (isPauseSignal(err)) {
|
|
2925
|
+
const observability2 = await collector.snapshot();
|
|
2926
|
+
throw new PauseSignal(err.pauseId, err.pauseRequest, observability2);
|
|
2927
|
+
}
|
|
2928
|
+
throw err;
|
|
2929
|
+
}
|
|
1955
2930
|
const parsedOutput = step.output.safeParse(output);
|
|
1956
2931
|
if (!parsedOutput.success)
|
|
1957
2932
|
throw new StepValidationError(step.name, "output", parsedOutput.error);
|
|
1958
|
-
const observability = collector.snapshot();
|
|
2933
|
+
const observability = await collector.snapshot();
|
|
1959
2934
|
return observability === undefined ? { output: parsedOutput.data } : { output: parsedOutput.data, observability };
|
|
1960
2935
|
}
|
|
1961
2936
|
async function runWorkflowSteps(opts) {
|
|
@@ -1989,7 +2964,14 @@ async function runWorkflowSteps(opts) {
|
|
|
1989
2964
|
await opts.onStepFailed?.(i, step.name, err, 0);
|
|
1990
2965
|
throw err;
|
|
1991
2966
|
}
|
|
1992
|
-
const collector = new StepObservabilityCollector;
|
|
2967
|
+
const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
|
|
2968
|
+
const coord = { runId: run.id, stepIndex: i };
|
|
2969
|
+
const pauseWithKind = function(req, kind) {
|
|
2970
|
+
return corePause(req, coord, kind);
|
|
2971
|
+
};
|
|
2972
|
+
const pause = function(req) {
|
|
2973
|
+
return corePause(req, coord);
|
|
2974
|
+
};
|
|
1993
2975
|
const stepCtx = {
|
|
1994
2976
|
input: parsedStepInput.data,
|
|
1995
2977
|
requestContext,
|
|
@@ -2000,16 +2982,23 @@ async function runWorkflowSteps(opts) {
|
|
|
2000
2982
|
invokeChild,
|
|
2001
2983
|
setMetadata: collector.setMetadata,
|
|
2002
2984
|
step: collector.step,
|
|
2003
|
-
agentEvents: collector.agentEvents
|
|
2985
|
+
agentEvents: collector.agentEvents,
|
|
2986
|
+
checkpoint: scopedCheckpoint(`step${i}`),
|
|
2987
|
+
pause,
|
|
2988
|
+
...buildPauseWrappers(pauseWithKind)
|
|
2004
2989
|
};
|
|
2005
2990
|
const startedAt = Date.now();
|
|
2006
2991
|
let output;
|
|
2007
2992
|
try {
|
|
2008
|
-
output = await step.run(stepCtx);
|
|
2993
|
+
output = await runWithActiveStep({ stepIndex: i }, () => Promise.resolve(step.run(stepCtx)));
|
|
2009
2994
|
} catch (err) {
|
|
2995
|
+
if (isPauseSignal(err)) {
|
|
2996
|
+
const observability3 = await collector.snapshot();
|
|
2997
|
+
throw new PauseSignal(err.pauseId, err.pauseRequest, observability3);
|
|
2998
|
+
}
|
|
2010
2999
|
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
2011
3000
|
const durationMs2 = Date.now() - startedAt;
|
|
2012
|
-
const observability2 = collector.snapshot();
|
|
3001
|
+
const observability2 = await collector.snapshot();
|
|
2013
3002
|
stepResults.push({
|
|
2014
3003
|
name: step.name,
|
|
2015
3004
|
status: "failed",
|
|
@@ -2024,7 +3013,7 @@ async function runWorkflowSteps(opts) {
|
|
|
2024
3013
|
if (!parsedOutput.success) {
|
|
2025
3014
|
const err = new StepValidationError(step.name, "output", parsedOutput.error);
|
|
2026
3015
|
const durationMs2 = Date.now() - startedAt;
|
|
2027
|
-
const observability2 = collector.snapshot();
|
|
3016
|
+
const observability2 = await collector.snapshot();
|
|
2028
3017
|
stepResults.push({
|
|
2029
3018
|
name: step.name,
|
|
2030
3019
|
status: "failed",
|
|
@@ -2037,7 +3026,7 @@ async function runWorkflowSteps(opts) {
|
|
|
2037
3026
|
}
|
|
2038
3027
|
const durationMs = Date.now() - startedAt;
|
|
2039
3028
|
current = parsedOutput.data;
|
|
2040
|
-
const observability = collector.snapshot();
|
|
3029
|
+
const observability = await collector.snapshot();
|
|
2041
3030
|
stepResults.push({
|
|
2042
3031
|
name: step.name,
|
|
2043
3032
|
status: "completed",
|
|
@@ -2162,9 +3151,13 @@ function defineStep(opts) {
|
|
|
2162
3151
|
}
|
|
2163
3152
|
// src/step-invocation/protocol.ts
|
|
2164
3153
|
var STEP_RESULT_PREFIX = "__AC_STEP_RESULT__";
|
|
3154
|
+
var STEP_PAUSE_PREFIX = "__AC_STEP_PAUSE__";
|
|
2165
3155
|
function stepResultLinePrefix(token) {
|
|
2166
3156
|
return `${STEP_RESULT_PREFIX}${token}:`;
|
|
2167
3157
|
}
|
|
3158
|
+
function stepPauseLinePrefix(token) {
|
|
3159
|
+
return `${STEP_PAUSE_PREFIX}${token}:`;
|
|
3160
|
+
}
|
|
2168
3161
|
var RUNNER_BUNDLE_PATH = "/tmp/runner.bundle.js";
|
|
2169
3162
|
var RUNNER_COMMAND = `node ${RUNNER_BUNDLE_PATH}`;
|
|
2170
3163
|
var STEP_ENV = {
|
|
@@ -2173,7 +3166,8 @@ var STEP_ENV = {
|
|
|
2173
3166
|
STEP_INDEX: "AC_STEP_INDEX",
|
|
2174
3167
|
STEP_INPUT_PATH: "AC_STEP_INPUT_PATH",
|
|
2175
3168
|
REQUEST_CONTEXT_PATH: "AC_REQUEST_CONTEXT_PATH",
|
|
2176
|
-
STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN"
|
|
3169
|
+
STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN",
|
|
3170
|
+
STEP_RESUME: "AC_STEP_RESUME"
|
|
2177
3171
|
};
|
|
2178
3172
|
function stepInputPath(stepIndex) {
|
|
2179
3173
|
return `/tmp/wf/step-input-${stepIndex}.json`;
|
|
@@ -2182,7 +3176,36 @@ function requestContextPath(stepIndex) {
|
|
|
2182
3176
|
return `/tmp/wf/request-context-${stepIndex}.json`;
|
|
2183
3177
|
}
|
|
2184
3178
|
// src/step-invocation/invoker.ts
|
|
2185
|
-
import { randomBytes } from "node:crypto";
|
|
3179
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3180
|
+
import { z as z10 } from "zod";
|
|
3181
|
+
var StepObservabilitySchema = z10.object({
|
|
3182
|
+
metadata: z10.record(z10.string(), z10.unknown()).optional(),
|
|
3183
|
+
events: z10.array(z10.unknown()).optional(),
|
|
3184
|
+
subSteps: z10.array(z10.object({
|
|
3185
|
+
name: z10.string(),
|
|
3186
|
+
startedAt: z10.number(),
|
|
3187
|
+
durationMs: z10.number(),
|
|
3188
|
+
status: z10.enum(["completed", "failed"]),
|
|
3189
|
+
error: z10.string().optional()
|
|
3190
|
+
})).optional()
|
|
3191
|
+
}).passthrough();
|
|
3192
|
+
var StepResultEnvelopeSchema = z10.discriminatedUnion("ok", [
|
|
3193
|
+
z10.object({
|
|
3194
|
+
ok: z10.literal(true),
|
|
3195
|
+
output: z10.unknown().optional(),
|
|
3196
|
+
observability: StepObservabilitySchema.optional()
|
|
3197
|
+
}),
|
|
3198
|
+
z10.object({
|
|
3199
|
+
ok: z10.literal(false),
|
|
3200
|
+
kind: z10.enum(["protocol", "user-step"]),
|
|
3201
|
+
error: z10.string().optional()
|
|
3202
|
+
})
|
|
3203
|
+
]);
|
|
3204
|
+
var StepPauseEnvelopeSchema = z10.object({
|
|
3205
|
+
pauseId: z10.string().min(1),
|
|
3206
|
+
pauseRequest: StepPauseRequestSchema,
|
|
3207
|
+
observability: StepObservabilitySchema.optional()
|
|
3208
|
+
});
|
|
2186
3209
|
function buildStepEnvs(args) {
|
|
2187
3210
|
return {
|
|
2188
3211
|
[STEP_ENV.RUN_ID]: args.runId,
|
|
@@ -2190,30 +3213,64 @@ function buildStepEnvs(args) {
|
|
|
2190
3213
|
[STEP_ENV.STEP_INDEX]: String(args.stepIndex),
|
|
2191
3214
|
[STEP_ENV.STEP_INPUT_PATH]: stepInputPath(args.stepIndex),
|
|
2192
3215
|
[STEP_ENV.REQUEST_CONTEXT_PATH]: requestContextPath(args.stepIndex),
|
|
2193
|
-
[STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken
|
|
3216
|
+
[STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken,
|
|
3217
|
+
[STEP_ENV.STEP_RESUME]: args.isResume ? "1" : "0"
|
|
2194
3218
|
};
|
|
2195
3219
|
}
|
|
2196
3220
|
function parseStepResult(stdout, resultToken) {
|
|
2197
|
-
const
|
|
2198
|
-
const
|
|
2199
|
-
|
|
3221
|
+
const resultPrefix = stepResultLinePrefix(resultToken);
|
|
3222
|
+
const pausePrefix = stepPauseLinePrefix(resultToken);
|
|
3223
|
+
let resultLine;
|
|
3224
|
+
let pauseLine;
|
|
3225
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3226
|
+
if (!pauseLine && line.startsWith(pausePrefix))
|
|
3227
|
+
pauseLine = line;
|
|
3228
|
+
if (!resultLine && line.startsWith(resultPrefix))
|
|
3229
|
+
resultLine = line;
|
|
3230
|
+
if (pauseLine && resultLine)
|
|
3231
|
+
break;
|
|
3232
|
+
}
|
|
3233
|
+
if (pauseLine) {
|
|
3234
|
+
try {
|
|
3235
|
+
const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
|
|
3236
|
+
const parsed2 = StepPauseEnvelopeSchema.safeParse(raw);
|
|
3237
|
+
if (!parsed2.success) {
|
|
3238
|
+
return { ok: false, error: { kind: "protocol", message: `step pause schema validation failed: ${parsed2.error.message}` } };
|
|
3239
|
+
}
|
|
3240
|
+
return {
|
|
3241
|
+
ok: "paused",
|
|
3242
|
+
pauseId: parsed2.data.pauseId,
|
|
3243
|
+
pauseRequest: parsed2.data.pauseRequest,
|
|
3244
|
+
...parsed2.data.observability ? { observability: parsed2.data.observability } : {}
|
|
3245
|
+
};
|
|
3246
|
+
} catch {
|
|
3247
|
+
return { ok: false, error: { kind: "protocol", message: "step pause JSON parse failed" } };
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (!resultLine)
|
|
2200
3251
|
return null;
|
|
2201
3252
|
let parsed;
|
|
2202
3253
|
try {
|
|
2203
|
-
|
|
3254
|
+
const raw = JSON.parse(resultLine.slice(resultPrefix.length));
|
|
3255
|
+
const result = StepResultEnvelopeSchema.safeParse(raw);
|
|
3256
|
+
if (!result.success) {
|
|
3257
|
+
const kind2 = raw && typeof raw === "object" && typeof raw.kind === "string" ? ` (kind=${raw.kind})` : "";
|
|
3258
|
+
return { ok: false, error: { kind: "protocol", message: `step result schema validation failed${kind2}: ${result.error.message}` } };
|
|
3259
|
+
}
|
|
3260
|
+
parsed = result.data;
|
|
2204
3261
|
} catch {
|
|
2205
3262
|
return { ok: false, error: { kind: "protocol", message: "step result JSON parse failed" } };
|
|
2206
3263
|
}
|
|
2207
3264
|
if (parsed.ok) {
|
|
2208
3265
|
return parsed.observability ? { ok: true, output: parsed.output, observability: parsed.observability } : { ok: true, output: parsed.output };
|
|
2209
3266
|
}
|
|
2210
|
-
const kind = parsed.kind
|
|
3267
|
+
const kind = parsed.kind;
|
|
2211
3268
|
const baseMessage = parsed.error ?? "step body failed without a message";
|
|
2212
|
-
const message =
|
|
3269
|
+
const message = baseMessage;
|
|
2213
3270
|
return { ok: false, error: { kind, message } };
|
|
2214
3271
|
}
|
|
2215
3272
|
async function invokeStep(sandbox, request, opts) {
|
|
2216
|
-
const resultToken =
|
|
3273
|
+
const resultToken = randomBytes2(16).toString("hex");
|
|
2217
3274
|
await Promise.all([
|
|
2218
3275
|
sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
|
|
2219
3276
|
sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
|
|
@@ -2223,10 +3280,13 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2223
3280
|
...buildStepEnvs({
|
|
2224
3281
|
runId: request.runId,
|
|
2225
3282
|
stepIndex: request.stepIndex,
|
|
2226
|
-
resultToken
|
|
3283
|
+
resultToken,
|
|
3284
|
+
isResume: opts?.isResume
|
|
2227
3285
|
})
|
|
2228
3286
|
};
|
|
2229
|
-
const
|
|
3287
|
+
const resultSentinel = stepResultLinePrefix(resultToken);
|
|
3288
|
+
const pauseSentinel = stepPauseLinePrefix(resultToken);
|
|
3289
|
+
const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
|
|
2230
3290
|
const makeLineSplitter = (sink, filterSentinel) => {
|
|
2231
3291
|
if (!sink)
|
|
2232
3292
|
return { onChunk: undefined, flush: () => {} };
|
|
@@ -2239,7 +3299,7 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2239
3299
|
`)) !== -1) {
|
|
2240
3300
|
const line = buf.slice(0, nl);
|
|
2241
3301
|
buf = buf.slice(nl + 1);
|
|
2242
|
-
if (filterSentinel && line
|
|
3302
|
+
if (filterSentinel && isSentinel(line))
|
|
2243
3303
|
continue;
|
|
2244
3304
|
sink(line);
|
|
2245
3305
|
}
|
|
@@ -2249,7 +3309,7 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2249
3309
|
return;
|
|
2250
3310
|
const line = buf;
|
|
2251
3311
|
buf = "";
|
|
2252
|
-
if (filterSentinel && line
|
|
3312
|
+
if (filterSentinel && isSentinel(line))
|
|
2253
3313
|
return;
|
|
2254
3314
|
sink(line);
|
|
2255
3315
|
}
|
|
@@ -2269,11 +3329,21 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2269
3329
|
if (parsed)
|
|
2270
3330
|
return parsed;
|
|
2271
3331
|
if (result.exitCode !== 0) {
|
|
3332
|
+
const stderrTail = result.stderr.trim().slice(-2000);
|
|
3333
|
+
const stdoutTail = result.stdout.trim().slice(-2000);
|
|
3334
|
+
const details = [
|
|
3335
|
+
stderrTail ? `stderr:
|
|
3336
|
+
${stderrTail}` : "",
|
|
3337
|
+
stdoutTail ? `stdout:
|
|
3338
|
+
${stdoutTail}` : ""
|
|
3339
|
+
].filter(Boolean).join(`
|
|
3340
|
+
`);
|
|
2272
3341
|
return {
|
|
2273
3342
|
ok: false,
|
|
2274
3343
|
error: {
|
|
2275
3344
|
kind: "runner-exit",
|
|
2276
|
-
message: `runner subprocess exited ${result.exitCode} before emitting a step result
|
|
3345
|
+
message: `runner subprocess exited ${result.exitCode} before emitting a step result${details ? `
|
|
3346
|
+
${details}` : ""}`,
|
|
2277
3347
|
exitCode: result.exitCode
|
|
2278
3348
|
}
|
|
2279
3349
|
};
|
|
@@ -2285,10 +3355,8 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2285
3355
|
}
|
|
2286
3356
|
// src/step-invocation/server.ts
|
|
2287
3357
|
import { readFileSync } from "node:fs";
|
|
2288
|
-
function
|
|
3358
|
+
function writeSentinelLine(line) {
|
|
2289
3359
|
return new Promise((resolve, reject) => {
|
|
2290
|
-
const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
|
|
2291
|
-
`;
|
|
2292
3360
|
process.stdout.write(line, (err) => {
|
|
2293
3361
|
if (err)
|
|
2294
3362
|
reject(err);
|
|
@@ -2297,13 +3365,27 @@ function emitResult(token, payload) {
|
|
|
2297
3365
|
});
|
|
2298
3366
|
});
|
|
2299
3367
|
}
|
|
3368
|
+
function emitResult(token, payload) {
|
|
3369
|
+
return writeSentinelLine(`${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
|
|
3370
|
+
`);
|
|
3371
|
+
}
|
|
3372
|
+
function emitPause(token, signal) {
|
|
3373
|
+
const body = JSON.stringify({
|
|
3374
|
+
pauseId: signal.pauseId,
|
|
3375
|
+
pauseRequest: signal.pauseRequest,
|
|
3376
|
+
...signal.observability ? { observability: signal.observability } : {}
|
|
3377
|
+
});
|
|
3378
|
+
return writeSentinelLine(`${STEP_PAUSE_PREFIX}${token}:${body}
|
|
3379
|
+
`);
|
|
3380
|
+
}
|
|
2300
3381
|
function scrubProtocolEnvs() {
|
|
2301
3382
|
for (const key of [
|
|
2302
3383
|
STEP_ENV.STEP_MODE,
|
|
2303
3384
|
STEP_ENV.STEP_INDEX,
|
|
2304
3385
|
STEP_ENV.STEP_INPUT_PATH,
|
|
2305
3386
|
STEP_ENV.REQUEST_CONTEXT_PATH,
|
|
2306
|
-
STEP_ENV.STEP_RESULT_TOKEN
|
|
3387
|
+
STEP_ENV.STEP_RESULT_TOKEN,
|
|
3388
|
+
STEP_ENV.STEP_RESUME
|
|
2307
3389
|
]) {
|
|
2308
3390
|
delete process.env[key];
|
|
2309
3391
|
}
|
|
@@ -2317,12 +3399,14 @@ async function serveStep(handler) {
|
|
|
2317
3399
|
}
|
|
2318
3400
|
let exitCode = 0;
|
|
2319
3401
|
let payload;
|
|
3402
|
+
let pauseSignal;
|
|
2320
3403
|
let setupResult;
|
|
2321
3404
|
try {
|
|
2322
3405
|
const runId = process.env[STEP_ENV.RUN_ID];
|
|
2323
3406
|
const stepIndexEnv = process.env[STEP_ENV.STEP_INDEX];
|
|
2324
3407
|
const inputPath = process.env[STEP_ENV.STEP_INPUT_PATH];
|
|
2325
3408
|
const ctxPath = process.env[STEP_ENV.REQUEST_CONTEXT_PATH];
|
|
3409
|
+
const isResume = process.env[STEP_ENV.STEP_RESUME] === "1";
|
|
2326
3410
|
const stepIndex = stepIndexEnv === undefined ? Number.NaN : Number(stepIndexEnv);
|
|
2327
3411
|
if (!runId || !inputPath || !ctxPath || !Number.isFinite(stepIndex)) {
|
|
2328
3412
|
throw new Error(`step-server missing required env: ${[
|
|
@@ -2333,8 +3417,8 @@ async function serveStep(handler) {
|
|
|
2333
3417
|
].filter(Boolean).join(", ")}`);
|
|
2334
3418
|
}
|
|
2335
3419
|
const input = JSON.parse(readFileSync(inputPath, "utf8"));
|
|
2336
|
-
const requestContext = JSON.parse(readFileSync(ctxPath, "utf8"));
|
|
2337
|
-
setupResult = { runId, stepIndex, input, requestContext };
|
|
3420
|
+
const requestContext = RequestContextWireSchema.parse(JSON.parse(readFileSync(ctxPath, "utf8")));
|
|
3421
|
+
setupResult = { runId, stepIndex, isResume, input, requestContext };
|
|
2338
3422
|
} catch (err) {
|
|
2339
3423
|
exitCode = 1;
|
|
2340
3424
|
payload = { ok: false, kind: "protocol", error: err instanceof Error ? err.message : String(err) };
|
|
@@ -2345,32 +3429,76 @@ async function serveStep(handler) {
|
|
|
2345
3429
|
const { output, observability } = await handler(setupResult);
|
|
2346
3430
|
payload = observability ? { ok: true, output, observability } : { ok: true, output };
|
|
2347
3431
|
} catch (err) {
|
|
2348
|
-
|
|
2349
|
-
|
|
3432
|
+
if (isPauseSignal(err)) {
|
|
3433
|
+
pauseSignal = err;
|
|
3434
|
+
} else {
|
|
3435
|
+
exitCode = 1;
|
|
3436
|
+
payload = { ok: false, kind: "user-step", error: err instanceof Error ? err.message : String(err) };
|
|
3437
|
+
}
|
|
2350
3438
|
}
|
|
2351
3439
|
}
|
|
3440
|
+
if (pauseSignal) {
|
|
3441
|
+
await emitPause(token, pauseSignal);
|
|
3442
|
+
process.exit(0);
|
|
3443
|
+
}
|
|
2352
3444
|
await emitResult(token, payload);
|
|
2353
3445
|
process.exit(exitCode);
|
|
2354
3446
|
}
|
|
2355
|
-
// src/step-invocation/types.ts
|
|
2356
|
-
class StepExecutionError extends Error {
|
|
2357
|
-
kind;
|
|
2358
|
-
exitCode;
|
|
2359
|
-
constructor(error, stepLabel) {
|
|
2360
|
-
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
2361
|
-
this.name = "StepExecutionError";
|
|
2362
|
-
this.kind = error.kind;
|
|
2363
|
-
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
3447
|
// src/agent/run-agent.ts
|
|
2367
|
-
import { z as
|
|
3448
|
+
import { z as z11 } from "zod";
|
|
3449
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2368
3450
|
|
|
2369
3451
|
// src/agent/protocol-suffix.md
|
|
2370
3452
|
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';
|
|
2371
3453
|
|
|
2372
3454
|
// src/agent/run-agent.ts
|
|
3455
|
+
async function runInboxPoller(opts) {
|
|
3456
|
+
let afterSeq = 0;
|
|
3457
|
+
const headers = {
|
|
3458
|
+
authorization: `Bearer ${opts.token}`,
|
|
3459
|
+
accept: "application/json"
|
|
3460
|
+
};
|
|
3461
|
+
let backoffMs = 0;
|
|
3462
|
+
while (!opts.signal.aborted) {
|
|
3463
|
+
if (backoffMs > 0) {
|
|
3464
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
3465
|
+
if (opts.signal.aborted)
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}` + `/api/v1/internal/runs/${opts.runId}/agents/${encodeURIComponent(opts.agentId)}/inbox?afterSeq=${afterSeq}`;
|
|
3469
|
+
try {
|
|
3470
|
+
const res = await fetch(url, { method: "GET", headers, signal: opts.signal });
|
|
3471
|
+
if (!res.ok) {
|
|
3472
|
+
if (res.status === 401 || res.status === 403)
|
|
3473
|
+
return;
|
|
3474
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
backoffMs = 0;
|
|
3478
|
+
const body = await res.json();
|
|
3479
|
+
for (const m of body.messages ?? []) {
|
|
3480
|
+
opts.inbox.push({ text: m.body, senderName: m.senderName });
|
|
3481
|
+
if (m.seq > afterSeq)
|
|
3482
|
+
afterSeq = m.seq;
|
|
3483
|
+
}
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
if (opts.signal.aborted)
|
|
3486
|
+
return;
|
|
3487
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
3488
|
+
if (backoffMs >= 4000) {
|
|
3489
|
+
process.stderr.write(`[inbox-poller] poll failed, backing off ${backoffMs}ms: ${err instanceof Error ? err.message : String(err)}
|
|
3490
|
+
`);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
2373
3495
|
var PROTOCOL_SUFFIX = stripFrontmatter(protocol_suffix_default);
|
|
3496
|
+
var HITL_ASK_INSTRUCTION = [
|
|
3497
|
+
"## Asking a human",
|
|
3498
|
+
"",
|
|
3499
|
+
"If you hit a decision or need information only a human can provide, set " + '`"needs_input": true` in your `<status>` block, put your question in a ' + '`"question"` field, set `"exit_signal": false`, and stop. The workflow ' + "pauses and a human answers; their reply arrives as your next message. Use " + "this only when you genuinely cannot proceed on your own — prefer making a " + "reasonable decision and continuing."
|
|
3500
|
+
].join(`
|
|
3501
|
+
`);
|
|
2374
3502
|
function stripFrontmatter(content) {
|
|
2375
3503
|
if (!content.startsWith("---"))
|
|
2376
3504
|
return content;
|
|
@@ -2379,29 +3507,29 @@ function stripFrontmatter(content) {
|
|
|
2379
3507
|
return end === -1 ? content : content.slice(end + 4).trimStart();
|
|
2380
3508
|
}
|
|
2381
3509
|
function zodTypeName(schema) {
|
|
2382
|
-
if (schema instanceof
|
|
3510
|
+
if (schema instanceof z11.ZodString)
|
|
2383
3511
|
return "string";
|
|
2384
|
-
if (schema instanceof
|
|
3512
|
+
if (schema instanceof z11.ZodNumber)
|
|
2385
3513
|
return "number";
|
|
2386
|
-
if (schema instanceof
|
|
3514
|
+
if (schema instanceof z11.ZodBoolean)
|
|
2387
3515
|
return "boolean";
|
|
2388
|
-
if (schema instanceof
|
|
3516
|
+
if (schema instanceof z11.ZodArray)
|
|
2389
3517
|
return `${zodTypeName(schema.element)}[]`;
|
|
2390
|
-
if (schema instanceof
|
|
3518
|
+
if (schema instanceof z11.ZodEnum)
|
|
2391
3519
|
return schema.options.map((o) => `"${o}"`).join(" | ");
|
|
2392
|
-
if (schema instanceof
|
|
3520
|
+
if (schema instanceof z11.ZodOptional)
|
|
2393
3521
|
return `${zodTypeName(schema.unwrap())} (optional)`;
|
|
2394
|
-
if (schema instanceof
|
|
3522
|
+
if (schema instanceof z11.ZodNullable)
|
|
2395
3523
|
return `${zodTypeName(schema.unwrap())} | null`;
|
|
2396
|
-
if (schema instanceof
|
|
3524
|
+
if (schema instanceof z11.ZodLiteral)
|
|
2397
3525
|
return `"${String(schema.value)}"`;
|
|
2398
|
-
if (schema instanceof
|
|
3526
|
+
if (schema instanceof z11.ZodObject)
|
|
2399
3527
|
return "object";
|
|
2400
3528
|
return "any";
|
|
2401
3529
|
}
|
|
2402
3530
|
function buildResponseFormatAppendix(schema) {
|
|
2403
3531
|
const header = ["## Response Format", "", "When you emit `exit_signal: true` in your `<status>` block, also include a `<response>` block containing JSON immediately after `<status>`:", ""];
|
|
2404
|
-
const shape = schema instanceof
|
|
3532
|
+
const shape = schema instanceof z11.ZodObject ? schema.shape : null;
|
|
2405
3533
|
const body = shape ? (() => {
|
|
2406
3534
|
const entries = Object.entries(shape);
|
|
2407
3535
|
const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
|
|
@@ -2426,44 +3554,96 @@ function buildResponseFormatAppendix(schema) {
|
|
|
2426
3554
|
return [...header, ...body].join(`
|
|
2427
3555
|
`);
|
|
2428
3556
|
}
|
|
3557
|
+
function resolveAgentId(explicitId) {
|
|
3558
|
+
if (explicitId)
|
|
3559
|
+
return explicitId;
|
|
3560
|
+
const activeCall = nextAgentCallInActiveStep();
|
|
3561
|
+
if (activeCall === null)
|
|
3562
|
+
return randomUUID2();
|
|
3563
|
+
return `step${activeCall.stepIndex}-agent-${activeCall.callIndex}`;
|
|
3564
|
+
}
|
|
2429
3565
|
async function agent(opts) {
|
|
2430
3566
|
const workingDir = opts.workingDir ?? "";
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
3567
|
+
const agentId = resolveAgentId(opts.agentId);
|
|
3568
|
+
const callbackUrl = process.env.AGENT_COMPOSE_URL;
|
|
3569
|
+
const callbackToken = process.env.AGENT_COMPOSE_RUN_TOKEN;
|
|
3570
|
+
const runId = process.env.RUN_ID;
|
|
3571
|
+
const inbox = callbackUrl && callbackToken && runId ? new AsyncQueue : undefined;
|
|
3572
|
+
const inboxAbort = new AbortController;
|
|
3573
|
+
if (inbox && callbackUrl && callbackToken && runId) {
|
|
3574
|
+
runInboxPoller({
|
|
3575
|
+
baseUrl: callbackUrl,
|
|
3576
|
+
token: callbackToken,
|
|
3577
|
+
runId,
|
|
3578
|
+
agentId,
|
|
3579
|
+
inbox,
|
|
3580
|
+
signal: inboxAbort.signal
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
if (callbackUrl && callbackToken && runId) {
|
|
3584
|
+
runControlPoller({
|
|
3585
|
+
baseUrl: callbackUrl,
|
|
3586
|
+
token: callbackToken,
|
|
3587
|
+
runId,
|
|
3588
|
+
agentId,
|
|
3589
|
+
signal: inboxAbort.signal
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
const activeStepForPause = getActiveStep();
|
|
3593
|
+
const steerPause = activeStepForPause && runId ? function steerPauseFn(req, agentScope) {
|
|
3594
|
+
return corePause(req, {
|
|
3595
|
+
runId,
|
|
3596
|
+
stepIndex: activeStepForPause.stepIndex,
|
|
3597
|
+
agentScope
|
|
3598
|
+
});
|
|
3599
|
+
} : undefined;
|
|
3600
|
+
const mode = opts.mode ?? "auto";
|
|
3601
|
+
try {
|
|
3602
|
+
return await agentLoop({
|
|
3603
|
+
runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, runtimeOpts),
|
|
3604
|
+
agentId,
|
|
3605
|
+
...opts.label !== undefined ? { label: opts.label } : {},
|
|
3606
|
+
...opts.budget?.turnsPerIteration !== undefined ? { turnsPerIteration: opts.budget.turnsPerIteration } : {},
|
|
3607
|
+
...opts.budget?.maxIterations !== undefined ? { maxIterations: opts.budget.maxIterations } : {},
|
|
3608
|
+
...opts.tools !== undefined ? { allowedTools: opts.tools } : {},
|
|
3609
|
+
...opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {},
|
|
3610
|
+
...workingDir ? { cwd: workingDir } : {},
|
|
3611
|
+
buildPrompt: (_lastStatus, iteration) => {
|
|
3612
|
+
if (iteration > 0)
|
|
3613
|
+
return PROTOCOL_SUFFIX;
|
|
3614
|
+
const base = stripFrontmatter(opts.prompt);
|
|
3615
|
+
const parts = [base, PROTOCOL_SUFFIX];
|
|
3616
|
+
if (mode === "hitl")
|
|
3617
|
+
parts.push(HITL_ASK_INSTRUCTION);
|
|
3618
|
+
if (opts.responseSchema)
|
|
3619
|
+
parts.push(buildResponseFormatAppendix(opts.responseSchema));
|
|
3620
|
+
return parts.join(`
|
|
2449
3621
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
3622
|
+
`);
|
|
3623
|
+
},
|
|
3624
|
+
...opts.events ? { onAgentLifecycleEvent: (event) => {
|
|
3625
|
+
opts.events?.emit(event);
|
|
3626
|
+
} } : {},
|
|
3627
|
+
...opts.onAgentEvent ? { onAgentEvent: opts.onAgentEvent } : {},
|
|
3628
|
+
...opts.onIteration ? { onIteration: opts.onIteration } : {},
|
|
3629
|
+
...opts.processors?.length ? { processors: opts.processors } : {},
|
|
3630
|
+
requestContext: opts.requestContext ?? RequestContext.fromReserved({
|
|
3631
|
+
teamId: "",
|
|
3632
|
+
runId: "",
|
|
3633
|
+
workflowId: "",
|
|
3634
|
+
factoryId: null,
|
|
3635
|
+
apiKeyScopes: [],
|
|
3636
|
+
parentRunId: null
|
|
3637
|
+
}),
|
|
3638
|
+
...inbox ? { inbox } : {},
|
|
3639
|
+
mode,
|
|
3640
|
+
consumeSteerPending: () => consumeSteerPending(agentId),
|
|
3641
|
+
...steerPause ? { pause: steerPause } : {}
|
|
3642
|
+
});
|
|
3643
|
+
} finally {
|
|
3644
|
+
inbox?.close();
|
|
3645
|
+
inboxAbort.abort();
|
|
3646
|
+
}
|
|
2467
3647
|
}
|
|
2468
3648
|
// src/runtimes/openai-desktop.ts
|
|
2469
3649
|
import OpenAI from "openai";
|
|
@@ -2476,6 +3656,8 @@ var MAX_TURNS = 40;
|
|
|
2476
3656
|
|
|
2477
3657
|
class OpenAIDesktopRunner {
|
|
2478
3658
|
sandbox;
|
|
3659
|
+
kind = "openai-desktop";
|
|
3660
|
+
model = "computer-use-preview";
|
|
2479
3661
|
openai;
|
|
2480
3662
|
label;
|
|
2481
3663
|
constructor(sandbox, opts) {
|