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.
Files changed (44) hide show
  1. package/dist/runtimes/copilot/copilot-adapter.d.ts.map +1 -1
  2. package/dist/runtimes/copilot/copilot-adapter.js +26 -7
  3. package/dist/runtimes/copilot/copilot-adapter.js.map +1 -1
  4. package/dist/runtimes/copilot/copilot-backend.d.ts +35 -1
  5. package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
  6. package/dist/runtimes/copilot/sdk-backend.d.ts +2 -2
  7. package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
  8. package/dist/runtimes/copilot/sdk-backend.js +38 -3
  9. package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
  10. package/dist/runtimes/copilot/subprocess-backend.d.ts +2 -2
  11. package/dist/runtimes/copilot/subprocess-backend.d.ts.map +1 -1
  12. package/dist/runtimes/copilot/subprocess-backend.js +4 -1
  13. package/dist/runtimes/copilot/subprocess-backend.js.map +1 -1
  14. package/dist/runtimes/mock/mock-runtime.d.ts +4 -4
  15. package/dist/runtimes/mock/mock-runtime.d.ts.map +1 -1
  16. package/dist/runtimes/mock/mock-runtime.js +20 -5
  17. package/dist/runtimes/mock/mock-runtime.js.map +1 -1
  18. package/dist/src/agent-node.d.ts +7 -1
  19. package/dist/src/agent-node.d.ts.map +1 -1
  20. package/dist/src/agent-node.js +27 -2
  21. package/dist/src/agent-node.js.map +1 -1
  22. package/dist/src/agent.d.ts +4 -1
  23. package/dist/src/agent.d.ts.map +1 -1
  24. package/dist/src/agent.js +434 -230
  25. package/dist/src/agent.js.map +1 -1
  26. package/dist/src/events.d.ts +1 -0
  27. package/dist/src/events.d.ts.map +1 -1
  28. package/dist/src/index.d.ts +2 -2
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +3 -1
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/panel-node.d.ts +7 -3
  33. package/dist/src/panel-node.d.ts.map +1 -1
  34. package/dist/src/panel-node.js +92 -62
  35. package/dist/src/panel-node.js.map +1 -1
  36. package/dist/src/scheduler.d.ts.map +1 -1
  37. package/dist/src/scheduler.js +79 -19
  38. package/dist/src/scheduler.js.map +1 -1
  39. package/dist/src/types.d.ts +68 -1
  40. package/dist/src/types.d.ts.map +1 -1
  41. package/dist/src/types.js.map +1 -1
  42. package/dist/state/reducer.js +2 -2
  43. package/dist/state/reducer.js.map +1 -1
  44. 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 may not exist
482
+ // Ignore a stale artifact that cannot be removed.
160
483
  }
161
484
  }
162
- // Step 2: Call setup hook (R5: runs before EVERY agent execution, R10: receives full NodeInput)
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
- // Step 3: Build prompt
171
- const promptOutput = config.promptBuilder(input);
172
- const promptStr = formatPrompt(promptOutput);
173
- // Step 4: Create session (COMP-3: cwdResolver overrides cwd for repo access)
174
- const sessionCwd = config.cwdResolver ? config.cwdResolver(input) : input.dir;
175
- session = await ctx.runtime.createSession({
176
- model: config.model ?? 'claude-opus-4.6',
177
- thinkingBudget: config.thinkingBudget,
178
- cwd: sessionCwd,
179
- addDirs: config.isolation ? [] : [input.dir],
180
- timeout: config.timeout ?? 3600,
181
- heartbeatTimeout: config.heartbeatTimeout ?? 120,
182
- systemMessage: config.systemMessage,
183
- availableTools: config.availableTools ?? (config.tools && config.tools.length > 0
184
- ? config.tools.map((tool) => tool.id)
185
- : undefined),
186
- excludedTools: config.excludedTools,
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 err;
506
+ throw withNodeUsage(new types_1.FlowAbortedError('Aborted before session attempt'), failedUsage, failedSubagentUsage);
367
507
  }
368
- // Step 8: GT-3 dual-condition crash recovery
369
- if (config.output &&
370
- wasCompletedBeforeCrash(input.dir, config.output, outputLines, config.completionIndicators)) {
371
- // Treat as success artifact was written before crash
372
- return 'recovered';
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
- throw err;
375
- });
376
- // Step 9: Handle success (idle or recovered)
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(artifactPath, 'utf-8');
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: lastUsage ? { usage: lastUsage } : undefined,
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 should not mask the primary error
617
+ // Teardown errors must not mask the primary result.
414
618
  }
415
619
  }
416
620
  }