condukt 0.7.0 → 0.8.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 +26 -7
- package/dist/runtimes/copilot/copilot-adapter.js.map +1 -1
- package/dist/runtimes/copilot/copilot-backend.d.ts +35 -1
- package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.d.ts +2 -2
- package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.js +38 -3
- package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
- 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 +7 -1
- package/dist/src/agent-node.d.ts.map +1 -1
- package/dist/src/agent-node.js +27 -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 +434 -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 +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel-node.d.ts +7 -3
- package/dist/src/panel-node.d.ts.map +1 -1
- package/dist/src/panel-node.js +92 -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 +68 -1
- package/dist/src/types.d.ts.map +1 -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 +1 -1
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,330 @@ 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
|
+
defaultAgent: config.defaultAgent,
|
|
266
|
+
excludedBuiltinAgents: config.excludedBuiltinAgents,
|
|
267
|
+
nodeId: ctx.nodeId,
|
|
268
|
+
memberId: config.memberId,
|
|
269
|
+
artifactFilename: config.output,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function attemptUsage(usage, subagentUsage) {
|
|
273
|
+
return { usage, subagentUsage: [...subagentUsage] };
|
|
274
|
+
}
|
|
275
|
+
function withAttemptUsage(error, usage) {
|
|
276
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
277
|
+
Object.assign(normalized, { attemptUsage: usage });
|
|
278
|
+
return normalized;
|
|
279
|
+
}
|
|
280
|
+
function withNodeUsage(error, attemptUsage, subagentUsage) {
|
|
281
|
+
if (attemptUsage.length === 0 && subagentUsage.length === 0)
|
|
282
|
+
return error;
|
|
283
|
+
Object.assign(error, {
|
|
284
|
+
nodeUsage: {
|
|
285
|
+
attemptUsage: [...attemptUsage],
|
|
286
|
+
subagentUsage: [...subagentUsage],
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
return error;
|
|
290
|
+
}
|
|
291
|
+
async function runSessionAttempt(config, input, ctx, prompt) {
|
|
292
|
+
let session = null;
|
|
293
|
+
const outputLines = [];
|
|
294
|
+
let lastUsage;
|
|
295
|
+
const subagentUsage = [];
|
|
296
|
+
try {
|
|
297
|
+
const creation = ctx.runtime.createSession(sessionConfig(config, input, ctx), { signal: ctx.signal });
|
|
298
|
+
const createdSession = await new Promise((resolve, reject) => {
|
|
299
|
+
const onAbort = () => reject(new types_1.FlowAbortedError('Aborted during session creation'));
|
|
300
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
301
|
+
creation.then((created) => {
|
|
302
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
303
|
+
if (ctx.signal.aborted) {
|
|
304
|
+
void created.abort();
|
|
305
|
+
reject(new types_1.FlowAbortedError('Aborted during session creation'));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
resolve(created);
|
|
309
|
+
}, (error) => {
|
|
310
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
311
|
+
reject(error);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
session = createdSession;
|
|
315
|
+
session.on('text', (text, parentToolCallId) => {
|
|
316
|
+
outputLines.push(text);
|
|
317
|
+
ctx.emitOutput({
|
|
318
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
319
|
+
content: text, parentToolCallId, ts: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
session.on('reasoning', (text) => {
|
|
323
|
+
ctx.emitOutput({
|
|
324
|
+
type: 'node:reasoning', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
325
|
+
content: text, ts: Date.now(),
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
session.on('tool_start', (tool, toolInput, args, callId, parentToolCallId) => {
|
|
329
|
+
ctx.emitOutput({
|
|
330
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
331
|
+
tool, phase: 'start', summary: toolInput, args, toolCallId: callId,
|
|
332
|
+
parentToolCallId, ts: Date.now(),
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
session.on('tool_complete', (tool, output, callId, parentToolCallId) => {
|
|
336
|
+
ctx.emitOutput({
|
|
337
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
338
|
+
tool, phase: 'complete', summary: output, toolCallId: callId,
|
|
339
|
+
parentToolCallId, ts: Date.now(),
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
session.on('tool_output', (tool, output, parentToolCallId) => {
|
|
343
|
+
outputLines.push(output);
|
|
344
|
+
ctx.emitOutput({
|
|
345
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
346
|
+
content: output, tool, parentToolCallId, ts: Date.now(),
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
session.on('intent', (intent) => {
|
|
350
|
+
ctx.emitOutput({
|
|
351
|
+
type: 'node:intent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
352
|
+
intent, ts: Date.now(),
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
session.on('usage', (data) => {
|
|
356
|
+
lastUsage = data;
|
|
357
|
+
ctx.emitOutput({
|
|
358
|
+
type: 'node:usage', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
359
|
+
inputTokens: typeof data.inputTokens === 'number' ? data.inputTokens : undefined,
|
|
360
|
+
outputTokens: typeof data.outputTokens === 'number' ? data.outputTokens : undefined,
|
|
361
|
+
totalTokens: typeof data.totalTokens === 'number' ? data.totalTokens : undefined,
|
|
362
|
+
model: typeof data.model === 'string' ? data.model : undefined,
|
|
363
|
+
ts: Date.now(),
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
session.on('tool_complete_rich', (tool, contents, callId) => {
|
|
367
|
+
const toolData = contentBlocksToToolData(contents);
|
|
368
|
+
if (toolData) {
|
|
369
|
+
ctx.emitOutput({
|
|
370
|
+
type: 'node:tool', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
371
|
+
tool, phase: 'complete', summary: '', toolCallId: callId,
|
|
372
|
+
toolSpecificData: toolData, ts: Date.now(),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
session.on('subagent_start', (name, data) => {
|
|
377
|
+
ctx.emitOutput({
|
|
378
|
+
type: 'node:subagent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
379
|
+
agentName: name, phase: 'start',
|
|
380
|
+
toolCallId: typeof data.toolCallId === 'string' ? data.toolCallId : undefined,
|
|
381
|
+
info: data, ts: Date.now(),
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
session.on('subagent_end', (name, data) => {
|
|
385
|
+
const totalTokens = typeof data.totalTokens === 'number' ? data.totalTokens : undefined;
|
|
386
|
+
const model = typeof data.model === 'string' ? data.model : undefined;
|
|
387
|
+
if (totalTokens !== undefined) {
|
|
388
|
+
subagentUsage.push({ totalTokens, ...(model !== undefined ? { model } : {}) });
|
|
389
|
+
}
|
|
390
|
+
ctx.emitOutput({
|
|
391
|
+
type: 'node:subagent', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
392
|
+
agentName: name, phase: 'end',
|
|
393
|
+
toolCallId: typeof data.toolCallId === 'string' ? data.toolCallId : undefined,
|
|
394
|
+
info: data,
|
|
395
|
+
error: typeof data.error === 'string' ? data.error : undefined,
|
|
396
|
+
ts: Date.now(),
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
session.on('permission', (data) => {
|
|
400
|
+
ctx.emitOutput({
|
|
401
|
+
type: 'node:permission', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
402
|
+
kind: typeof data.kind === 'string' ? data.kind : undefined,
|
|
403
|
+
detail: typeof data.detail === 'string' ? data.detail : undefined,
|
|
404
|
+
approved: typeof data.approved === 'boolean' ? data.approved : undefined,
|
|
405
|
+
ts: Date.now(),
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
session.on('compaction', (phase, summary) => {
|
|
409
|
+
const message = phase === 'start'
|
|
410
|
+
? '\n--- Context compaction started ---\n'
|
|
411
|
+
: `\n--- Context compaction complete${summary ? `: ${summary}` : ''} ---\n`;
|
|
412
|
+
outputLines.push(message);
|
|
413
|
+
ctx.emitOutput({
|
|
414
|
+
type: 'node:output', executionId: ctx.executionId, nodeId: ctx.nodeId,
|
|
415
|
+
content: message, ts: Date.now(),
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
const activeSession = session;
|
|
419
|
+
activeSession.send(prompt);
|
|
420
|
+
await new Promise((resolve, reject) => {
|
|
421
|
+
const onAbort = () => {
|
|
422
|
+
activeSession.abort().then(() => reject(new types_1.FlowAbortedError('Aborted during session execution')), () => reject(new types_1.FlowAbortedError('Aborted during session execution')));
|
|
423
|
+
};
|
|
424
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
425
|
+
activeSession.on('idle', () => {
|
|
426
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
427
|
+
resolve();
|
|
428
|
+
});
|
|
429
|
+
activeSession.on('error', (error) => {
|
|
430
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
431
|
+
reject(error);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
return { outputLines, usage: lastUsage, subagentUsage };
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (!ctx.signal.aborted && config.output && wasCompletedBeforeCrash(input.dir, config.output, outputLines, config.completionIndicators)) {
|
|
438
|
+
return { outputLines, usage: lastUsage, subagentUsage };
|
|
439
|
+
}
|
|
440
|
+
throw withAttemptUsage(error, attemptUsage(lastUsage, subagentUsage));
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
if (session) {
|
|
444
|
+
try {
|
|
445
|
+
await session.abort();
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// Session may already be closed.
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
128
454
|
// Agent factory
|
|
129
455
|
// ---------------------------------------------------------------------------
|
|
130
456
|
/**
|
|
@@ -143,274 +469,152 @@ function formatPrompt(promptOutput) {
|
|
|
143
469
|
*/
|
|
144
470
|
function agent(config) {
|
|
145
471
|
return async (input, ctx) => {
|
|
146
|
-
// Abort check: bail early if already aborted
|
|
147
472
|
if (ctx.signal.aborted) {
|
|
148
473
|
throw new types_1.FlowAbortedError('Aborted before agent start');
|
|
149
474
|
}
|
|
150
|
-
// Step 1: Delete stale artifact
|
|
151
475
|
if (config.output) {
|
|
152
476
|
const artifactPath = path.join(input.dir, config.output);
|
|
153
477
|
try {
|
|
154
|
-
if (fs.existsSync(artifactPath))
|
|
478
|
+
if (fs.existsSync(artifactPath))
|
|
155
479
|
fs.unlinkSync(artifactPath);
|
|
156
|
-
}
|
|
157
480
|
}
|
|
158
481
|
catch {
|
|
159
|
-
// Ignore
|
|
482
|
+
// Ignore a stale artifact that cannot be removed.
|
|
160
483
|
}
|
|
161
484
|
}
|
|
162
|
-
|
|
163
|
-
if (config.setup) {
|
|
485
|
+
if (config.setup)
|
|
164
486
|
await config.setup(input);
|
|
165
|
-
}
|
|
166
|
-
let session = null;
|
|
167
|
-
const outputLines = [];
|
|
168
|
-
let lastUsage;
|
|
169
487
|
try {
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
nodeId: ctx.nodeId,
|
|
188
|
-
artifactFilename: config.output,
|
|
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
|
|
488
|
+
const prompt = formatPrompt(config.promptBuilder(input));
|
|
489
|
+
const policy = config.retry ?? {};
|
|
490
|
+
const maxAttempts = positiveInteger(policy.maxAttempts, DEFAULT_MAX_ATTEMPTS);
|
|
491
|
+
const budgetMs = policy.budgetMs === undefined
|
|
492
|
+
? undefined
|
|
493
|
+
: nonNegativeFinite(policy.budgetMs, 0);
|
|
494
|
+
const retryDeadlineMs = ctx.retryDeadlineMs
|
|
495
|
+
?? (budgetMs === undefined ? undefined : Date.now() + budgetMs);
|
|
496
|
+
const sharedContext = retryDeadlineMs === undefined
|
|
497
|
+
? ctx
|
|
498
|
+
: { ...ctx, retryDeadlineMs };
|
|
499
|
+
let attempt = 1;
|
|
500
|
+
let result;
|
|
501
|
+
let lastFailure;
|
|
502
|
+
const failedUsage = [];
|
|
503
|
+
const failedSubagentUsage = [];
|
|
504
|
+
while (attempt <= maxAttempts) {
|
|
365
505
|
if (ctx.signal.aborted) {
|
|
366
|
-
throw
|
|
506
|
+
throw withNodeUsage(new types_1.FlowAbortedError('Aborted before session attempt'), failedUsage, failedSubagentUsage);
|
|
367
507
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
508
|
+
const remainingMs = retryDeadlineMs === undefined
|
|
509
|
+
? undefined
|
|
510
|
+
: retryDeadlineMs - Date.now();
|
|
511
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
512
|
+
throw withNodeUsage(lastFailure ?? new Error('Retry deadline exhausted'), failedUsage, failedSubagentUsage);
|
|
373
513
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
514
|
+
const budgetController = new AbortController();
|
|
515
|
+
const budgetTimer = remainingMs === undefined
|
|
516
|
+
? undefined
|
|
517
|
+
: setTimeout(() => budgetController.abort(), Math.min(remainingMs, 2_147_483_647));
|
|
518
|
+
const attemptContext = remainingMs === undefined
|
|
519
|
+
? sharedContext
|
|
520
|
+
: {
|
|
521
|
+
...sharedContext,
|
|
522
|
+
signal: AbortSignal.any([ctx.signal, budgetController.signal]),
|
|
523
|
+
};
|
|
524
|
+
try {
|
|
525
|
+
result = await runSessionAttempt(config, input, attemptContext, prompt);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
const currentError = error instanceof Error ? error : new Error(String(error));
|
|
530
|
+
const consumed = currentError.attemptUsage;
|
|
531
|
+
if (consumed?.usage)
|
|
532
|
+
failedUsage.push(consumed.usage);
|
|
533
|
+
if (consumed)
|
|
534
|
+
failedSubagentUsage.push(...consumed.subagentUsage);
|
|
535
|
+
if (ctx.signal.aborted) {
|
|
536
|
+
throw withNodeUsage(new types_1.FlowAbortedError('Aborted during session attempt'), failedUsage, failedSubagentUsage);
|
|
537
|
+
}
|
|
538
|
+
if (budgetController.signal.aborted) {
|
|
539
|
+
throw withNodeUsage(lastFailure ?? (currentError instanceof types_1.FlowAbortedError
|
|
540
|
+
? new Error('Retry deadline exhausted')
|
|
541
|
+
: currentError), failedUsage, failedSubagentUsage);
|
|
542
|
+
}
|
|
543
|
+
if (currentError instanceof types_1.FlowAbortedError) {
|
|
544
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
545
|
+
}
|
|
546
|
+
lastFailure = currentError;
|
|
547
|
+
const retryMetadata = readRetryMetadata(currentError);
|
|
548
|
+
const meta = { attempt, ...retryMetadata };
|
|
549
|
+
const retriable = (policy.isRetriable ?? isRetriableModelError)(currentError, meta);
|
|
550
|
+
if (!retriable || attempt >= maxAttempts) {
|
|
551
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
552
|
+
}
|
|
553
|
+
const delayMs = retryDelayMs(policy, attempt);
|
|
554
|
+
if (retryDeadlineMs !== undefined && Date.now() + delayMs >= retryDeadlineMs) {
|
|
555
|
+
throw withNodeUsage(currentError, failedUsage, failedSubagentUsage);
|
|
556
|
+
}
|
|
557
|
+
attempt += 1;
|
|
558
|
+
try {
|
|
559
|
+
await waitForRetry(delayMs, ctx.signal);
|
|
560
|
+
if (ctx.emitState) {
|
|
561
|
+
await ctx.emitState({
|
|
562
|
+
type: 'node:retrying',
|
|
563
|
+
executionId: ctx.executionId,
|
|
564
|
+
nodeId: ctx.nodeId,
|
|
565
|
+
attempt: ctx.nextRetryAttempt?.() ?? attempt,
|
|
566
|
+
ts: Date.now(),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (retryError) {
|
|
571
|
+
const normalizedRetryError = retryError instanceof Error
|
|
572
|
+
? retryError
|
|
573
|
+
: new Error(String(retryError));
|
|
574
|
+
throw withNodeUsage(normalizedRetryError, failedUsage, failedSubagentUsage);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
if (budgetTimer !== undefined)
|
|
579
|
+
clearTimeout(budgetTimer);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (!result)
|
|
583
|
+
throw new Error('Agent session exhausted without a result');
|
|
377
584
|
let content;
|
|
378
585
|
if (config.output) {
|
|
379
|
-
const artifactPath = path.join(input.dir, config.output);
|
|
380
586
|
try {
|
|
381
|
-
content = fs.readFileSync(
|
|
587
|
+
content = fs.readFileSync(path.join(input.dir, config.output), 'utf-8');
|
|
382
588
|
}
|
|
383
589
|
catch {
|
|
384
|
-
// Artifact not written — that's fine for nodes that don't always produce output
|
|
385
590
|
content = undefined;
|
|
386
591
|
}
|
|
387
592
|
}
|
|
388
593
|
const action = config.actionParser && content
|
|
389
594
|
? config.actionParser(content)
|
|
390
595
|
: 'default';
|
|
596
|
+
const metadata = {};
|
|
597
|
+
const usage = [...failedUsage, ...(result.usage ? [result.usage] : [])];
|
|
598
|
+
const subagentUsage = [...failedSubagentUsage, ...result.subagentUsage];
|
|
599
|
+
if (usage.length > 0)
|
|
600
|
+
metadata.usage = usage.at(-1);
|
|
601
|
+
if (usage.length > 1)
|
|
602
|
+
metadata.attemptUsage = usage;
|
|
603
|
+
if (subagentUsage.length > 0)
|
|
604
|
+
metadata.subagentUsage = subagentUsage;
|
|
391
605
|
return {
|
|
392
606
|
action,
|
|
393
607
|
artifact: content,
|
|
394
|
-
metadata:
|
|
608
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
395
609
|
};
|
|
396
610
|
}
|
|
397
611
|
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
612
|
if (config.teardown) {
|
|
409
613
|
try {
|
|
410
614
|
await config.teardown(input);
|
|
411
615
|
}
|
|
412
616
|
catch {
|
|
413
|
-
// Teardown errors
|
|
617
|
+
// Teardown errors must not mask the primary result.
|
|
414
618
|
}
|
|
415
619
|
}
|
|
416
620
|
}
|