@strands-agents/sdk 1.14.0 → 1.16.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 (155) hide show
  1. package/dist/src/agent/agent-delegation.d.ts +9 -30
  2. package/dist/src/agent/agent-delegation.d.ts.map +1 -1
  3. package/dist/src/agent/agent-delegation.js +26 -98
  4. package/dist/src/agent/agent-delegation.js.map +1 -1
  5. package/dist/src/agent/agent.d.ts +26 -8
  6. package/dist/src/agent/agent.d.ts.map +1 -1
  7. package/dist/src/agent/agent.js +132 -53
  8. package/dist/src/agent/agent.js.map +1 -1
  9. package/dist/src/agent/continuation.d.ts +41 -0
  10. package/dist/src/agent/continuation.d.ts.map +1 -1
  11. package/dist/src/agent/continuation.js +41 -0
  12. package/dist/src/agent/continuation.js.map +1 -1
  13. package/dist/src/background-tasks/background-tasks.d.ts +41 -0
  14. package/dist/src/background-tasks/background-tasks.d.ts.map +1 -0
  15. package/dist/src/background-tasks/background-tasks.js +402 -0
  16. package/dist/src/background-tasks/background-tasks.js.map +1 -0
  17. package/dist/src/background-tasks/errors.d.ts +15 -0
  18. package/dist/src/background-tasks/errors.d.ts.map +1 -0
  19. package/dist/src/background-tasks/errors.js +21 -0
  20. package/dist/src/background-tasks/errors.js.map +1 -0
  21. package/dist/src/background-tasks/in-process/engine.d.ts +95 -0
  22. package/dist/src/background-tasks/in-process/engine.d.ts.map +1 -0
  23. package/dist/src/background-tasks/in-process/engine.js +306 -0
  24. package/dist/src/background-tasks/in-process/engine.js.map +1 -0
  25. package/dist/src/background-tasks/in-process/manager.d.ts +41 -0
  26. package/dist/src/background-tasks/in-process/manager.d.ts.map +1 -0
  27. package/dist/src/background-tasks/in-process/manager.js +214 -0
  28. package/dist/src/background-tasks/in-process/manager.js.map +1 -0
  29. package/dist/src/background-tasks/in-process/types.d.ts +71 -0
  30. package/dist/src/background-tasks/in-process/types.d.ts.map +1 -0
  31. package/dist/src/background-tasks/in-process/types.js +2 -0
  32. package/dist/src/background-tasks/in-process/types.js.map +1 -0
  33. package/dist/src/background-tasks/manager.d.ts +59 -0
  34. package/dist/src/background-tasks/manager.d.ts.map +1 -0
  35. package/dist/src/background-tasks/manager.js +2 -0
  36. package/dist/src/background-tasks/manager.js.map +1 -0
  37. package/dist/src/background-tasks/timer.d.ts +3 -0
  38. package/dist/src/background-tasks/timer.d.ts.map +1 -0
  39. package/dist/src/background-tasks/timer.js +11 -0
  40. package/dist/src/background-tasks/timer.js.map +1 -0
  41. package/dist/src/background-tasks/types.d.ts +60 -0
  42. package/dist/src/background-tasks/types.d.ts.map +1 -0
  43. package/dist/src/background-tasks/types.js +11 -0
  44. package/dist/src/background-tasks/types.js.map +1 -0
  45. package/dist/src/hooks/events.d.ts +6 -5
  46. package/dist/src/hooks/events.d.ts.map +1 -1
  47. package/dist/src/hooks/events.js +5 -4
  48. package/dist/src/hooks/events.js.map +1 -1
  49. package/dist/src/hooks/types.d.ts +1 -0
  50. package/dist/src/hooks/types.d.ts.map +1 -1
  51. package/dist/src/hooks/types.js +1 -0
  52. package/dist/src/hooks/types.js.map +1 -1
  53. package/dist/src/index.d.ts +3 -0
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +2 -0
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/models/bedrock.d.ts +11 -0
  58. package/dist/src/models/bedrock.d.ts.map +1 -1
  59. package/dist/src/models/bedrock.js +61 -10
  60. package/dist/src/models/bedrock.js.map +1 -1
  61. package/dist/src/models/google/adapters.d.ts.map +1 -1
  62. package/dist/src/models/google/adapters.js +18 -4
  63. package/dist/src/models/google/adapters.js.map +1 -1
  64. package/dist/src/models/google/model.d.ts.map +1 -1
  65. package/dist/src/models/google/model.js +9 -7
  66. package/dist/src/models/google/model.js.map +1 -1
  67. package/dist/src/models/google/types.d.ts +2 -0
  68. package/dist/src/models/google/types.d.ts.map +1 -1
  69. package/dist/src/models/model.d.ts +4 -0
  70. package/dist/src/models/model.d.ts.map +1 -1
  71. package/dist/src/models/model.js.map +1 -1
  72. package/dist/src/models/openai/cache.d.ts +31 -0
  73. package/dist/src/models/openai/cache.d.ts.map +1 -0
  74. package/dist/src/models/openai/cache.js +56 -0
  75. package/dist/src/models/openai/cache.js.map +1 -0
  76. package/dist/src/models/openai/chat-adapter.d.ts.map +1 -1
  77. package/dist/src/models/openai/chat-adapter.js +2 -0
  78. package/dist/src/models/openai/chat-adapter.js.map +1 -1
  79. package/dist/src/models/openai/model.d.ts.map +1 -1
  80. package/dist/src/models/openai/model.js +9 -3
  81. package/dist/src/models/openai/model.js.map +1 -1
  82. package/dist/src/models/openai/responses-adapter.d.ts.map +1 -1
  83. package/dist/src/models/openai/responses-adapter.js +3 -1
  84. package/dist/src/models/openai/responses-adapter.js.map +1 -1
  85. package/dist/src/models/openai/types.d.ts +5 -1
  86. package/dist/src/models/openai/types.d.ts.map +1 -1
  87. package/dist/src/models/routing/fallback-strategy.d.ts +13 -0
  88. package/dist/src/models/routing/fallback-strategy.d.ts.map +1 -0
  89. package/dist/src/models/routing/fallback-strategy.js +33 -0
  90. package/dist/src/models/routing/fallback-strategy.js.map +1 -0
  91. package/dist/src/models/routing/index.d.ts +11 -0
  92. package/dist/src/models/routing/index.d.ts.map +1 -0
  93. package/dist/src/models/routing/index.js +9 -0
  94. package/dist/src/models/routing/index.js.map +1 -0
  95. package/dist/src/models/routing/router.d.ts +161 -0
  96. package/dist/src/models/routing/router.d.ts.map +1 -0
  97. package/dist/src/models/routing/router.js +474 -0
  98. package/dist/src/models/routing/router.js.map +1 -0
  99. package/dist/src/models/routing/strategy.d.ts +53 -0
  100. package/dist/src/models/routing/strategy.d.ts.map +1 -0
  101. package/dist/src/models/routing/strategy.js +2 -0
  102. package/dist/src/models/routing/strategy.js.map +1 -0
  103. package/dist/src/models/streaming.d.ts +12 -0
  104. package/dist/src/models/streaming.d.ts.map +1 -1
  105. package/dist/src/models/streaming.js +17 -0
  106. package/dist/src/models/streaming.js.map +1 -1
  107. package/dist/src/retry/default-model-retry-strategy.d.ts +3 -8
  108. package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -1
  109. package/dist/src/retry/default-model-retry-strategy.js +3 -8
  110. package/dist/src/retry/default-model-retry-strategy.js.map +1 -1
  111. package/dist/src/retry/model-retry-strategy.d.ts +6 -9
  112. package/dist/src/retry/model-retry-strategy.d.ts.map +1 -1
  113. package/dist/src/retry/model-retry-strategy.js +8 -12
  114. package/dist/src/retry/model-retry-strategy.js.map +1 -1
  115. package/dist/src/session/session-manager.d.ts +4 -0
  116. package/dist/src/session/session-manager.d.ts.map +1 -1
  117. package/dist/src/session/session-manager.js +6 -0
  118. package/dist/src/session/session-manager.js.map +1 -1
  119. package/dist/src/storage/local-file-storage.d.ts +4 -6
  120. package/dist/src/storage/local-file-storage.d.ts.map +1 -1
  121. package/dist/src/storage/local-file-storage.js +12 -10
  122. package/dist/src/storage/local-file-storage.js.map +1 -1
  123. package/dist/src/storage/search/keyword.d.ts +1 -0
  124. package/dist/src/storage/search/keyword.d.ts.map +1 -1
  125. package/dist/src/storage/search/keyword.js +124 -0
  126. package/dist/src/storage/search/keyword.js.map +1 -1
  127. package/dist/src/storage/search/qmd.d.ts +66 -0
  128. package/dist/src/storage/search/qmd.d.ts.map +1 -0
  129. package/dist/src/storage/search/qmd.js +124 -0
  130. package/dist/src/storage/search/qmd.js.map +1 -0
  131. package/dist/src/telemetry/meter.d.ts +8 -6
  132. package/dist/src/telemetry/meter.d.ts.map +1 -1
  133. package/dist/src/telemetry/meter.js +8 -7
  134. package/dist/src/telemetry/meter.js.map +1 -1
  135. package/dist/src/telemetry/tracer.d.ts.map +1 -1
  136. package/dist/src/telemetry/tracer.js +9 -2
  137. package/dist/src/telemetry/tracer.js.map +1 -1
  138. package/dist/src/tools/executors/executor.d.ts +11 -1
  139. package/dist/src/tools/executors/executor.d.ts.map +1 -1
  140. package/dist/src/tools/executors/executor.js +50 -16
  141. package/dist/src/tools/executors/executor.js.map +1 -1
  142. package/dist/src/tsconfig.tsbuildinfo +1 -1
  143. package/dist/src/types/agent.d.ts +12 -2
  144. package/dist/src/types/agent.d.ts.map +1 -1
  145. package/dist/src/types/agent.js +3 -2
  146. package/dist/src/types/agent.js.map +1 -1
  147. package/dist/src/vended-memory-stores/file-memory-store/index.d.ts +1 -1
  148. package/dist/src/vended-memory-stores/file-memory-store/index.d.ts.map +1 -1
  149. package/dist/src/vended-memory-stores/file-memory-store/store.d.ts +31 -0
  150. package/dist/src/vended-memory-stores/file-memory-store/store.d.ts.map +1 -1
  151. package/dist/src/vended-memory-stores/file-memory-store/store.js +48 -25
  152. package/dist/src/vended-memory-stores/file-memory-store/store.js.map +1 -1
  153. package/dist/src/vended-tools/file-editor/file-editor.js +35 -30
  154. package/dist/src/vended-tools/file-editor/file-editor.js.map +1 -1
  155. package/package.json +21 -4
@@ -7,8 +7,9 @@ import { isValidToolName } from '../tools/tool.js';
7
7
  import { cloneSystemPrompt, systemPromptFromData } from '../types/messages.js';
8
8
  import { normalizeError, ConcurrentInvocationError, StructuredOutputError } from '../errors.js';
9
9
  import { Model } from '../models/model.js';
10
+ import { ModelRouter } from '../models/routing/router.js';
10
11
  import { ModelPlugin } from '../plugins/model-plugin.js';
11
- import { isModelStreamEvent } from '../models/streaming.js';
12
+ import { totalPromptTokens, isModelStreamEvent } from '../models/streaming.js';
12
13
  import { ToolRegistry } from '../registry/tool-registry.js';
13
14
  import { StateStore } from '../state-store.js';
14
15
  import { serializeStateSerializable, loadStateSerializable } from '../types/serializable.js';
@@ -46,6 +47,7 @@ import { takeSnapshot as takeSnapshotInternal, loadSnapshot as loadSnapshotInter
46
47
  import { defaultSandbox } from '../sandbox/default.js';
47
48
  import { summarizeContextTool, truncateContextTool, pinContextTool, createTokenUsageMiddleware, } from '../context-manager/modes/agentic/agentic-context.js';
48
49
  import { ContextManager } from '../context-manager/context-manager.js';
50
+ import { BackgroundTasks } from '../background-tasks/background-tasks.js';
49
51
  /**
50
52
  * Supported string presets for the `contextManager` parameter.
51
53
  */
@@ -142,6 +144,7 @@ export class Agent {
142
144
  * The model provider used by the agent for inference.
143
145
  */
144
146
  model;
147
+ _modelRouter;
145
148
  /**
146
149
  * The system prompt to pass to the model provider.
147
150
  */
@@ -166,6 +169,7 @@ export class Agent {
166
169
  * The session manager for saving and restoring agent sessions, if configured.
167
170
  */
168
171
  sessionManager;
172
+ _sessionId;
169
173
  /**
170
174
  * The memory manager for cross-session memory retrieval and storage, if configured.
171
175
  */
@@ -184,6 +188,20 @@ export class Agent {
184
188
  get sandbox() {
185
189
  return this._sandbox || defaultSandbox.get();
186
190
  }
191
+ /**
192
+ * A stable, unique identifier for the current conversation session.
193
+ *
194
+ * If a SessionManager is attached, delegates to its sessionId.
195
+ * Otherwise, lazily generates and caches a random 8-character hex string.
196
+ */
197
+ get sessionId() {
198
+ if (this.sessionManager)
199
+ return this.sessionManager.sessionId;
200
+ if (!this._sessionId) {
201
+ this._sessionId = globalThis.crypto.randomUUID().slice(0, 8);
202
+ }
203
+ return this._sessionId;
204
+ }
187
205
  _hooksRegistry;
188
206
  _middlewareRegistry;
189
207
  _pluginRegistry;
@@ -208,6 +226,7 @@ export class Agent {
208
226
  _checkpointing;
209
227
  /** Direct tool caller — created via {@link ToolCaller.create} factory. */
210
228
  _toolCaller;
229
+ _backgroundTasks;
211
230
  /**
212
231
  * Creates an instance of the Agent.
213
232
  * @param config - The configuration for the agent.
@@ -231,11 +250,19 @@ export class Agent {
231
250
  ? new MemoryManager(config.memoryManager)
232
251
  : undefined;
233
252
  this._sandbox = config?.sandbox;
234
- if (typeof config?.model === 'string') {
235
- this.model = new BedrockModel({ modelId: config.model });
253
+ const configuredModel = config?.model;
254
+ if (typeof configuredModel === 'string') {
255
+ this.model = new BedrockModel({ modelId: configuredModel });
256
+ }
257
+ else if (configuredModel instanceof ModelRouter) {
258
+ this._modelRouter = configuredModel;
259
+ this.model = configuredModel.defaultModel;
236
260
  }
237
261
  else {
238
- this.model = config?.model ?? new BedrockModel();
262
+ this.model = configuredModel ?? new BedrockModel();
263
+ }
264
+ if (config?.plugins?.some((plugin) => plugin instanceof ModelRouter)) {
265
+ throw new Error('ModelRouter must be passed through Agent({ model }), not plugins');
239
266
  }
240
267
  // Validate and assign conversation manager
241
268
  if (this.model.stateful) {
@@ -258,6 +285,7 @@ export class Agent {
258
285
  this._interventionRegistry = new InterventionRegistry(config?.interventions ?? [], this._hooksRegistry);
259
286
  // Initialize middleware registry
260
287
  this._middlewareRegistry = new MiddlewareRegistry();
288
+ this._modelRouter?.attachToAgent(this);
261
289
  if (config?.contextManager === 'agentic') {
262
290
  this._middlewareRegistry.addInput(InvokeModelStage.Input, createTokenUsageMiddleware());
263
291
  }
@@ -284,10 +312,26 @@ export class Agent {
284
312
  // The plugin is a no-op when no delegation tools fire.
285
313
  const hasAgentDelegation = (config?.plugins ?? []).some((p) => p.name === 'strands:agent-delegation');
286
314
  const contextManagerPlugin = config?.contextManager instanceof ContextManager ? config.contextManager : undefined;
315
+ this._backgroundTasks = config?.backgroundTasks
316
+ ? new BackgroundTasks(config.backgroundTasks === true ? {} : config.backgroundTasks, (tool, context, middlewareInterrupt) => this._toolExecutor.executeBackground({
317
+ agent: this,
318
+ middlewareRegistry: this._middlewareRegistry,
319
+ // Isolate mutable local trace state from overlapping agent work. OTel spans
320
+ // still use the global provider; local traces are not added to AgentResult.traces.
321
+ tracer: new Tracer(config?.traceAttributes),
322
+ meter: this._meter,
323
+ cancelSignal: context.cancelSignal,
324
+ toolInterrupt: context.interrupt,
325
+ middlewareInterrupt,
326
+ toolGuard: (selectedTool) => this._backgroundTasks.assertToolCanRun(selectedTool),
327
+ }, context.toolUse, tool, context.invocationState, (event) => this._invokeCallbacks(event)))
328
+ : undefined;
287
329
  this._pluginRegistry = new PluginRegistry([
330
+ ...(this._modelRouter ? [this._modelRouter] : []),
288
331
  this._conversationManager,
289
332
  ...retryStrategies,
290
333
  ...(config?.plugins ?? []),
334
+ ...(this._backgroundTasks ? [this._backgroundTasks] : []),
291
335
  ...(!hasAgentDelegation ? [new AgentDelegation()] : []),
292
336
  ...((config?.contextManager === 'auto' || config?.contextManager === 'agentic') && !hasOffloader
293
337
  ? [
@@ -940,7 +984,11 @@ export class Agent {
940
984
  * ```
941
985
  */
942
986
  loadSnapshot(snapshot) {
987
+ if (this._initialized)
988
+ this._backgroundTasks?.assertCanLoadSnapshot();
943
989
  loadSnapshotInternal(this, snapshot);
990
+ if (this._initialized && 'state' in snapshot.data)
991
+ this._backgroundTasks?._loadAppState();
944
992
  }
945
993
  /**
946
994
  * Invokes hook callbacks and printer for a stream event.
@@ -1042,6 +1090,24 @@ export class Agent {
1042
1090
  cycleId,
1043
1091
  messages: this.messages,
1044
1092
  });
1093
+ // Closes cycle telemetry exactly once, however the cycle exits: return,
1094
+ // continue, throw, or the consumer closing the public iterator (which
1095
+ // resumes the generator at the suspended yield and runs the finally).
1096
+ // Called before building an AgentResult so its metrics include this cycle.
1097
+ let cycleClosed = false;
1098
+ const closeCycle = (error) => {
1099
+ if (cycleClosed) {
1100
+ return;
1101
+ }
1102
+ cycleClosed = true;
1103
+ this._meter.endCycle(cycleStartTime);
1104
+ if (error) {
1105
+ this._tracer.endAgentLoopSpan(cycleSpan, { error });
1106
+ }
1107
+ else {
1108
+ this._tracer.endAgentLoopSpan(cycleSpan);
1109
+ }
1110
+ };
1045
1111
  try {
1046
1112
  // Normalize input and append user messages on first invocation only
1047
1113
  if (currentArgs !== undefined) {
@@ -1073,8 +1139,7 @@ export class Agent {
1073
1139
  if (structuredOutputTool && structuredOutputChoice) {
1074
1140
  throw new StructuredOutputError('The model failed to invoke the structured output tool even after it was forced.');
1075
1141
  }
1076
- this._meter.endCycle(cycleStartTime);
1077
- this._tracer.endAgentLoopSpan(cycleSpan);
1142
+ closeCycle();
1078
1143
  // Schema set, model ignored the tool — drop the response and force the tool next cycle.
1079
1144
  // Appending the plain-text turn here would leave the conversation ending on an
1080
1145
  // assistant message, which providers like Bedrock reject as assistant prefill.
@@ -1105,8 +1170,7 @@ export class Agent {
1105
1170
  const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
1106
1171
  yield this._appendMessage(modelResult.message, invocationState);
1107
1172
  yield this._appendMessage(toolResultMessage, invocationState);
1108
- this._meter.endCycle(cycleStartTime);
1109
- this._tracer.endAgentLoopSpan(cycleSpan);
1173
+ closeCycle();
1110
1174
  result = new AgentResult({
1111
1175
  stopReason: 'cancelled',
1112
1176
  lastMessage: modelResult.message,
@@ -1126,8 +1190,7 @@ export class Agent {
1126
1190
  const priorResumePosition = resumePosition;
1127
1191
  resumePosition = undefined;
1128
1192
  if (priorResumePosition !== 'afterModel') {
1129
- this._meter.endCycle(cycleStartTime);
1130
- this._tracer.endAgentLoopSpan(cycleSpan);
1193
+ closeCycle();
1131
1194
  result = new AgentResult({
1132
1195
  stopReason: 'checkpoint',
1133
1196
  lastMessage: modelResult.message,
@@ -1143,19 +1206,18 @@ export class Agent {
1143
1206
  }
1144
1207
  // Execute tools
1145
1208
  const toolsResult = yield* this.executeTools(assistantMessage, invocationState, completedToolResults);
1146
- // When the consumer breaks the stream (e.g. agent.cancel() + break),
1147
- // yield* returns undefined because the inner generator was closed.
1209
+ // Reached when the consumer breaks the stream during tool execution.
1210
+ // _streamCore's drain loop calls .return(), which runs the finally in
1211
+ // executeTools and yields its AfterToolsEvent, and the drain's next()
1212
+ // then completes the closed generator, so this yield* evaluates to
1213
+ // undefined instead of unwinding.
1148
1214
  if (!toolsResult) {
1149
- this._meter.endCycle(cycleStartTime);
1150
- this._tracer.endAgentLoopSpan(cycleSpan);
1151
1215
  continue;
1152
1216
  }
1153
1217
  const toolResultMessage = toolsResult.message;
1154
1218
  // Tools were skipped (not executed) — preserve pending state so the next resume
1155
1219
  // can run them.
1156
1220
  if (this.isCancelled && toolsResult.toolsSkipped && this._interruptState.pendingToolExecution) {
1157
- this._meter.endCycle(cycleStartTime);
1158
- this._tracer.endAgentLoopSpan(cycleSpan);
1159
1221
  continue;
1160
1222
  }
1161
1223
  /**
@@ -1173,15 +1235,17 @@ export class Agent {
1173
1235
  if (this._interruptState.activated) {
1174
1236
  this._interruptState.deactivate();
1175
1237
  }
1176
- this._meter.endCycle(cycleStartTime);
1177
- this._tracer.endAgentLoopSpan(cycleSpan);
1178
- // Hook requested halt: exit without calling the model again
1238
+ closeCycle();
1239
+ // Hook requested halt with content: exit without calling the model again.
1179
1240
  const { afterToolsEvent } = toolsResult;
1180
- if (afterToolsEvent.endTurn) {
1181
- const endTurnText = typeof afterToolsEvent.endTurn === 'string'
1182
- ? afterToolsEvent.endTurn
1183
- : 'Turn ended early by hook after tool execution';
1184
- const lastMessage = new Message({ role: 'assistant', content: [new TextBlock(endTurnText)] });
1241
+ const endTurnValue = afterToolsEvent.endTurn;
1242
+ if (endTurnValue === true || (endTurnValue !== false && endTurnValue.length > 0)) {
1243
+ const endTurnContent = Array.isArray(endTurnValue)
1244
+ ? [...endTurnValue]
1245
+ : [
1246
+ new TextBlock(typeof endTurnValue === 'string' ? endTurnValue : 'Turn ended early by hook after tool execution'),
1247
+ ];
1248
+ const lastMessage = new Message({ role: 'assistant', content: endTurnContent });
1185
1249
  yield this._appendMessage(lastMessage, invocationState);
1186
1250
  result = new AgentResult({
1187
1251
  stopReason: 'endTurn',
@@ -1224,10 +1288,12 @@ export class Agent {
1224
1288
  }
1225
1289
  }
1226
1290
  catch (error) {
1227
- this._meter.endCycle(cycleStartTime);
1228
- this._tracer.endAgentLoopSpan(cycleSpan, { error: error });
1291
+ closeCycle(error);
1229
1292
  throw error;
1230
1293
  }
1294
+ finally {
1295
+ closeCycle();
1296
+ }
1231
1297
  }
1232
1298
  }
1233
1299
  catch (error) {
@@ -1451,23 +1517,23 @@ export class Agent {
1451
1517
  if (this.systemPrompt !== undefined) {
1452
1518
  streamOptions.systemPrompt = this.systemPrompt;
1453
1519
  }
1454
- // Add tool choice if provided
1455
1520
  if (toolChoice) {
1456
1521
  streamOptions.toolChoice = toolChoice;
1457
1522
  }
1458
1523
  let attemptCount = 1;
1459
1524
  while (true) {
1460
- // Estimate input tokens for the upcoming model call (non-fatal if estimation fails)
1525
+ const selectedModel = this._modelForAttempt(invocationState);
1461
1526
  let projectedInputTokens;
1462
1527
  try {
1528
+ // Context management continues to size against the agent's default model.
1463
1529
  projectedInputTokens = await this._estimateInputTokens(streamOptions);
1464
1530
  }
1465
- catch (e) {
1466
- logger.debug(`error=<${e}> | token estimation failed, proceeding without estimate`);
1531
+ catch (error) {
1532
+ logger.debug(`error=<${error}> | token estimation failed, proceeding without estimate`);
1467
1533
  }
1468
1534
  const beforeModelCallEvent = new BeforeModelCallEvent({
1469
1535
  agent: this,
1470
- model: this.model,
1536
+ model: selectedModel,
1471
1537
  invocationState,
1472
1538
  ...(projectedInputTokens !== undefined && { projectedInputTokens }),
1473
1539
  });
@@ -1488,14 +1554,14 @@ export class Agent {
1488
1554
  const stopData = { message, stopReason: 'endTurn' };
1489
1555
  const afterModelCallEvent = new AfterModelCallEvent({
1490
1556
  agent: this,
1491
- model: this.model,
1557
+ model: selectedModel,
1492
1558
  attemptCount,
1493
1559
  stopData,
1494
1560
  invocationState,
1495
1561
  });
1496
1562
  yield afterModelCallEvent;
1497
1563
  if (afterModelCallEvent.retry) {
1498
- attemptCount += 1;
1564
+ attemptCount = this._nextAttemptCount(selectedModel, invocationState, attemptCount);
1499
1565
  continue;
1500
1566
  }
1501
1567
  return { message, stopReason: 'endTurn' };
@@ -1510,9 +1576,11 @@ export class Agent {
1510
1576
  logger.debug(`error=<${error}> | token estimation failed after continuation input, proceeding without estimate`);
1511
1577
  }
1512
1578
  }
1579
+ const invokedModelRef = {};
1513
1580
  try {
1514
- const result = yield* this._invokeModelWithMiddleware(invocationState, toolChoice, projectedInputTokens);
1515
- // Accumulate token usage and model latency metrics
1581
+ const result = yield* this._invokeModelWithMiddleware(invocationState, selectedModel, invokedModelRef, toolChoice, projectedInputTokens);
1582
+ const routedModel = this._modelRouter?.getRoutedModel(this, invocationState) ?? selectedModel;
1583
+ const model = invokedModelRef.model ?? selectedModel;
1516
1584
  this._meter.updateCycle(result.metadata);
1517
1585
  yield new ModelMessageEvent({
1518
1586
  agent: this,
@@ -1520,7 +1588,6 @@ export class Agent {
1520
1588
  stopReason: result.stopReason,
1521
1589
  invocationState,
1522
1590
  });
1523
- // Handle user content redaction if guardrails blocked input
1524
1591
  if (result.redaction?.userMessage) {
1525
1592
  this._redactLastMessage(result.redaction.userMessage);
1526
1593
  }
@@ -1531,45 +1598,50 @@ export class Agent {
1531
1598
  };
1532
1599
  const afterModelCallEvent = new AfterModelCallEvent({
1533
1600
  agent: this,
1534
- model: this.model,
1601
+ model,
1535
1602
  attemptCount,
1536
1603
  stopData,
1537
1604
  invocationState,
1538
1605
  });
1539
1606
  yield afterModelCallEvent;
1540
1607
  if (afterModelCallEvent.retry) {
1541
- attemptCount += 1;
1608
+ attemptCount = this._nextAttemptCount(routedModel, invocationState, attemptCount);
1542
1609
  continue;
1543
1610
  }
1544
1611
  return result;
1545
1612
  }
1546
1613
  catch (error) {
1614
+ const routedModel = this._modelRouter?.getRoutedModel(this, invocationState) ?? selectedModel;
1615
+ // A failure before the terminal ran (e.g. in input middleware) is attributed to the routed model.
1616
+ const failedModel = invokedModelRef.model ?? routedModel;
1547
1617
  const modelError = normalizeError(error);
1548
- // Create error event
1549
1618
  const errorEvent = new AfterModelCallEvent({
1550
1619
  agent: this,
1551
- model: this.model,
1620
+ model: failedModel,
1552
1621
  attemptCount,
1553
1622
  error: modelError,
1554
1623
  invocationState,
1555
1624
  });
1556
- // Yield error event - stream will invoke hooks
1557
1625
  yield errorEvent;
1558
- // Let CancelledError propagate directly no retry
1559
- // (we emit the AfterModelCall because we already emitted Before and we guarentee the pair)
1626
+ // Preserve the Before/After event pair, but never retry cancellation.
1560
1627
  if (error instanceof CancelledError) {
1561
1628
  throw error;
1562
1629
  }
1563
- // After yielding, hooks have been invoked and may have set retry
1564
1630
  if (errorEvent.retry) {
1565
- attemptCount += 1;
1631
+ attemptCount = this._nextAttemptCount(routedModel, invocationState, attemptCount);
1566
1632
  continue;
1567
1633
  }
1568
- // Re-throw error
1569
1634
  throw error;
1570
1635
  }
1571
1636
  }
1572
1637
  }
1638
+ _modelForAttempt(invocationState) {
1639
+ return this._modelRouter?.getRoutedModel(this, invocationState) ?? this.model;
1640
+ }
1641
+ _nextAttemptCount(model, invocationState, attemptCount) {
1642
+ const nextModel = this._modelRouter?.getRoutedModel(this, invocationState);
1643
+ return nextModel !== undefined && nextModel !== model ? 1 : attemptCount + 1;
1644
+ }
1573
1645
  /**
1574
1646
  * Invokes the model through the InvokeModelStage middleware chain.
1575
1647
  * Builds an InvokeModelContext from current agent state and composes the
@@ -1577,13 +1649,15 @@ export class Agent {
1577
1649
  * using context fields directly (not re-derived from the agent).
1578
1650
  *
1579
1651
  * @param invocationState - Per-invocation state shared across hooks and tools
1652
+ * @param selectedModel - Model the chain starts with; middleware may replace it
1653
+ * @param invokedModelRef - Slot filled with the model the terminal actually invoked
1580
1654
  * @param toolChoice - Optional tool choice to force specific tool usage
1581
1655
  * @returns StreamAggregatedResult from the model (or middleware short-circuit)
1582
1656
  */
1583
- async *_invokeModelWithMiddleware(invocationState, toolChoice, projectedInputTokens) {
1657
+ async *_invokeModelWithMiddleware(invocationState, selectedModel, invokedModelRef, toolChoice, projectedInputTokens) {
1584
1658
  const context = {
1585
1659
  agent: this,
1586
- model: this.model,
1660
+ model: selectedModel,
1587
1661
  messages: this.messages.map((msg) => msg.clone()),
1588
1662
  ...(this.systemPrompt !== undefined && { systemPrompt: cloneSystemPrompt(this.systemPrompt) }),
1589
1663
  toolSpecs: deepCopy(this._toolRegistry.list().map((tool) => tool.toolSpec)),
@@ -1600,6 +1674,7 @@ export class Agent {
1600
1674
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1601
1675
  const self = this;
1602
1676
  const middlewareResult = yield* this._middlewareRegistry.invoke(InvokeModelStage, context, async function* (ctx) {
1677
+ invokedModelRef.model = ctx.model;
1603
1678
  const modelId = ctx.model.modelId;
1604
1679
  const modelSpan = self._tracer.startModelInvokeSpan({
1605
1680
  messages: ctx.messages,
@@ -1749,6 +1824,10 @@ export class Agent {
1749
1824
  tracer: this._tracer,
1750
1825
  meter: this._meter,
1751
1826
  cancelSignal: this._abortSignal,
1827
+ ...(this._backgroundTasks && {
1828
+ backgroundTasks: this._backgroundTasks,
1829
+ backgroundTaskPassId: assistantMessage.trackingId,
1830
+ }),
1752
1831
  }, {
1753
1832
  toolUseBlocks,
1754
1833
  toolResultBlocks,
@@ -1826,10 +1905,10 @@ export class Agent {
1826
1905
  /**
1827
1906
  * Estimate the input token count for the next model call.
1828
1907
  *
1829
- * Uses the token counting strategy: reads inputTokens + outputTokens
1830
- * from the last assistant message's metadata as a known baseline, then estimates
1831
- * only new messages added after it. Falls back to full estimation when no metadata
1832
- * is available (cold start or first call).
1908
+ * Uses the token counting strategy: reads the total prompt the model processed (including cached
1909
+ * tokens) plus outputTokens from the last assistant message's metadata as a known baseline, then
1910
+ * estimates only new messages added after it. Falls back to full estimation when no metadata is
1911
+ * available (cold start or first call).
1833
1912
  *
1834
1913
  * @param streamOptions - The stream options containing system prompt and tool specs
1835
1914
  * @returns Estimated input token count
@@ -1846,7 +1925,7 @@ export class Agent {
1846
1925
  let estimate;
1847
1926
  if (lastAssistantIdx >= 0) {
1848
1927
  const usage = this.messages[lastAssistantIdx].metadata.usage;
1849
- const knownBaseline = usage.inputTokens + usage.outputTokens;
1928
+ const knownBaseline = totalPromptTokens(usage) + usage.outputTokens;
1850
1929
  const newMessages = this.messages.slice(lastAssistantIdx + 1);
1851
1930
  if (newMessages.length === 0) {
1852
1931
  estimate = knownBaseline;