la-machina-engine 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +483 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -30
- package/dist/index.d.ts +84 -30
- package/dist/index.js +483 -109
- package/dist/index.js.map +1 -1
- package/dist/node-mcp.cjs +1 -1
- package/dist/node-mcp.cjs.map +1 -1
- package/dist/node-mcp.d.cts +8 -3
- package/dist/node-mcp.d.ts +8 -3
- package/dist/node-mcp.js +1 -1
- package/dist/node-mcp.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2291,39 +2291,7 @@ var AISdkAdapter = class {
|
|
|
2291
2291
|
...request.toolChoice === "required" ? { toolChoice: "required" } : {},
|
|
2292
2292
|
maxRetries: this.options.maxRetries ?? 2
|
|
2293
2293
|
});
|
|
2294
|
-
|
|
2295
|
-
switch (event.type) {
|
|
2296
|
-
case "text-delta":
|
|
2297
|
-
yield {
|
|
2298
|
-
type: "text",
|
|
2299
|
-
index: 0,
|
|
2300
|
-
text: event.text ?? event.textDelta ?? ""
|
|
2301
|
-
};
|
|
2302
|
-
break;
|
|
2303
|
-
case "tool-call":
|
|
2304
|
-
yield {
|
|
2305
|
-
type: "tool_use",
|
|
2306
|
-
index: 0,
|
|
2307
|
-
id: event.toolCallId ?? "",
|
|
2308
|
-
name: event.toolName ?? "",
|
|
2309
|
-
// AI SDK v4: tool args are in `event.input` (not `event.args`)
|
|
2310
|
-
input: event.input ?? event.args ?? {}
|
|
2311
|
-
};
|
|
2312
|
-
break;
|
|
2313
|
-
case "finish": {
|
|
2314
|
-
const usage = event.totalUsage ?? event.usage ?? {};
|
|
2315
|
-
yield {
|
|
2316
|
-
type: "message_stop",
|
|
2317
|
-
stopReason: mapFinishReason(event.finishReason),
|
|
2318
|
-
usage: {
|
|
2319
|
-
input: usage.inputTokens ?? usage.promptTokens ?? 0,
|
|
2320
|
-
output: usage.outputTokens ?? usage.completionTokens ?? 0
|
|
2321
|
-
}
|
|
2322
|
-
};
|
|
2323
|
-
break;
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2294
|
+
yield* normalizeAiSdkStream(result.fullStream);
|
|
2327
2295
|
}
|
|
2328
2296
|
async getModel() {
|
|
2329
2297
|
if (this.model !== null) return this.model;
|
|
@@ -2368,6 +2336,53 @@ var AISdkAdapter = class {
|
|
|
2368
2336
|
return this.model;
|
|
2369
2337
|
}
|
|
2370
2338
|
};
|
|
2339
|
+
async function* normalizeAiSdkStream(fullStream) {
|
|
2340
|
+
let sawFinish = false;
|
|
2341
|
+
for await (const event of fullStream) {
|
|
2342
|
+
switch (event.type) {
|
|
2343
|
+
case "text-delta":
|
|
2344
|
+
yield {
|
|
2345
|
+
type: "text",
|
|
2346
|
+
index: 0,
|
|
2347
|
+
text: event.text ?? event.textDelta ?? ""
|
|
2348
|
+
};
|
|
2349
|
+
break;
|
|
2350
|
+
case "tool-call":
|
|
2351
|
+
yield {
|
|
2352
|
+
type: "tool_use",
|
|
2353
|
+
index: 0,
|
|
2354
|
+
id: event.toolCallId ?? "",
|
|
2355
|
+
name: event.toolName ?? "",
|
|
2356
|
+
// AI SDK v4: tool args are in `event.input` (not `event.args`)
|
|
2357
|
+
input: event.input ?? event.args ?? {}
|
|
2358
|
+
};
|
|
2359
|
+
break;
|
|
2360
|
+
case "error": {
|
|
2361
|
+
const status = extractStatus(event.error);
|
|
2362
|
+
const message = errorMessage(event.error);
|
|
2363
|
+
throw new ApiError(status !== null ? `${status} ${message}` : message, status);
|
|
2364
|
+
}
|
|
2365
|
+
case "finish": {
|
|
2366
|
+
sawFinish = true;
|
|
2367
|
+
const usage = event.totalUsage ?? event.usage ?? {};
|
|
2368
|
+
yield {
|
|
2369
|
+
type: "message_stop",
|
|
2370
|
+
stopReason: mapFinishReason(event.finishReason),
|
|
2371
|
+
usage: {
|
|
2372
|
+
input: usage.inputTokens ?? usage.promptTokens ?? 0,
|
|
2373
|
+
output: usage.outputTokens ?? usage.completionTokens ?? 0
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
break;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (!sawFinish) {
|
|
2381
|
+
throw new StreamIncompleteError(
|
|
2382
|
+
"AI SDK stream ended without a finish event; assistant output is partial"
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2371
2386
|
function mapFinishReason(reason) {
|
|
2372
2387
|
switch (reason) {
|
|
2373
2388
|
case "stop":
|
|
@@ -2380,6 +2395,23 @@ function mapFinishReason(reason) {
|
|
|
2380
2395
|
return "end_turn";
|
|
2381
2396
|
}
|
|
2382
2397
|
}
|
|
2398
|
+
function errorMessage(err) {
|
|
2399
|
+
if (err instanceof Error) return err.message;
|
|
2400
|
+
if (typeof err === "string") return err;
|
|
2401
|
+
try {
|
|
2402
|
+
return JSON.stringify(err);
|
|
2403
|
+
} catch {
|
|
2404
|
+
return String(err);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
function extractStatus(err) {
|
|
2408
|
+
if (err !== null && typeof err === "object") {
|
|
2409
|
+
const o = err;
|
|
2410
|
+
const s = o.statusCode ?? o.status;
|
|
2411
|
+
if (typeof s === "number") return s;
|
|
2412
|
+
}
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2383
2415
|
|
|
2384
2416
|
// src/model/factory.ts
|
|
2385
2417
|
function createModelAdapter(config, options = {}) {
|
|
@@ -2858,7 +2890,28 @@ function normalizeMessages(messages) {
|
|
|
2858
2890
|
}
|
|
2859
2891
|
return ensureToolResultPairing(fixed);
|
|
2860
2892
|
}
|
|
2861
|
-
function ensureToolResultPairing(
|
|
2893
|
+
function ensureToolResultPairing(input) {
|
|
2894
|
+
const allToolUseIds = /* @__PURE__ */ new Set();
|
|
2895
|
+
for (const msg of input) {
|
|
2896
|
+
if (!Array.isArray(msg.content)) continue;
|
|
2897
|
+
for (const b of msg.content) {
|
|
2898
|
+
const block = b;
|
|
2899
|
+
if (block.type === "tool_use" && block.id) allToolUseIds.add(block.id);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
const isOrphanResult = (b) => {
|
|
2903
|
+
const x = b;
|
|
2904
|
+
return x.type === "tool_result" && x.tool_use_id !== void 0 && !allToolUseIds.has(x.tool_use_id);
|
|
2905
|
+
};
|
|
2906
|
+
const messages = [];
|
|
2907
|
+
for (const msg of input) {
|
|
2908
|
+
if (!Array.isArray(msg.content) || !msg.content.some(isOrphanResult)) {
|
|
2909
|
+
messages.push(msg);
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2912
|
+
const filtered = msg.content.filter((b) => !isOrphanResult(b));
|
|
2913
|
+
if (filtered.length > 0) messages.push({ ...msg, content: filtered });
|
|
2914
|
+
}
|
|
2862
2915
|
const pendingToolUseIds = /* @__PURE__ */ new Set();
|
|
2863
2916
|
for (const msg of messages) {
|
|
2864
2917
|
if (!Array.isArray(msg.content)) continue;
|
|
@@ -2899,15 +2952,45 @@ init_cjs_shims();
|
|
|
2899
2952
|
|
|
2900
2953
|
// src/compact/dropMiddle.ts
|
|
2901
2954
|
init_cjs_shims();
|
|
2955
|
+
|
|
2956
|
+
// src/compact/grouping.ts
|
|
2957
|
+
init_cjs_shims();
|
|
2958
|
+
function groupByRound(messages) {
|
|
2959
|
+
if (messages.length === 0) return [];
|
|
2960
|
+
const groups = [];
|
|
2961
|
+
let current = [];
|
|
2962
|
+
for (const msg of messages) {
|
|
2963
|
+
if (msg.role === "assistant" && current.length > 0) {
|
|
2964
|
+
groups.push(current);
|
|
2965
|
+
current = [];
|
|
2966
|
+
}
|
|
2967
|
+
current.push(msg);
|
|
2968
|
+
}
|
|
2969
|
+
if (current.length > 0) groups.push(current);
|
|
2970
|
+
return groups;
|
|
2971
|
+
}
|
|
2972
|
+
function trailingRounds(messages, minMessages) {
|
|
2973
|
+
if (messages.length === 0) return { tail: [], start: 0 };
|
|
2974
|
+
const groups = groupByRound(messages);
|
|
2975
|
+
let count = 0;
|
|
2976
|
+
let g = groups.length;
|
|
2977
|
+
while (g > 0 && count < minMessages) {
|
|
2978
|
+
g--;
|
|
2979
|
+
count += groups[g].length;
|
|
2980
|
+
}
|
|
2981
|
+
const tail = groups.slice(g).flat();
|
|
2982
|
+
return { tail, start: messages.length - tail.length };
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// src/compact/dropMiddle.ts
|
|
2902
2986
|
function dropMiddle(messages, keepLast) {
|
|
2903
2987
|
const firstUserIndex = messages.findIndex((m) => m.role === "user");
|
|
2904
2988
|
if (firstUserIndex === -1) {
|
|
2905
2989
|
return { messages: [...messages], strategy: "drop-middle", dropped: 0 };
|
|
2906
2990
|
}
|
|
2907
|
-
const
|
|
2908
|
-
const tail =
|
|
2909
|
-
const
|
|
2910
|
-
const droppedCount = Math.max(0, tailStart - middleStart);
|
|
2991
|
+
const body = messages.slice(firstUserIndex + 1);
|
|
2992
|
+
const { tail } = trailingRounds(body, keepLast);
|
|
2993
|
+
const droppedCount = body.length - tail.length;
|
|
2911
2994
|
if (droppedCount === 0) {
|
|
2912
2995
|
return { messages: [...messages], strategy: "drop-middle", dropped: 0 };
|
|
2913
2996
|
}
|
|
@@ -3047,15 +3130,16 @@ REMINDER: Do NOT call any tools. Respond with plain text only \u2014 an <analysi
|
|
|
3047
3130
|
}
|
|
3048
3131
|
|
|
3049
3132
|
// src/compact/summarize.ts
|
|
3133
|
+
var SUMMARY_TIMEOUT_MS = 6e4;
|
|
3050
3134
|
async function summarizeCompact(options) {
|
|
3051
|
-
const { messages, config, client, system } = options;
|
|
3135
|
+
const { messages, config, client, system, signal } = options;
|
|
3052
3136
|
const firstUserIndex = messages.findIndex((m) => m.role === "user");
|
|
3053
3137
|
if (firstUserIndex === -1) {
|
|
3054
3138
|
return { messages: [...messages], strategy: "summarize", dropped: 0 };
|
|
3055
3139
|
}
|
|
3056
|
-
const
|
|
3057
|
-
const tail =
|
|
3058
|
-
const middle =
|
|
3140
|
+
const body = messages.slice(firstUserIndex + 1);
|
|
3141
|
+
const { tail } = trailingRounds(body, config.keepLast);
|
|
3142
|
+
const middle = body.slice(0, body.length - tail.length);
|
|
3059
3143
|
const droppedCount = middle.length;
|
|
3060
3144
|
if (droppedCount === 0) {
|
|
3061
3145
|
return { messages: [...messages], strategy: "summarize", dropped: 0 };
|
|
@@ -3070,7 +3154,13 @@ async function summarizeCompact(options) {
|
|
|
3070
3154
|
];
|
|
3071
3155
|
let summaryText;
|
|
3072
3156
|
try {
|
|
3073
|
-
summaryText = await generateSummary(
|
|
3157
|
+
summaryText = await generateSummary(
|
|
3158
|
+
client,
|
|
3159
|
+
system,
|
|
3160
|
+
summaryMessages,
|
|
3161
|
+
config.summaryMaxTokens,
|
|
3162
|
+
signal
|
|
3163
|
+
);
|
|
3074
3164
|
} catch {
|
|
3075
3165
|
const marker = {
|
|
3076
3166
|
role: "user",
|
|
@@ -3095,13 +3185,16 @@ ${summaryText}`
|
|
|
3095
3185
|
summaryLength: summaryText.length
|
|
3096
3186
|
};
|
|
3097
3187
|
}
|
|
3098
|
-
async function generateSummary(client, system, messages, maxTokens) {
|
|
3188
|
+
async function generateSummary(client, system, messages, maxTokens, signal) {
|
|
3099
3189
|
const pieces = [];
|
|
3190
|
+
const timeoutSignal = AbortSignal.timeout(SUMMARY_TIMEOUT_MS);
|
|
3191
|
+
const abortSignal = signal !== void 0 ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
3100
3192
|
for await (const event of client.streamMessage({
|
|
3101
3193
|
messages,
|
|
3102
3194
|
system,
|
|
3103
3195
|
maxTokens,
|
|
3104
|
-
temperature: 0
|
|
3196
|
+
temperature: 0,
|
|
3197
|
+
abortSignal
|
|
3105
3198
|
})) {
|
|
3106
3199
|
if (event.type === "text") {
|
|
3107
3200
|
pieces.push(event.text);
|
|
@@ -3117,8 +3210,8 @@ async function generateSummary(client, system, messages, maxTokens) {
|
|
|
3117
3210
|
|
|
3118
3211
|
// src/compact/compactor.ts
|
|
3119
3212
|
async function compactIfNeeded(options) {
|
|
3120
|
-
const { messages, usage, contextLimit, config, client, system } = options;
|
|
3121
|
-
const used = usage.input + usage.output + (usage.cacheReadInput ?? 0) + (usage.cacheCreationInput ?? 0);
|
|
3213
|
+
const { messages, usage, contextLimit, config, client, system, signal, liveContextTokens } = options;
|
|
3214
|
+
const used = liveContextTokens !== void 0 && liveContextTokens > 0 ? liveContextTokens : usage.input + usage.output + (usage.cacheReadInput ?? 0) + (usage.cacheCreationInput ?? 0);
|
|
3122
3215
|
const ratio = used / contextLimit;
|
|
3123
3216
|
if (ratio <= config.threshold) {
|
|
3124
3217
|
if (config.microcompact && messages.length > config.keepLast + 4) {
|
|
@@ -3132,10 +3225,10 @@ async function compactIfNeeded(options) {
|
|
|
3132
3225
|
}
|
|
3133
3226
|
return { compacted: false, result: null, messages: [...messages] };
|
|
3134
3227
|
}
|
|
3135
|
-
const result = await executeStrategy(messages, config, client, system);
|
|
3136
|
-
return { compacted:
|
|
3228
|
+
const result = await executeStrategy(messages, config, client, system, signal);
|
|
3229
|
+
return { compacted: result.dropped > 0, result, messages: result.messages };
|
|
3137
3230
|
}
|
|
3138
|
-
async function executeStrategy(messages, config, client, system) {
|
|
3231
|
+
async function executeStrategy(messages, config, client, system, signal) {
|
|
3139
3232
|
let workingMessages = [...messages];
|
|
3140
3233
|
if (config.microcompact) {
|
|
3141
3234
|
const mcResult = microcompact({
|
|
@@ -3152,14 +3245,16 @@ async function executeStrategy(messages, config, client, system) {
|
|
|
3152
3245
|
messages: workingMessages,
|
|
3153
3246
|
config,
|
|
3154
3247
|
client,
|
|
3155
|
-
system
|
|
3248
|
+
system,
|
|
3249
|
+
...signal !== void 0 ? { signal } : {}
|
|
3156
3250
|
});
|
|
3157
3251
|
case "session-memory":
|
|
3158
3252
|
return summarizeCompact({
|
|
3159
3253
|
messages: workingMessages,
|
|
3160
3254
|
config,
|
|
3161
3255
|
client,
|
|
3162
|
-
system
|
|
3256
|
+
system,
|
|
3257
|
+
...signal !== void 0 ? { signal } : {}
|
|
3163
3258
|
});
|
|
3164
3259
|
case "auto":
|
|
3165
3260
|
default:
|
|
@@ -3168,7 +3263,8 @@ async function executeStrategy(messages, config, client, system) {
|
|
|
3168
3263
|
messages: workingMessages,
|
|
3169
3264
|
config,
|
|
3170
3265
|
client,
|
|
3171
|
-
system
|
|
3266
|
+
system,
|
|
3267
|
+
...signal !== void 0 ? { signal } : {}
|
|
3172
3268
|
});
|
|
3173
3269
|
} catch {
|
|
3174
3270
|
return dropMiddle(workingMessages, config.keepLast);
|
|
@@ -3333,7 +3429,8 @@ async function agentLoop(options) {
|
|
|
3333
3429
|
let recoveryCount = 0;
|
|
3334
3430
|
let maxTokensEscalated = false;
|
|
3335
3431
|
let escalatedMaxTokens;
|
|
3336
|
-
|
|
3432
|
+
const MAX_EMERGENCY_413 = 1;
|
|
3433
|
+
let emergency413Count = 0;
|
|
3337
3434
|
const MAX_API_RETRIES = 3;
|
|
3338
3435
|
const BASE_BACKOFF_MS = 1e3;
|
|
3339
3436
|
let apiRetryCount = 0;
|
|
@@ -3352,7 +3449,6 @@ async function agentLoop(options) {
|
|
|
3352
3449
|
}
|
|
3353
3450
|
};
|
|
3354
3451
|
for (; ; ) {
|
|
3355
|
-
compactedThisTurn = false;
|
|
3356
3452
|
const turnStartedAt = Date.now();
|
|
3357
3453
|
const turnStartTokens = { ...ctx.getTokensUsed() };
|
|
3358
3454
|
if (options.runSignal?.aborted === true) {
|
|
@@ -3403,7 +3499,7 @@ async function agentLoop(options) {
|
|
|
3403
3499
|
});
|
|
3404
3500
|
}
|
|
3405
3501
|
let messages = ctx.getMessages();
|
|
3406
|
-
const mightCompact = ctx.
|
|
3502
|
+
const mightCompact = ctx.getLastPromptTokens() > contextLimit * compactionConfig.threshold;
|
|
3407
3503
|
if (mightCompact) await fireProgress("compacting");
|
|
3408
3504
|
const compactResult = await compactIfNeeded({
|
|
3409
3505
|
messages,
|
|
@@ -3411,7 +3507,10 @@ async function agentLoop(options) {
|
|
|
3411
3507
|
contextLimit,
|
|
3412
3508
|
config: compactionConfig,
|
|
3413
3509
|
client,
|
|
3414
|
-
system
|
|
3510
|
+
system,
|
|
3511
|
+
liveContextTokens: ctx.getLastPromptTokens(),
|
|
3512
|
+
// WS-3b — live, not cumulative
|
|
3513
|
+
...options.runSignal !== void 0 ? { signal: options.runSignal } : {}
|
|
3415
3514
|
});
|
|
3416
3515
|
if (compactResult.compacted) {
|
|
3417
3516
|
messages = compactResult.messages;
|
|
@@ -3459,6 +3558,7 @@ async function agentLoop(options) {
|
|
|
3459
3558
|
const normalizedMessages = normalizeMessages(
|
|
3460
3559
|
messagesForApi
|
|
3461
3560
|
);
|
|
3561
|
+
const effectiveToolChoice = options.toolChoice === "required" && cumulativeToolCalls > 0 ? "auto" : options.toolChoice;
|
|
3462
3562
|
try {
|
|
3463
3563
|
for await (const event of client.streamMessage({
|
|
3464
3564
|
messages: normalizedMessages,
|
|
@@ -3466,7 +3566,7 @@ async function agentLoop(options) {
|
|
|
3466
3566
|
tools: anthropicTools,
|
|
3467
3567
|
...options.runSignal !== void 0 ? { abortSignal: options.runSignal } : {},
|
|
3468
3568
|
...escalatedMaxTokens !== void 0 ? { maxTokens: escalatedMaxTokens } : {},
|
|
3469
|
-
...
|
|
3569
|
+
...effectiveToolChoice !== void 0 ? { toolChoice: effectiveToolChoice } : {}
|
|
3470
3570
|
})) {
|
|
3471
3571
|
const handled = consumeEvent(event);
|
|
3472
3572
|
if (handled.text !== void 0) textBlocks.push(handled.text);
|
|
@@ -3481,8 +3581,17 @@ async function agentLoop(options) {
|
|
|
3481
3581
|
if (isAbortSignalAborted(options.runSignal)) {
|
|
3482
3582
|
return failed(new RunTimeoutError(options.runTimeoutMs ?? 0), transcript);
|
|
3483
3583
|
}
|
|
3484
|
-
if (isPromptTooLong(err)
|
|
3485
|
-
|
|
3584
|
+
if (isPromptTooLong(err)) {
|
|
3585
|
+
if (emergency413Count >= MAX_EMERGENCY_413) {
|
|
3586
|
+
return failed(
|
|
3587
|
+
new EngineError(
|
|
3588
|
+
"ERR_PROMPT_TOO_LONG",
|
|
3589
|
+
"Prompt exceeds the model context window even after emergency compaction"
|
|
3590
|
+
),
|
|
3591
|
+
transcript
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
emergency413Count++;
|
|
3486
3595
|
const emergency = await compactIfNeeded({
|
|
3487
3596
|
messages,
|
|
3488
3597
|
usage: ctx.getTokensUsed(),
|
|
@@ -3490,9 +3599,11 @@ async function agentLoop(options) {
|
|
|
3490
3599
|
// force below threshold
|
|
3491
3600
|
config: { ...compactionConfig, threshold: 0, keepLast: 4 },
|
|
3492
3601
|
client,
|
|
3493
|
-
system
|
|
3602
|
+
system,
|
|
3603
|
+
...options.runSignal !== void 0 ? { signal: options.runSignal } : {}
|
|
3494
3604
|
});
|
|
3495
|
-
|
|
3605
|
+
const madeProgress = emergency.compacted && emergency.messages.length < messages.length;
|
|
3606
|
+
if (madeProgress) {
|
|
3496
3607
|
await options.inspect?.appendEvent({
|
|
3497
3608
|
type: "compaction_413",
|
|
3498
3609
|
turn: ctx.getTurnCount(),
|
|
@@ -3508,6 +3619,13 @@ async function agentLoop(options) {
|
|
|
3508
3619
|
});
|
|
3509
3620
|
continue;
|
|
3510
3621
|
}
|
|
3622
|
+
return failed(
|
|
3623
|
+
new EngineError(
|
|
3624
|
+
"ERR_PROMPT_TOO_LONG",
|
|
3625
|
+
"Prompt too long and compaction could not reduce it"
|
|
3626
|
+
),
|
|
3627
|
+
transcript
|
|
3628
|
+
);
|
|
3511
3629
|
}
|
|
3512
3630
|
if (isRetryable(err) && apiRetryCount < MAX_API_RETRIES) {
|
|
3513
3631
|
apiRetryCount++;
|
|
@@ -3979,6 +4097,15 @@ var RunContext = class {
|
|
|
3979
4097
|
messages = [];
|
|
3980
4098
|
turnCount = 0;
|
|
3981
4099
|
tokensUsed = { input: 0, output: 0 };
|
|
4100
|
+
/**
|
|
4101
|
+
* Engine 056 / WS-3b — size of the most recent request's prompt
|
|
4102
|
+
* (`input + cacheReadInput` from the last `message_stop`). This is the
|
|
4103
|
+
* LIVE context size, used to decide compaction. `tokensUsed` is a
|
|
4104
|
+
* cumulative BILLING counter that grows ~quadratically over a long run
|
|
4105
|
+
* (each turn re-sends the whole context), so using it for the threshold
|
|
4106
|
+
* triggered compaction far too early and then re-summarized every turn.
|
|
4107
|
+
*/
|
|
4108
|
+
lastPromptTokens = 0;
|
|
3982
4109
|
lastUuid = null;
|
|
3983
4110
|
/**
|
|
3984
4111
|
* Plan 019 — names of tools whose capability-stub returned an
|
|
@@ -4133,6 +4260,13 @@ var RunContext = class {
|
|
|
4133
4260
|
getTokensUsed() {
|
|
4134
4261
|
return this.tokensUsed;
|
|
4135
4262
|
}
|
|
4263
|
+
/**
|
|
4264
|
+
* Engine 056 / WS-3b — live context size: `input + cacheReadInput` of
|
|
4265
|
+
* the most recent request. 0 before the first response of a run/resume.
|
|
4266
|
+
*/
|
|
4267
|
+
getLastPromptTokens() {
|
|
4268
|
+
return this.lastPromptTokens;
|
|
4269
|
+
}
|
|
4136
4270
|
/** Plan 019 — record that a capability-stubbed tool fired during this run. */
|
|
4137
4271
|
recordCapabilityMissing(toolName) {
|
|
4138
4272
|
this.capabilitiesMissing.add(toolName);
|
|
@@ -4187,6 +4321,7 @@ var RunContext = class {
|
|
|
4187
4321
|
if (usage.cacheReadInput !== void 0) {
|
|
4188
4322
|
this.tokensUsed.cacheReadInput = (this.tokensUsed.cacheReadInput ?? 0) + usage.cacheReadInput;
|
|
4189
4323
|
}
|
|
4324
|
+
this.lastPromptTokens = usage.input + (usage.cacheReadInput ?? 0);
|
|
4190
4325
|
}
|
|
4191
4326
|
// ---------- internal ----------
|
|
4192
4327
|
async writeEntry(entry) {
|
|
@@ -4511,13 +4646,18 @@ var TranscriptWriter = class {
|
|
|
4511
4646
|
await writeMeta(this.storage, this.logPath, this.meta);
|
|
4512
4647
|
}
|
|
4513
4648
|
}
|
|
4514
|
-
flushLog() {
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4649
|
+
async flushLog() {
|
|
4650
|
+
for (; ; ) {
|
|
4651
|
+
if (this.flushInFlight !== null) {
|
|
4652
|
+
await this.flushInFlight;
|
|
4653
|
+
continue;
|
|
4654
|
+
}
|
|
4655
|
+
if (this.buffer.length === 0) return;
|
|
4656
|
+
this.flushInFlight = this.doFlush().finally(() => {
|
|
4657
|
+
this.flushInFlight = null;
|
|
4658
|
+
});
|
|
4659
|
+
await this.flushInFlight;
|
|
4660
|
+
}
|
|
4521
4661
|
}
|
|
4522
4662
|
async setStatus(status) {
|
|
4523
4663
|
this.meta = {
|
|
@@ -4530,9 +4670,7 @@ var TranscriptWriter = class {
|
|
|
4530
4670
|
async close() {
|
|
4531
4671
|
if (this.closed) return;
|
|
4532
4672
|
this.clearIdleTimer();
|
|
4533
|
-
|
|
4534
|
-
await this.flushLog();
|
|
4535
|
-
}
|
|
4673
|
+
await this.flushLog();
|
|
4536
4674
|
this.closed = true;
|
|
4537
4675
|
}
|
|
4538
4676
|
// ---------- internals ----------
|
|
@@ -4589,8 +4727,16 @@ function formatShardName(index) {
|
|
|
4589
4727
|
async function loadWriterState(storage, logPath) {
|
|
4590
4728
|
const existing = await readMeta(storage, logPath);
|
|
4591
4729
|
if (existing === null) return null;
|
|
4592
|
-
|
|
4593
|
-
|
|
4730
|
+
let maxShard = existing.lastShardIndex ?? -1;
|
|
4731
|
+
try {
|
|
4732
|
+
const names = await storage.listDir(logPath);
|
|
4733
|
+
for (const name of names) {
|
|
4734
|
+
const m = /^(\d{6})\.jsonl$/.exec(name);
|
|
4735
|
+
if (m !== null) maxShard = Math.max(maxShard, parseInt(m[1], 10));
|
|
4736
|
+
}
|
|
4737
|
+
} catch {
|
|
4738
|
+
}
|
|
4739
|
+
return { nextShardIndex: maxShard + 1, meta: existing };
|
|
4594
4740
|
}
|
|
4595
4741
|
|
|
4596
4742
|
// src/subagent/runner.ts
|
|
@@ -7007,7 +7153,14 @@ var McpClient = class {
|
|
|
7007
7153
|
}
|
|
7008
7154
|
let tools;
|
|
7009
7155
|
try {
|
|
7010
|
-
const response = await
|
|
7156
|
+
const response = await withTimeout(
|
|
7157
|
+
client.listTools(),
|
|
7158
|
+
this.options.connectTimeoutMs,
|
|
7159
|
+
() => new McpConnectionError(
|
|
7160
|
+
this.serverName,
|
|
7161
|
+
`tools/list did not complete within ${this.options.connectTimeoutMs}ms`
|
|
7162
|
+
)
|
|
7163
|
+
);
|
|
7011
7164
|
tools = normalizeToolList(response, this.serverName);
|
|
7012
7165
|
} catch (err) {
|
|
7013
7166
|
await safeClose(client);
|
|
@@ -7016,6 +7169,9 @@ var McpClient = class {
|
|
|
7016
7169
|
this.sdkClient = client;
|
|
7017
7170
|
this.toolCache = tools;
|
|
7018
7171
|
this.connected = true;
|
|
7172
|
+
client.onclose = () => {
|
|
7173
|
+
this.connected = false;
|
|
7174
|
+
};
|
|
7019
7175
|
this.options.logger.info("mcp.connect ok", {
|
|
7020
7176
|
server: this.serverName,
|
|
7021
7177
|
type: this.options.config.type,
|
|
@@ -7054,12 +7210,22 @@ var McpClient = class {
|
|
|
7054
7210
|
}
|
|
7055
7211
|
async close() {
|
|
7056
7212
|
const client = this.sdkClient;
|
|
7213
|
+
const pid = this.childPid;
|
|
7057
7214
|
this.sdkClient = null;
|
|
7058
7215
|
this.connected = false;
|
|
7059
7216
|
this.toolCache = null;
|
|
7060
7217
|
this.childPid = null;
|
|
7061
7218
|
if (client !== null) {
|
|
7062
|
-
|
|
7219
|
+
try {
|
|
7220
|
+
await safeClose(client);
|
|
7221
|
+
} finally {
|
|
7222
|
+
if (pid !== null) {
|
|
7223
|
+
try {
|
|
7224
|
+
process.kill(pid, "SIGKILL");
|
|
7225
|
+
} catch {
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
}
|
|
7063
7229
|
}
|
|
7064
7230
|
}
|
|
7065
7231
|
/**
|
|
@@ -7681,7 +7847,6 @@ function buildPermissionPolicy(config) {
|
|
|
7681
7847
|
const rules = parseRules(config.rules);
|
|
7682
7848
|
return {
|
|
7683
7849
|
check: (toolName) => {
|
|
7684
|
-
if (isSafeTool(toolName)) return ALLOW;
|
|
7685
7850
|
for (const rule of rules) {
|
|
7686
7851
|
if (matchesRule(toolName, rule)) {
|
|
7687
7852
|
if (rule.action === "allow") return ALLOW;
|
|
@@ -7691,6 +7856,7 @@ function buildPermissionPolicy(config) {
|
|
|
7691
7856
|
};
|
|
7692
7857
|
}
|
|
7693
7858
|
}
|
|
7859
|
+
if (isSafeTool(toolName)) return ALLOW;
|
|
7694
7860
|
return {
|
|
7695
7861
|
allowed: false,
|
|
7696
7862
|
reason: `Permission denied: "${toolName}" \u2014 no matching rule (mode: rules, fall-closed)`
|
|
@@ -8091,7 +8257,7 @@ function getMcpSection(options) {
|
|
|
8091
8257
|
if (options.mcpTools.length === 0) return null;
|
|
8092
8258
|
const byServer = /* @__PURE__ */ new Map();
|
|
8093
8259
|
for (const tool of options.mcpTools) {
|
|
8094
|
-
const match = tool.name.match(/^mcp__(
|
|
8260
|
+
const match = tool.name.match(/^mcp__(.+?)__(.+)$/);
|
|
8095
8261
|
if (!match) continue;
|
|
8096
8262
|
const server = match[1];
|
|
8097
8263
|
const toolName = match[2];
|
|
@@ -8442,6 +8608,7 @@ init_contract();
|
|
|
8442
8608
|
var ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
8443
8609
|
var DEFAULT_MAX_BODY_BYTES = 256 * 1024;
|
|
8444
8610
|
var DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024;
|
|
8611
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
8445
8612
|
var DEFAULT_MAX_PAGES = 5;
|
|
8446
8613
|
var MAX_PAGES_HARD_CAP = 50;
|
|
8447
8614
|
var DEFAULT_MAX_ITEMS = 500;
|
|
@@ -8473,6 +8640,7 @@ function createApiCallTool(opts) {
|
|
|
8473
8640
|
}
|
|
8474
8641
|
const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
8475
8642
|
const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
8643
|
+
const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
8476
8644
|
const inputSchema19 = import_zod23.z.object({
|
|
8477
8645
|
service: import_zod23.z.enum(serviceNames),
|
|
8478
8646
|
method: import_zod23.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
@@ -8534,6 +8702,7 @@ function createApiCallTool(opts) {
|
|
|
8534
8702
|
input,
|
|
8535
8703
|
fetchFn,
|
|
8536
8704
|
maxResponseBytes,
|
|
8705
|
+
requestTimeoutMs,
|
|
8537
8706
|
env: opts.env,
|
|
8538
8707
|
resolveAuth: opts.resolveAuth,
|
|
8539
8708
|
resolveBaseUrl: opts.resolveBaseUrl,
|
|
@@ -8621,14 +8790,18 @@ function createApiCallTool(opts) {
|
|
|
8621
8790
|
...authHeaders
|
|
8622
8791
|
// wins last — model cannot override
|
|
8623
8792
|
},
|
|
8624
|
-
...bodyText !== void 0 ? { body: bodyText } : {}
|
|
8793
|
+
...bodyText !== void 0 ? { body: bodyText } : {},
|
|
8794
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
8795
|
+
// WS-5c
|
|
8625
8796
|
});
|
|
8626
8797
|
} catch (err) {
|
|
8627
8798
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8628
8799
|
return errResult(`network error: ${msg}`);
|
|
8629
8800
|
}
|
|
8801
|
+
const tooLarge = responseTooLarge(res);
|
|
8802
|
+
if (tooLarge !== null) return errResult(tooLarge);
|
|
8630
8803
|
const raw = await res.text();
|
|
8631
|
-
const content = raw
|
|
8804
|
+
const content = byteLength2(raw) > maxResponseBytes ? raw.slice(0, maxResponseBytes) + "\n\u2026[TRUNCATED]" : raw;
|
|
8632
8805
|
await invokeHook(opts.onResponse, {
|
|
8633
8806
|
service: svc.name,
|
|
8634
8807
|
method: input.method,
|
|
@@ -8648,7 +8821,21 @@ function createApiCallTool(opts) {
|
|
|
8648
8821
|
function errResult(msg) {
|
|
8649
8822
|
return { content: msg, isError: true };
|
|
8650
8823
|
}
|
|
8824
|
+
function hasDotDotSegment(path) {
|
|
8825
|
+
let decoded = path;
|
|
8826
|
+
for (let i = 0; i < 2; i++) {
|
|
8827
|
+
try {
|
|
8828
|
+
const next = decodeURIComponent(decoded);
|
|
8829
|
+
if (next === decoded) break;
|
|
8830
|
+
decoded = next;
|
|
8831
|
+
} catch {
|
|
8832
|
+
break;
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8835
|
+
return /(^|[/\\])\.\.([/\\]|$)/.test(decoded);
|
|
8836
|
+
}
|
|
8651
8837
|
function pathAllowed(path, allowed) {
|
|
8838
|
+
if (hasDotDotSegment(path)) return false;
|
|
8652
8839
|
if (!allowed || allowed.length === 0) return true;
|
|
8653
8840
|
for (const a of allowed) {
|
|
8654
8841
|
if (typeof a === "string") {
|
|
@@ -8680,6 +8867,25 @@ function resolveAllowedPaths(svc) {
|
|
|
8680
8867
|
}
|
|
8681
8868
|
return void 0;
|
|
8682
8869
|
}
|
|
8870
|
+
var RESPONSE_HARD_CAP_BYTES = 25 * 1024 * 1024;
|
|
8871
|
+
function resolveSameOriginLink(nextLink, base) {
|
|
8872
|
+
try {
|
|
8873
|
+
const resolved = new URL(nextLink, base);
|
|
8874
|
+
if (resolved.origin !== new URL(base).origin) return null;
|
|
8875
|
+
return resolved.toString();
|
|
8876
|
+
} catch {
|
|
8877
|
+
return null;
|
|
8878
|
+
}
|
|
8879
|
+
}
|
|
8880
|
+
function responseTooLarge(res) {
|
|
8881
|
+
const cl = res.headers.get("content-length");
|
|
8882
|
+
if (cl === null) return null;
|
|
8883
|
+
const declared = Number.parseInt(cl, 10);
|
|
8884
|
+
if (Number.isFinite(declared) && declared > RESPONSE_HARD_CAP_BYTES) {
|
|
8885
|
+
return `ERR_API_RESPONSE_TOO_LARGE: upstream declared ${declared} bytes (hard cap ${RESPONSE_HARD_CAP_BYTES})`;
|
|
8886
|
+
}
|
|
8887
|
+
return null;
|
|
8888
|
+
}
|
|
8683
8889
|
function byteLength2(s) {
|
|
8684
8890
|
return new TextEncoder().encode(s).byteLength;
|
|
8685
8891
|
}
|
|
@@ -8900,7 +9106,16 @@ async function executeAutoPaginated(args) {
|
|
|
8900
9106
|
pageQuery[p.request.cursorParam] = nextCursor;
|
|
8901
9107
|
}
|
|
8902
9108
|
} else if (p.mode === "link-header") {
|
|
8903
|
-
if (nextLink !== null)
|
|
9109
|
+
if (nextLink !== null) {
|
|
9110
|
+
const safe = resolveSameOriginLink(nextLink, effectiveBaseUrl);
|
|
9111
|
+
if (safe === null) break;
|
|
9112
|
+
try {
|
|
9113
|
+
if (!pathAllowed(new URL(safe).pathname, resolveAllowedPaths(svc))) break;
|
|
9114
|
+
} catch {
|
|
9115
|
+
break;
|
|
9116
|
+
}
|
|
9117
|
+
pageUrl = safe;
|
|
9118
|
+
}
|
|
8904
9119
|
}
|
|
8905
9120
|
const url = pageUrl ?? buildUrl(effectiveBaseUrl, input.path, pageQuery);
|
|
8906
9121
|
await invokeHook(args.onRequest, {
|
|
@@ -8917,7 +9132,9 @@ async function executeAutoPaginated(args) {
|
|
|
8917
9132
|
...svc.defaultHeaders ?? {},
|
|
8918
9133
|
...userHeaders,
|
|
8919
9134
|
...authHeaders
|
|
8920
|
-
}
|
|
9135
|
+
},
|
|
9136
|
+
signal: AbortSignal.timeout(args.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS)
|
|
9137
|
+
// WS-5c
|
|
8921
9138
|
});
|
|
8922
9139
|
} catch (err) {
|
|
8923
9140
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -8930,6 +9147,16 @@ async function executeAutoPaginated(args) {
|
|
|
8930
9147
|
});
|
|
8931
9148
|
}
|
|
8932
9149
|
lastStatus = res.status;
|
|
9150
|
+
const tooLargePage = responseTooLarge(res);
|
|
9151
|
+
if (tooLargePage !== null) {
|
|
9152
|
+
return paginationErr({
|
|
9153
|
+
code: "ERR_API_PAGINATION_PAGE_FAILED",
|
|
9154
|
+
message: `${input.method} ${input.path} page ${pagesFetched + 1}: ${tooLargePage}`,
|
|
9155
|
+
pagesFetched,
|
|
9156
|
+
itemsFetched: aggregated.length,
|
|
9157
|
+
partialItems: aggregated
|
|
9158
|
+
});
|
|
9159
|
+
}
|
|
8933
9160
|
const raw = await res.text();
|
|
8934
9161
|
const captured = raw.length > maxResponseBytes ? raw.slice(0, maxResponseBytes) + "\n\u2026[TRUNCATED]" : raw;
|
|
8935
9162
|
await invokeHook(args.onResponse, {
|
|
@@ -10658,7 +10885,15 @@ var R2BindingStorageAdapter = class {
|
|
|
10658
10885
|
var ENGINE_DATA_FOLDER = ".claude";
|
|
10659
10886
|
var WORKSPACES_FOLDER = "workspaces";
|
|
10660
10887
|
var KNOWLEDGE_FOLDER = "knowledge";
|
|
10888
|
+
function assertSafeWorkspaceId(workspaceId) {
|
|
10889
|
+
if (!/^[A-Za-z0-9._-]+$/.test(workspaceId) || workspaceId === "." || workspaceId === "..") {
|
|
10890
|
+
throw new StorageError(
|
|
10891
|
+
`Invalid storage.workspaceId "${workspaceId}": must be non-empty and match [A-Za-z0-9._-] with no path separators or "."/".." traversal`
|
|
10892
|
+
);
|
|
10893
|
+
}
|
|
10894
|
+
}
|
|
10661
10895
|
async function createEngineStorage(config, options = {}) {
|
|
10896
|
+
assertSafeWorkspaceId(config.workspaceId);
|
|
10662
10897
|
switch (config.provider) {
|
|
10663
10898
|
case "local":
|
|
10664
10899
|
return createLocalStorage(config, options);
|
|
@@ -10670,7 +10905,12 @@ async function createEngineStorage(config, options = {}) {
|
|
|
10670
10905
|
}
|
|
10671
10906
|
async function createLocalStorage(config, options) {
|
|
10672
10907
|
const path = await import("path");
|
|
10673
|
-
|
|
10908
|
+
let rootPath = config.rootPath;
|
|
10909
|
+
if (rootPath === "~" || rootPath.startsWith("~/")) {
|
|
10910
|
+
const os = await import("os");
|
|
10911
|
+
rootPath = path.join(os.homedir(), rootPath.slice(1));
|
|
10912
|
+
}
|
|
10913
|
+
const tenantRoot = path.join(rootPath, WORKSPACES_FOLDER, config.workspaceId);
|
|
10674
10914
|
const workspaceRoot = path.join(tenantRoot, ENGINE_DATA_FOLDER);
|
|
10675
10915
|
const out = { workspace: new LocalStorageAdapter(workspaceRoot) };
|
|
10676
10916
|
if (options.withKnowledge) {
|
|
@@ -11153,7 +11393,7 @@ function extractPausedData(pending) {
|
|
|
11153
11393
|
|
|
11154
11394
|
// src/engine/state.ts
|
|
11155
11395
|
init_cjs_shims();
|
|
11156
|
-
var RunStateManager = class {
|
|
11396
|
+
var RunStateManager = class _RunStateManager {
|
|
11157
11397
|
constructor(storage) {
|
|
11158
11398
|
this.storage = storage;
|
|
11159
11399
|
}
|
|
@@ -11161,9 +11401,24 @@ var RunStateManager = class {
|
|
|
11161
11401
|
path(runId, nodeId) {
|
|
11162
11402
|
return `projects/${runId}/nodes/${nodeId}/state.json`;
|
|
11163
11403
|
}
|
|
11404
|
+
/** A terminal status is final — once set it must never be overwritten. */
|
|
11405
|
+
static isTerminal(status) {
|
|
11406
|
+
return status === "done" || status === "failed" || status === "cancelled";
|
|
11407
|
+
}
|
|
11408
|
+
/**
|
|
11409
|
+
* Persist a full state. Returns the persisted state so mutators can
|
|
11410
|
+
* forward it. Deliberately NO read-before-write: `write` is on the hot
|
|
11411
|
+
* path (heartbeat + every async-timing patch), so it must stay a single
|
|
11412
|
+
* I/O — adding a read here measurably slowed the async lifecycle and
|
|
11413
|
+
* widened existing read-modify-write windows. Terminal-status
|
|
11414
|
+
* precedence (Engine 056 / WS-1c) lives in `finalize` instead — the
|
|
11415
|
+
* only path that writes a terminal status, and one that already reads
|
|
11416
|
+
* `current`, so the guard costs nothing extra there.
|
|
11417
|
+
*/
|
|
11164
11418
|
async write(state) {
|
|
11165
11419
|
const content = JSON.stringify(state, null, 2);
|
|
11166
11420
|
await this.storage.writeFile(this.path(state.runId, state.nodeId), content);
|
|
11421
|
+
return state;
|
|
11167
11422
|
}
|
|
11168
11423
|
async read(runId, nodeId) {
|
|
11169
11424
|
try {
|
|
@@ -11184,8 +11439,7 @@ var RunStateManager = class {
|
|
|
11184
11439
|
throw new Error(`RunStateManager.update: no state found for ${runId}/${nodeId}`);
|
|
11185
11440
|
}
|
|
11186
11441
|
const next = { ...current, ...patch };
|
|
11187
|
-
|
|
11188
|
-
return next;
|
|
11442
|
+
return this.write(next);
|
|
11189
11443
|
}
|
|
11190
11444
|
/**
|
|
11191
11445
|
* Merge async lifecycle timing fields into the durable state.
|
|
@@ -11201,8 +11455,7 @@ var RunStateManager = class {
|
|
|
11201
11455
|
...current,
|
|
11202
11456
|
asyncTiming: { ...current.asyncTiming ?? {}, ...patch }
|
|
11203
11457
|
};
|
|
11204
|
-
|
|
11205
|
-
return next;
|
|
11458
|
+
return this.write(next);
|
|
11206
11459
|
}
|
|
11207
11460
|
async appendManualWebhookRetry(runId, nodeId, row) {
|
|
11208
11461
|
const current = await this.read(runId, nodeId);
|
|
@@ -11216,8 +11469,7 @@ var RunStateManager = class {
|
|
|
11216
11469
|
...current,
|
|
11217
11470
|
manualWebhookRetries: [...retries, row]
|
|
11218
11471
|
};
|
|
11219
|
-
|
|
11220
|
-
return next;
|
|
11472
|
+
return this.write(next);
|
|
11221
11473
|
}
|
|
11222
11474
|
/**
|
|
11223
11475
|
* Update just the heartbeat + progress (cheap, called every turn).
|
|
@@ -11234,13 +11486,32 @@ var RunStateManager = class {
|
|
|
11234
11486
|
}
|
|
11235
11487
|
/**
|
|
11236
11488
|
* Mark terminal state with response. Used at end of run/resume.
|
|
11489
|
+
*
|
|
11490
|
+
* Engine 056 / WS-1c: create-or-overwrite — does NOT throw when prior
|
|
11491
|
+
* state is missing (a transient read failure in the start() catch path
|
|
11492
|
+
* must still be able to record a terminal state rather than leaving the
|
|
11493
|
+
* run stranded). When state exists its fields (webhook config, timing)
|
|
11494
|
+
* are merged; otherwise a minimal base is synthesized.
|
|
11495
|
+
*
|
|
11496
|
+
* Also sets the `webhookPending` outbox marker (WS-1b) in the same write
|
|
11497
|
+
* iff a webhook is configured and this status is a subscribed event.
|
|
11237
11498
|
*/
|
|
11238
11499
|
async finalize(runId, nodeId, response) {
|
|
11239
|
-
|
|
11240
|
-
|
|
11500
|
+
const status = response.status === "done" ? "done" : response.status === "paused" ? "paused" : "failed";
|
|
11501
|
+
const current = await this.read(runId, nodeId);
|
|
11502
|
+
if (current !== null && _RunStateManager.isTerminal(current.status) && current.status !== status) {
|
|
11503
|
+
return current;
|
|
11504
|
+
}
|
|
11505
|
+
const base = current ?? _RunStateManager.initial(runId, nodeId);
|
|
11506
|
+
const webhookPending = base.webhook !== void 0 && base.webhook.events.includes(status);
|
|
11507
|
+
const next = {
|
|
11508
|
+
...base,
|
|
11509
|
+
status,
|
|
11241
11510
|
lastHeartbeat: Date.now(),
|
|
11242
|
-
response
|
|
11243
|
-
|
|
11511
|
+
response,
|
|
11512
|
+
webhookPending
|
|
11513
|
+
};
|
|
11514
|
+
return this.write(next);
|
|
11244
11515
|
}
|
|
11245
11516
|
/**
|
|
11246
11517
|
* List all state files under a runId (one per node). Returns empty array
|
|
@@ -11263,6 +11534,14 @@ var RunStateManager = class {
|
|
|
11263
11534
|
/**
|
|
11264
11535
|
* Scan all state files and return those with stale heartbeats.
|
|
11265
11536
|
* Used by recoverOrphanedRuns().
|
|
11537
|
+
*
|
|
11538
|
+
* Engine 056 / WS-1a: includes stale `queued` as well as `running`. A
|
|
11539
|
+
* run can be stranded in `queued` when the initial `running` write (or
|
|
11540
|
+
* the scheduled background work) fails after `start()` returns — its
|
|
11541
|
+
* heartbeat never advances past creation, so a `queued` state older than
|
|
11542
|
+
* the threshold is orphaned and must be recoverable. `paused` is
|
|
11543
|
+
* intentionally excluded (human-in-the-loop waits are legitimately
|
|
11544
|
+
* long-lived; the orchestrator reconciles those separately).
|
|
11266
11545
|
*/
|
|
11267
11546
|
async findOrphaned(staleThresholdMs) {
|
|
11268
11547
|
const projectsRoot = "projects";
|
|
@@ -11273,7 +11552,8 @@ var RunStateManager = class {
|
|
|
11273
11552
|
for (const runId of runIds) {
|
|
11274
11553
|
const states = await this.scanRun(runId);
|
|
11275
11554
|
for (const state of states) {
|
|
11276
|
-
|
|
11555
|
+
const recoverable = state.status === "running" || state.status === "queued";
|
|
11556
|
+
if (recoverable && now - state.lastHeartbeat > staleThresholdMs) {
|
|
11277
11557
|
orphaned.push(state);
|
|
11278
11558
|
}
|
|
11279
11559
|
}
|
|
@@ -11340,8 +11620,12 @@ var NodeBackgroundExecutor = class {
|
|
|
11340
11620
|
// src/engine/webhook.ts
|
|
11341
11621
|
init_cjs_shims();
|
|
11342
11622
|
var RETRY_DELAYS_MS = [
|
|
11343
|
-
0
|
|
11344
|
-
// attempt 1: immediate
|
|
11623
|
+
0,
|
|
11624
|
+
// attempt 1: immediate
|
|
11625
|
+
300,
|
|
11626
|
+
// attempt 2: +300ms
|
|
11627
|
+
1200
|
|
11628
|
+
// attempt 3: +1.2s
|
|
11345
11629
|
];
|
|
11346
11630
|
var MAX_ATTEMPTS = RETRY_DELAYS_MS.length;
|
|
11347
11631
|
async function signPayload(secret, timestamp, body) {
|
|
@@ -11443,6 +11727,41 @@ var WebhookDispatcher = class {
|
|
|
11443
11727
|
};
|
|
11444
11728
|
|
|
11445
11729
|
// src/engine/engine.ts
|
|
11730
|
+
var HANDOFF_POST_TIMEOUT_MS = 3e4;
|
|
11731
|
+
function buildUnpairedSiblingResults(messages, pendingId) {
|
|
11732
|
+
let lastAssistant;
|
|
11733
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11734
|
+
if (messages[i]?.role === "assistant") {
|
|
11735
|
+
lastAssistant = messages[i];
|
|
11736
|
+
break;
|
|
11737
|
+
}
|
|
11738
|
+
}
|
|
11739
|
+
if (lastAssistant === void 0 || !Array.isArray(lastAssistant.content)) return [];
|
|
11740
|
+
const toolUseIds = [];
|
|
11741
|
+
for (const block of lastAssistant.content) {
|
|
11742
|
+
const b = block;
|
|
11743
|
+
if (b.type === "tool_use" && typeof b.id === "string") toolUseIds.push(b.id);
|
|
11744
|
+
}
|
|
11745
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
11746
|
+
for (const m of messages) {
|
|
11747
|
+
if (!Array.isArray(m.content)) continue;
|
|
11748
|
+
for (const block of m.content) {
|
|
11749
|
+
const b = block;
|
|
11750
|
+
if (b.type === "tool_result" && typeof b.tool_use_id === "string") resolved.add(b.tool_use_id);
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
const blocks = [];
|
|
11754
|
+
for (const id of toolUseIds) {
|
|
11755
|
+
if (id === pendingId || resolved.has(id)) continue;
|
|
11756
|
+
blocks.push({
|
|
11757
|
+
type: "tool_result",
|
|
11758
|
+
tool_use_id: id,
|
|
11759
|
+
content: "[not executed \u2014 the run paused on a sibling tool call in the same turn; this tool was never dispatched]",
|
|
11760
|
+
is_error: true
|
|
11761
|
+
});
|
|
11762
|
+
}
|
|
11763
|
+
return blocks;
|
|
11764
|
+
}
|
|
11446
11765
|
var Engine = class {
|
|
11447
11766
|
config;
|
|
11448
11767
|
internals;
|
|
@@ -11656,6 +11975,18 @@ var Engine = class {
|
|
|
11656
11975
|
logPath,
|
|
11657
11976
|
...capabilitiesMissing.length > 0 ? { capabilitiesMissing } : {}
|
|
11658
11977
|
});
|
|
11978
|
+
} catch (err) {
|
|
11979
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11980
|
+
await writer.setStatus("failed").catch(() => {
|
|
11981
|
+
});
|
|
11982
|
+
return {
|
|
11983
|
+
runId,
|
|
11984
|
+
status: "failed",
|
|
11985
|
+
data: null,
|
|
11986
|
+
meta: { nodeId: options.nodeId, durationMs: Date.now() - startTime },
|
|
11987
|
+
errors: [{ code: "ERR_RUN_FAILED", message }],
|
|
11988
|
+
timestamp: Date.now()
|
|
11989
|
+
};
|
|
11659
11990
|
} finally {
|
|
11660
11991
|
runTimeout.clear();
|
|
11661
11992
|
await writer.close();
|
|
@@ -11814,6 +12145,7 @@ var Engine = class {
|
|
|
11814
12145
|
});
|
|
11815
12146
|
if (snapshot.pendingToolCall) {
|
|
11816
12147
|
const pending = snapshot.pendingToolCall;
|
|
12148
|
+
const siblingBlocks = buildUnpairedSiblingResults(ctx.getMessages(), pending.toolUseId);
|
|
11817
12149
|
if (snapshot.pauseReason === "awaiting_tool_result") {
|
|
11818
12150
|
if (options.toolResult === void 0) {
|
|
11819
12151
|
throw new EngineError(
|
|
@@ -11827,14 +12159,33 @@ var Engine = class {
|
|
|
11827
12159
|
`toolResult.id "${options.toolResult.id}" does not match pending toolUseId "${pending.toolUseId}" \u2014 resume aborted`
|
|
11828
12160
|
);
|
|
11829
12161
|
}
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
12162
|
+
if (siblingBlocks.length === 0) {
|
|
12163
|
+
await ctx.addToolResult(
|
|
12164
|
+
pending.toolUseId,
|
|
12165
|
+
options.toolResult.content,
|
|
12166
|
+
options.toolResult.isError ?? false
|
|
12167
|
+
);
|
|
12168
|
+
} else {
|
|
12169
|
+
await ctx.addMixedUserMessage([
|
|
12170
|
+
{
|
|
12171
|
+
type: "tool_result",
|
|
12172
|
+
tool_use_id: pending.toolUseId,
|
|
12173
|
+
content: options.toolResult.content,
|
|
12174
|
+
...options.toolResult.isError === true ? { is_error: true } : {}
|
|
12175
|
+
},
|
|
12176
|
+
...siblingBlocks
|
|
12177
|
+
]);
|
|
12178
|
+
}
|
|
11835
12179
|
} else if (options.gateAnswer !== void 0) {
|
|
11836
12180
|
const answer = typeof options.gateAnswer === "string" ? options.gateAnswer : JSON.stringify(options.gateAnswer);
|
|
11837
|
-
|
|
12181
|
+
if (siblingBlocks.length === 0) {
|
|
12182
|
+
await ctx.addToolResult(pending.toolUseId, answer, false);
|
|
12183
|
+
} else {
|
|
12184
|
+
await ctx.addMixedUserMessage([
|
|
12185
|
+
{ type: "tool_result", tool_use_id: pending.toolUseId, content: answer },
|
|
12186
|
+
...siblingBlocks
|
|
12187
|
+
]);
|
|
12188
|
+
}
|
|
11838
12189
|
} else {
|
|
11839
12190
|
const inputJson = JSON.stringify(pending.input ?? {}, null, 2);
|
|
11840
12191
|
await ctx.addMixedUserMessage([
|
|
@@ -11843,6 +12194,7 @@ var Engine = class {
|
|
|
11843
12194
|
tool_use_id: pending.toolUseId,
|
|
11844
12195
|
content: `APPROVAL_GATE_RELEASED: the prior ${pending.toolName} call was paused for human approval and has now been approved. Retry is required.`
|
|
11845
12196
|
},
|
|
12197
|
+
...siblingBlocks,
|
|
11846
12198
|
{
|
|
11847
12199
|
type: "text",
|
|
11848
12200
|
text: `The human has approved the paused ${pending.toolName} tool call. You MUST now re-issue the EXACT same tool call to complete the work \u2014 do not change the arguments, do not answer in text, do not declare the task done. Approved arguments (copy verbatim):
|
|
@@ -11983,8 +12335,8 @@ ${inputJson}
|
|
|
11983
12335
|
backgroundStartedAt: Date.now()
|
|
11984
12336
|
});
|
|
11985
12337
|
if (signal.aborted) return;
|
|
11986
|
-
await stateManager.update(runId, options.nodeId, { status: "running" });
|
|
11987
12338
|
try {
|
|
12339
|
+
await stateManager.update(runId, options.nodeId, { status: "running" });
|
|
11988
12340
|
await this.recordAsyncTiming(stateManager, runId, options.nodeId, {
|
|
11989
12341
|
runCallStartedAt: Date.now()
|
|
11990
12342
|
});
|
|
@@ -12329,12 +12681,12 @@ ${inputJson}
|
|
|
12329
12681
|
completedAt: Date.now()
|
|
12330
12682
|
});
|
|
12331
12683
|
} catch (err) {
|
|
12332
|
-
const
|
|
12684
|
+
const errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
12333
12685
|
await this.recordManualWebhookRetry(stateManager, runId, targetNodeId, {
|
|
12334
12686
|
deliveryId,
|
|
12335
12687
|
startedAt: retryStartedAt,
|
|
12336
12688
|
completedAt: Date.now(),
|
|
12337
|
-
errorMessage
|
|
12689
|
+
errorMessage: errorMessage2
|
|
12338
12690
|
});
|
|
12339
12691
|
throw err;
|
|
12340
12692
|
}
|
|
@@ -12409,7 +12761,8 @@ ${inputJson}
|
|
|
12409
12761
|
body: JSON.stringify({
|
|
12410
12762
|
runId,
|
|
12411
12763
|
workspaceId: this.config.storage.workspaceId
|
|
12412
|
-
})
|
|
12764
|
+
}),
|
|
12765
|
+
signal: AbortSignal.timeout(HANDOFF_POST_TIMEOUT_MS)
|
|
12413
12766
|
});
|
|
12414
12767
|
if (!res.ok) {
|
|
12415
12768
|
return {
|
|
@@ -12457,11 +12810,29 @@ ${inputJson}
|
|
|
12457
12810
|
} catch {
|
|
12458
12811
|
}
|
|
12459
12812
|
}
|
|
12813
|
+
/**
|
|
12814
|
+
* Engine 056 / WS-1b: read state.json with a few retries. `read` maps
|
|
12815
|
+
* ALL errors (incl. transient storage failures) to `null`; at webhook
|
|
12816
|
+
* time the terminal state was just written by `finalize`, so a `null`
|
|
12817
|
+
* here is far more likely a transient read than a genuinely-absent
|
|
12818
|
+
* state. Without this, a single blip silently drops the webhook with no
|
|
12819
|
+
* trace — a stuck node. Returns `null` only if every attempt comes back
|
|
12820
|
+
* empty (then the run is left terminal + `webhookPending` for the
|
|
12821
|
+
* caller's reconciler).
|
|
12822
|
+
*/
|
|
12823
|
+
async readStateResilient(stateManager, runId, nodeId, attempts = 3) {
|
|
12824
|
+
for (let i = 0; i < attempts; i++) {
|
|
12825
|
+
const s = await stateManager.read(runId, nodeId);
|
|
12826
|
+
if (s !== null) return s;
|
|
12827
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 100));
|
|
12828
|
+
}
|
|
12829
|
+
return null;
|
|
12830
|
+
}
|
|
12460
12831
|
async maybeFireWebhook(stateManager, runId, nodeId, response) {
|
|
12461
12832
|
await this.recordAsyncTiming(stateManager, runId, nodeId, {
|
|
12462
12833
|
webhookCheckStartedAt: Date.now()
|
|
12463
12834
|
});
|
|
12464
|
-
const state = await
|
|
12835
|
+
const state = await this.readStateResilient(stateManager, runId, nodeId);
|
|
12465
12836
|
if (state === null || state.webhook === void 0) return;
|
|
12466
12837
|
const event = response.status === "done" ? "done" : response.status === "paused" ? "paused" : "failed";
|
|
12467
12838
|
if (!state.webhook.events.includes(event)) return;
|
|
@@ -12500,7 +12871,10 @@ ${inputJson}
|
|
|
12500
12871
|
...latest.webhook,
|
|
12501
12872
|
deliveries: [...latest.webhook.deliveries, result.delivery]
|
|
12502
12873
|
};
|
|
12503
|
-
await stateManager.update(runId, nodeId, {
|
|
12874
|
+
await stateManager.update(runId, nodeId, {
|
|
12875
|
+
webhook: updated,
|
|
12876
|
+
webhookPending: result.delivery.status !== "delivered"
|
|
12877
|
+
});
|
|
12504
12878
|
await this.recordAsyncTiming(stateManager, runId, nodeId, {
|
|
12505
12879
|
webhookStatePersistedAt: Date.now()
|
|
12506
12880
|
});
|