condukt 0.6.21 → 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/README.md +35 -0
- 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 +23 -20
- package/dist/runtimes/mock/mock-runtime.d.ts.map +1 -1
- package/dist/runtimes/mock/mock-runtime.js +79 -24
- package/dist/runtimes/mock/mock-runtime.js.map +1 -1
- package/dist/src/agent-node.d.ts +90 -0
- package/dist/src/agent-node.d.ts.map +1 -0
- package/dist/src/agent-node.js +383 -0
- package/dist/src/agent-node.js.map +1 -0
- package/dist/src/agent.d.ts +4 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +438 -224
- package/dist/src/agent.js.map +1 -1
- package/dist/src/dry-run.d.ts +16 -0
- package/dist/src/dry-run.d.ts.map +1 -0
- package/dist/src/dry-run.js +44 -0
- package/dist/src/dry-run.js.map +1 -0
- package/dist/src/events.d.ts +1 -0
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/index.d.ts +19 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/panel-node.d.ts +37 -0
- package/dist/src/panel-node.d.ts.map +1 -0
- package/dist/src/panel-node.js +175 -0
- package/dist/src/panel-node.js.map +1 -0
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +311 -5
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/types.d.ts +115 -3
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +7 -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,264 +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
487
|
try {
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (ctx.signal.aborted) {
|
|
187
|
-
await session.abort();
|
|
188
|
-
throw new types_1.FlowAbortedError('Aborted after session creation');
|
|
189
|
-
}
|
|
190
|
-
// Step 5: Wire session events to emitOutput
|
|
191
|
-
session.on('text', (text, parentToolCallId) => {
|
|
192
|
-
outputLines.push(text);
|
|
193
|
-
ctx.emitOutput({
|
|
194
|
-
type: 'node:output',
|
|
195
|
-
executionId: ctx.executionId,
|
|
196
|
-
nodeId: ctx.nodeId,
|
|
197
|
-
content: text,
|
|
198
|
-
parentToolCallId,
|
|
199
|
-
ts: Date.now(),
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
session.on('reasoning', (text) => {
|
|
203
|
-
ctx.emitOutput({
|
|
204
|
-
type: 'node:reasoning',
|
|
205
|
-
executionId: ctx.executionId,
|
|
206
|
-
nodeId: ctx.nodeId,
|
|
207
|
-
content: text,
|
|
208
|
-
ts: Date.now(),
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
session.on('tool_start', (tool, toolInput, args, callId, parentToolCallId) => {
|
|
212
|
-
ctx.emitOutput({
|
|
213
|
-
type: 'node:tool',
|
|
214
|
-
executionId: ctx.executionId,
|
|
215
|
-
nodeId: ctx.nodeId,
|
|
216
|
-
tool,
|
|
217
|
-
phase: 'start',
|
|
218
|
-
summary: toolInput,
|
|
219
|
-
args,
|
|
220
|
-
toolCallId: callId,
|
|
221
|
-
parentToolCallId,
|
|
222
|
-
ts: Date.now(),
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
session.on('tool_complete', (tool, output, callId, parentToolCallId) => {
|
|
226
|
-
ctx.emitOutput({
|
|
227
|
-
type: 'node:tool',
|
|
228
|
-
executionId: ctx.executionId,
|
|
229
|
-
nodeId: ctx.nodeId,
|
|
230
|
-
tool,
|
|
231
|
-
phase: 'complete',
|
|
232
|
-
summary: output,
|
|
233
|
-
toolCallId: callId,
|
|
234
|
-
parentToolCallId,
|
|
235
|
-
ts: Date.now(),
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
session.on('tool_output', (tool, output, parentToolCallId) => {
|
|
239
|
-
outputLines.push(output);
|
|
240
|
-
ctx.emitOutput({
|
|
241
|
-
type: 'node:output',
|
|
242
|
-
executionId: ctx.executionId,
|
|
243
|
-
nodeId: ctx.nodeId,
|
|
244
|
-
content: output,
|
|
245
|
-
tool,
|
|
246
|
-
parentToolCallId,
|
|
247
|
-
ts: Date.now(),
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
// Rich events (SdkBackend emits these; SubprocessBackend silently accepts the on() call)
|
|
251
|
-
session.on('intent', (intent) => {
|
|
252
|
-
ctx.emitOutput({
|
|
253
|
-
type: 'node:intent',
|
|
254
|
-
executionId: ctx.executionId,
|
|
255
|
-
nodeId: ctx.nodeId,
|
|
256
|
-
intent,
|
|
257
|
-
ts: Date.now(),
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
session.on('usage', (data) => {
|
|
261
|
-
ctx.emitOutput({
|
|
262
|
-
type: 'node:usage',
|
|
263
|
-
executionId: ctx.executionId,
|
|
264
|
-
nodeId: ctx.nodeId,
|
|
265
|
-
inputTokens: typeof data.inputTokens === 'number' ? data.inputTokens : undefined,
|
|
266
|
-
outputTokens: typeof data.outputTokens === 'number' ? data.outputTokens : undefined,
|
|
267
|
-
totalTokens: typeof data.totalTokens === 'number' ? data.totalTokens : undefined,
|
|
268
|
-
model: typeof data.model === 'string' ? data.model : undefined,
|
|
269
|
-
ts: Date.now(),
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
session.on('tool_complete_rich', (tool, contents, callId) => {
|
|
273
|
-
const toolData = contentBlocksToToolData(contents);
|
|
274
|
-
if (toolData) {
|
|
275
|
-
// Emit a supplemental node:tool event with structured data
|
|
276
|
-
ctx.emitOutput({
|
|
277
|
-
type: 'node:tool',
|
|
278
|
-
executionId: ctx.executionId,
|
|
279
|
-
nodeId: ctx.nodeId,
|
|
280
|
-
tool,
|
|
281
|
-
phase: 'complete',
|
|
282
|
-
summary: '',
|
|
283
|
-
toolCallId: callId,
|
|
284
|
-
toolSpecificData: toolData,
|
|
285
|
-
ts: Date.now(),
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
session.on('subagent_start', (name, data) => {
|
|
290
|
-
const toolCallId = typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
291
|
-
ctx.emitOutput({
|
|
292
|
-
type: 'node:subagent',
|
|
293
|
-
executionId: ctx.executionId,
|
|
294
|
-
nodeId: ctx.nodeId,
|
|
295
|
-
agentName: name,
|
|
296
|
-
phase: 'start',
|
|
297
|
-
toolCallId,
|
|
298
|
-
info: data,
|
|
299
|
-
ts: Date.now(),
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
session.on('subagent_end', (name, data) => {
|
|
303
|
-
const toolCallId = typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
304
|
-
ctx.emitOutput({
|
|
305
|
-
type: 'node:subagent',
|
|
306
|
-
executionId: ctx.executionId,
|
|
307
|
-
nodeId: ctx.nodeId,
|
|
308
|
-
agentName: name,
|
|
309
|
-
phase: 'end',
|
|
310
|
-
toolCallId,
|
|
311
|
-
info: data,
|
|
312
|
-
error: typeof data.error === 'string' ? data.error : undefined,
|
|
313
|
-
ts: Date.now(),
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
session.on('permission', (data) => {
|
|
317
|
-
ctx.emitOutput({
|
|
318
|
-
type: 'node:permission',
|
|
319
|
-
executionId: ctx.executionId,
|
|
320
|
-
nodeId: ctx.nodeId,
|
|
321
|
-
kind: typeof data.kind === 'string' ? data.kind : undefined,
|
|
322
|
-
detail: typeof data.detail === 'string' ? data.detail : undefined,
|
|
323
|
-
approved: typeof data.approved === 'boolean' ? data.approved : undefined,
|
|
324
|
-
ts: Date.now(),
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
session.on('compaction', (phase, summary) => {
|
|
328
|
-
const msg = phase === 'start'
|
|
329
|
-
? '\n--- Context compaction started ---\n'
|
|
330
|
-
: `\n--- Context compaction complete${summary ? `: ${summary}` : ''} ---\n`;
|
|
331
|
-
outputLines.push(msg);
|
|
332
|
-
ctx.emitOutput({
|
|
333
|
-
type: 'node:output',
|
|
334
|
-
executionId: ctx.executionId,
|
|
335
|
-
nodeId: ctx.nodeId,
|
|
336
|
-
content: msg,
|
|
337
|
-
ts: Date.now(),
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
// Step 6: Send prompt
|
|
341
|
-
session.send(promptStr);
|
|
342
|
-
// Step 7: Wait for completion — wrap in a Promise that resolves on idle, rejects on error
|
|
343
|
-
const result = await new Promise((resolve, reject) => {
|
|
344
|
-
// Listen for abort signal during session execution
|
|
345
|
-
const onAbort = () => {
|
|
346
|
-
session.abort().then(() => reject(new types_1.FlowAbortedError('Aborted during session execution')), () => reject(new types_1.FlowAbortedError('Aborted during session execution')));
|
|
347
|
-
};
|
|
348
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
349
|
-
session.on('idle', () => {
|
|
350
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
351
|
-
resolve('idle');
|
|
352
|
-
});
|
|
353
|
-
session.on('error', (err) => {
|
|
354
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
355
|
-
reject(err);
|
|
356
|
-
});
|
|
357
|
-
}).catch((err) => {
|
|
358
|
-
// 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) {
|
|
359
505
|
if (ctx.signal.aborted) {
|
|
360
|
-
throw
|
|
506
|
+
throw withNodeUsage(new types_1.FlowAbortedError('Aborted before session attempt'), failedUsage, failedSubagentUsage);
|
|
361
507
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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);
|
|
367
513
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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');
|
|
371
584
|
let content;
|
|
372
585
|
if (config.output) {
|
|
373
|
-
const artifactPath = path.join(input.dir, config.output);
|
|
374
586
|
try {
|
|
375
|
-
content = fs.readFileSync(
|
|
587
|
+
content = fs.readFileSync(path.join(input.dir, config.output), 'utf-8');
|
|
376
588
|
}
|
|
377
589
|
catch {
|
|
378
|
-
// Artifact not written — that's fine for nodes that don't always produce output
|
|
379
590
|
content = undefined;
|
|
380
591
|
}
|
|
381
592
|
}
|
|
382
593
|
const action = config.actionParser && content
|
|
383
594
|
? config.actionParser(content)
|
|
384
595
|
: 'default';
|
|
385
|
-
|
|
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;
|
|
605
|
+
return {
|
|
606
|
+
action,
|
|
607
|
+
artifact: content,
|
|
608
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
609
|
+
};
|
|
386
610
|
}
|
|
387
611
|
finally {
|
|
388
|
-
// AI-1: Close session on all paths (abort is idempotent)
|
|
389
|
-
if (session) {
|
|
390
|
-
try {
|
|
391
|
-
await session.abort();
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
// Session may already be closed — ignore
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// Step 10: Teardown (always runs, R10: receives full NodeInput)
|
|
398
612
|
if (config.teardown) {
|
|
399
613
|
try {
|
|
400
614
|
await config.teardown(input);
|
|
401
615
|
}
|
|
402
616
|
catch {
|
|
403
|
-
// Teardown errors
|
|
617
|
+
// Teardown errors must not mask the primary result.
|
|
404
618
|
}
|
|
405
619
|
}
|
|
406
620
|
}
|