@n8n/agents 0.26.3 → 0.27.1

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 (70) hide show
  1. package/dist/build.tsbuildinfo +1 -1
  2. package/dist/runtime/loop/agent-runtime.d.ts +24 -51
  3. package/dist/runtime/loop/agent-runtime.js +321 -318
  4. package/dist/runtime/loop/agent-runtime.js.map +1 -1
  5. package/dist/runtime/loop/checkpoint-data.d.ts +12 -0
  6. package/dist/runtime/loop/checkpoint-data.js +65 -0
  7. package/dist/runtime/loop/checkpoint-data.js.map +1 -0
  8. package/dist/runtime/loop/execution-counter.js +1 -5
  9. package/dist/runtime/loop/execution-counter.js.map +1 -1
  10. package/dist/runtime/loop/generate-sink.d.ts +2 -2
  11. package/dist/runtime/loop/generate-sink.js +3 -5
  12. package/dist/runtime/loop/generate-sink.js.map +1 -1
  13. package/dist/runtime/loop/run-output-sink.d.ts +3 -78
  14. package/dist/runtime/loop/run-output-sink.js +7 -0
  15. package/dist/runtime/loop/run-output-sink.js.map +1 -1
  16. package/dist/runtime/loop/runtime-context.d.ts +17 -1
  17. package/dist/runtime/loop/runtime-context.js +49 -20
  18. package/dist/runtime/loop/runtime-context.js.map +1 -1
  19. package/dist/runtime/loop/runtime-helpers.d.ts +1 -1
  20. package/dist/runtime/loop/stream-sink.d.ts +9 -2
  21. package/dist/runtime/loop/stream-sink.js +78 -56
  22. package/dist/runtime/loop/stream-sink.js.map +1 -1
  23. package/dist/runtime/memory/memory-orchestrator.d.ts +1 -1
  24. package/dist/runtime/memory/memory-orchestrator.js +8 -8
  25. package/dist/runtime/memory/memory-orchestrator.js.map +1 -1
  26. package/dist/runtime/memory/memory-store.d.ts +1 -1
  27. package/dist/runtime/memory/memory-store.js +7 -2
  28. package/dist/runtime/memory/memory-store.js.map +1 -1
  29. package/dist/runtime/memory/tool-result-memory.d.ts +7 -0
  30. package/dist/runtime/memory/tool-result-memory.js +65 -0
  31. package/dist/runtime/memory/tool-result-memory.js.map +1 -0
  32. package/dist/runtime/model/messages.js +20 -9
  33. package/dist/runtime/model/messages.js.map +1 -1
  34. package/dist/runtime/model/raw-error.d.ts +1 -1
  35. package/dist/runtime/state/run-state.d.ts +2 -2
  36. package/dist/runtime/state/run-state.js +4 -4
  37. package/dist/runtime/state/run-state.js.map +1 -1
  38. package/dist/runtime/telemetry/runtime-telemetry.d.ts +1 -1
  39. package/dist/runtime/tools/delegate-sub-agent-runner.d.ts +13 -0
  40. package/dist/runtime/tools/delegate-sub-agent-runner.js +361 -0
  41. package/dist/runtime/tools/delegate-sub-agent-runner.js.map +1 -0
  42. package/dist/runtime/tools/delegate-sub-agent-tool.d.ts +5 -241
  43. package/dist/runtime/tools/delegate-sub-agent-tool.js +94 -487
  44. package/dist/runtime/tools/delegate-sub-agent-tool.js.map +1 -1
  45. package/dist/runtime/tools/tool-adapter.js +28 -28
  46. package/dist/runtime/tools/tool-adapter.js.map +1 -1
  47. package/dist/runtime/tools/tool-call-executor.d.ts +19 -86
  48. package/dist/runtime/tools/tool-call-executor.js +236 -579
  49. package/dist/runtime/tools/tool-call-executor.js.map +1 -1
  50. package/dist/runtime/tools/tool-call-runner.d.ts +25 -0
  51. package/dist/runtime/tools/tool-call-runner.js +328 -0
  52. package/dist/runtime/tools/tool-call-runner.js.map +1 -0
  53. package/dist/runtime/tools/tool-result-guard.d.ts +2 -7
  54. package/dist/runtime/tools/tool-result-guard.js +13 -70
  55. package/dist/runtime/tools/tool-result-guard.js.map +1 -1
  56. package/dist/types/runtime/agent-loop.d.ts +73 -0
  57. package/dist/types/runtime/agent-loop.js +3 -0
  58. package/dist/types/runtime/agent-loop.js.map +1 -0
  59. package/dist/types/runtime/agent-runtime.d.ts +51 -0
  60. package/dist/types/runtime/agent-runtime.js +3 -0
  61. package/dist/types/runtime/agent-runtime.js.map +1 -0
  62. package/dist/types/runtime/delegation.d.ts +234 -0
  63. package/dist/types/runtime/delegation.js +82 -0
  64. package/dist/types/runtime/delegation.js.map +1 -0
  65. package/dist/types/runtime/tool-execution.d.ts +103 -0
  66. package/dist/types/runtime/tool-execution.js +3 -0
  67. package/dist/types/runtime/tool-execution.js.map +1 -0
  68. package/dist/types/sdk/agent-builder.d.ts +1 -1
  69. package/dist/types/sdk/memory.d.ts +2 -1
  70. package/package.json +6 -6
@@ -1,22 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentRuntime = void 0;
4
+ const checkpoint_data_1 = require("./checkpoint-data");
4
5
  const execution_counter_1 = require("./execution-counter");
5
6
  const generate_sink_1 = require("./generate-sink");
6
7
  const hydrate_file_parts_1 = require("./hydrate-file-parts");
7
8
  const runtime_context_1 = require("./runtime-context");
8
9
  const runtime_helpers_1 = require("./runtime-helpers");
9
10
  const stream_sink_1 = require("./stream-sink");
10
- const cancellation_1 = require("../../sdk/cancellation");
11
11
  const catalog_1 = require("../../sdk/catalog");
12
12
  const model_1 = require("../../utils/model");
13
- const parse_1 = require("../../utils/parse");
14
13
  const workspace_1 = require("../../workspace");
15
14
  const logger_1 = require("../logger");
16
15
  const memory_orchestrator_1 = require("../memory/memory-orchestrator");
17
16
  const title_generation_1 = require("../memory/title-generation");
18
17
  const message_list_1 = require("../model/message-list");
19
- const model_factory_1 = require("../model/model-factory");
20
18
  const model_token_counter_1 = require("../model/model-token-counter");
21
19
  const prompt_cache_1 = require("../model/prompt-cache");
22
20
  const active_skills_1 = require("../skills/active-skills");
@@ -28,7 +26,7 @@ const runtime_telemetry_1 = require("../telemetry/runtime-telemetry");
28
26
  const deferred_tool_manager_1 = require("../tools/deferred-tool-manager");
29
27
  const fix_tool_call_1 = require("../tools/fix-tool-call");
30
28
  const tool_call_executor_1 = require("../tools/tool-call-executor");
31
- const MAX_LOOP_ITERATIONS = 30;
29
+ const MAX_LOOP_ITERATIONS = 100;
32
30
  const MAX_EMPTY_TURN_RETRIES = 2;
33
31
  const logger = (0, logger_1.createFilteredLogger)();
34
32
  const EMPTY_MESSAGE_LIST = {
@@ -135,70 +133,22 @@ class AgentRuntime {
135
133
  }
136
134
  async resume(method, data, options) {
137
135
  this.runId = options.runId;
138
- const state = await this.runState.resume(this.runId);
139
- if (!state) {
140
- throw new run_state_1.StaleResumeError(`No suspended run found for runId: ${this.runId}`);
141
- }
142
- const toolCall = state.pendingToolCalls[options.toolCallId];
143
- if (!toolCall) {
144
- throw new run_state_1.StaleResumeError(`No tool call found for toolCallId: ${options.toolCallId}`);
145
- }
146
- if (options.hostMetadata !== undefined && !state.persistence) {
147
- throw new Error('Cannot update host metadata without persistence');
148
- }
149
- const list = message_list_1.AgentMessageList.deserialize(state.messageList);
150
- this.context.hydrateDeferredToolsFromList(list);
151
- await (0, hydrate_file_parts_1.hydrateFileParts)(list.messages(), this.config.fileStore, {
152
- threadId: state.persistence?.threadId,
153
- });
154
- const tool = this.context
155
- .getCurrentTools(state.persistence)
156
- .find((t) => t.name === toolCall.toolName);
157
- if (!tool)
158
- throw new Error(`Tool ${toolCall.toolName} not found`);
159
- let resumeData = data;
136
+ const { state, list, resumeData } = await this.prepareToolResume(data, options);
160
137
  let abortScope;
161
138
  let resumeClaimed = false;
162
- const resumeSchema = toolCall.suspended ? toolCall.resumeSchema : tool.resumeSchema;
163
- if (!(0, cancellation_1.isCancellation)(resumeData) && resumeSchema) {
164
- const parseResult = await (0, parse_1.parseWithSchema)(resumeSchema, data, { stripUnknown: true });
165
- if (!parseResult.success) {
166
- throw new Error(`Invalid resume payload: ${parseResult.error}`);
167
- }
168
- resumeData = parseResult.data;
169
- }
170
139
  try {
171
140
  const { runId: _rid, toolCallId: _tcid, onResumeClaimed: _onResumeClaimed, hostMetadata, ...callerExecOptions } = options;
172
- const persisted = state.executionOptions ?? {};
173
- const persistedMaxIterations = persisted.maxIterations;
174
- const callerMaxIterations = callerExecOptions.maxIterations;
175
- if (callerMaxIterations !== undefined &&
176
- persistedMaxIterations !== undefined &&
177
- callerMaxIterations < persistedMaxIterations) {
178
- throw new Error(`Cannot decrease maxIterations when resuming a run. Expected >= ${persistedMaxIterations}, received ${callerMaxIterations}.`);
179
- }
180
- const mergedMaxIterations = callerMaxIterations ?? persistedMaxIterations;
181
- const mergedExecOptions = {
182
- ...callerExecOptions,
183
- ...(mergedMaxIterations !== undefined ? { maxIterations: mergedMaxIterations } : {}),
184
- ...(state.iterationCount !== undefined ? { iterationCount: state.iterationCount } : {}),
185
- };
141
+ const mergedExecOptions = (0, checkpoint_data_1.mergeResumeExecutionOptions)(state, callerExecOptions);
186
142
  const claimed = await this.runState.claimResume(this.runId, state);
187
143
  if (!claimed) {
188
144
  throw new run_state_1.StaleResumeError(`Run ${this.runId} is not suspended. Cannot resume.`);
189
145
  }
190
146
  resumeClaimed = true;
191
147
  const resumeOptions = {
192
- persistence: state.persistence
193
- ? {
194
- ...state.persistence,
195
- ...(state.persistence.hostMetadata || hostMetadata
196
- ? { hostMetadata: { ...state.persistence.hostMetadata, ...hostMetadata } }
197
- : {}),
198
- }
199
- : undefined,
148
+ persistence: (0, checkpoint_data_1.mergeResumePersistence)(state.persistence, hostMetadata),
200
149
  ...mergedExecOptions,
201
150
  };
151
+ this.updateState({ persistence: resumeOptions.persistence });
202
152
  await options.onResumeClaimed?.();
203
153
  abortScope = this.eventBus.createAbortScope(resumeOptions.abortSignal);
204
154
  const activeAbortScope = abortScope;
@@ -207,109 +157,43 @@ class AgentRuntime {
207
157
  resumeToolCallId: options.toolCallId,
208
158
  resumeData,
209
159
  };
210
- await this.ensureModelCost();
211
- await this.memory.setListObservationLogMemory(list, state.persistence);
212
- await this.memory.applyObservationMask(list, state.persistence);
213
- if (method === 'generate') {
214
- const sink = new generate_sink_1.GenerateSink(this.createRunServices());
215
- const rawResult = await this.telemetry.withRootSpan('generate', resumeOptions, this.runId, async () => await this.runAgentLoop({
216
- list,
217
- options: resumeOptions,
218
- abortScope: activeAbortScope,
219
- pendingResume,
220
- }, sink));
221
- try {
222
- return this.finalizeGenerate(rawResult, list);
223
- }
224
- finally {
225
- abortScope.dispose();
226
- }
227
- }
228
- return {
229
- runId: this.runId,
230
- stream: this.startStream({
231
- list,
232
- options: resumeOptions,
233
- abortScope: activeAbortScope,
234
- pendingResume,
235
- }),
236
- getState: () => this.getState(),
160
+ await this.prepareResumeMemory(list, state.persistence);
161
+ const ctx = {
162
+ list,
163
+ options: resumeOptions,
164
+ abortScope: activeAbortScope,
165
+ pendingResume,
237
166
  };
167
+ if (method === 'generate')
168
+ return await this.generateResumedRun(ctx);
169
+ return this.createResumedStream(ctx);
238
170
  }
239
171
  catch (error) {
240
- const isAbort = abortScope?.isAborted ?? false;
241
- abortScope?.dispose();
242
- if (error instanceof run_state_1.StaleResumeError)
243
- throw error;
244
- this.updateState({ status: isAbort ? 'cancelled' : 'failed' });
245
- if (!isAbort) {
246
- this.eventBus.emit({ type: "error", message: String(error), error });
247
- }
248
- if (resumeClaimed)
249
- await this.cleanupRun();
250
- if (method === 'generate') {
251
- return {
252
- runId: this.runId,
253
- messages: [],
254
- finishReason: 'error',
255
- error,
256
- getState: () => this.getState(),
257
- };
258
- }
259
- return { runId: this.runId, stream: (0, runtime_helpers_1.makeErrorStream)(error), getState: () => this.getState() };
172
+ return await this.handleResumeFailure(method, error, abortScope, resumeClaimed);
260
173
  }
261
174
  }
262
175
  async crashResume(options) {
263
176
  this.runId = options.runId;
264
- const state = await this.runState.loadForCrashResume(this.runId);
265
- if (!state)
266
- throw new Error(`No checkpoint found for runId: ${this.runId}`);
267
- if (state.status !== 'running') {
268
- throw new Error(`Checkpoint for runId ${this.runId} has status '${state.status}' — crashResume only accepts step checkpoints; use resume() for suspended runs`);
269
- }
270
- if (Object.keys(state.pendingToolCalls).length > 0) {
271
- throw new Error(`Checkpoint for runId ${this.runId} has pending tool calls — crashResume only accepts step checkpoints`);
272
- }
273
- const list = message_list_1.AgentMessageList.deserialize(state.messageList);
274
- this.context.hydrateDeferredToolsFromList(list);
275
- await (0, hydrate_file_parts_1.hydrateFileParts)(list.messages(), this.config.fileStore, {
276
- threadId: state.persistence?.threadId,
277
- });
177
+ const state = await this.loadStepCheckpoint();
178
+ const list = await this.restoreCheckpointMessages(state);
278
179
  let abortScope;
279
180
  try {
280
181
  const { runId: _rid, contextNotes, ...callerExecOptions } = options;
281
- const persisted = state.executionOptions ?? {};
282
- const persistedMaxIterations = persisted.maxIterations;
283
- const callerMaxIterations = callerExecOptions.maxIterations;
284
- if (callerMaxIterations !== undefined &&
285
- persistedMaxIterations !== undefined &&
286
- callerMaxIterations < persistedMaxIterations) {
287
- throw new Error(`Cannot decrease maxIterations when resuming a run. Expected >= ${persistedMaxIterations}, received ${callerMaxIterations}.`);
288
- }
289
- const mergedMaxIterations = callerMaxIterations ?? persistedMaxIterations;
290
182
  const resumeOptions = {
291
183
  persistence: state.persistence,
292
- ...callerExecOptions,
293
- ...(mergedMaxIterations !== undefined ? { maxIterations: mergedMaxIterations } : {}),
294
- ...(state.iterationCount !== undefined ? { iterationCount: state.iterationCount } : {}),
184
+ ...(0, checkpoint_data_1.mergeResumeExecutionOptions)(state, callerExecOptions),
295
185
  };
296
186
  for (const note of contextNotes ?? []) {
297
187
  list.addInput([{ role: 'user', content: [{ type: 'text', text: note }] }]);
298
188
  }
299
189
  abortScope = this.eventBus.createAbortScope(resumeOptions.abortSignal);
300
190
  const activeAbortScope = abortScope;
301
- await this.ensureModelCost();
302
- await this.memory.setListObservationLogMemory(list, state.persistence);
303
- await this.memory.applyObservationMask(list, state.persistence);
304
- return {
305
- runId: this.runId,
306
- stream: this.startStream({
307
- list,
308
- options: resumeOptions,
309
- abortScope: activeAbortScope,
310
- }),
311
- getState: () => this.getState(),
312
- };
191
+ await this.prepareResumeMemory(list, state.persistence);
192
+ return this.createResumedStream({
193
+ list,
194
+ options: resumeOptions,
195
+ abortScope: activeAbortScope,
196
+ });
313
197
  }
314
198
  catch (error) {
315
199
  const isAbort = abortScope?.isAborted ?? false;
@@ -321,6 +205,92 @@ class AgentRuntime {
321
205
  return { runId: this.runId, stream: (0, runtime_helpers_1.makeErrorStream)(error), getState: () => this.getState() };
322
206
  }
323
207
  }
208
+ async prepareToolResume(data, options) {
209
+ const state = await this.runState.resume(this.runId);
210
+ if (!state) {
211
+ throw new run_state_1.StaleResumeError(`No suspended run found for runId: ${this.runId}`);
212
+ }
213
+ const toolCall = state.pendingToolCalls[options.toolCallId];
214
+ if (!toolCall) {
215
+ throw new run_state_1.StaleResumeError(`No tool call found for toolCallId: ${options.toolCallId}`);
216
+ }
217
+ if (options.hostMetadata !== undefined && !state.persistence) {
218
+ throw new Error('Cannot update host metadata without persistence');
219
+ }
220
+ const list = await this.restoreCheckpointMessages(state);
221
+ const tool = this.context
222
+ .getCurrentTools(state.persistence)
223
+ .find((t) => t.name === toolCall.toolName);
224
+ if (!tool)
225
+ throw new Error(`Tool ${toolCall.toolName} not found`);
226
+ const resumeSchema = toolCall.suspended ? toolCall.resumeSchema : tool.resumeSchema;
227
+ const resumeData = await (0, checkpoint_data_1.parseResumeData)(data, resumeSchema);
228
+ return { state, list, resumeData };
229
+ }
230
+ async handleResumeFailure(method, error, abortScope, resumeClaimed) {
231
+ const isAbort = abortScope?.isAborted ?? false;
232
+ abortScope?.dispose();
233
+ if (error instanceof run_state_1.StaleResumeError)
234
+ throw error;
235
+ this.updateState({ status: isAbort ? 'cancelled' : 'failed' });
236
+ if (!isAbort) {
237
+ this.eventBus.emit({ type: "error", message: String(error), error });
238
+ }
239
+ if (resumeClaimed)
240
+ await this.cleanupRun();
241
+ if (method === 'generate') {
242
+ return {
243
+ runId: this.runId,
244
+ messages: [],
245
+ finishReason: 'error',
246
+ error,
247
+ getState: () => this.getState(),
248
+ };
249
+ }
250
+ return { runId: this.runId, stream: (0, runtime_helpers_1.makeErrorStream)(error), getState: () => this.getState() };
251
+ }
252
+ async restoreCheckpointMessages(state) {
253
+ const list = message_list_1.AgentMessageList.deserialize(state.messageList);
254
+ this.context.hydrateDeferredToolsFromList(list);
255
+ await (0, hydrate_file_parts_1.hydrateFileParts)(list.messages(), this.config.fileStore, {
256
+ threadId: state.persistence?.threadId,
257
+ });
258
+ return list;
259
+ }
260
+ async prepareResumeMemory(list, persistence) {
261
+ await this.ensureModelCost();
262
+ await this.memory.setListObservationLogMemory(list, persistence);
263
+ await this.memory.applyObservationMask(list, persistence);
264
+ }
265
+ async generateResumedRun(ctx) {
266
+ const sink = new generate_sink_1.GenerateSink(this.createRunServices());
267
+ const rawResult = await this.telemetry.withRootSpan('generate', ctx.options, this.runId, async () => await this.runAgentLoop(ctx, sink));
268
+ try {
269
+ return this.finalizeGenerate(rawResult, ctx.list);
270
+ }
271
+ finally {
272
+ ctx.abortScope.dispose();
273
+ }
274
+ }
275
+ createResumedStream(ctx) {
276
+ return {
277
+ runId: this.runId,
278
+ stream: this.startStream(ctx),
279
+ getState: () => this.getState(),
280
+ };
281
+ }
282
+ async loadStepCheckpoint() {
283
+ const state = await this.runState.loadForCrashResume(this.runId);
284
+ if (!state)
285
+ throw new Error(`No checkpoint found for runId: ${this.runId}`);
286
+ if (state.status !== 'running') {
287
+ throw new Error(`Checkpoint for runId ${this.runId} has status '${state.status}' — crashResume only accepts step checkpoints; use resume() for suspended runs`);
288
+ }
289
+ if (Object.keys(state.pendingToolCalls).length > 0) {
290
+ throw new Error(`Checkpoint for runId ${this.runId} has pending tool calls — crashResume only accepts step checkpoints`);
291
+ }
292
+ return state;
293
+ }
324
294
  async buildMessageList(input, options) {
325
295
  const list = new message_list_1.AgentMessageList();
326
296
  await this.memory.loadInto(list, options);
@@ -407,176 +377,222 @@ class AgentRuntime {
407
377
  }
408
378
  }
409
379
  async runAgentLoop(ctx, sink) {
410
- const { list, options, abortScope, pendingResume } = ctx;
380
+ const { prepared, state } = await this.prepareLoop(ctx);
381
+ if (prepared.pendingResume) {
382
+ const settlement = await this.resumePendingTools(prepared, sink, state, prepared.pendingResume);
383
+ if (settlement.suspended)
384
+ return settlement.result;
385
+ }
386
+ for (; state.iterationCount < state.maxIterations; state.iterationCount++) {
387
+ const settlement = await this.runLoopIteration(prepared, sink, state);
388
+ if (settlement.suspended)
389
+ return settlement.result;
390
+ if (state.reachedStopCondition)
391
+ break;
392
+ }
393
+ if (!state.reachedStopCondition && state.iterationCount >= state.maxIterations) {
394
+ state.lastFinishReason = 'max-iterations';
395
+ }
396
+ return await sink.finishComplete({
397
+ list: ctx.list,
398
+ options: ctx.options,
399
+ finishReason: state.lastFinishReason,
400
+ usage: state.totalUsage,
401
+ structuredOutput: state.structuredOutput,
402
+ });
403
+ }
404
+ async prepareLoop(ctx) {
405
+ const { list, options } = ctx;
411
406
  await this.activeSkills?.restore(list, options?.persistence);
412
407
  this.context.hydrateDeferredToolsFromList(list);
413
408
  list.mcpConnectionNote = (0, runtime_helpers_1.formatMcpConnectionNote)(this.config.mcpConnectionFailures ?? []);
414
- let totalUsage;
415
- let lastFinishReason = 'stop';
416
- let structuredOutput;
417
409
  const runTelemetry = this.telemetry.resolve(options);
418
- const staticLoopContext = this.context.buildStaticLoopContext({
410
+ const staticContext = this.context.buildStaticLoopContext({
419
411
  ...options,
420
412
  persistence: options?.persistence,
421
413
  });
422
- const instructionProviderOptions = (0, prompt_cache_1.mergeProviderOptions)((0, prompt_cache_1.buildInstructionPromptCacheOptions)(this.config.promptCaching, this.modelIdString), this.config.instructionProviderOptions);
423
- const maxIterations = options?.maxIterations ?? MAX_LOOP_ITERATIONS;
424
- let iterationCount = options?.iterationCount ?? 0;
425
- let reachedStopCondition = false;
426
- const inputMessages = new Set(list.inputDelta());
414
+ const instructionProviderOptions = this.context.buildInstructionProviderOptions();
415
+ const state = {
416
+ totalUsage: undefined,
417
+ lastFinishReason: 'stop',
418
+ structuredOutput: undefined,
419
+ maxIterations: options?.maxIterations ?? MAX_LOOP_ITERATIONS,
420
+ iterationCount: options?.iterationCount ?? 0,
421
+ reachedStopCondition: false,
422
+ };
423
+ return {
424
+ prepared: {
425
+ ...ctx,
426
+ runTelemetry,
427
+ staticContext,
428
+ instructionProviderOptions,
429
+ canDiscardRejectedInput: this.canDiscardRejectedInput(ctx),
430
+ },
431
+ state,
432
+ };
433
+ }
434
+ canDiscardRejectedInput(ctx) {
435
+ const inputMessages = new Set(ctx.list.inputDelta());
427
436
  const inputIds = new Set([...inputMessages].map((message) => message.id));
428
- const canDiscardRejectedInput = ctx.isFreshRun === true &&
437
+ return (ctx.isFreshRun === true &&
429
438
  [...inputMessages].some((message) => 'role' in message &&
430
439
  message.role === 'user' &&
431
440
  Array.isArray(message.content) &&
432
441
  message.content.some((part) => part.type === 'file' && part.data !== undefined)) &&
433
- !list.messages().some((message) => !inputMessages.has(message) && inputIds.has(message.id));
434
- const buildToolBatchContext = (toolMap) => ({
442
+ !ctx.list
443
+ .messages()
444
+ .some((message) => !inputMessages.has(message) && inputIds.has(message.id)));
445
+ }
446
+ buildToolBatchContext(ctx, toolMap) {
447
+ return {
435
448
  toolMap,
436
- list,
449
+ list: ctx.list,
437
450
  runId: this.runId,
438
- persistence: options?.persistence,
439
- telemetry: runTelemetry,
440
- executionCounter: options?.executionCounter,
441
- abortSignal: abortScope.signal,
442
- isAborted: () => abortScope.isAborted,
451
+ persistence: ctx.options?.persistence,
452
+ telemetry: ctx.runTelemetry,
453
+ executionCounter: ctx.options?.executionCounter,
454
+ abortSignal: ctx.abortScope.signal,
455
+ isAborted: () => ctx.abortScope.isAborted,
456
+ };
457
+ }
458
+ async resumePendingTools(ctx, sink, state, pendingResume) {
459
+ const { toolMap } = this.context.buildToolLoopContext(ctx.staticContext.aiProviderTools, ctx.options?.persistence, ctx.options?.executionCounter, ctx.list);
460
+ const batch = await this.toolExecutor.iteratePendingToolCallsConcurrent({
461
+ ...this.buildToolBatchContext(ctx, toolMap),
462
+ pendingResume,
443
463
  });
444
- const finishToolBatch = async (batch, toolMap, nextIteration) => {
445
- const hasPending = Object.keys(batch.pending).length > 0;
446
- let completed = false;
447
- try {
448
- this.assertNotAborted(abortScope);
449
- await sink.emitToolBatch(batch);
450
- this.assertNotAborted(abortScope);
451
- if (!hasPending) {
452
- completed = true;
453
- return { suspended: false };
454
- }
455
- await this.persistSuspension(batch.pending, options, list, totalUsage, maxIterations, nextIteration);
456
- this.assertNotAborted(abortScope);
457
- const result = await sink.finishSuspended({
458
- suspendRunId: this.runId,
459
- list,
460
- usage: totalUsage,
461
- suspensions: batch.suspensions,
462
- });
463
- this.assertNotAborted(abortScope);
464
- completed = true;
465
- return { suspended: true, result };
466
- }
467
- finally {
468
- if (!completed && hasPending) {
469
- await this.toolExecutor.cleanupPendingToolCalls(batch.pending, buildToolBatchContext(toolMap), abortScope.isAborted ? 'Run aborted' : 'Parent run failed before suspension');
470
- try {
471
- await this.runState.cancel(this.runId);
472
- }
473
- catch {
474
- }
475
- }
476
- }
464
+ const settlement = await this.finishToolBatch(ctx, sink, state, batch, toolMap, state.iterationCount);
465
+ if (settlement.suspended)
466
+ return settlement;
467
+ await this.memory.maybeObserveMidRun(ctx.list, ctx.options);
468
+ return settlement;
469
+ }
470
+ async runLoopIteration(ctx, sink, state) {
471
+ this.assertNotAborted(ctx.abortScope);
472
+ this.eventBus.emit({ type: "turn_start" });
473
+ const { toolMap, modelCallContext } = await this.prepareModelCall(ctx, state.iterationCount);
474
+ const turn = await this.callModelWithRetries(ctx, sink, state, modelCallContext);
475
+ this.assertNotAborted(ctx.abortScope);
476
+ state.lastFinishReason = turn.finishReason;
477
+ if (!(0, runtime_helpers_1.isReasoningOnlyStop)(turn))
478
+ ctx.list.addResponse(turn.newMessages);
479
+ sink.onTurnFolded?.();
480
+ if (turn.aiFinishReason !== 'tool-calls') {
481
+ if (turn.errorReason)
482
+ throw new Error(turn.errorReason.message);
483
+ state.structuredOutput = turn.structuredOutput;
484
+ this.emitTurnEnd(turn.newMessages, (0, runtime_helpers_1.extractSettledToolCalls)(turn.newMessages));
485
+ state.reachedStopCondition = true;
486
+ return { suspended: false };
487
+ }
488
+ const batch = await this.toolExecutor.iterateToolCallsConcurrent({
489
+ ...this.buildToolBatchContext(ctx, toolMap),
490
+ toolCalls: turn.toolCalls,
491
+ });
492
+ const settlement = await this.finishToolBatch(ctx, sink, state, batch, toolMap, state.iterationCount + 1);
493
+ if (settlement.suspended)
494
+ return settlement;
495
+ await this.completeToolTurn(ctx, state, turn);
496
+ return settlement;
497
+ }
498
+ async prepareModelCall(ctx, iterationCount) {
499
+ const { list, options, abortScope, staticContext } = ctx;
500
+ for (const toolName of this.activeSkills?.toolDependencies() ?? []) {
501
+ this.deferredToolManager?.load(toolName);
502
+ }
503
+ const tools = this.context.buildToolLoopContext(staticContext.aiProviderTools, options?.persistence, options?.executionCounter, list);
504
+ const hostVolatileInstructions = await this.resolveVolatileInstructions(options?.persistence);
505
+ const prompt = this.context.buildModelPrompt({
506
+ list,
507
+ tools,
508
+ instructionProviderOptions: ctx.instructionProviderOptions,
509
+ hostVolatileInstructions,
510
+ activeSkills: this.activeSkills,
511
+ });
512
+ const modelCallContext = {
513
+ model: staticContext.model,
514
+ system: prompt.system,
515
+ messages: prompt.messages,
516
+ abortSignal: abortScope.signal,
517
+ hasTools: tools.hasTools,
518
+ aiTools: prompt.aiTools,
519
+ reasoning: staticContext.reasoning,
520
+ providerOptions: staticContext.providerOptions,
521
+ outputSpec: staticContext.outputSpec,
522
+ maxOutputTokens: staticContext.maxOutputTokens,
523
+ aiSdkOptions: this.buildAiSdkOptions(tools.toolMap, options),
524
+ onInputRejected: this.createInputRejectionHandler(ctx, iterationCount),
477
525
  };
478
- if (pendingResume) {
479
- const pendingLoopContext = this.context.buildToolLoopContext(staticLoopContext.aiProviderTools, options?.persistence, options?.executionCounter, list);
480
- const batch = await this.toolExecutor.iteratePendingToolCallsConcurrent({
481
- ...buildToolBatchContext(pendingLoopContext.toolMap),
482
- pendingResume,
483
- });
484
- const finalized = await finishToolBatch(batch, pendingLoopContext.toolMap, iterationCount);
485
- if (finalized.suspended)
486
- return finalized.result;
487
- await this.memory.maybeObserveMidRun(list, options);
526
+ return { toolMap: tools.toolMap, modelCallContext };
527
+ }
528
+ createInputRejectionHandler(ctx, iterationCount) {
529
+ if (!ctx.canDiscardRejectedInput || iterationCount !== 0)
530
+ return undefined;
531
+ return async () => {
532
+ if (ctx.abortScope.isAborted)
533
+ return;
534
+ await this.memory.discardRejectedInput(ctx.list, ctx.options);
535
+ this.updateState({ messageList: ctx.list.serialize() });
536
+ };
537
+ }
538
+ async callModelWithRetries(ctx, sink, state, modelCallContext) {
539
+ let turn = await sink.callModel(modelCallContext);
540
+ for (let retry = 0; retry < MAX_EMPTY_TURN_RETRIES && (0, runtime_helpers_1.isEmptyModelTurn)(turn); retry++) {
541
+ this.recordTurnUsage(ctx, sink, state, turn);
542
+ this.assertNotAborted(ctx.abortScope);
543
+ turn = await sink.callModel(modelCallContext);
488
544
  }
489
- for (; iterationCount < maxIterations; iterationCount++) {
545
+ this.recordTurnUsage(ctx, sink, state, turn);
546
+ return turn;
547
+ }
548
+ recordTurnUsage(ctx, sink, state, turn) {
549
+ state.totalUsage = (0, runtime_helpers_1.mergeUsage)(state.totalUsage, turn.usage);
550
+ (0, execution_counter_1.incrementTokenCountFromUsage)(ctx.options?.executionCounter, turn.usage);
551
+ sink.reportUsage(state.totalUsage);
552
+ }
553
+ async finishToolBatch(ctx, sink, state, batch, toolMap, nextIteration) {
554
+ const { list, options, abortScope } = ctx;
555
+ const hasPending = Object.keys(batch.pending).length > 0;
556
+ let completed = false;
557
+ try {
490
558
  this.assertNotAborted(abortScope);
491
- this.eventBus.emit({ type: "turn_start" });
492
- for (const toolName of this.activeSkills?.toolDependencies() ?? []) {
493
- this.deferredToolManager?.load(toolName);
494
- }
495
- const { toolMap, aiTools, hasTools, effectiveInstructions, volatileInstructions, staticToolCacheName, } = this.context.buildToolLoopContext(staticLoopContext.aiProviderTools, options?.persistence, options?.executionCounter, list);
496
- const hostVolatileInstructions = await this.resolveVolatileInstructions(options?.persistence);
497
- const combinedVolatileInstructions = [volatileInstructions, hostVolatileInstructions]
498
- .map((value) => value?.trim())
499
- .filter((value) => Boolean(value))
500
- .join('\n\n');
501
- const { system, messages } = list.forLlm([effectiveInstructions, this.activeSkills?.instructions()]
502
- .filter(Boolean)
503
- .join('\n\n'), instructionProviderOptions, combinedVolatileInstructions || undefined, (0, model_factory_1.supportsSplitSystemMessages)(this.config.model));
504
- const cached = (0, prompt_cache_1.applyRuntimeCacheBreakpoints)({
505
- system,
506
- messages: this.activeSkills?.modelMessages(messages, list) ?? messages,
507
- aiTools,
508
- promptCaching: this.config.promptCaching,
509
- modelId: this.modelIdString,
510
- staticToolCacheName,
511
- });
512
- const modelCallContext = {
513
- model: staticLoopContext.model,
514
- system,
515
- messages: cached.messages,
516
- abortSignal: abortScope.signal,
517
- hasTools,
518
- aiTools: cached.aiTools,
519
- reasoning: staticLoopContext.reasoning,
520
- providerOptions: staticLoopContext.providerOptions,
521
- outputSpec: staticLoopContext.outputSpec,
522
- maxOutputTokens: staticLoopContext.maxOutputTokens,
523
- aiSdkOptions: this.buildAiSdkOptions(toolMap, options),
524
- onInputRejected: canDiscardRejectedInput && iterationCount === 0
525
- ? async () => {
526
- if (abortScope.isAborted)
527
- return;
528
- await this.memory.discardRejectedInput(list, options);
529
- this.updateState({ messageList: list.serialize() });
530
- }
531
- : undefined,
532
- };
533
- let turn = await sink.callModel(modelCallContext);
534
- for (let emptyRetry = 0; emptyRetry < MAX_EMPTY_TURN_RETRIES && (0, runtime_helpers_1.isEmptyModelTurn)(turn); emptyRetry++) {
535
- totalUsage = (0, runtime_helpers_1.mergeUsage)(totalUsage, turn.usage);
536
- (0, execution_counter_1.incrementTokenCountFromUsage)(options?.executionCounter, turn.usage);
537
- sink.reportUsage(totalUsage);
538
- this.assertNotAborted(abortScope);
539
- turn = await sink.callModel(modelCallContext);
540
- }
541
- totalUsage = (0, runtime_helpers_1.mergeUsage)(totalUsage, turn.usage);
542
- (0, execution_counter_1.incrementTokenCountFromUsage)(options?.executionCounter, turn.usage);
543
- sink.reportUsage(totalUsage);
559
+ await sink.emitToolBatch(batch);
544
560
  this.assertNotAborted(abortScope);
545
- lastFinishReason = turn.finishReason;
546
- if (!(0, runtime_helpers_1.isReasoningOnlyStop)(turn))
547
- list.addResponse(turn.newMessages);
548
- sink.onTurnFolded?.();
549
- if (turn.aiFinishReason !== 'tool-calls') {
550
- if (turn.errorReason)
551
- throw new Error(turn.errorReason.message);
552
- structuredOutput = turn.structuredOutput;
553
- this.emitTurnEnd(turn.newMessages, (0, runtime_helpers_1.extractSettledToolCalls)(turn.newMessages));
554
- reachedStopCondition = true;
555
- break;
561
+ if (!hasPending) {
562
+ completed = true;
563
+ return { suspended: false };
556
564
  }
557
- const batch = await this.toolExecutor.iterateToolCallsConcurrent({
558
- ...buildToolBatchContext(toolMap),
559
- toolCalls: turn.toolCalls,
565
+ await this.persistSuspension(batch.pending, options, list, state.totalUsage, state.maxIterations, nextIteration);
566
+ this.assertNotAborted(abortScope);
567
+ const result = await sink.finishSuspended({
568
+ suspendRunId: this.runId,
569
+ list,
570
+ usage: state.totalUsage,
571
+ suspensions: batch.suspensions,
560
572
  });
561
- const finalized = await finishToolBatch(batch, toolMap, iterationCount + 1);
562
- if (finalized.suspended)
563
- return finalized.result;
564
- this.emitTurnEnd(turn.newMessages, (0, runtime_helpers_1.extractSettledToolCalls)(list.responseDelta()));
565
- await this.memory.maybeObserveMidRun(list, options);
566
- if (options?.stepCheckpoints) {
567
- await this.persistStepCheckpoint(list, totalUsage, options, maxIterations, iterationCount + 1);
568
- }
573
+ this.assertNotAborted(abortScope);
574
+ completed = true;
575
+ return { suspended: true, result };
569
576
  }
570
- if (!reachedStopCondition && iterationCount >= maxIterations) {
571
- lastFinishReason = 'max-iterations';
577
+ finally {
578
+ if (!completed && hasPending)
579
+ await this.cleanupFailedSuspension(ctx, batch.pending, toolMap);
580
+ }
581
+ }
582
+ async cleanupFailedSuspension(ctx, pending, toolMap) {
583
+ await this.toolExecutor.cleanupPendingToolCalls(pending, this.buildToolBatchContext(ctx, toolMap), ctx.abortScope.isAborted ? 'Run aborted' : 'Parent run failed before suspension');
584
+ try {
585
+ await this.runState.cancel(this.runId, this.getState());
586
+ }
587
+ catch {
588
+ }
589
+ }
590
+ async completeToolTurn(ctx, state, turn) {
591
+ this.emitTurnEnd(turn.newMessages, (0, runtime_helpers_1.extractSettledToolCalls)(ctx.list.responseDelta()));
592
+ await this.memory.maybeObserveMidRun(ctx.list, ctx.options);
593
+ if (ctx.options?.stepCheckpoints) {
594
+ await this.persistStepCheckpoint(ctx.list, state.totalUsage, ctx.options, state.maxIterations, state.iterationCount + 1);
572
595
  }
573
- return await sink.finishComplete({
574
- list,
575
- options,
576
- finishReason: lastFinishReason,
577
- usage: totalUsage,
578
- structuredOutput,
579
- });
580
596
  }
581
597
  async resolveVolatileInstructions(persistence) {
582
598
  try {
@@ -597,15 +613,7 @@ class AgentRuntime {
597
613
  options: ctx.options,
598
614
  withRootSpan: async (operation, options, runId, fn) => await this.telemetry.withRootSpan(operation, options, runId, fn),
599
615
  runLoop: async (guard) => {
600
- for (const failure of this.config.mcpConnectionFailures ?? []) {
601
- void guard.write({
602
- type: 'warning',
603
- message: failure.error,
604
- code: 'mcp_connection_failed',
605
- source: 'mcp',
606
- server: failure.server,
607
- });
608
- }
616
+ this.writeMcpConnectionWarnings(guard);
609
617
  const resolvedList = ctx.list ?? (await this.initRun(ctx.input, ctx.options));
610
618
  list = resolvedList;
611
619
  sink = new stream_sink_1.StreamSink(guard, this.createRunServices(), ctx.options);
@@ -632,52 +640,47 @@ class AgentRuntime {
632
640
  emitError: (error) => this.eventBus.emit({ type: "error", message: String(error), error }),
633
641
  });
634
642
  }
635
- async persistSuspension(pendingToolCalls, options, list, totalUsage, maxIterations, iterationCount) {
636
- const resolvedMaxIterations = maxIterations ?? options?.maxIterations;
637
- const resolvedIterationCount = iterationCount ?? options?.iterationCount;
638
- const executionOptions = resolvedMaxIterations !== undefined ? { maxIterations: resolvedMaxIterations } : undefined;
639
- for (const pending of Object.values(pendingToolCalls)) {
640
- if (!pending.suspended)
641
- continue;
642
- const payload = typeof pending.suspendPayload === 'object' && pending.suspendPayload !== null
643
- ? pending.suspendPayload
644
- : undefined;
645
- list.markToolCallSuspended(pending.toolCallId, {
646
- ...(typeof payload?.message === 'string' ? { message: payload.message } : {}),
647
- ...(typeof payload?.requestId === 'string' ? { requestId: payload.requestId } : {}),
643
+ writeMcpConnectionWarnings(guard) {
644
+ for (const failure of this.config.mcpConnectionFailures ?? []) {
645
+ void guard.write({
646
+ type: 'warning',
647
+ message: failure.error,
648
+ code: 'mcp_connection_failed',
649
+ source: 'mcp',
650
+ server: failure.server,
648
651
  });
649
652
  }
653
+ }
654
+ async persistSuspension(pendingToolCalls, options, list, totalUsage, maxIterations, iterationCount) {
655
+ const checkpointOptions = (0, checkpoint_data_1.buildCheckpointOptions)(options, maxIterations, iterationCount);
656
+ (0, checkpoint_data_1.markSuspendedToolCalls)(list, pendingToolCalls);
650
657
  const state = {
651
658
  persistence: options?.persistence,
652
659
  status: 'suspended',
653
660
  messageList: list.serialize(),
654
661
  pendingToolCalls,
655
662
  usage: totalUsage,
656
- executionOptions,
657
- ...(resolvedIterationCount !== undefined ? { iterationCount: resolvedIterationCount } : {}),
663
+ ...checkpointOptions,
658
664
  };
659
665
  await this.runState.suspend(this.runId, state);
660
666
  this.updateState({ status: 'suspended', pendingToolCalls, messageList: list.serialize() });
661
667
  await this.memory.persistTurnDelta(list, options);
662
668
  }
663
669
  async persistStepCheckpoint(list, totalUsage, options, maxIterations, iterationCount) {
664
- const resolvedMaxIterations = maxIterations ?? options?.maxIterations;
665
- const resolvedIterationCount = iterationCount ?? options?.iterationCount;
666
- const executionOptions = resolvedMaxIterations !== undefined ? { maxIterations: resolvedMaxIterations } : undefined;
670
+ const checkpointOptions = (0, checkpoint_data_1.buildCheckpointOptions)(options, maxIterations, iterationCount);
667
671
  const state = {
668
672
  persistence: options?.persistence,
669
673
  status: 'running',
670
674
  messageList: list.serialize(),
671
675
  pendingToolCalls: {},
672
676
  usage: totalUsage,
673
- executionOptions,
674
- ...(resolvedIterationCount !== undefined ? { iterationCount: resolvedIterationCount } : {}),
677
+ ...checkpointOptions,
675
678
  };
676
679
  await this.runState.checkpointStep(this.runId, state);
677
680
  }
678
681
  async cleanupRun() {
679
682
  try {
680
- await this.runState.complete(this.runId);
683
+ await this.runState.complete(this.runId, this.getState());
681
684
  }
682
685
  catch (error) {
683
686
  logger.warn('Failed to clean up agent run checkpoint', { runId: this.runId, error });