@sentry/junior 0.195.0 → 0.197.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 (76) hide show
  1. package/dist/{agent-hooks-VIBTIP5F.js → agent-hooks-CM4EKXL4.js} +13 -13
  2. package/dist/api/schema.js +3 -3
  3. package/dist/api.js +27 -27
  4. package/dist/app.d.ts +8 -2
  5. package/dist/app.js +102 -108
  6. package/dist/{catalog-runtime-6E6SGRZK.js → catalog-runtime-V7WGCXUD.js} +4 -4
  7. package/dist/chat/agent/handoff.d.ts +73 -0
  8. package/dist/chat/config.d.ts +2 -2
  9. package/dist/chat/model-profile.d.ts +12 -0
  10. package/dist/chat/no-reply.d.ts +4 -3
  11. package/dist/chat/services/automated-turn-limit.d.ts +13 -34
  12. package/dist/chat/services/subscribed-decision.d.ts +8 -0
  13. package/dist/chat/slack/automated-turn-limit-notice.d.ts +2 -24
  14. package/dist/chat/tools/types.d.ts +6 -2
  15. package/dist/{chunk-RKA22BQP.js → chunk-2K26W3FP.js} +1 -1
  16. package/dist/{chunk-FE3Y4KES.js → chunk-4USCVHQZ.js} +5 -5
  17. package/dist/{chunk-PFQRZ4IB.js → chunk-4UUKQB6I.js} +7 -7
  18. package/dist/{chunk-KQ3GZGBK.js → chunk-5GWV7SXF.js} +4 -11
  19. package/dist/{chunk-7PQMTME5.js → chunk-5MMQLB2B.js} +1 -1
  20. package/dist/{chunk-FPQPYJEG.js → chunk-6ZQ3CE6X.js} +1 -1
  21. package/dist/{chunk-HUQOXMDG.js → chunk-7ENMKNEK.js} +1 -1
  22. package/dist/{chunk-GFBR4S3Q.js → chunk-7NL7T6VU.js} +1 -1
  23. package/dist/{chunk-I4RH3KLJ.js → chunk-BGSAFVJO.js} +1 -1
  24. package/dist/{chunk-3VWIZVK7.js → chunk-BQ7UXCUO.js} +1 -1
  25. package/dist/{chunk-LMYRF6HX.js → chunk-BR5GNDPS.js} +85 -14
  26. package/dist/{chunk-L62JVQPL.js → chunk-C2SOA3KN.js} +3 -3
  27. package/dist/{chunk-JBSCMOLR.js → chunk-DBTI74IT.js} +6 -6
  28. package/dist/{chunk-X3PBQ4BK.js → chunk-DHWYAHAM.js} +1 -1
  29. package/dist/{chunk-RQO67SAO.js → chunk-DLXA2ZXJ.js} +2 -2
  30. package/dist/{chunk-KBPWC7NU.js → chunk-EZKISZ3Q.js} +1 -1
  31. package/dist/{chunk-6TRQLG3D.js → chunk-FCONFDZ4.js} +1 -1
  32. package/dist/{chunk-7UED4D5T.js → chunk-H6QL3FWC.js} +22 -22
  33. package/dist/{chunk-5HNXLP4W.js → chunk-HDSDOBWN.js} +1 -1
  34. package/dist/{chunk-QKHZF2LH.js → chunk-IGSAXSM7.js} +4 -4
  35. package/dist/{chunk-HE5PZAPS.js → chunk-ITP4OR7R.js} +4 -4
  36. package/dist/{chunk-QPZL2IYT.js → chunk-LBN67OZR.js} +1 -1
  37. package/dist/{chunk-R5OHONYU.js → chunk-LIK4T6CF.js} +6 -6
  38. package/dist/{chunk-RZ3PBNDV.js → chunk-LMTHFU5D.js} +7 -7
  39. package/dist/{chunk-5NR7N567.js → chunk-MJCJ62RZ.js} +4 -4
  40. package/dist/{chunk-PYTRZZ4V.js → chunk-NANQFFGO.js} +8 -8
  41. package/dist/{chunk-D4YV5TDV.js → chunk-NHJU2ZAL.js} +11 -0
  42. package/dist/{chunk-FEZIEEOP.js → chunk-O2H7JUEQ.js} +289 -334
  43. package/dist/{chunk-CE2P6TOV.js → chunk-O5RG7SKU.js} +3 -3
  44. package/dist/{chunk-7MYXH5LA.js → chunk-PF3PEX4Q.js} +1 -1
  45. package/dist/{chunk-C4NY6L7S.js → chunk-PM5WSDAD.js} +1 -1
  46. package/dist/{chunk-KUQRWV53.js → chunk-S3DRTWPX.js} +5 -5
  47. package/dist/{chunk-N67T4VY3.js → chunk-SBCWV4E4.js} +9 -9
  48. package/dist/{chunk-6GGDBXXP.js → chunk-T5MK3RUT.js} +3 -3
  49. package/dist/{chunk-OQHO62VA.js → chunk-V2IX5Q5L.js} +8 -8
  50. package/dist/{chunk-DTYOEHUP.js → chunk-VAUPAX3O.js} +4 -4
  51. package/dist/{chunk-BJZML4RT.js → chunk-WOJG25QZ.js} +9 -9
  52. package/dist/{chunk-QHJZSYKY.js → chunk-XALIC5SY.js} +2 -2
  53. package/dist/{chunk-VYDIA2UL.js → chunk-YC5CFKME.js} +2 -6
  54. package/dist/{chunk-LM7UE523.js → chunk-YLBJBBYX.js} +3 -3
  55. package/dist/{chunk-R5BGW4WR.js → chunk-YZQLXALE.js} +2 -2
  56. package/dist/cli/chat.js +35 -35
  57. package/dist/cli/check.js +5 -5
  58. package/dist/cli/plugins.js +14 -14
  59. package/dist/cli/snapshot-create.js +10 -10
  60. package/dist/cli/upgrade.js +7 -7
  61. package/dist/{db-FVCSDROR.js → db-6BD722UD.js} +8 -8
  62. package/dist/instrumentation.js +3 -3
  63. package/dist/nitro.js +7 -7
  64. package/dist/{oauth-callback-server-Z5PXTDOE.js → oauth-callback-server-P6AIT27Q.js} +31 -31
  65. package/dist/{runner-RHYU7OGV.js → runner-OKPDZOJC.js} +23 -23
  66. package/dist/{skills-QTN73DN5.js → skills-CQFX3V2I.js} +5 -5
  67. package/dist/spawn-GA5MADGU.js +42 -0
  68. package/dist/task-runner-7IIYO44C.js +36 -0
  69. package/dist/{validation-2ZK7GA6N.js → validation-STB4W433.js} +5 -5
  70. package/dist/version.js +1 -1
  71. package/dist/{work-7IBKSZRU.js → work-B7N5TEYI.js} +23 -23
  72. package/dist/worker-W6PBEHCA.js +24 -0
  73. package/package.json +5 -5
  74. package/dist/spawn-R56OWLUH.js +0 -42
  75. package/dist/task-runner-LLJZTLRP.js +0 -36
  76. package/dist/worker-RNVQVCO7.js +0 -24
@@ -7,7 +7,7 @@ import {
7
7
  PluginCredentialFailureError,
8
8
  agentNameSchema,
9
9
  createPluginAuthOrchestration
10
- } from "./chunk-JBSCMOLR.js";
10
+ } from "./chunk-DBTI74IT.js";
11
11
  import {
12
12
  parseSandboxEgressAuthRequiredSignal,
13
13
  parseSandboxEgressPermissionDeniedSignal,
@@ -38,7 +38,7 @@ import {
38
38
  searchSlackUsers,
39
39
  selectPluginGrant,
40
40
  toMcpProviderError
41
- } from "./chunk-OQHO62VA.js";
41
+ } from "./chunk-V2IX5Q5L.js";
42
42
  import {
43
43
  RetryableDeliveryError,
44
44
  actorFromRun,
@@ -53,20 +53,19 @@ import {
53
53
  surfaceFromRun,
54
54
  toolInvocationDestination,
55
55
  uploadFilesToConversation
56
- } from "./chunk-R5OHONYU.js";
56
+ } from "./chunk-LIK4T6CF.js";
57
57
  import {
58
58
  CooperativeTurnYieldError,
59
59
  NO_REPLY_MARKER,
60
60
  TurnInputCommitLostError,
61
61
  abandonReplacedPendingAuth,
62
62
  canReusePendingAuthLink,
63
- containsNoReplyMarker,
64
63
  decideReply,
65
64
  getAssistantReplyText,
66
65
  isNoReplyMarker,
67
66
  isTurnInputCommitLostError,
68
67
  sanitizeAssistantText
69
- } from "./chunk-VYDIA2UL.js";
68
+ } from "./chunk-YC5CFKME.js";
70
69
  import {
71
70
  AGENTS_REMOVAL_NOTICE,
72
71
  AGENTS_REPLACEMENT_NOTICE,
@@ -122,7 +121,7 @@ import {
122
121
  stripRuntimeTurnContext,
123
122
  trimTrailingAssistantMessages,
124
123
  truncateText
125
- } from "./chunk-RZ3PBNDV.js";
124
+ } from "./chunk-LMTHFU5D.js";
126
125
  import {
127
126
  SCHEDULED_TASK_SYSTEM_ACTOR,
128
127
  WORKSPACE_SNAPSHOT_FAILED_EVENT,
@@ -163,13 +162,13 @@ import {
163
162
  workspaceSnapshotFinishedEvent,
164
163
  workspaceSnapshotWatch,
165
164
  writableEventTask
166
- } from "./chunk-DTYOEHUP.js";
165
+ } from "./chunk-VAUPAX3O.js";
167
166
  import {
168
167
  parseSlackThreadId
169
- } from "./chunk-6TRQLG3D.js";
168
+ } from "./chunk-FCONFDZ4.js";
170
169
  import {
171
170
  appendAndEnqueueInboundMessage
172
- } from "./chunk-QKHZF2LH.js";
171
+ } from "./chunk-IGSAXSM7.js";
173
172
  import {
174
173
  SHORT_TITLE_MAX_LENGTH,
175
174
  ToolInputError,
@@ -177,7 +176,7 @@ import {
177
176
  generateShortTitle,
178
177
  hasAgentTurnUsage,
179
178
  resolveTaskTitle
180
- } from "./chunk-R5BGW4WR.js";
179
+ } from "./chunk-YZQLXALE.js";
181
180
  import {
182
181
  bindSandboxFileSystem,
183
182
  createSandboxSession,
@@ -188,7 +187,7 @@ import {
188
187
  resolve,
189
188
  setSnapshotSpanAttributes,
190
189
  stopSession
191
- } from "./chunk-L62JVQPL.js";
190
+ } from "./chunk-C2SOA3KN.js";
192
191
  import {
193
192
  privateTraceResultAttributes
194
193
  } from "./chunk-UIE3R5XU.js";
@@ -198,7 +197,7 @@ import {
198
197
  } from "./chunk-X6GDDWQH.js";
199
198
  import {
200
199
  getTurnRequestDeadline
201
- } from "./chunk-I4RH3KLJ.js";
200
+ } from "./chunk-BGSAFVJO.js";
202
201
  import {
203
202
  getVercelConversationWorkQueue
204
203
  } from "./chunk-DKIJ2RP5.js";
@@ -245,23 +244,22 @@ import {
245
244
  updateWorkspace,
246
245
  withSlackRetries,
247
246
  workspaceRepoCheckoutPath
248
- } from "./chunk-PFQRZ4IB.js";
247
+ } from "./chunk-4UUKQB6I.js";
249
248
  import {
250
249
  JUNIOR_THREAD_STATE_TTL_MS
251
250
  } from "./chunk-3Y47VMHV.js";
252
251
  import {
253
- destinationKey,
254
252
  getConversationEventStore,
255
253
  getConversationMessageSearchStore,
256
254
  getConversationStore,
257
255
  getDb,
258
256
  getSqlExecutor
259
- } from "./chunk-KQ3GZGBK.js";
257
+ } from "./chunk-5GWV7SXF.js";
260
258
  import {
261
259
  juniorArtifacts,
262
260
  juniorIdentities,
263
261
  juniorSchedulerRuns
264
- } from "./chunk-FPQPYJEG.js";
262
+ } from "./chunk-6ZQ3CE6X.js";
265
263
  import {
266
264
  SANDBOX_DATA_ROOT,
267
265
  SANDBOX_REPOS_ROOT,
@@ -292,19 +290,19 @@ import {
292
290
  throwSandboxOperationError,
293
291
  workspaceSnapshotNotReadyUserMessage,
294
292
  wrapSandboxSetupError
295
- } from "./chunk-LM7UE523.js";
293
+ } from "./chunk-YLBJBBYX.js";
296
294
  import {
297
295
  fenceLock,
298
296
  getStateAdapter,
299
297
  withLock
300
- } from "./chunk-7MYXH5LA.js";
298
+ } from "./chunk-PF3PEX4Q.js";
301
299
  import {
302
300
  KNOWN_CONVERSATION_EVENT_TYPES,
303
301
  contextProvenance,
304
302
  instructionActors,
305
303
  instructionProvenanceFor,
306
304
  sameActorIdentity
307
- } from "./chunk-GFBR4S3Q.js";
305
+ } from "./chunk-7NL7T6VU.js";
308
306
  import {
309
307
  ACTIVE_TURN_COMPACTION_SUMMARY_PREFIX,
310
308
  COMPACTION_SUMMARY_PREFIX,
@@ -330,14 +328,16 @@ import {
330
328
  resolveGatewayCredential,
331
329
  resolveGatewayModel,
332
330
  unwrapCurrentInstruction
333
- } from "./chunk-LMYRF6HX.js";
331
+ } from "./chunk-BR5GNDPS.js";
334
332
  import {
335
333
  ModelProfileNotConfiguredError,
336
334
  TURN_REASONING_LEVELS,
335
+ formatModelProfile,
336
+ formatModelProfiles,
337
337
  modelIdForProfile,
338
338
  modelProfileSchema,
339
339
  profileConfig
340
- } from "./chunk-D4YV5TDV.js";
340
+ } from "./chunk-NHJU2ZAL.js";
341
341
  import {
342
342
  canExposeConversationPayload,
343
343
  resolveConversationPrivacy,
@@ -347,16 +347,16 @@ import {
347
347
  toGenAiMessagesTraceAttributes,
348
348
  toGenAiPayloadMetadata,
349
349
  toGenAiPayloadTraceAttributes
350
- } from "./chunk-KBPWC7NU.js";
350
+ } from "./chunk-EZKISZ3Q.js";
351
351
  import {
352
352
  discoverSkills,
353
353
  findSkillByName,
354
354
  loadSkillsByName,
355
355
  parseSkillInvocation
356
- } from "./chunk-RQO67SAO.js";
356
+ } from "./chunk-DLXA2ZXJ.js";
357
357
  import {
358
358
  pluginCatalogRuntime
359
- } from "./chunk-RKA22BQP.js";
359
+ } from "./chunk-2K26W3FP.js";
360
360
  import {
361
361
  CredentialUnavailableError,
362
362
  OAuthProviderError,
@@ -364,7 +364,7 @@ import {
364
364
  getOAuthProviderErrorAttributes,
365
365
  resolveAuthTokenPlaceholder,
366
366
  resolvePluginCommandEnv
367
- } from "./chunk-X3PBQ4BK.js";
367
+ } from "./chunk-DHWYAHAM.js";
368
368
  import {
369
369
  credentialContextForActor,
370
370
  credentialUserSubjectId
@@ -400,7 +400,7 @@ import {
400
400
  summarizeMessageText,
401
401
  withLogContext,
402
402
  withSpan
403
- } from "./chunk-HUQOXMDG.js";
403
+ } from "./chunk-7ENMKNEK.js";
404
404
  import {
405
405
  startInactiveSpan,
406
406
  withActiveSpan
@@ -924,11 +924,6 @@ function buildTurnResult(input) {
924
924
  if (!isProviderError) {
925
925
  logInfo("ai.no_reply_marker.accepted", markerAttributes);
926
926
  }
927
- } else if (containsNoReplyMarker(rawPrimaryText)) {
928
- logWarn("ai.no_reply_marker.mixed_text", {
929
- "app.ai.no_reply_marker": true,
930
- "app.ai.no_reply_marker_mode": "mixed"
931
- });
932
927
  }
933
928
  if (!primaryText && !completedWithoutTerminalText && !isProviderError) {
934
929
  logWarn("ai.model_response.empty", {
@@ -1146,11 +1141,11 @@ function trimContextForRouter(text) {
1146
1141
  originalCharCount: trimmed.length
1147
1142
  };
1148
1143
  }
1149
- function buildClassifierSystemPrompt(profileNames, defaultProfile) {
1144
+ function buildClassifierSystemPrompt(profiles, profileNames, defaultProfile) {
1145
+ const profileList = formatModelProfiles(profiles, profileNames);
1150
1146
  return [
1151
1147
  "You choose the model profile most likely to produce a complete, source-grounded answer.",
1152
1148
  "Choose exactly one bucket: none, low, medium, high, or xhigh.",
1153
- `Choose profile from the configured profiles: ${profileNames.join(", ")}.`,
1154
1149
  "Choose profile independently from the reasoning bucket.",
1155
1150
  "",
1156
1151
  "Use none only for greetings, acknowledgments, and turns that need no substantive assistant work.",
@@ -1159,9 +1154,11 @@ function buildClassifierSystemPrompt(profileNames, defaultProfile) {
1159
1154
  "Use high for research-heavy work, non-trivial drafting, or explicit requests to be thorough.",
1160
1155
  "Use xhigh for the most complex tasks: code changes, debugging/root-cause analysis, broad refactors, architecture decisions, multi-file implementation, or any task where deep reasoning across multiple systems or files is required.",
1161
1156
  "When unsure between two non-none buckets, choose the higher bucket. Do not use low as the default.",
1162
- `The default profile is ${defaultProfile}.`,
1163
- "Profile names are configured labels. Do not assume that a non-default profile is more capable than the default profile.",
1164
- "Choose a non-default profile only when its label clearly matches the task. Otherwise keep the default profile.",
1157
+ "",
1158
+ `Default profile: "${defaultProfile}". Keep it unless another profile's description is a clearly better fit.`,
1159
+ "Configured profiles (use each description's use and avoid cases; names are labels only; non-default does not mean stronger):",
1160
+ profileList,
1161
+ "Choose a non-default profile only when its description clearly fits the task. If no description clearly fits, keep the default profile.",
1165
1162
  "",
1166
1163
  "Classify based on the substance of the task, not the length of the current message. When the current instruction is a short affirmation (for example: 'go', 'do it', 'yes please', 'proceed') and prior thread context contains a pending task, classify the pending task \u2014 not the affirmation.",
1167
1164
  "",
@@ -1299,6 +1296,7 @@ async function classifyTurn(args) {
1299
1296
  prompt: args.prompt,
1300
1297
  thinkingLevel: "low",
1301
1298
  system: buildClassifierSystemPrompt(
1299
+ args.profiles,
1302
1300
  Object.keys(args.profiles),
1303
1301
  args.defaultProfile
1304
1302
  ),
@@ -1733,7 +1731,7 @@ var TOOL_POLICY_RULES = [
1733
1731
  "- Event tasks make the task creator's connected credentials available by default when the requested work needs user-bound authorization. Do not ask for separate confirmation merely to use credentials needed for the requested work. On creation, omit credentialMode for the creator default and set system only when the creator explicitly requires it. For later changes, creator always means the task's original createdBy actor, never the current requester. If the requester is not that creator, do not attempt to enable creator credential use or suggest that confirmation could authorize it.",
1734
1732
  "- Event tasks list for the current destination, not one thread. Public tasks can also be updated or deleted by task id from another destination in the same workspace. When listing them, use createdBy to explain creator-only credential changes and warn when triggerAvailable is false; an unavailable task remains stored but cannot receive events until its plugin event is enabled again.",
1735
1733
  "- Scheduled tasks make the task creator's connected credentials available by default when the requested work needs user-bound authorization. Do not ask for separate confirmation merely to use credentials needed for the requested work. On creation, omit credential_mode for the creator default and set system only when the creator explicitly requires it. For later changes, creator always means the task's original created_by actor, never the current requester. If the requester is not that creator, do not attempt to enable creator credential use or suggest that confirmation could authorize it.",
1736
- "- For code changes, debugging or root-cause analysis, broad refactors, and software architecture decisions, use `handoff` before substantive analysis only when it offers a profile that better matches the task. Do not switch merely because the task involves code.",
1734
+ "- When another model profile fits the task better, call `handoff` before substantial work. Follow the profile list and selection rules in the tool description.",
1737
1735
  "- Run `jr-rpc config get|set|unset|list` for provider defaults and `jr-rpc plugins list` for installed plugin introspection as standalone bash commands; do not chain them with `cd`, `&&`, pipes, or provider commands.",
1738
1736
  "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1739
1737
  ];
@@ -3119,11 +3117,8 @@ function sleep2(ms) {
3119
3117
  timer.unref?.();
3120
3118
  });
3121
3119
  }
3122
- function scopeKey(scope) {
3123
- if (scope.kind === "conversation") {
3124
- return `${AUTOMATED_TURN_LIMIT_PREFIX}:conversation:${scope.conversationId}`;
3125
- }
3126
- return `${AUTOMATED_TURN_LIMIT_PREFIX}:destination:${destinationKey(scope.destination)}`;
3120
+ function conversationKey(conversationId) {
3121
+ return `${AUTOMATED_TURN_LIMIT_PREFIX}:conversation:${conversationId}`;
3127
3122
  }
3128
3123
  function lockKey(key) {
3129
3124
  return `${key}:lock`;
@@ -3132,7 +3127,7 @@ function parseState(value) {
3132
3127
  const parsed = automatedTurnLimitStateSchema.safeParse(value);
3133
3128
  return parsed.success ? parsed.data : void 0;
3134
3129
  }
3135
- async function acquireScopeLock(state, key, waitDeadlineMs = Date.now() + AUTOMATED_TURN_LIMIT_LOCK_WAIT_MS) {
3130
+ async function acquireConversationLock(state, key, waitDeadlineMs = Date.now() + AUTOMATED_TURN_LIMIT_LOCK_WAIT_MS) {
3136
3131
  while (true) {
3137
3132
  const lock = await state.acquireLock(
3138
3133
  lockKey(key),
@@ -3147,8 +3142,8 @@ async function acquireScopeLock(state, key, waitDeadlineMs = Date.now() + AUTOMA
3147
3142
  await sleep2(AUTOMATED_TURN_LIMIT_LOCK_RETRY_MS);
3148
3143
  }
3149
3144
  }
3150
- async function withScopeLock(state, key, callback) {
3151
- const lock = await acquireScopeLock(state, key);
3145
+ async function withConversationLock(state, key, callback) {
3146
+ const lock = await acquireConversationLock(state, key);
3152
3147
  try {
3153
3148
  return await callback();
3154
3149
  } finally {
@@ -3162,36 +3157,14 @@ function emptyState(nowMs) {
3162
3157
  updatedAtMs: nowMs
3163
3158
  };
3164
3159
  }
3165
- function resumeInForScope(scope) {
3166
- return scope.kind === "conversation" ? "thread" : "channel";
3167
- }
3168
3160
  function isAutomatedTurnSource(source) {
3169
3161
  if (!source) {
3170
3162
  return false;
3171
3163
  }
3172
3164
  return source.kind === "resource_event" || source.kind === "event_task" || source.kind === "scheduled_task" || source.kind === "plugin_dispatch" || source.kind === "agent_invocation";
3173
3165
  }
3174
- function automatedTurnLimitScopeForSource(args) {
3175
- if (!isAutomatedTurnSource(args.source)) {
3176
- return void 0;
3177
- }
3178
- if (args.source?.kind === "resource_event") {
3179
- return {
3180
- kind: "conversation",
3181
- conversationId: args.conversationId
3182
- };
3183
- }
3184
- if (args.destination) {
3185
- return { kind: "destination", destination: args.destination };
3186
- }
3187
- return {
3188
- kind: "conversation",
3189
- conversationId: args.conversationId
3190
- };
3191
- }
3192
3166
  function buildAutomatedTurnLimitResponse(args) {
3193
- const place = args.resumeIn === "channel" ? "channel" : "thread";
3194
- return `I stopped automatic updates after ${args.maxTurns} replies without a new message from you. Send a message or @mention me in this ${place} to continue.`;
3167
+ return `I stopped automatic updates after ${args.maxTurns} replies without a new message from you. Send a message or @mention me in this thread to continue.`;
3195
3168
  }
3196
3169
  async function admitAutomatedTurn(args) {
3197
3170
  if (args.maxTurns < 1) {
@@ -3200,8 +3173,8 @@ async function admitAutomatedTurn(args) {
3200
3173
  const state = args.state ?? getStateAdapter();
3201
3174
  await state.connect();
3202
3175
  const nowMs = args.nowMs ?? Date.now();
3203
- const key = scopeKey(args.scope);
3204
- return await withScopeLock(state, key, async () => {
3176
+ const key = conversationKey(args.conversationId);
3177
+ return await withConversationLock(state, key, async () => {
3205
3178
  const current = parseState(await state.get(key)) ?? emptyState(nowMs);
3206
3179
  if (current.paused || current.consecutiveAutomatedTurns >= args.maxTurns) {
3207
3180
  const shouldPostNotice = current.noticePostedAtMs === void 0;
@@ -3236,9 +3209,8 @@ async function countAutomatedTurn(args) {
3236
3209
  const state = args.state ?? getStateAdapter();
3237
3210
  await state.connect();
3238
3211
  const nowMs = args.nowMs ?? Date.now();
3239
- const key = scopeKey(args.scope);
3240
- const resumeIn = resumeInForScope(args.scope);
3241
- return await withScopeLock(state, key, async () => {
3212
+ const key = conversationKey(args.conversationId);
3213
+ return await withConversationLock(state, key, async () => {
3242
3214
  const current = parseState(await state.get(key)) ?? emptyState(nowMs);
3243
3215
  const consecutiveAutomatedTurns = current.consecutiveAutomatedTurns + 1;
3244
3216
  const paused = consecutiveAutomatedTurns >= args.maxTurns;
@@ -3247,16 +3219,15 @@ async function countAutomatedTurn(args) {
3247
3219
  consecutiveAutomatedTurns,
3248
3220
  paused,
3249
3221
  updatedAtMs: nowMs,
3250
- // Claim the notice slot before the destination post. Mark success after
3251
- // the post lands; clear the claim if the post fails or is skipped.
3222
+ // Claim the notice slot before posting. Mark success after the post
3223
+ // lands; clear the claim if the post fails or is skipped.
3252
3224
  ...shouldPostNotice ? { noticePostedAtMs: nowMs } : current.noticePostedAtMs !== void 0 ? { noticePostedAtMs: current.noticePostedAtMs } : void 0
3253
3225
  };
3254
3226
  await state.set(key, next, JUNIOR_THREAD_STATE_TTL_MS);
3255
3227
  return {
3256
3228
  consecutiveAutomatedTurns,
3257
3229
  paused,
3258
- shouldPostNotice,
3259
- resumeIn
3230
+ shouldPostNotice
3260
3231
  };
3261
3232
  });
3262
3233
  }
@@ -3264,8 +3235,8 @@ async function clearAutomatedTurnLimitNoticeClaim(args) {
3264
3235
  const state = args.state ?? getStateAdapter();
3265
3236
  await state.connect();
3266
3237
  const nowMs = args.nowMs ?? Date.now();
3267
- const key = scopeKey(args.scope);
3268
- await withScopeLock(state, key, async () => {
3238
+ const key = conversationKey(args.conversationId);
3239
+ await withConversationLock(state, key, async () => {
3269
3240
  const current = parseState(await state.get(key));
3270
3241
  if (!current || current.noticePostedAtMs === void 0) {
3271
3242
  return;
@@ -3284,41 +3255,26 @@ async function clearAutomatedTurnLimitNoticeClaim(args) {
3284
3255
  async function resetAutomatedTurnLimit(args) {
3285
3256
  const state = args.state ?? getStateAdapter();
3286
3257
  await state.connect();
3287
- const key = scopeKey(args.scope);
3288
- await withScopeLock(state, key, async () => {
3258
+ const key = conversationKey(args.conversationId);
3259
+ await withConversationLock(state, key, async () => {
3289
3260
  await state.delete(key);
3290
3261
  });
3291
3262
  }
3292
3263
  async function recordFinishedTurnForAutomatedLimit(args) {
3293
3264
  const nowMs = args.nowMs ?? Date.now();
3294
- const automatedScope = automatedTurnLimitScopeForSource({
3295
- conversationId: args.conversationId,
3296
- destination: args.destination,
3297
- source: args.source
3298
- });
3299
- if (automatedScope) {
3265
+ if (isAutomatedTurnSource(args.source)) {
3300
3266
  return await countAutomatedTurn({
3267
+ conversationId: args.conversationId,
3301
3268
  maxTurns: args.maxTurns,
3302
3269
  nowMs,
3303
- scope: automatedScope,
3304
3270
  state: args.state
3305
3271
  });
3306
3272
  }
3307
3273
  await resetAutomatedTurnLimit({
3274
+ conversationId: args.conversationId,
3308
3275
  nowMs,
3309
- scope: {
3310
- kind: "conversation",
3311
- conversationId: args.conversationId
3312
- },
3313
3276
  state: args.state
3314
3277
  });
3315
- if (args.destination) {
3316
- await resetAutomatedTurnLimit({
3317
- nowMs,
3318
- scope: { kind: "destination", destination: args.destination },
3319
- state: args.state
3320
- });
3321
- }
3322
3278
  return void 0;
3323
3279
  }
3324
3280
 
@@ -3331,10 +3287,6 @@ async function releaseFailedNoticeClaim(args) {
3331
3287
  }
3332
3288
  }
3333
3289
  async function postAutomatedTurnLimitNoticeForConversation(args) {
3334
- const scope = args.scope ?? {
3335
- kind: "conversation",
3336
- conversationId: args.conversationId
3337
- };
3338
3290
  try {
3339
3291
  const conversation = await getConversationStore().get({
3340
3292
  conversationId: args.conversationId
@@ -3346,18 +3298,17 @@ async function postAutomatedTurnLimitNoticeForConversation(args) {
3346
3298
  "app.automated_turn_limit.reason": location ? "unsupported_location" : "missing_location"
3347
3299
  });
3348
3300
  await releaseFailedNoticeClaim({
3349
- nowMs: args.nowMs,
3350
- scope
3301
+ conversationId: args.conversationId,
3302
+ nowMs: args.nowMs
3351
3303
  });
3352
3304
  return false;
3353
3305
  }
3354
3306
  await postSlackMessage({
3355
3307
  channelId: location.channelId,
3356
3308
  text: buildAutomatedTurnLimitResponse({
3357
- maxTurns: args.maxTurns,
3358
- resumeIn: args.resumeIn ?? "thread"
3309
+ maxTurns: args.maxTurns
3359
3310
  }),
3360
- ...location.threadTs ? { threadTs: location.threadTs } : void 0
3311
+ ...args.threadTs ? { threadTs: args.threadTs } : location.threadTs ? { threadTs: location.threadTs } : void 0
3361
3312
  });
3362
3313
  return true;
3363
3314
  } catch (error) {
@@ -3365,45 +3316,8 @@ async function postAutomatedTurnLimitNoticeForConversation(args) {
3365
3316
  conversationId: args.conversationId
3366
3317
  });
3367
3318
  await releaseFailedNoticeClaim({
3368
- nowMs: args.nowMs,
3369
- scope
3370
- });
3371
- return false;
3372
- }
3373
- }
3374
- async function postAutomatedTurnLimitNoticeForDestination(args) {
3375
- const scope = args.scope ?? {
3376
- kind: "destination",
3377
- destination: args.destination
3378
- };
3379
- if (args.destination.platform !== "slack") {
3380
- logWarn("automated_turn_limit.notice_skipped", {
3381
- "app.automated_turn_limit.reason": "unsupported_destination"
3382
- });
3383
- await releaseFailedNoticeClaim({
3384
- nowMs: args.nowMs,
3385
- scope
3386
- });
3387
- return false;
3388
- }
3389
- try {
3390
- await postSlackMessage({
3391
- channelId: args.destination.channelId,
3392
- text: buildAutomatedTurnLimitResponse({
3393
- maxTurns: args.maxTurns,
3394
- resumeIn: args.resumeIn ?? "channel"
3395
- }),
3396
- ...args.threadTs ? { threadTs: args.threadTs } : void 0
3397
- });
3398
- return true;
3399
- } catch (error) {
3400
- logException(error, "automated_turn_limit.notice_failed", {
3401
- "app.slack.channel_id": args.destination.channelId,
3402
- "app.slack.team_id": args.destination.teamId
3403
- });
3404
- await releaseFailedNoticeClaim({
3405
- nowMs: args.nowMs,
3406
- scope
3319
+ conversationId: args.conversationId,
3320
+ nowMs: args.nowMs
3407
3321
  });
3408
3322
  return false;
3409
3323
  }
@@ -3412,39 +3326,11 @@ async function maybePostAutomatedTurnLimitNotice(args) {
3412
3326
  if (!args.update?.shouldPostNotice) {
3413
3327
  return;
3414
3328
  }
3415
- if (args.update.resumeIn === "thread") {
3416
- await postAutomatedTurnLimitNoticeForConversation({
3417
- conversationId: args.conversationId,
3418
- maxTurns: args.maxTurns,
3419
- nowMs: args.nowMs,
3420
- resumeIn: "thread",
3421
- scope: {
3422
- kind: "conversation",
3423
- conversationId: args.conversationId
3424
- }
3425
- });
3426
- return;
3427
- }
3428
- if (args.destination) {
3429
- await postAutomatedTurnLimitNoticeForDestination({
3430
- destination: args.destination,
3431
- maxTurns: args.maxTurns,
3432
- nowMs: args.nowMs,
3433
- resumeIn: "channel",
3434
- scope: { kind: "destination", destination: args.destination },
3435
- threadTs: args.threadTs
3436
- });
3437
- return;
3438
- }
3439
3329
  await postAutomatedTurnLimitNoticeForConversation({
3440
3330
  conversationId: args.conversationId,
3441
3331
  maxTurns: args.maxTurns,
3442
3332
  nowMs: args.nowMs,
3443
- resumeIn: args.update.resumeIn,
3444
- scope: {
3445
- kind: "conversation",
3446
- conversationId: args.conversationId
3447
- }
3333
+ threadTs: args.threadTs
3448
3334
  });
3449
3335
  }
3450
3336
 
@@ -3452,7 +3338,7 @@ async function maybePostAutomatedTurnLimitNotice(args) {
3452
3338
  function replyContractLines() {
3453
3339
  return [
3454
3340
  "When you reply, follow any reply format in the instructions.",
3455
- `If no visible reply is needed, make the final message exactly ${NO_REPLY_MARKER}.`,
3341
+ `If no visible reply is needed, your final assistant message must be exactly ${NO_REPLY_MARKER} and nothing else.`,
3456
3342
  "Otherwise briefly summarize what you acted on and what you did or need next."
3457
3343
  ];
3458
3344
  }
@@ -3589,12 +3475,9 @@ function createResourceEventInboundMessage(input) {
3589
3475
  async function enqueueResourceEventNotification(args) {
3590
3476
  const maxTurns = botConfig.maxConsecutiveAutomatedTurns;
3591
3477
  const decision = await admitAutomatedTurn({
3478
+ conversationId: args.subscription.conversationId,
3592
3479
  maxTurns,
3593
3480
  nowMs: args.event.occurredAtMs,
3594
- scope: {
3595
- kind: "conversation",
3596
- conversationId: args.subscription.conversationId
3597
- },
3598
3481
  state: args.state
3599
3482
  });
3600
3483
  if (decision.status === "paused") {
@@ -3609,12 +3492,7 @@ async function enqueueResourceEventNotification(args) {
3609
3492
  await postAutomatedTurnLimitNoticeForConversation({
3610
3493
  conversationId: args.subscription.conversationId,
3611
3494
  maxTurns,
3612
- nowMs: args.event.occurredAtMs,
3613
- resumeIn: "thread",
3614
- scope: {
3615
- kind: "conversation",
3616
- conversationId: args.subscription.conversationId
3617
- }
3495
+ nowMs: args.event.occurredAtMs
3618
3496
  });
3619
3497
  }
3620
3498
  return { status: "duplicate" };
@@ -14258,12 +14136,19 @@ function stripReplacementHistoryField(value) {
14258
14136
  // src/chat/tools/handoff/tool.ts
14259
14137
  import { z as z59 } from "zod";
14260
14138
  var HANDOFF_TOOL_NAME = "handoff";
14139
+ function formatHandoffProfiles(handoff) {
14140
+ return handoff.profiles.map(
14141
+ (profile) => `- ${formatModelProfile(profile.name, profile.description)}`
14142
+ ).join("\n");
14143
+ }
14261
14144
  function createHandoffTool(handoff) {
14262
- const profileSchema = z59.enum(handoff.profiles);
14145
+ const profileSchema = z59.enum(
14146
+ handoff.profiles.map((profile) => profile.name)
14147
+ );
14263
14148
  const handoffResultSchema = juniorToolOutputSchema.extend({
14264
14149
  model_profile: modelProfileSchema
14265
14150
  });
14266
- const profileNames = handoff.profiles.map((profile) => `\`${profile}\``).join(", ");
14151
+ const profileList = formatHandoffProfiles(handoff);
14267
14152
  return zodTool({
14268
14153
  annotations: {
14269
14154
  destructiveHint: false,
@@ -14271,10 +14156,21 @@ function createHandoffTool(handoff) {
14271
14156
  openWorldHint: false,
14272
14157
  readOnlyHint: false
14273
14158
  },
14274
- description: `Switch to another model profile and continue the same task. Profiles: ${profileNames}. Call this as the only tool.`,
14159
+ description: [
14160
+ "Switch this conversation to another configured model profile and continue the same task.",
14161
+ "Call this as the only tool in the assistant turn, before substantial analysis or implementation, when a listed profile's description fits the task better than the current profile.",
14162
+ "Use each description's use and avoid cases. Do not select by the profile name or assume that a non-default profile is stronger.",
14163
+ "Do not call this for ordinary lookups, short answers, light investigation, or only because the task mentions code.",
14164
+ "Do not call this after you have done the difficult work on the current profile. Do not combine it with other tools in the same assistant message.",
14165
+ "A successful handoff becomes the active profile for later turns. Another handoff can change it again.",
14166
+ `Available profiles:
14167
+ ${profileList}`
14168
+ ].join(" "),
14275
14169
  executionMode: "sequential",
14276
14170
  inputSchema: z59.object({
14277
- profile: profileSchema.describe("Target model profile")
14171
+ profile: profileSchema.describe(
14172
+ "Exact configured profile name. Choose the profile whose description best fits the remaining task. If none clearly fits better, do not call handoff."
14173
+ )
14278
14174
  }).strict(),
14279
14175
  outputSchema: handoffResultSchema,
14280
14176
  execute: async (input, options) => {
@@ -18987,6 +18883,145 @@ ${summary}`
18987
18883
  return { compacted: true, piMessages: messages };
18988
18884
  }
18989
18885
 
18886
+ // src/chat/agent/handoff.ts
18887
+ var EXCLUSIVE_TOOL_NAMES = [HANDOFF_TOOL_NAME, "switchWorkspace"];
18888
+ function exclusiveToolName(toolCalls) {
18889
+ for (const name of EXCLUSIVE_TOOL_NAMES) {
18890
+ if (toolCalls.some((call) => call.name === name)) {
18891
+ return name;
18892
+ }
18893
+ }
18894
+ return void 0;
18895
+ }
18896
+ function handoffControl(args) {
18897
+ if (!args.enabled) {
18898
+ return void 0;
18899
+ }
18900
+ const profileNames = [
18901
+ botConfig.defaultProfile,
18902
+ ...Object.keys(botConfig.profiles).filter((profile) => profile !== botConfig.defaultProfile).sort()
18903
+ ].filter((profile) => profile !== args.activeProfile);
18904
+ const [firstProfileName, ...otherProfileNames] = profileNames;
18905
+ if (!firstProfileName) {
18906
+ return void 0;
18907
+ }
18908
+ const toHandoffProfile = (name) => ({
18909
+ name,
18910
+ description: profileConfig(botConfig, name).description
18911
+ });
18912
+ return {
18913
+ profiles: [
18914
+ toHandoffProfile(firstProfileName),
18915
+ ...otherProfileNames.map(toHandoffProfile)
18916
+ ],
18917
+ execute: args.execute
18918
+ };
18919
+ }
18920
+ function toolsForHandoffProfile(args) {
18921
+ const control = handoffControl({
18922
+ activeProfile: args.activeProfile,
18923
+ enabled: args.enabled,
18924
+ execute: args.execute
18925
+ });
18926
+ if (!control) {
18927
+ return args.agentTools.filter((tool) => tool.name !== HANDOFF_TOOL_NAME);
18928
+ }
18929
+ const handoffAgentTool = args.agentTools.find(
18930
+ (tool) => tool.name === HANDOFF_TOOL_NAME
18931
+ );
18932
+ if (!handoffAgentTool) {
18933
+ throw new Error("Handoff control is missing its Pi tool");
18934
+ }
18935
+ const definition = createHandoffTool(control);
18936
+ return args.agentTools.map(
18937
+ (tool) => tool.name === HANDOFF_TOOL_NAME ? {
18938
+ ...handoffAgentTool,
18939
+ description: definition.description,
18940
+ parameters: definition.inputSchema,
18941
+ prepareArguments: definition.prepareArguments
18942
+ } : tool
18943
+ );
18944
+ }
18945
+ async function commitHandoff(args) {
18946
+ if (args.profile === args.activeModelProfile) {
18947
+ return void 0;
18948
+ }
18949
+ const runtimeContext = retainRuntimeTurnContext(
18950
+ args.runtimeContextSourceMessages ?? args.sourceMessages
18951
+ );
18952
+ const phaseUsageSummary = extractGenAiUsageSummary(
18953
+ ...args.sourceMessages.slice(args.beforeMessageCount).filter(isAssistantMessage)
18954
+ );
18955
+ const phaseUsage = hasAgentTurnUsage(phaseUsageSummary) ? phaseUsageSummary : void 0;
18956
+ const selectedProfile = profileConfig(botConfig, args.profile);
18957
+ const handoffReasoningLevel = selectedProfile.reasoningLevel ?? args.turnRoute.reasoningLevel;
18958
+ const target = {
18959
+ modelId: selectedProfile.modelId,
18960
+ modelProfile: args.profile,
18961
+ reasoningLevel: handoffReasoningLevel
18962
+ };
18963
+ void (async () => {
18964
+ await args.onStatus?.({ text: "Switching models" });
18965
+ })().catch((error) => {
18966
+ logWarn("assistant.status.observer.failed", {
18967
+ "exception.message": error instanceof Error ? error.message : String(error)
18968
+ });
18969
+ });
18970
+ const handoffMessages = await compactContextForHandoff(
18971
+ {
18972
+ conversationContext: args.conversationContext,
18973
+ conversationId: args.conversationId,
18974
+ piMessages: args.sourceMessages,
18975
+ runtimeContext,
18976
+ signal: args.signal,
18977
+ triggeringToolCallId: args.triggeringToolCallId,
18978
+ target,
18979
+ metadata: args.metadata
18980
+ },
18981
+ {
18982
+ completeText: (completeArgs) => completeText(completeArgs)
18983
+ }
18984
+ );
18985
+ const turnRoute = handoffReasoningLevel === args.turnRoute.reasoningLevel ? args.turnRoute : {
18986
+ ...args.turnRoute,
18987
+ reasoningLevel: handoffReasoningLevel,
18988
+ reason: `profile_reasoning_override:${args.profile}:${args.turnRoute.reason}`
18989
+ };
18990
+ return {
18991
+ messages: handoffMessages,
18992
+ model: resolveGatewayModel(target.modelId),
18993
+ modelId: target.modelId,
18994
+ modelProfile: args.profile,
18995
+ phaseUsage,
18996
+ thinkingLevel: toPiReasoningLevel(handoffReasoningLevel),
18997
+ turnRoute
18998
+ };
18999
+ }
19000
+ function applyHandoff(args) {
19001
+ const replacement = [...args.pending.messages];
19002
+ args.agent.state.messages = replacement;
19003
+ args.agent.state.model = args.pending.model;
19004
+ args.agent.state.thinkingLevel = args.pending.thinkingLevel;
19005
+ args.agent.state.tools = args.tools;
19006
+ args.resume.setBeforeMessageCount(replacement.length);
19007
+ args.resume.setTurnStartMessageIndex(0);
19008
+ args.resume.adoptCommittedBoundary(replacement);
19009
+ setSpanAttributes({
19010
+ "gen_ai.agent.model": args.pending.modelId,
19011
+ "gen_ai.agent.model_profile": args.pending.modelProfile,
19012
+ "gen_ai.agent.reasoning.level": args.pending.turnRoute.reasoningLevel
19013
+ });
19014
+ return {
19015
+ context: {
19016
+ systemPrompt: args.baseInstructions,
19017
+ messages: replacement,
19018
+ tools: args.tools
19019
+ },
19020
+ model: args.pending.model,
19021
+ thinkingLevel: args.pending.thinkingLevel
19022
+ };
19023
+ }
19024
+
18990
19025
  // src/chat/agent/index.ts
18991
19026
  var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
18992
19027
  async function readRunConversationVisibility(conversationId) {
@@ -19382,93 +19417,56 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19382
19417
  activeModelProfile = routedModelProfile;
19383
19418
  activeModelId = routedModelId;
19384
19419
  let pendingPiHookError;
19385
- let pendingHandoff;
19386
19420
  const currentAgentMessages = () => agent ? [...agent.state.messages] : [];
19387
- const handoffProfiles = [
19388
- botConfig.defaultProfile,
19389
- ...Object.keys(botConfig.profiles).filter((profile) => profile !== botConfig.defaultProfile).sort()
19390
- ];
19391
19421
  const usageSinceCurrentBoundary = (messages) => {
19392
19422
  const usage = extractGenAiUsageSummary(
19393
19423
  ...messages.slice(runResume.beforeMessageCount).filter(isAssistantMessage)
19394
19424
  );
19395
19425
  return hasAgentTurnUsage(usage) ? usage : void 0;
19396
19426
  };
19427
+ let baseInstructions = "";
19428
+ let pendingHandoff;
19429
+ const handoffMetadata = {
19430
+ threadId: conversationId,
19431
+ channelId: slackChannelId,
19432
+ actorId: slackActor?.userId,
19433
+ runId
19434
+ };
19397
19435
  const scheduleHandoff = async (args) => {
19398
- if (args.profile === activeModelProfile) {
19399
- return;
19400
- }
19401
- const runtimeContext = retainRuntimeTurnContext(
19402
- args.runtimeContextSourceMessages ?? args.sourceMessages
19403
- );
19404
- const phaseUsage = usageSinceCurrentBoundary(args.sourceMessages);
19405
- const selectedProfile = profileConfig(botConfig, args.profile);
19406
- const handoffReasoningLevel = selectedProfile.reasoningLevel ?? turnRoute.reasoningLevel;
19407
- const target = {
19408
- modelId: selectedProfile.modelId,
19409
- modelProfile: args.profile,
19410
- reasoningLevel: handoffReasoningLevel
19411
- };
19412
- const handoffModel = resolveGatewayModel(target.modelId);
19413
- const handoffThinkingLevel = toPiReasoningLevel(handoffReasoningLevel);
19414
- void (async () => {
19415
- await observers.onStatus?.({ text: "Switching models" });
19416
- })().catch((error) => {
19417
- logWarn("assistant.status.observer.failed", {
19418
- "exception.message": error instanceof Error ? error.message : String(error)
19419
- });
19436
+ const pending = await commitHandoff({
19437
+ activeModelProfile,
19438
+ beforeMessageCount: runResume.beforeMessageCount,
19439
+ conversationContext: input.conversationContext,
19440
+ conversationId,
19441
+ metadata: handoffMetadata,
19442
+ onStatus: observers.onStatus,
19443
+ profile: args.profile,
19444
+ runtimeContextSourceMessages: args.runtimeContextSourceMessages,
19445
+ signal: args.signal,
19446
+ sourceMessages: args.sourceMessages,
19447
+ triggeringToolCallId: args.triggeringToolCallId,
19448
+ turnRoute
19420
19449
  });
19421
- const handoffMessages = await compactContextForHandoff(
19422
- {
19423
- conversationContext: input.conversationContext,
19424
- conversationId,
19425
- piMessages: args.sourceMessages,
19426
- runtimeContext,
19427
- signal: args.signal,
19428
- triggeringToolCallId: args.triggeringToolCallId,
19429
- target,
19430
- metadata: {
19431
- threadId: conversationId,
19432
- channelId: slackChannelId,
19433
- actorId: slackActor?.userId,
19434
- runId
19435
- }
19436
- },
19437
- {
19438
- completeText: (args2) => completeText(args2)
19439
- }
19440
- );
19441
- if (handoffReasoningLevel !== turnRoute.reasoningLevel) {
19442
- turnRoute = {
19443
- ...turnRoute,
19444
- reasoningLevel: handoffReasoningLevel,
19445
- reason: `profile_reasoning_override:${args.profile}:${turnRoute.reason}`
19446
- };
19450
+ if (!pending) {
19451
+ return;
19447
19452
  }
19448
- priorPhaseUsage = addAgentTurnUsage(priorPhaseUsage, phaseUsage);
19449
- pendingHandoff = {
19450
- messages: handoffMessages,
19451
- model: handoffModel,
19452
- thinkingLevel: handoffThinkingLevel
19453
- };
19454
- activeModelProfile = args.profile;
19455
- activeModelId = target.modelId;
19456
- };
19457
- const handoffControlFor = (activeProfile) => {
19458
- const profiles = handoffProfiles.filter(
19459
- (profile) => profile !== activeProfile
19460
- );
19461
- return profiles.length > 0 ? {
19462
- profiles,
19463
- execute: async (profile, options) => await scheduleHandoff({
19464
- profile,
19465
- signal: options.signal,
19466
- sourceMessages: [...agent.state.messages],
19467
- triggeringToolCallId: options.toolCallId
19468
- })
19469
- } : void 0;
19453
+ priorPhaseUsage = addAgentTurnUsage(priorPhaseUsage, pending.phaseUsage);
19454
+ pendingHandoff = pending;
19455
+ activeModelProfile = pending.modelProfile;
19456
+ activeModelId = pending.modelId;
19457
+ turnRoute = pending.turnRoute;
19470
19458
  };
19471
- const requestHandoff = handoffEnabled ? handoffControlFor(activeModelProfile) : void 0;
19459
+ const handoffExecute = async (profile, options) => await scheduleHandoff({
19460
+ profile,
19461
+ signal: options.signal,
19462
+ sourceMessages: [...agent.state.messages],
19463
+ triggeringToolCallId: options.toolCallId
19464
+ });
19465
+ const requestHandoff = handoffControl({
19466
+ activeProfile: activeModelProfile,
19467
+ enabled: handoffEnabled,
19468
+ execute: handoffExecute
19469
+ });
19472
19470
  setTags({
19473
19471
  ...runLogContext,
19474
19472
  modelId: activeModelId
@@ -19529,32 +19527,27 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19529
19527
  await recordAgentsTransition(initialRepositoryInstructions);
19530
19528
  }
19531
19529
  const getPendingAuthPause = wiring.getPendingAuthPause;
19532
- const toolsWithoutHandoff = wiring.agentTools.filter(
19533
- (tool) => tool.name !== HANDOFF_TOOL_NAME
19534
- );
19535
- const handoffAgentTool = wiring.agentTools.find(
19536
- (tool) => tool.name === HANDOFF_TOOL_NAME
19537
- );
19538
- const toolsForActiveProfile = () => {
19539
- const handoff = handoffEnabled ? handoffControlFor(activeModelProfile) : void 0;
19540
- if (!handoff) {
19541
- return toolsWithoutHandoff;
19542
- }
19543
- if (!handoffAgentTool) {
19544
- throw new Error("Handoff control is missing its Pi tool");
19545
- }
19546
- const definition = createHandoffTool(handoff);
19547
- return wiring.agentTools.map(
19548
- (tool) => tool.name === HANDOFF_TOOL_NAME ? {
19549
- ...handoffAgentTool,
19550
- description: definition.description,
19551
- parameters: definition.inputSchema,
19552
- prepareArguments: definition.prepareArguments
19553
- } : tool
19554
- );
19530
+ const applyPendingHandoff = () => {
19531
+ if (!pendingHandoff) {
19532
+ return void 0;
19533
+ }
19534
+ const pending = pendingHandoff;
19535
+ pendingHandoff = void 0;
19536
+ return applyHandoff({
19537
+ agent,
19538
+ baseInstructions,
19539
+ pending,
19540
+ resume: runResume,
19541
+ tools: toolsForHandoffProfile({
19542
+ activeProfile: pending.modelProfile,
19543
+ agentTools: wiring.agentTools,
19544
+ enabled: handoffEnabled,
19545
+ execute: handoffExecute
19546
+ })
19547
+ });
19555
19548
  };
19556
19549
  const {
19557
- baseInstructions,
19550
+ baseInstructions: assembledBaseInstructions,
19558
19551
  contextContentParts: promptContextContentParts,
19559
19552
  inputMessages,
19560
19553
  inputMessagesAttribute,
@@ -19590,6 +19583,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19590
19583
  toolRuntimeContext: wiring.toolRuntimeContext,
19591
19584
  userContentParts
19592
19585
  });
19586
+ baseInstructions = assembledBaseInstructions;
19593
19587
  const repositoryInstructionsContext = createRepositoryInstructionsContext({
19594
19588
  capture: wiring.captureRepositoryInstructions,
19595
19589
  hasSandbox: () => Boolean(wiring.getSandboxRef()),
@@ -19604,36 +19598,6 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19604
19598
  shouldPromptAgent
19605
19599
  });
19606
19600
  runResume.setTurnContexts(turnContexts);
19607
- const applyPendingHandoff = () => {
19608
- if (!pendingHandoff) {
19609
- return void 0;
19610
- }
19611
- const { messages, model, thinkingLevel } = pendingHandoff;
19612
- const replacement = [...messages];
19613
- pendingHandoff = void 0;
19614
- agent.state.messages = replacement;
19615
- agent.state.model = model;
19616
- agent.state.thinkingLevel = thinkingLevel;
19617
- const tools = toolsForActiveProfile();
19618
- agent.state.tools = tools;
19619
- runResume.setBeforeMessageCount(replacement.length);
19620
- runResume.setTurnStartMessageIndex(0);
19621
- runResume.adoptCommittedBoundary(replacement);
19622
- setSpanAttributes({
19623
- "gen_ai.agent.model": activeModelId,
19624
- "gen_ai.agent.model_profile": activeModelProfile,
19625
- "gen_ai.agent.reasoning.level": turnRoute.reasoningLevel
19626
- });
19627
- return {
19628
- context: {
19629
- systemPrompt: baseInstructions,
19630
- messages: replacement,
19631
- tools
19632
- },
19633
- model,
19634
- thinkingLevel
19635
- };
19636
- };
19637
19601
  const applyActiveContextCompaction = async (messages, hookSignal, pendingMessages, clearSteeringQueueOnCommit = false, pairPendingRuntimeContext = false) => {
19638
19602
  const compaction = await compactActiveContextIfNeeded(
19639
19603
  {
@@ -19764,13 +19728,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19764
19728
  const toolCalls2 = assistantMessage.content.filter(
19765
19729
  (part) => part.type === "toolCall"
19766
19730
  );
19767
- const containsHandoff = toolCalls2.some(
19768
- (call) => call.name === HANDOFF_TOOL_NAME
19769
- );
19770
- const containsWorkspaceSwitch = toolCalls2.some(
19771
- (call) => call.name === "switchWorkspace"
19772
- );
19773
- const exclusiveTool = containsHandoff ? HANDOFF_TOOL_NAME : containsWorkspaceSwitch ? "switchWorkspace" : void 0;
19731
+ const exclusiveTool = exclusiveToolName(toolCalls2);
19774
19732
  if (exclusiveTool && toolCalls2.length !== 1) {
19775
19733
  return {
19776
19734
  block: true,
@@ -19847,10 +19805,9 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19847
19805
  if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
19848
19806
  return;
19849
19807
  }
19850
- const containsHandoff = event.message.content.some(
19808
+ if (event.message.content.some(
19851
19809
  (part) => part.type === "toolCall" && part.name === HANDOFF_TOOL_NAME
19852
- );
19853
- if (containsHandoff) {
19810
+ )) {
19854
19811
  return;
19855
19812
  }
19856
19813
  return deliverAssistantMessage(event.message);
@@ -20000,9 +19957,9 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
20000
19957
  }
20001
19958
  return result2;
20002
19959
  };
20003
- const requestedProfile = activeModelProfile === botConfig.defaultProfile ? turnRoute.profile : void 0;
20004
19960
  let run2;
20005
19961
  let handoffApplied = false;
19962
+ const requestedProfile = activeModelProfile === botConfig.defaultProfile ? turnRoute.profile : void 0;
20006
19963
  if (requestedProfile && requestedProfile !== botConfig.defaultProfile) {
20007
19964
  const handoffAbortController = new AbortController();
20008
19965
  await runAgentStep(
@@ -20296,9 +20253,7 @@ export {
20296
20253
  normalizeSandboxEgressTracePropagationDomains,
20297
20254
  shouldPropagateSandboxEgressTrace,
20298
20255
  resolveSandboxEgressProviderForHost,
20299
- admitAutomatedTurn,
20300
20256
  recordFinishedTurnForAutomatedLimit,
20301
- postAutomatedTurnLimitNoticeForDestination,
20302
20257
  maybePostAutomatedTurnLimitNotice,
20303
20258
  renderTaskInput,
20304
20259
  RESOURCE_EVENT_MESSAGE_AUTHOR,