condukt 0.7.0 → 0.9.0

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