@agent-compose/sdk 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/active-step.d.ts +60 -0
- package/dist/agent/agent-loop-steer.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +46 -0
- package/dist/agent/async-queue.d.ts +29 -0
- package/dist/agent/protocol.d.ts +9 -1
- package/dist/agent/resolve-agent-id.test.d.ts +1 -0
- package/dist/agent/run-agent.d.ts +16 -3
- package/dist/agent/steer-control.d.ts +57 -0
- package/dist/agent/steer-control.test.d.ts +1 -0
- package/dist/client.d.ts +161 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1339 -157
- package/dist/pause/__tests__/agent-loop-checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/errors.test.d.ts +1 -0
- package/dist/pause/__tests__/manager.test.d.ts +1 -0
- package/dist/pause/__tests__/pause-core.test.d.ts +1 -0
- package/dist/pause/__tests__/state-dir.test.d.ts +1 -0
- package/dist/pause/__tests__/wrappers.test.d.ts +1 -0
- package/dist/pause/checkpoint.d.ts +28 -0
- package/dist/pause/errors.d.ts +52 -0
- package/dist/pause/manager.d.ts +63 -0
- package/dist/pause/pause-core.d.ts +101 -0
- package/dist/pause/state-dir.d.ts +80 -0
- package/dist/pause/wrappers.d.ts +41 -0
- package/dist/request-context/request-context.d.ts +12 -0
- package/dist/runtimes/claude.d.ts +6 -0
- package/dist/runtimes/openai-desktop.d.ts +2 -0
- package/dist/runtimes/openai-desktop.js +1336 -156
- package/dist/runtimes/vercel.d.ts +12 -0
- package/dist/runtimes/vercel.js +50 -7
- package/dist/runtimes/vercel.test.d.ts +1 -0
- package/dist/sse.d.ts +2 -3
- package/dist/step-invocation/index.d.ts +2 -2
- package/dist/step-invocation/invoker.d.ts +3 -0
- package/dist/step-invocation/protocol.d.ts +12 -0
- package/dist/step-invocation/server.d.ts +1 -0
- package/dist/step-invocation/types.d.ts +40 -5
- package/dist/types/events.d.ts +9 -0
- package/dist/types/execution-context.d.ts +25 -0
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/runtime.d.ts +55 -0
- package/dist/types/sandbox.d.ts +4 -4
- package/dist/utils/schemas.d.ts +2 -0
- package/dist/workflow-steps/__tests__/pause-wiring.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +2 -0
- package/dist/workflow-steps/observability.d.ts +43 -11
- package/dist/workflow-steps/run-callback.d.ts +39 -0
- package/dist/workflow-steps/runner.d.ts +8 -0
- package/package.json +1 -1
- package/src/active-step.ts +124 -0
- package/src/agent/agent-loop.ts +253 -19
- package/src/agent/async-queue.ts +61 -0
- package/src/agent/protocol.ts +12 -2
- package/src/agent/run-agent.ts +184 -8
- package/src/agent/steer-control.ts +125 -0
- package/src/client.ts +277 -0
- package/src/index.ts +18 -2
- package/src/pause/checkpoint.ts +44 -0
- package/src/pause/errors.ts +70 -0
- package/src/pause/manager.ts +177 -0
- package/src/pause/pause-core.ts +267 -0
- package/src/pause/state-dir.ts +262 -0
- package/src/pause/wrappers.ts +79 -0
- package/src/request-context/request-context.ts +17 -2
- package/src/runtimes/claude.ts +101 -6
- package/src/runtimes/openai-desktop.ts +11 -0
- package/src/runtimes/vercel.ts +26 -0
- package/src/sandbox.ts +39 -20
- package/src/sse.ts +8 -6
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +107 -29
- package/src/step-invocation/protocol.ts +16 -0
- package/src/step-invocation/server.ts +45 -12
- package/src/step-invocation/types.ts +43 -7
- package/src/tools/coding.ts +16 -5
- package/src/types/events.ts +9 -0
- package/src/types/execution-context.ts +25 -0
- package/src/types/protocol.ts +8 -0
- package/src/types/runtime.ts +52 -0
- package/src/types/sandbox.ts +8 -4
- package/src/types/workflow.ts +6 -1
- package/src/utils/bundler.ts +8 -3
- package/src/utils/schemas.ts +2 -0
- package/src/workflow-steps/index.ts +3 -0
- package/src/workflow-steps/observability.ts +84 -13
- package/src/workflow-steps/run-callback.ts +72 -0
- package/src/workflow-steps/runner.ts +70 -8
- package/dist/utils/discovery.d.ts +0 -2
- package/src/utils/discovery.ts +0 -4
|
@@ -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,27 +2469,28 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
1599
2469
|
sandboxId: sb.sandboxId,
|
|
1600
2470
|
commands: {
|
|
1601
2471
|
async run(cmd, opts) {
|
|
1602
|
-
const sudo = opts?.sudo === true;
|
|
1603
|
-
if (opts?.background) {
|
|
1604
|
-
sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts.cwd, env: mergeEnvs(opts.envs), detached: true, ...sudo ? { sudo: true } : {} }).catch((err) => console.error(`[sandbox] background command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1605
|
-
return { exitCode: 0, stdout: "" };
|
|
1606
|
-
}
|
|
1607
2472
|
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
1608
2473
|
let stdout = "";
|
|
1609
|
-
|
|
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 } : {} });
|
|
1610
2476
|
await pRetry(async (attempt) => {
|
|
1611
2477
|
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
2478
|
+
if (attempt > 1) {
|
|
2479
|
+
stdout = "";
|
|
2480
|
+
stderr = "";
|
|
2481
|
+
}
|
|
1612
2482
|
for await (const log of h.logs()) {
|
|
1613
2483
|
if (log.stream === "stdout") {
|
|
1614
2484
|
stdout += log.data;
|
|
1615
2485
|
opts?.onStdout?.(log.data);
|
|
1616
2486
|
} else {
|
|
2487
|
+
stderr += log.data;
|
|
1617
2488
|
opts?.onStderr?.(log.data);
|
|
1618
2489
|
}
|
|
1619
2490
|
}
|
|
1620
2491
|
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
1621
2492
|
const finished = await handle.wait();
|
|
1622
|
-
return { exitCode: finished.exitCode, stdout };
|
|
2493
|
+
return { exitCode: finished.exitCode, stdout, stderr };
|
|
1623
2494
|
}
|
|
1624
2495
|
},
|
|
1625
2496
|
files: {
|
|
@@ -1653,6 +2524,7 @@ function makeLocalSandboxProvider() {
|
|
|
1653
2524
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1654
2525
|
});
|
|
1655
2526
|
let stdout = "";
|
|
2527
|
+
let stderr = "";
|
|
1656
2528
|
proc.stdout?.setEncoding("utf8");
|
|
1657
2529
|
proc.stderr?.setEncoding("utf8");
|
|
1658
2530
|
proc.stdout?.on("data", (chunk) => {
|
|
@@ -1660,17 +2532,18 @@ function makeLocalSandboxProvider() {
|
|
|
1660
2532
|
opts?.onStdout?.(chunk);
|
|
1661
2533
|
});
|
|
1662
2534
|
proc.stderr?.on("data", (chunk) => {
|
|
2535
|
+
stderr += chunk;
|
|
1663
2536
|
opts?.onStderr?.(chunk);
|
|
1664
2537
|
});
|
|
1665
2538
|
proc.on("error", reject);
|
|
1666
|
-
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout }));
|
|
2539
|
+
proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
|
|
1667
2540
|
});
|
|
1668
2541
|
}
|
|
1669
2542
|
},
|
|
1670
2543
|
files: {
|
|
1671
2544
|
async write(path, content) {
|
|
1672
|
-
await
|
|
1673
|
-
await
|
|
2545
|
+
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
2546
|
+
await fs2.writeFile(path, content);
|
|
1674
2547
|
}
|
|
1675
2548
|
},
|
|
1676
2549
|
async kill() {}
|
|
@@ -1845,10 +2718,18 @@ async function killAllSandboxes(onError) {
|
|
|
1845
2718
|
}));
|
|
1846
2719
|
}
|
|
1847
2720
|
// src/workflow-steps/observability.ts
|
|
2721
|
+
var LIVE_EMIT_DRAIN_TIMEOUT_MS = 1500;
|
|
2722
|
+
|
|
1848
2723
|
class StepObservabilityCollector {
|
|
1849
2724
|
metadata = {};
|
|
1850
2725
|
events = [];
|
|
1851
2726
|
subSteps = [];
|
|
2727
|
+
liveEmitter;
|
|
2728
|
+
ackedSeqs = new Set;
|
|
2729
|
+
inFlight = new Set;
|
|
2730
|
+
constructor(opts = {}) {
|
|
2731
|
+
this.liveEmitter = opts.liveEmitter;
|
|
2732
|
+
}
|
|
1852
2733
|
setMetadata = async (data) => {
|
|
1853
2734
|
Object.assign(this.metadata, data);
|
|
1854
2735
|
};
|
|
@@ -1864,6 +2745,8 @@ class StepObservabilityCollector {
|
|
|
1864
2745
|
});
|
|
1865
2746
|
return result;
|
|
1866
2747
|
} catch (err) {
|
|
2748
|
+
if (isPauseSignal(err))
|
|
2749
|
+
throw err;
|
|
1867
2750
|
this.subSteps.push({
|
|
1868
2751
|
name,
|
|
1869
2752
|
startedAt,
|
|
@@ -1876,26 +2759,92 @@ class StepObservabilityCollector {
|
|
|
1876
2759
|
};
|
|
1877
2760
|
agentEvents = {
|
|
1878
2761
|
emit: (event) => {
|
|
2762
|
+
const seq = this.events.length;
|
|
1879
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
|
+
}
|
|
1880
2772
|
}
|
|
1881
2773
|
};
|
|
1882
|
-
|
|
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();
|
|
1883
2784
|
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
1884
|
-
const hasEvents = this.events.length > 0;
|
|
1885
2785
|
const hasSubSteps = this.subSteps.length > 0;
|
|
2786
|
+
const remainingEvents = this.events.filter((_, idx) => !this.ackedSeqs.has(idx));
|
|
2787
|
+
const hasEvents = remainingEvents.length > 0;
|
|
1886
2788
|
if (!hasMetadata && !hasEvents && !hasSubSteps)
|
|
1887
2789
|
return;
|
|
1888
2790
|
const result = {};
|
|
1889
2791
|
if (hasMetadata)
|
|
1890
2792
|
result.metadata = { ...this.metadata };
|
|
1891
2793
|
if (hasEvents)
|
|
1892
|
-
result.events =
|
|
2794
|
+
result.events = remainingEvents;
|
|
1893
2795
|
if (hasSubSteps)
|
|
1894
2796
|
result.subSteps = [...this.subSteps];
|
|
1895
2797
|
return result;
|
|
1896
2798
|
}
|
|
1897
2799
|
}
|
|
1898
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
|
+
|
|
1899
2848
|
// src/workflow-steps/runner.ts
|
|
1900
2849
|
class StepValidationError extends Error {
|
|
1901
2850
|
stepName;
|
|
@@ -1938,7 +2887,14 @@ async function runWorkflowSingleStep(opts) {
|
|
|
1938
2887
|
const parsedInput = step.input.safeParse(opts.input);
|
|
1939
2888
|
if (!parsedInput.success)
|
|
1940
2889
|
throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
1941
|
-
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
|
+
};
|
|
1942
2898
|
const stepCtx = {
|
|
1943
2899
|
input: parsedInput.data,
|
|
1944
2900
|
requestContext: opts.requestContext,
|
|
@@ -1951,13 +2907,30 @@ async function runWorkflowSingleStep(opts) {
|
|
|
1951
2907
|
}),
|
|
1952
2908
|
setMetadata: collector.setMetadata,
|
|
1953
2909
|
step: collector.step,
|
|
1954
|
-
agentEvents: collector.agentEvents
|
|
2910
|
+
agentEvents: collector.agentEvents,
|
|
2911
|
+
checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
|
|
2912
|
+
pause,
|
|
2913
|
+
...buildPauseWrappers(pauseWithKind)
|
|
1955
2914
|
};
|
|
1956
|
-
|
|
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
|
+
}
|
|
1957
2930
|
const parsedOutput = step.output.safeParse(output);
|
|
1958
2931
|
if (!parsedOutput.success)
|
|
1959
2932
|
throw new StepValidationError(step.name, "output", parsedOutput.error);
|
|
1960
|
-
const observability = collector.snapshot();
|
|
2933
|
+
const observability = await collector.snapshot();
|
|
1961
2934
|
return observability === undefined ? { output: parsedOutput.data } : { output: parsedOutput.data, observability };
|
|
1962
2935
|
}
|
|
1963
2936
|
async function runWorkflowSteps(opts) {
|
|
@@ -1991,7 +2964,14 @@ async function runWorkflowSteps(opts) {
|
|
|
1991
2964
|
await opts.onStepFailed?.(i, step.name, err, 0);
|
|
1992
2965
|
throw err;
|
|
1993
2966
|
}
|
|
1994
|
-
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
|
+
};
|
|
1995
2975
|
const stepCtx = {
|
|
1996
2976
|
input: parsedStepInput.data,
|
|
1997
2977
|
requestContext,
|
|
@@ -2002,16 +2982,23 @@ async function runWorkflowSteps(opts) {
|
|
|
2002
2982
|
invokeChild,
|
|
2003
2983
|
setMetadata: collector.setMetadata,
|
|
2004
2984
|
step: collector.step,
|
|
2005
|
-
agentEvents: collector.agentEvents
|
|
2985
|
+
agentEvents: collector.agentEvents,
|
|
2986
|
+
checkpoint: scopedCheckpoint(`step${i}`),
|
|
2987
|
+
pause,
|
|
2988
|
+
...buildPauseWrappers(pauseWithKind)
|
|
2006
2989
|
};
|
|
2007
2990
|
const startedAt = Date.now();
|
|
2008
2991
|
let output;
|
|
2009
2992
|
try {
|
|
2010
|
-
output = await step.run(stepCtx);
|
|
2993
|
+
output = await runWithActiveStep({ stepIndex: i }, () => Promise.resolve(step.run(stepCtx)));
|
|
2011
2994
|
} catch (err) {
|
|
2995
|
+
if (isPauseSignal(err)) {
|
|
2996
|
+
const observability3 = await collector.snapshot();
|
|
2997
|
+
throw new PauseSignal(err.pauseId, err.pauseRequest, observability3);
|
|
2998
|
+
}
|
|
2012
2999
|
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
2013
3000
|
const durationMs2 = Date.now() - startedAt;
|
|
2014
|
-
const observability2 = collector.snapshot();
|
|
3001
|
+
const observability2 = await collector.snapshot();
|
|
2015
3002
|
stepResults.push({
|
|
2016
3003
|
name: step.name,
|
|
2017
3004
|
status: "failed",
|
|
@@ -2026,7 +3013,7 @@ async function runWorkflowSteps(opts) {
|
|
|
2026
3013
|
if (!parsedOutput.success) {
|
|
2027
3014
|
const err = new StepValidationError(step.name, "output", parsedOutput.error);
|
|
2028
3015
|
const durationMs2 = Date.now() - startedAt;
|
|
2029
|
-
const observability2 = collector.snapshot();
|
|
3016
|
+
const observability2 = await collector.snapshot();
|
|
2030
3017
|
stepResults.push({
|
|
2031
3018
|
name: step.name,
|
|
2032
3019
|
status: "failed",
|
|
@@ -2039,7 +3026,7 @@ async function runWorkflowSteps(opts) {
|
|
|
2039
3026
|
}
|
|
2040
3027
|
const durationMs = Date.now() - startedAt;
|
|
2041
3028
|
current = parsedOutput.data;
|
|
2042
|
-
const observability = collector.snapshot();
|
|
3029
|
+
const observability = await collector.snapshot();
|
|
2043
3030
|
stepResults.push({
|
|
2044
3031
|
name: step.name,
|
|
2045
3032
|
status: "completed",
|
|
@@ -2164,9 +3151,13 @@ function defineStep(opts) {
|
|
|
2164
3151
|
}
|
|
2165
3152
|
// src/step-invocation/protocol.ts
|
|
2166
3153
|
var STEP_RESULT_PREFIX = "__AC_STEP_RESULT__";
|
|
3154
|
+
var STEP_PAUSE_PREFIX = "__AC_STEP_PAUSE__";
|
|
2167
3155
|
function stepResultLinePrefix(token) {
|
|
2168
3156
|
return `${STEP_RESULT_PREFIX}${token}:`;
|
|
2169
3157
|
}
|
|
3158
|
+
function stepPauseLinePrefix(token) {
|
|
3159
|
+
return `${STEP_PAUSE_PREFIX}${token}:`;
|
|
3160
|
+
}
|
|
2170
3161
|
var RUNNER_BUNDLE_PATH = "/tmp/runner.bundle.js";
|
|
2171
3162
|
var RUNNER_COMMAND = `node ${RUNNER_BUNDLE_PATH}`;
|
|
2172
3163
|
var STEP_ENV = {
|
|
@@ -2175,7 +3166,8 @@ var STEP_ENV = {
|
|
|
2175
3166
|
STEP_INDEX: "AC_STEP_INDEX",
|
|
2176
3167
|
STEP_INPUT_PATH: "AC_STEP_INPUT_PATH",
|
|
2177
3168
|
REQUEST_CONTEXT_PATH: "AC_REQUEST_CONTEXT_PATH",
|
|
2178
|
-
STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN"
|
|
3169
|
+
STEP_RESULT_TOKEN: "AC_STEP_RESULT_TOKEN",
|
|
3170
|
+
STEP_RESUME: "AC_STEP_RESUME"
|
|
2179
3171
|
};
|
|
2180
3172
|
function stepInputPath(stepIndex) {
|
|
2181
3173
|
return `/tmp/wf/step-input-${stepIndex}.json`;
|
|
@@ -2184,7 +3176,36 @@ function requestContextPath(stepIndex) {
|
|
|
2184
3176
|
return `/tmp/wf/request-context-${stepIndex}.json`;
|
|
2185
3177
|
}
|
|
2186
3178
|
// src/step-invocation/invoker.ts
|
|
2187
|
-
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
|
+
});
|
|
2188
3209
|
function buildStepEnvs(args) {
|
|
2189
3210
|
return {
|
|
2190
3211
|
[STEP_ENV.RUN_ID]: args.runId,
|
|
@@ -2192,30 +3213,64 @@ function buildStepEnvs(args) {
|
|
|
2192
3213
|
[STEP_ENV.STEP_INDEX]: String(args.stepIndex),
|
|
2193
3214
|
[STEP_ENV.STEP_INPUT_PATH]: stepInputPath(args.stepIndex),
|
|
2194
3215
|
[STEP_ENV.REQUEST_CONTEXT_PATH]: requestContextPath(args.stepIndex),
|
|
2195
|
-
[STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken
|
|
3216
|
+
[STEP_ENV.STEP_RESULT_TOKEN]: args.resultToken,
|
|
3217
|
+
[STEP_ENV.STEP_RESUME]: args.isResume ? "1" : "0"
|
|
2196
3218
|
};
|
|
2197
3219
|
}
|
|
2198
3220
|
function parseStepResult(stdout, resultToken) {
|
|
2199
|
-
const
|
|
2200
|
-
const
|
|
2201
|
-
|
|
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)
|
|
2202
3251
|
return null;
|
|
2203
3252
|
let parsed;
|
|
2204
3253
|
try {
|
|
2205
|
-
|
|
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;
|
|
2206
3261
|
} catch {
|
|
2207
3262
|
return { ok: false, error: { kind: "protocol", message: "step result JSON parse failed" } };
|
|
2208
3263
|
}
|
|
2209
3264
|
if (parsed.ok) {
|
|
2210
3265
|
return parsed.observability ? { ok: true, output: parsed.output, observability: parsed.observability } : { ok: true, output: parsed.output };
|
|
2211
3266
|
}
|
|
2212
|
-
const kind = parsed.kind
|
|
3267
|
+
const kind = parsed.kind;
|
|
2213
3268
|
const baseMessage = parsed.error ?? "step body failed without a message";
|
|
2214
|
-
const message =
|
|
3269
|
+
const message = baseMessage;
|
|
2215
3270
|
return { ok: false, error: { kind, message } };
|
|
2216
3271
|
}
|
|
2217
3272
|
async function invokeStep(sandbox, request, opts) {
|
|
2218
|
-
const resultToken =
|
|
3273
|
+
const resultToken = randomBytes2(16).toString("hex");
|
|
2219
3274
|
await Promise.all([
|
|
2220
3275
|
sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
|
|
2221
3276
|
sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
|
|
@@ -2225,10 +3280,13 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2225
3280
|
...buildStepEnvs({
|
|
2226
3281
|
runId: request.runId,
|
|
2227
3282
|
stepIndex: request.stepIndex,
|
|
2228
|
-
resultToken
|
|
3283
|
+
resultToken,
|
|
3284
|
+
isResume: opts?.isResume
|
|
2229
3285
|
})
|
|
2230
3286
|
};
|
|
2231
|
-
const
|
|
3287
|
+
const resultSentinel = stepResultLinePrefix(resultToken);
|
|
3288
|
+
const pauseSentinel = stepPauseLinePrefix(resultToken);
|
|
3289
|
+
const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
|
|
2232
3290
|
const makeLineSplitter = (sink, filterSentinel) => {
|
|
2233
3291
|
if (!sink)
|
|
2234
3292
|
return { onChunk: undefined, flush: () => {} };
|
|
@@ -2241,7 +3299,7 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2241
3299
|
`)) !== -1) {
|
|
2242
3300
|
const line = buf.slice(0, nl);
|
|
2243
3301
|
buf = buf.slice(nl + 1);
|
|
2244
|
-
if (filterSentinel && line
|
|
3302
|
+
if (filterSentinel && isSentinel(line))
|
|
2245
3303
|
continue;
|
|
2246
3304
|
sink(line);
|
|
2247
3305
|
}
|
|
@@ -2251,7 +3309,7 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2251
3309
|
return;
|
|
2252
3310
|
const line = buf;
|
|
2253
3311
|
buf = "";
|
|
2254
|
-
if (filterSentinel && line
|
|
3312
|
+
if (filterSentinel && isSentinel(line))
|
|
2255
3313
|
return;
|
|
2256
3314
|
sink(line);
|
|
2257
3315
|
}
|
|
@@ -2271,11 +3329,21 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2271
3329
|
if (parsed)
|
|
2272
3330
|
return parsed;
|
|
2273
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
|
+
`);
|
|
2274
3341
|
return {
|
|
2275
3342
|
ok: false,
|
|
2276
3343
|
error: {
|
|
2277
3344
|
kind: "runner-exit",
|
|
2278
|
-
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}` : ""}`,
|
|
2279
3347
|
exitCode: result.exitCode
|
|
2280
3348
|
}
|
|
2281
3349
|
};
|
|
@@ -2287,10 +3355,8 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
2287
3355
|
}
|
|
2288
3356
|
// src/step-invocation/server.ts
|
|
2289
3357
|
import { readFileSync } from "node:fs";
|
|
2290
|
-
function
|
|
3358
|
+
function writeSentinelLine(line) {
|
|
2291
3359
|
return new Promise((resolve, reject) => {
|
|
2292
|
-
const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
|
|
2293
|
-
`;
|
|
2294
3360
|
process.stdout.write(line, (err) => {
|
|
2295
3361
|
if (err)
|
|
2296
3362
|
reject(err);
|
|
@@ -2299,13 +3365,27 @@ function emitResult(token, payload) {
|
|
|
2299
3365
|
});
|
|
2300
3366
|
});
|
|
2301
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
|
+
}
|
|
2302
3381
|
function scrubProtocolEnvs() {
|
|
2303
3382
|
for (const key of [
|
|
2304
3383
|
STEP_ENV.STEP_MODE,
|
|
2305
3384
|
STEP_ENV.STEP_INDEX,
|
|
2306
3385
|
STEP_ENV.STEP_INPUT_PATH,
|
|
2307
3386
|
STEP_ENV.REQUEST_CONTEXT_PATH,
|
|
2308
|
-
STEP_ENV.STEP_RESULT_TOKEN
|
|
3387
|
+
STEP_ENV.STEP_RESULT_TOKEN,
|
|
3388
|
+
STEP_ENV.STEP_RESUME
|
|
2309
3389
|
]) {
|
|
2310
3390
|
delete process.env[key];
|
|
2311
3391
|
}
|
|
@@ -2319,12 +3399,14 @@ async function serveStep(handler) {
|
|
|
2319
3399
|
}
|
|
2320
3400
|
let exitCode = 0;
|
|
2321
3401
|
let payload;
|
|
3402
|
+
let pauseSignal;
|
|
2322
3403
|
let setupResult;
|
|
2323
3404
|
try {
|
|
2324
3405
|
const runId = process.env[STEP_ENV.RUN_ID];
|
|
2325
3406
|
const stepIndexEnv = process.env[STEP_ENV.STEP_INDEX];
|
|
2326
3407
|
const inputPath = process.env[STEP_ENV.STEP_INPUT_PATH];
|
|
2327
3408
|
const ctxPath = process.env[STEP_ENV.REQUEST_CONTEXT_PATH];
|
|
3409
|
+
const isResume = process.env[STEP_ENV.STEP_RESUME] === "1";
|
|
2328
3410
|
const stepIndex = stepIndexEnv === undefined ? Number.NaN : Number(stepIndexEnv);
|
|
2329
3411
|
if (!runId || !inputPath || !ctxPath || !Number.isFinite(stepIndex)) {
|
|
2330
3412
|
throw new Error(`step-server missing required env: ${[
|
|
@@ -2335,8 +3417,8 @@ async function serveStep(handler) {
|
|
|
2335
3417
|
].filter(Boolean).join(", ")}`);
|
|
2336
3418
|
}
|
|
2337
3419
|
const input = JSON.parse(readFileSync(inputPath, "utf8"));
|
|
2338
|
-
const requestContext = JSON.parse(readFileSync(ctxPath, "utf8"));
|
|
2339
|
-
setupResult = { runId, stepIndex, input, requestContext };
|
|
3420
|
+
const requestContext = RequestContextWireSchema.parse(JSON.parse(readFileSync(ctxPath, "utf8")));
|
|
3421
|
+
setupResult = { runId, stepIndex, isResume, input, requestContext };
|
|
2340
3422
|
} catch (err) {
|
|
2341
3423
|
exitCode = 1;
|
|
2342
3424
|
payload = { ok: false, kind: "protocol", error: err instanceof Error ? err.message : String(err) };
|
|
@@ -2347,32 +3429,76 @@ async function serveStep(handler) {
|
|
|
2347
3429
|
const { output, observability } = await handler(setupResult);
|
|
2348
3430
|
payload = observability ? { ok: true, output, observability } : { ok: true, output };
|
|
2349
3431
|
} catch (err) {
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
+
}
|
|
2352
3438
|
}
|
|
2353
3439
|
}
|
|
3440
|
+
if (pauseSignal) {
|
|
3441
|
+
await emitPause(token, pauseSignal);
|
|
3442
|
+
process.exit(0);
|
|
3443
|
+
}
|
|
2354
3444
|
await emitResult(token, payload);
|
|
2355
3445
|
process.exit(exitCode);
|
|
2356
3446
|
}
|
|
2357
|
-
// src/step-invocation/types.ts
|
|
2358
|
-
class StepExecutionError extends Error {
|
|
2359
|
-
kind;
|
|
2360
|
-
exitCode;
|
|
2361
|
-
constructor(error, stepLabel) {
|
|
2362
|
-
super(`[${error.kind}] ${stepLabel}: ${error.message}`);
|
|
2363
|
-
this.name = "StepExecutionError";
|
|
2364
|
-
this.kind = error.kind;
|
|
2365
|
-
this.exitCode = error.kind === "runner-exit" ? error.exitCode : undefined;
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
3447
|
// src/agent/run-agent.ts
|
|
2369
|
-
import { z as
|
|
3448
|
+
import { z as z11 } from "zod";
|
|
3449
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2370
3450
|
|
|
2371
3451
|
// src/agent/protocol-suffix.md
|
|
2372
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';
|
|
2373
3453
|
|
|
2374
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
|
+
}
|
|
2375
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
|
+
`);
|
|
2376
3502
|
function stripFrontmatter(content) {
|
|
2377
3503
|
if (!content.startsWith("---"))
|
|
2378
3504
|
return content;
|
|
@@ -2381,29 +3507,29 @@ function stripFrontmatter(content) {
|
|
|
2381
3507
|
return end === -1 ? content : content.slice(end + 4).trimStart();
|
|
2382
3508
|
}
|
|
2383
3509
|
function zodTypeName(schema) {
|
|
2384
|
-
if (schema instanceof
|
|
3510
|
+
if (schema instanceof z11.ZodString)
|
|
2385
3511
|
return "string";
|
|
2386
|
-
if (schema instanceof
|
|
3512
|
+
if (schema instanceof z11.ZodNumber)
|
|
2387
3513
|
return "number";
|
|
2388
|
-
if (schema instanceof
|
|
3514
|
+
if (schema instanceof z11.ZodBoolean)
|
|
2389
3515
|
return "boolean";
|
|
2390
|
-
if (schema instanceof
|
|
3516
|
+
if (schema instanceof z11.ZodArray)
|
|
2391
3517
|
return `${zodTypeName(schema.element)}[]`;
|
|
2392
|
-
if (schema instanceof
|
|
3518
|
+
if (schema instanceof z11.ZodEnum)
|
|
2393
3519
|
return schema.options.map((o) => `"${o}"`).join(" | ");
|
|
2394
|
-
if (schema instanceof
|
|
3520
|
+
if (schema instanceof z11.ZodOptional)
|
|
2395
3521
|
return `${zodTypeName(schema.unwrap())} (optional)`;
|
|
2396
|
-
if (schema instanceof
|
|
3522
|
+
if (schema instanceof z11.ZodNullable)
|
|
2397
3523
|
return `${zodTypeName(schema.unwrap())} | null`;
|
|
2398
|
-
if (schema instanceof
|
|
3524
|
+
if (schema instanceof z11.ZodLiteral)
|
|
2399
3525
|
return `"${String(schema.value)}"`;
|
|
2400
|
-
if (schema instanceof
|
|
3526
|
+
if (schema instanceof z11.ZodObject)
|
|
2401
3527
|
return "object";
|
|
2402
3528
|
return "any";
|
|
2403
3529
|
}
|
|
2404
3530
|
function buildResponseFormatAppendix(schema) {
|
|
2405
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>`:", ""];
|
|
2406
|
-
const shape = schema instanceof
|
|
3532
|
+
const shape = schema instanceof z11.ZodObject ? schema.shape : null;
|
|
2407
3533
|
const body = shape ? (() => {
|
|
2408
3534
|
const entries = Object.entries(shape);
|
|
2409
3535
|
const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
|
|
@@ -2428,44 +3554,96 @@ function buildResponseFormatAppendix(schema) {
|
|
|
2428
3554
|
return [...header, ...body].join(`
|
|
2429
3555
|
`);
|
|
2430
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
|
+
}
|
|
2431
3565
|
async function agent(opts) {
|
|
2432
3566
|
const workingDir = opts.workingDir ?? "";
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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(`
|
|
2451
3621
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
+
}
|
|
2469
3647
|
}
|
|
2470
3648
|
// src/runtimes/openai-desktop.ts
|
|
2471
3649
|
import OpenAI from "openai";
|
|
@@ -2478,6 +3656,8 @@ var MAX_TURNS = 40;
|
|
|
2478
3656
|
|
|
2479
3657
|
class OpenAIDesktopRunner {
|
|
2480
3658
|
sandbox;
|
|
3659
|
+
kind = "openai-desktop";
|
|
3660
|
+
model = "computer-use-preview";
|
|
2481
3661
|
openai;
|
|
2482
3662
|
label;
|
|
2483
3663
|
constructor(sandbox, opts) {
|