condukt 0.7.0 → 0.9.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/runtimes/copilot/copilot-adapter.d.ts.map +1 -1
- package/dist/runtimes/copilot/copilot-adapter.js +29 -7
- package/dist/runtimes/copilot/copilot-adapter.js.map +1 -1
- package/dist/runtimes/copilot/copilot-backend.d.ts +38 -1
- package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/index.d.ts +2 -0
- package/dist/runtimes/copilot/index.d.ts.map +1 -1
- package/dist/runtimes/copilot/index.js +4 -1
- package/dist/runtimes/copilot/index.js.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.d.ts +16 -8
- package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.js +208 -59
- package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
- package/dist/runtimes/copilot/subagents.d.ts +16 -0
- package/dist/runtimes/copilot/subagents.d.ts.map +1 -0
- package/dist/runtimes/copilot/subagents.js +37 -0
- package/dist/runtimes/copilot/subagents.js.map +1 -0
- package/dist/runtimes/copilot/subprocess-backend.d.ts +2 -2
- package/dist/runtimes/copilot/subprocess-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/subprocess-backend.js +4 -1
- package/dist/runtimes/copilot/subprocess-backend.js.map +1 -1
- package/dist/runtimes/mock/mock-runtime.d.ts +4 -4
- package/dist/runtimes/mock/mock-runtime.d.ts.map +1 -1
- package/dist/runtimes/mock/mock-runtime.js +20 -5
- package/dist/runtimes/mock/mock-runtime.js.map +1 -1
- package/dist/src/agent-node.d.ts +8 -1
- package/dist/src/agent-node.d.ts.map +1 -1
- package/dist/src/agent-node.js +31 -2
- package/dist/src/agent-node.js.map +1 -1
- package/dist/src/agent.d.ts +4 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +437 -230
- package/dist/src/agent.js.map +1 -1
- package/dist/src/events.d.ts +1 -0
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel-node.d.ts +8 -3
- package/dist/src/panel-node.d.ts.map +1 -1
- package/dist/src/panel-node.js +96 -62
- package/dist/src/panel-node.js.map +1 -1
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +79 -19
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/types.d.ts +88 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +12 -1
- package/dist/src/types.js.map +1 -1
- package/dist/state/reducer.js +2 -2
- package/dist/state/reducer.js.map +1 -1
- package/package.json +3 -2
package/dist/src/agent.js
CHANGED
|
@@ -44,6 +44,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
})();
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.wasCompletedBeforeCrash = wasCompletedBeforeCrash;
|
|
47
|
+
exports.isRetriableModelError = isRetriableModelError;
|
|
48
|
+
exports.retryDelayMs = retryDelayMs;
|
|
47
49
|
exports.agent = agent;
|
|
48
50
|
const fs = __importStar(require("node:fs"));
|
|
49
51
|
const path = __importStar(require("node:path"));
|
|
@@ -125,6 +127,331 @@ function formatPrompt(promptOutput) {
|
|
|
125
127
|
return `${promptOutput.system}\n\n${promptOutput.user}`;
|
|
126
128
|
}
|
|
127
129
|
// ---------------------------------------------------------------------------
|
|
130
|
+
// Model-call retry
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
const DEFAULT_MAX_ATTEMPTS = 1;
|
|
133
|
+
const DEFAULT_BACKOFF_BASE_MS = 5_000;
|
|
134
|
+
const DEFAULT_BACKOFF_MAX_MS = 120_000;
|
|
135
|
+
const TRANSIENT_ERROR_CODES = new Set([
|
|
136
|
+
'ECONNRESET', 'ECONNREFUSED', 'ECONNABORTED', 'EHOSTUNREACH',
|
|
137
|
+
'ENETDOWN', 'ENETRESET', 'ENETUNREACH', 'EPIPE', 'ETIMEDOUT',
|
|
138
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_SOCKET',
|
|
139
|
+
]);
|
|
140
|
+
function numericStatus(value) {
|
|
141
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
142
|
+
return value;
|
|
143
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
144
|
+
return undefined;
|
|
145
|
+
const parsed = Number(value);
|
|
146
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
147
|
+
}
|
|
148
|
+
function readRetryMetadata(error) {
|
|
149
|
+
let current = error;
|
|
150
|
+
let statusCode;
|
|
151
|
+
let errorCode;
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
154
|
+
seen.add(current);
|
|
155
|
+
const candidate = current;
|
|
156
|
+
statusCode ??= numericStatus(candidate.statusCode ?? candidate.status);
|
|
157
|
+
errorCode ??= candidate.errorCode ?? candidate.code;
|
|
158
|
+
current = candidate.cause;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...(statusCode !== undefined ? { statusCode } : {}),
|
|
162
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function errorChain(error) {
|
|
166
|
+
const chain = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
let current = error;
|
|
169
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
170
|
+
seen.add(current);
|
|
171
|
+
chain.push(current);
|
|
172
|
+
current = current.cause;
|
|
173
|
+
}
|
|
174
|
+
return chain;
|
|
175
|
+
}
|
|
176
|
+
function hasPermanentMarker(message) {
|
|
177
|
+
return /\b(?:auth(?:entication|orization)?|unauthori[sz]ed|forbidden|permission denied|invalid request|unprocessable|abort(?:ed)?|cancel(?:led|ed)?|cancellation)\b/.test(message);
|
|
178
|
+
}
|
|
179
|
+
function hasTransientTimeoutMarker(message) {
|
|
180
|
+
return /heartbeat(?: timeout)?|idle(?: stall| timeout)|no output for \d+(?:\.\d+)?s|session timed out|request timed out|connect(?:ion)? timeout|socket timeout|etimedout/.test(message);
|
|
181
|
+
}
|
|
182
|
+
/** Default fail-closed classifier for model/session failures. */
|
|
183
|
+
function isRetriableModelError(error, meta) {
|
|
184
|
+
if (error instanceof types_1.FlowAbortedError)
|
|
185
|
+
return false;
|
|
186
|
+
const messages = errorChain(error)
|
|
187
|
+
.map((entry) => `${entry.name} ${entry.message}`.toLowerCase());
|
|
188
|
+
if (messages.some(hasPermanentMarker))
|
|
189
|
+
return false;
|
|
190
|
+
if (meta.statusCode === 400 || meta.statusCode === 401
|
|
191
|
+
|| meta.statusCode === 403 || meta.statusCode === 422) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (meta.statusCode === 429
|
|
195
|
+
|| (meta.statusCode !== undefined && meta.statusCode >= 500 && meta.statusCode <= 599)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
const code = meta.errorCode?.toUpperCase();
|
|
199
|
+
if (code && TRANSIENT_ERROR_CODES.has(code))
|
|
200
|
+
return true;
|
|
201
|
+
return messages.some(hasTransientTimeoutMarker);
|
|
202
|
+
}
|
|
203
|
+
function positiveInteger(value, fallback) {
|
|
204
|
+
if (value === undefined || !Number.isFinite(value))
|
|
205
|
+
return fallback;
|
|
206
|
+
return Math.max(1, Math.trunc(value));
|
|
207
|
+
}
|
|
208
|
+
function nonNegativeFinite(value, fallback) {
|
|
209
|
+
if (value === undefined || !Number.isFinite(value))
|
|
210
|
+
return fallback;
|
|
211
|
+
return Math.max(0, value);
|
|
212
|
+
}
|
|
213
|
+
function retryDelayMs(policy, failedAttempt) {
|
|
214
|
+
const configuredBase = policy.backoffBaseMs;
|
|
215
|
+
const base = configuredBase !== undefined
|
|
216
|
+
&& Number.isFinite(configuredBase)
|
|
217
|
+
&& configuredBase > 0
|
|
218
|
+
? configuredBase
|
|
219
|
+
: DEFAULT_BACKOFF_BASE_MS;
|
|
220
|
+
const configuredMax = policy.backoffMaxMs;
|
|
221
|
+
const max = configuredMax !== undefined
|
|
222
|
+
&& Number.isFinite(configuredMax)
|
|
223
|
+
&& configuredMax > 0
|
|
224
|
+
? configuredMax
|
|
225
|
+
: DEFAULT_BACKOFF_MAX_MS;
|
|
226
|
+
const exponent = Math.min(52, Math.max(0, failedAttempt - 1));
|
|
227
|
+
const exponential = Math.min(max, base * (2 ** exponent));
|
|
228
|
+
const jittered = policy.jitter === false ? exponential : Math.random() * exponential;
|
|
229
|
+
return Math.max(100, Number.isFinite(jittered) ? jittered : 100);
|
|
230
|
+
}
|
|
231
|
+
async function waitForRetry(delayMs, signal) {
|
|
232
|
+
if (signal.aborted)
|
|
233
|
+
throw new types_1.FlowAbortedError('Aborted before retry');
|
|
234
|
+
if (delayMs <= 0)
|
|
235
|
+
return;
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
let timer;
|
|
238
|
+
const onAbort = () => {
|
|
239
|
+
if (timer !== undefined)
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
reject(new types_1.FlowAbortedError('Aborted during retry backoff'));
|
|
242
|
+
};
|
|
243
|
+
timer = setTimeout(() => {
|
|
244
|
+
signal.removeEventListener('abort', onAbort);
|
|
245
|
+
resolve();
|
|
246
|
+
}, delayMs);
|
|
247
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function sessionConfig(config, input, ctx) {
|
|
251
|
+
const sessionCwd = config.cwdResolver ? config.cwdResolver(input) : input.dir;
|
|
252
|
+
return {
|
|
253
|
+
model: config.model ?? 'claude-opus-4.6',
|
|
254
|
+
thinkingBudget: config.thinkingBudget,
|
|
255
|
+
cwd: sessionCwd,
|
|
256
|
+
addDirs: config.isolation ? [] : [input.dir],
|
|
257
|
+
timeout: config.timeout ?? 3600,
|
|
258
|
+
heartbeatTimeout: config.heartbeatTimeout ?? 120,
|
|
259
|
+
systemMessage: config.systemMessage,
|
|
260
|
+
availableTools: config.availableTools ?? (config.tools && config.tools.length > 0
|
|
261
|
+
? config.tools.map((tool) => tool.id)
|
|
262
|
+
: undefined),
|
|
263
|
+
excludedTools: config.excludedTools,
|
|
264
|
+
customAgents: config.customAgents,
|
|
265
|
+
subagentRoster: config.subagentRoster,
|
|
266
|
+
defaultAgent: config.defaultAgent,
|
|
267
|
+
excludedBuiltinAgents: config.excludedBuiltinAgents,
|
|
268
|
+
nodeId: ctx.nodeId,
|
|
269
|
+
memberId: config.memberId,
|
|
270
|
+
artifactFilename: config.output,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function attemptUsage(usage, subagentUsage) {
|
|
274
|
+
return { usage, subagentUsage: [...subagentUsage] };
|
|
275
|
+
}
|
|
276
|
+
function withAttemptUsage(error, usage) {
|
|
277
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
278
|
+
Object.assign(normalized, { attemptUsage: usage });
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
function withNodeUsage(error, attemptUsage, subagentUsage) {
|
|
282
|
+
if (attemptUsage.length === 0 && subagentUsage.length === 0)
|
|
283
|
+
return error;
|
|
284
|
+
Object.assign(error, {
|
|
285
|
+
nodeUsage: {
|
|
286
|
+
attemptUsage: [...attemptUsage],
|
|
287
|
+
subagentUsage: [...subagentUsage],
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
return error;
|
|
291
|
+
}
|
|
292
|
+
async function runSessionAttempt(config, input, ctx, prompt) {
|
|
293
|
+
let session = null;
|
|
294
|
+
const outputLines = [];
|
|
295
|
+
let lastUsage;
|
|
296
|
+
const subagentUsage = [];
|
|
297
|
+
try {
|
|
298
|
+
const creation = ctx.runtime.createSession(sessionConfig(config, input, ctx), { signal: ctx.signal });
|
|
299
|
+
const createdSession = await new Promise((resolve, reject) => {
|
|
300
|
+
const onAbort = () => reject(new types_1.FlowAbortedError('Aborted during session creation'));
|
|
301
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
302
|
+
creation.then((created) => {
|
|
303
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
304
|
+
if (ctx.signal.aborted) {
|
|
305
|
+
void created.abort();
|
|
306
|
+
reject(new types_1.FlowAbortedError('Aborted during session creation'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
resolve(created);
|
|
310
|
+
}, (error) => {
|
|
311
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
312
|
+
reject(error);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
session = createdSession;
|
|
316
|
+
session.on('text', (text, parentToolCallId) => {
|
|
317
|
+
outputLines.push(text);
|
|
318
|
+
ctx.emitOutput({
|
|
319
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
320
|
+
content: text, parentToolCallId, ts: Date.now(),
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
session.on('reasoning', (text) => {
|
|
324
|
+
ctx.emitOutput({
|
|
325
|
+
type: 'node:reasoning', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
326
|
+
content: text, ts: Date.now(),
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
session.on('tool_start', (tool, toolInput, args, callId, parentToolCallId) => {
|
|
330
|
+
ctx.emitOutput({
|
|
331
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
332
|
+
tool, phase: 'start', summary: toolInput, args, toolCallId: callId,
|
|
333
|
+
parentToolCallId, ts: Date.now(),
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
session.on('tool_complete', (tool, output, callId, parentToolCallId) => {
|
|
337
|
+
ctx.emitOutput({
|
|
338
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
339
|
+
tool, phase: 'complete', summary: output, toolCallId: callId,
|
|
340
|
+
parentToolCallId, ts: Date.now(),
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
session.on('tool_output', (tool, output, parentToolCallId) => {
|
|
344
|
+
outputLines.push(output);
|
|
345
|
+
ctx.emitOutput({
|
|
346
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
347
|
+
content: output, tool, parentToolCallId, ts: Date.now(),
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
session.on('intent', (intent) => {
|
|
351
|
+
ctx.emitOutput({
|
|
352
|
+
type: 'node:intent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
353
|
+
intent, ts: Date.now(),
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
session.on('usage', (data) => {
|
|
357
|
+
lastUsage = data;
|
|
358
|
+
ctx.emitOutput({
|
|
359
|
+
type: 'node:usage', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
360
|
+
inputTokens: typeof data.inputTokens === 'number' ? data.inputTokens : undefined,
|
|
361
|
+
outputTokens: typeof data.outputTokens === 'number' ? data.outputTokens : undefined,
|
|
362
|
+
totalTokens: typeof data.totalTokens === 'number' ? data.totalTokens : undefined,
|
|
363
|
+
model: typeof data.model === 'string' ? data.model : undefined,
|
|
364
|
+
ts: Date.now(),
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
session.on('tool_complete_rich', (tool, contents, callId) => {
|
|
368
|
+
const toolData = contentBlocksToToolData(contents);
|
|
369
|
+
if (toolData) {
|
|
370
|
+
ctx.emitOutput({
|
|
371
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
372
|
+
tool, phase: 'complete', summary: '', toolCallId: callId,
|
|
373
|
+
toolSpecificData: toolData, ts: Date.now(),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
session.on('subagent_start', (name, data) => {
|
|
378
|
+
ctx.emitOutput({
|
|
379
|
+
type: 'node:subagent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
380
|
+
agentName: name, phase: 'start',
|
|
381
|
+
toolCallId: typeof data.toolCallId === 'string' ? data.toolCallId : undefined,
|
|
382
|
+
info: data, ts: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
session.on('subagent_end', (name, data) => {
|
|
386
|
+
const totalTokens = typeof data.totalTokens === 'number' ? data.totalTokens : undefined;
|
|
387
|
+
const model = typeof data.model === 'string' ? data.model : undefined;
|
|
388
|
+
if (totalTokens !== undefined) {
|
|
389
|
+
subagentUsage.push({ totalTokens, ...(model !== undefined ? { model } : {}) });
|
|
390
|
+
}
|
|
391
|
+
ctx.emitOutput({
|
|
392
|
+
type: 'node:subagent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
393
|
+
agentName: name, phase: 'end',
|
|
394
|
+
toolCallId: typeof data.toolCallId === 'string' ? data.toolCallId : undefined,
|
|
395
|
+
info: data,
|
|
396
|
+
error: typeof data.error === 'string' ? data.error : undefined,
|
|
397
|
+
ts: Date.now(),
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
session.on('permission', (data) => {
|
|
401
|
+
ctx.emitOutput({
|
|
402
|
+
type: 'node:permission', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
403
|
+
kind: typeof data.kind === 'string' ? data.kind : undefined,
|
|
404
|
+
detail: typeof data.detail === 'string' ? data.detail : undefined,
|
|
405
|
+
approved: typeof data.approved === 'boolean' ? data.approved : undefined,
|
|
406
|
+
ts: Date.now(),
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
session.on('compaction', (phase, summary) => {
|
|
410
|
+
const message = phase === 'start'
|
|
411
|
+
? '\n--- Context compaction started ---\n'
|
|
412
|
+
: `\n--- Context compaction complete${summary ? `: ${summary}` : ''} ---\n`;
|
|
413
|
+
outputLines.push(message);
|
|
414
|
+
ctx.emitOutput({
|
|
415
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
416
|
+
content: message, ts: Date.now(),
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
const activeSession = session;
|
|
420
|
+
activeSession.send(prompt);
|
|
421
|
+
await new Promise((resolve, reject) => {
|
|
422
|
+
const onAbort = () => {
|
|
423
|
+
activeSession.abort().then(() => reject(new types_1.FlowAbortedError('Aborted during session execution')), () => reject(new types_1.FlowAbortedError('Aborted during session execution')));
|
|
424
|
+
};
|
|
425
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
426
|
+
activeSession.on('idle', () => {
|
|
427
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
428
|
+
resolve();
|
|
429
|
+
});
|
|
430
|
+
activeSession.on('error', (error) => {
|
|
431
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
432
|
+
reject(error);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
return { outputLines, usage: lastUsage, subagentUsage };
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (!ctx.signal.aborted && config.output && wasCompletedBeforeCrash(input.dir, config.output, outputLines, config.completionIndicators)) {
|
|
439
|
+
return { outputLines, usage: lastUsage, subagentUsage };
|
|
440
|
+
}
|
|
441
|
+
throw withAttemptUsage(error, attemptUsage(lastUsage, subagentUsage));
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
if (session) {
|
|
445
|
+
try {
|
|
446
|
+
await session.abort();
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Session may already be closed.
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
128
455
|
// Agent factory
|
|
129
456
|
// ---------------------------------------------------------------------------
|
|
130
457
|
/**
|
|
@@ -143,274 +470,154 @@ function formatPrompt(promptOutput) {
|
|
|
143
470
|
*/
|
|
144
471
|
function agent(config) {
|
|
145
472
|
return async (input, ctx) => {
|
|
146
|
-
// Abort check: bail early if already aborted
|
|
147
473
|
if (ctx.signal.aborted) {
|
|
148
474
|
throw new types_1.FlowAbortedError('Aborted before agent start');
|
|
149
475
|
}
|
|
150
|
-
// Step 1: Delete stale artifact
|
|
151
476
|
if (config.output) {
|
|
152
477
|
const artifactPath = path.join(input.dir, config.output);
|
|
153
478
|
try {
|
|
154
|
-
if (fs.existsSync(artifactPath))
|
|
479
|
+
if (fs.existsSync(artifactPath))
|
|
155
480
|
fs.unlinkSync(artifactPath);
|
|
156
|
-
}
|
|
157
481
|
}
|
|
158
482
|
catch {
|
|
159
|
-
// Ignore
|
|
483
|
+
// Ignore a stale artifact that cannot be removed.
|
|
160
484
|
}
|
|
161
485
|
}
|
|
162
|
-
|
|
163
|
-
if (config.setup) {
|
|
486
|
+
if (config.setup)
|
|
164
487
|
await config.setup(input);
|
|
165
|
-
}
|
|
166
|
-
let session = null;
|
|
167
|
-
const outputLines = [];
|
|
168
|
-
let lastUsage;
|
|
169
488
|
try {
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
// Abort check after session creation
|
|
191
|
-
if (ctx.signal.aborted) {
|
|
192
|
-
await session.abort();
|
|
193
|
-
throw new types_1.FlowAbortedError('Aborted after session creation');
|
|
194
|
-
}
|
|
195
|
-
// Step 5: Wire session events to emitOutput
|
|
196
|
-
session.on('text', (text, parentToolCallId) => {
|
|
197
|
-
outputLines.push(text);
|
|
198
|
-
ctx.emitOutput({
|
|
199
|
-
type: 'node:output',
|
|
200
|
-
executionId: ctx.executionId,
|
|
201
|
-
nodeId: ctx.nodeId,
|
|
202
|
-
content: text,
|
|
203
|
-
parentToolCallId,
|
|
204
|
-
ts: Date.now(),
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
session.on('reasoning', (text) => {
|
|
208
|
-
ctx.emitOutput({
|
|
209
|
-
type: 'node:reasoning',
|
|
210
|
-
executionId: ctx.executionId,
|
|
211
|
-
nodeId: ctx.nodeId,
|
|
212
|
-
content: text,
|
|
213
|
-
ts: Date.now(),
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
session.on('tool_start', (tool, toolInput, args, callId, parentToolCallId) => {
|
|
217
|
-
ctx.emitOutput({
|
|
218
|
-
type: 'node:tool',
|
|
219
|
-
executionId: ctx.executionId,
|
|
220
|
-
nodeId: ctx.nodeId,
|
|
221
|
-
tool,
|
|
222
|
-
phase: 'start',
|
|
223
|
-
summary: toolInput,
|
|
224
|
-
args,
|
|
225
|
-
toolCallId: callId,
|
|
226
|
-
parentToolCallId,
|
|
227
|
-
ts: Date.now(),
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
session.on('tool_complete', (tool, output, callId, parentToolCallId) => {
|
|
231
|
-
ctx.emitOutput({
|
|
232
|
-
type: 'node:tool',
|
|
233
|
-
executionId: ctx.executionId,
|
|
234
|
-
nodeId: ctx.nodeId,
|
|
235
|
-
tool,
|
|
236
|
-
phase: 'complete',
|
|
237
|
-
summary: output,
|
|
238
|
-
toolCallId: callId,
|
|
239
|
-
parentToolCallId,
|
|
240
|
-
ts: Date.now(),
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
session.on('tool_output', (tool, output, parentToolCallId) => {
|
|
244
|
-
outputLines.push(output);
|
|
245
|
-
ctx.emitOutput({
|
|
246
|
-
type: 'node:output',
|
|
247
|
-
executionId: ctx.executionId,
|
|
248
|
-
nodeId: ctx.nodeId,
|
|
249
|
-
content: output,
|
|
250
|
-
tool,
|
|
251
|
-
parentToolCallId,
|
|
252
|
-
ts: Date.now(),
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
// Rich events (SdkBackend emits these; SubprocessBackend silently accepts the on() call)
|
|
256
|
-
session.on('intent', (intent) => {
|
|
257
|
-
ctx.emitOutput({
|
|
258
|
-
type: 'node:intent',
|
|
259
|
-
executionId: ctx.executionId,
|
|
260
|
-
nodeId: ctx.nodeId,
|
|
261
|
-
intent,
|
|
262
|
-
ts: Date.now(),
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
session.on('usage', (data) => {
|
|
266
|
-
lastUsage = data;
|
|
267
|
-
ctx.emitOutput({
|
|
268
|
-
type: 'node:usage',
|
|
269
|
-
executionId: ctx.executionId,
|
|
270
|
-
nodeId: ctx.nodeId,
|
|
271
|
-
inputTokens: typeof data.inputTokens === 'number' ? data.inputTokens : undefined,
|
|
272
|
-
outputTokens: typeof data.outputTokens === 'number' ? data.outputTokens : undefined,
|
|
273
|
-
totalTokens: typeof data.totalTokens === 'number' ? data.totalTokens : undefined,
|
|
274
|
-
model: typeof data.model === 'string' ? data.model : undefined,
|
|
275
|
-
ts: Date.now(),
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
session.on('tool_complete_rich', (tool, contents, callId) => {
|
|
279
|
-
const toolData = contentBlocksToToolData(contents);
|
|
280
|
-
if (toolData) {
|
|
281
|
-
// Emit a supplemental node:tool event with structured data
|
|
282
|
-
ctx.emitOutput({
|
|
283
|
-
type: 'node:tool',
|
|
284
|
-
executionId: ctx.executionId,
|
|
285
|
-
nodeId: ctx.nodeId,
|
|
286
|
-
tool,
|
|
287
|
-
phase: 'complete',
|
|
288
|
-
summary: '',
|
|
289
|
-
toolCallId: callId,
|
|
290
|
-
toolSpecificData: toolData,
|
|
291
|
-
ts: Date.now(),
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
session.on('subagent_start', (name, data) => {
|
|
296
|
-
const toolCallId = typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
297
|
-
ctx.emitOutput({
|
|
298
|
-
type: 'node:subagent',
|
|
299
|
-
executionId: ctx.executionId,
|
|
300
|
-
nodeId: ctx.nodeId,
|
|
301
|
-
agentName: name,
|
|
302
|
-
phase: 'start',
|
|
303
|
-
toolCallId,
|
|
304
|
-
info: data,
|
|
305
|
-
ts: Date.now(),
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
session.on('subagent_end', (name, data) => {
|
|
309
|
-
const toolCallId = typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
310
|
-
ctx.emitOutput({
|
|
311
|
-
type: 'node:subagent',
|
|
312
|
-
executionId: ctx.executionId,
|
|
313
|
-
nodeId: ctx.nodeId,
|
|
314
|
-
agentName: name,
|
|
315
|
-
phase: 'end',
|
|
316
|
-
toolCallId,
|
|
317
|
-
info: data,
|
|
318
|
-
error: typeof data.error === 'string' ? data.error : undefined,
|
|
319
|
-
ts: Date.now(),
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
session.on('permission', (data) => {
|
|
323
|
-
ctx.emitOutput({
|
|
324
|
-
type: 'node:permission',
|
|
325
|
-
executionId: ctx.executionId,
|
|
326
|
-
nodeId: ctx.nodeId,
|
|
327
|
-
kind: typeof data.kind === 'string' ? data.kind : undefined,
|
|
328
|
-
detail: typeof data.detail === 'string' ? data.detail : undefined,
|
|
329
|
-
approved: typeof data.approved === 'boolean' ? data.approved : undefined,
|
|
330
|
-
ts: Date.now(),
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
session.on('compaction', (phase, summary) => {
|
|
334
|
-
const msg = phase === 'start'
|
|
335
|
-
? '\n--- Context compaction started ---\n'
|
|
336
|
-
: `\n--- Context compaction complete${summary ? `: ${summary}` : ''} ---\n`;
|
|
337
|
-
outputLines.push(msg);
|
|
338
|
-
ctx.emitOutput({
|
|
339
|
-
type: 'node:output',
|
|
340
|
-
executionId: ctx.executionId,
|
|
341
|
-
nodeId: ctx.nodeId,
|
|
342
|
-
content: msg,
|
|
343
|
-
ts: Date.now(),
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
// Step 6: Send prompt
|
|
347
|
-
session.send(promptStr);
|
|
348
|
-
// Step 7: Wait for completion — wrap in a Promise that resolves on idle, rejects on error
|
|
349
|
-
const result = await new Promise((resolve, reject) => {
|
|
350
|
-
// Listen for abort signal during session execution
|
|
351
|
-
const onAbort = () => {
|
|
352
|
-
session.abort().then(() => reject(new types_1.FlowAbortedError('Aborted during session execution')), () => reject(new types_1.FlowAbortedError('Aborted during session execution')));
|
|
353
|
-
};
|
|
354
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
355
|
-
session.on('idle', () => {
|
|
356
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
357
|
-
resolve('idle');
|
|
358
|
-
});
|
|
359
|
-
session.on('error', (err) => {
|
|
360
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
361
|
-
reject(err);
|
|
362
|
-
});
|
|
363
|
-
}).catch((err) => {
|
|
364
|
-
// AI-4: Don't attempt GT-3 recovery on aborted sessions
|
|
489
|
+
const prompt = formatPrompt(config.promptBuilder(input));
|
|
490
|
+
const policy = config.retry ?? {};
|
|
491
|
+
const maxAttempts = positiveInteger(policy.maxAttempts, DEFAULT_MAX_ATTEMPTS);
|
|
492
|
+
const budgetMs = policy.budgetMs === undefined
|
|
493
|
+
? config.retry === undefined
|
|
494
|
+
? undefined
|
|
495
|
+
: (config.timeout ?? 3600) * 1000
|
|
496
|
+
: nonNegativeFinite(policy.budgetMs, 0);
|
|
497
|
+
const retryDeadlineMs = ctx.retryDeadlineMs
|
|
498
|
+
?? (budgetMs === undefined ? undefined : Date.now() + budgetMs);
|
|
499
|
+
const sharedContext = retryDeadlineMs === undefined
|
|
500
|
+
? ctx
|
|
501
|
+
: { ...ctx, retryDeadlineMs };
|
|
502
|
+
let attempt = 1;
|
|
503
|
+
let result;
|
|
504
|
+
let lastFailure;
|
|
505
|
+
const failedUsage = [];
|
|
506
|
+
const failedSubagentUsage = [];
|
|
507
|
+
while (attempt <= maxAttempts) {
|
|
365
508
|
if (ctx.signal.aborted) {
|
|
366
|
-
throw
|
|
509
|
+
throw withNodeUsage(new types_1.FlowAbortedError('Aborted before session attempt'), failedUsage, failedSubagentUsage);
|
|
367
510
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
511
|
+
const remainingMs = retryDeadlineMs === undefined
|
|
512
|
+
? undefined
|
|
513
|
+
: retryDeadlineMs - Date.now();
|
|
514
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
515
|
+
throw withNodeUsage(lastFailure ?? new Error('Retry deadline exhausted'), failedUsage, failedSubagentUsage);
|
|
373
516
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
517
|
+
const budgetController = new AbortController();
|
|
518
|
+
const budgetTimer = remainingMs === undefined
|
|
519
|
+
? undefined
|
|
520
|
+
: setTimeout(() => budgetController.abort(), Math.min(remainingMs, 2_147_483_647));
|
|
521
|
+
const attemptContext = remainingMs === undefined
|
|
522
|
+
? sharedContext
|
|
523
|
+
: {
|
|
524
|
+
...sharedContext,
|
|
525
|
+
signal: AbortSignal.any([ctx.signal, budgetController.signal]),
|
|
526
|
+
};
|
|
527
|
+
try {
|
|
528
|
+
result = await runSessionAttempt(config, input, attemptContext, prompt);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
const currentError = error instanceof Error ? error : new Error(String(error));
|
|
533
|
+
const consumed = currentError.attemptUsage;
|
|
534
|
+
if (consumed?.usage)
|
|
535
|
+
failedUsage.push(consumed.usage);
|
|
536
|
+
if (consumed)
|
|
537
|
+
failedSubagentUsage.push(...consumed.subagentUsage);
|
|
538
|
+
if (ctx.signal.aborted) {
|
|
539
|
+
throw withNodeUsage(new types_1.FlowAbortedError('Aborted during session attempt'), failedUsage, failedSubagentUsage);
|
|
540
|
+
}
|
|
541
|
+
if (budgetController.signal.aborted) {
|
|
542
|
+
throw withNodeUsage(lastFailure ?? (currentError instanceof types_1.FlowAbortedError
|
|
543
|
+
? new Error('Retry deadline exhausted')
|
|
544
|
+
: currentError), failedUsage, failedSubagentUsage);
|
|
545
|
+
}
|
|
546
|
+
if (currentError instanceof types_1.FlowAbortedError) {
|
|
547
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
548
|
+
}
|
|
549
|
+
lastFailure = currentError;
|
|
550
|
+
const retryMetadata = readRetryMetadata(currentError);
|
|
551
|
+
const meta = { attempt, ...retryMetadata };
|
|
552
|
+
const retriable = (policy.isRetriable ?? isRetriableModelError)(currentError, meta);
|
|
553
|
+
if (!retriable || attempt >= maxAttempts) {
|
|
554
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
555
|
+
}
|
|
556
|
+
const delayMs = retryDelayMs(policy, attempt);
|
|
557
|
+
if (retryDeadlineMs !== undefined && Date.now() + delayMs >= retryDeadlineMs) {
|
|
558
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
559
|
+
}
|
|
560
|
+
attempt += 1;
|
|
561
|
+
try {
|
|
562
|
+
await waitForRetry(delayMs, ctx.signal);
|
|
563
|
+
if (ctx.emitState) {
|
|
564
|
+
await ctx.emitState({
|
|
565
|
+
type: 'node:retrying',
|
|
566
|
+
executionId: ctx.executionId,
|
|
567
|
+
nodeId: ctx.nodeId,
|
|
568
|
+
attempt: ctx.nextRetryAttempt?.() ?? attempt,
|
|
569
|
+
ts: Date.now(),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (retryError) {
|
|
574
|
+
const normalizedRetryError = retryError instanceof Error
|
|
575
|
+
? retryError
|
|
576
|
+
: new Error(String(retryError));
|
|
577
|
+
throw withNodeUsage(normalizedRetryError, failedUsage, failedSubagentUsage);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
if (budgetTimer !== undefined)
|
|
582
|
+
clearTimeout(budgetTimer);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!result)
|
|
586
|
+
throw new Error('Agent session exhausted without a result');
|
|
377
587
|
let content;
|
|
378
588
|
if (config.output) {
|
|
379
|
-
const artifactPath = path.join(input.dir, config.output);
|
|
380
589
|
try {
|
|
381
|
-
content = fs.readFileSync(
|
|
590
|
+
content = fs.readFileSync(path.join(input.dir, config.output), 'utf-8');
|
|
382
591
|
}
|
|
383
592
|
catch {
|
|
384
|
-
// Artifact not written — that's fine for nodes that don't always produce output
|
|
385
593
|
content = undefined;
|
|
386
594
|
}
|
|
387
595
|
}
|
|
388
596
|
const action = config.actionParser && content
|
|
389
597
|
? config.actionParser(content)
|
|
390
598
|
: 'default';
|
|
599
|
+
const metadata = {};
|
|
600
|
+
const usage = [...failedUsage, ...(result.usage ? [result.usage] : [])];
|
|
601
|
+
const subagentUsage = [...failedSubagentUsage, ...result.subagentUsage];
|
|
602
|
+
if (usage.length > 0)
|
|
603
|
+
metadata.usage = usage.at(-1);
|
|
604
|
+
if (usage.length > 1)
|
|
605
|
+
metadata.attemptUsage = usage;
|
|
606
|
+
if (subagentUsage.length > 0)
|
|
607
|
+
metadata.subagentUsage = subagentUsage;
|
|
391
608
|
return {
|
|
392
609
|
action,
|
|
393
610
|
artifact: content,
|
|
394
|
-
metadata:
|
|
611
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
395
612
|
};
|
|
396
613
|
}
|
|
397
614
|
finally {
|
|
398
|
-
// AI-1: Close session on all paths (abort is idempotent)
|
|
399
|
-
if (session) {
|
|
400
|
-
try {
|
|
401
|
-
await session.abort();
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
// Session may already be closed — ignore
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// Step 10: Teardown (always runs, R10: receives full NodeInput)
|
|
408
615
|
if (config.teardown) {
|
|
409
616
|
try {
|
|
410
617
|
await config.teardown(input);
|
|
411
618
|
}
|
|
412
619
|
catch {
|
|
413
|
-
// Teardown errors
|
|
620
|
+
// Teardown errors must not mask the primary result.
|
|
414
621
|
}
|
|
415
622
|
}
|
|
416
623
|
}
|