@sentry/junior 0.29.0 → 0.31.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.
package/dist/app.js CHANGED
@@ -2,9 +2,8 @@ import {
2
2
  discoverSkills,
3
3
  findSkillByName,
4
4
  loadSkillsByName,
5
- logCapabilityCatalogLoadedOnce,
6
5
  parseSkillInvocation
7
- } from "./chunk-ICIRAL6Y.js";
6
+ } from "./chunk-3SH2A7VQ.js";
8
7
  import {
9
8
  GEN_AI_PROVIDER_NAME,
10
9
  MISSING_GATEWAY_CREDENTIALS_ERROR,
@@ -40,6 +39,8 @@ import {
40
39
  createRequestContext,
41
40
  extractGenAiUsageSummary,
42
41
  getActiveTraceId,
42
+ getPluginCapabilityProviders,
43
+ getPluginCatalogSignature,
43
44
  getPluginDefinition,
44
45
  getPluginMcpProviders,
45
46
  getPluginOAuthConfig,
@@ -392,6 +393,26 @@ function defaultConversationState() {
392
393
  }
393
394
  };
394
395
  }
396
+ function coercePendingAuthState(value) {
397
+ if (!isRecord(value)) {
398
+ return void 0;
399
+ }
400
+ const kind = value.kind;
401
+ const provider = toOptionalString(value.provider);
402
+ const requesterId = toOptionalString(value.requesterId);
403
+ const sessionId = toOptionalString(value.sessionId);
404
+ const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
405
+ if (kind !== "mcp" && kind !== "plugin" || !provider || !requesterId || !sessionId || typeof linkSentAtMs !== "number") {
406
+ return void 0;
407
+ }
408
+ return {
409
+ kind,
410
+ provider,
411
+ requesterId,
412
+ sessionId,
413
+ linkSentAtMs
414
+ };
415
+ }
395
416
  function coerceThreadConversationState(value) {
396
417
  if (!isRecord(value)) {
397
418
  return defaultConversationState();
@@ -442,7 +463,8 @@ function coerceThreadConversationState(value) {
442
463
  const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
443
464
  const processing = {
444
465
  activeTurnId: toOptionalString(rawProcessing.activeTurnId),
445
- lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs)
466
+ lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
467
+ pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
446
468
  };
447
469
  const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
448
470
  const stats = {
@@ -2001,11 +2023,13 @@ function buildThreadParticipants(messages) {
2001
2023
  return participants;
2002
2024
  }
2003
2025
 
2004
- // src/chat/runtime/turn.ts
2026
+ // src/chat/state/turn-id.ts
2005
2027
  function buildDeterministicTurnId(messageId) {
2006
2028
  const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
2007
2029
  return `turn_${sanitized}`;
2008
2030
  }
2031
+
2032
+ // src/chat/runtime/turn.ts
2009
2033
  var RetryableTurnError = class extends Error {
2010
2034
  code = "retryable_turn";
2011
2035
  metadata;
@@ -2031,12 +2055,16 @@ function startActiveTurn(args) {
2031
2055
  args.updateConversationStats(args.conversation);
2032
2056
  }
2033
2057
  function markTurnCompleted(args) {
2034
- args.conversation.processing.activeTurnId = void 0;
2058
+ if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2059
+ args.conversation.processing.activeTurnId = void 0;
2060
+ }
2035
2061
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2036
2062
  args.updateConversationStats(args.conversation);
2037
2063
  }
2038
2064
  function markTurnFailed(args) {
2039
- args.conversation.processing.activeTurnId = void 0;
2065
+ if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2066
+ args.conversation.processing.activeTurnId = void 0;
2067
+ }
2040
2068
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2041
2069
  args.markConversationMessage(args.conversation, args.userMessageId, {
2042
2070
  replied: false,
@@ -2856,18 +2884,21 @@ function formatConfigurationValue(value) {
2856
2884
  function renderIdentityBlock(tag, fields) {
2857
2885
  const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key, value]) => `- ${key}: ${escapeXml(value)}`);
2858
2886
  if (lines.length === 0) {
2859
- return [`<${tag}>`, "none", `</${tag}>`].join("\n");
2887
+ return [`<${tag}>`, "none", `</${tag}>`];
2860
2888
  }
2861
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
2889
+ return [`<${tag}>`, ...lines, `</${tag}>`];
2890
+ }
2891
+ function renderTag(tag, lines) {
2892
+ return [`<${tag}>`, ...lines, `</${tag}>`];
2862
2893
  }
2863
- function renderTag(tag, content) {
2894
+ function renderTagBlock(tag, content) {
2864
2895
  return [`<${tag}>`, content, `</${tag}>`].join("\n");
2865
2896
  }
2866
2897
  function formatAvailableSkillsForPrompt(skills) {
2867
2898
  if (skills.length === 0) {
2868
- return "<available_skills>\n</available_skills>";
2899
+ return "<available-skills>\n</available-skills>";
2869
2900
  }
2870
- const lines = ["<available_skills>"];
2901
+ const lines = ["<available-skills>"];
2871
2902
  for (const skill of skills) {
2872
2903
  const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
2873
2904
  lines.push(" <skill>");
@@ -2879,45 +2910,37 @@ function formatAvailableSkillsForPrompt(skills) {
2879
2910
  if (skill.pluginProvider) {
2880
2911
  lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
2881
2912
  }
2882
- if (skill.usesConfig && skill.usesConfig.length > 0) {
2883
- lines.push(
2884
- ` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
2885
- );
2886
- }
2887
2913
  lines.push(" </skill>");
2888
2914
  }
2889
- lines.push("</available_skills>");
2915
+ lines.push("</available-skills>");
2890
2916
  return lines.join("\n");
2891
2917
  }
2892
2918
  function formatLoadedSkillsForPrompt(skills) {
2893
2919
  if (skills.length === 0) {
2894
- return "<loaded_skills>\n</loaded_skills>";
2920
+ return "<loaded-skills>\n</loaded-skills>";
2895
2921
  }
2896
- const lines = ["<loaded_skills>"];
2922
+ const lines = ["<loaded-skills>"];
2897
2923
  for (const skill of skills) {
2898
2924
  const skillDir = workspaceSkillDir(skill.name);
2899
2925
  lines.push(
2900
2926
  ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
2901
2927
  );
2902
2928
  lines.push(`References are relative to ${escapeXml(skillDir)}.`);
2903
- if (skill.usesConfig && skill.usesConfig.length > 0) {
2904
- lines.push(
2905
- `Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
2906
- );
2907
- }
2908
2929
  lines.push("");
2909
2930
  lines.push(skill.body);
2910
2931
  lines.push(" </skill>");
2911
2932
  }
2912
- lines.push("</loaded_skills>");
2933
+ lines.push("</loaded-skills>");
2913
2934
  return lines.join("\n");
2914
2935
  }
2915
2936
  function formatProviderCatalogForPrompt() {
2916
2937
  const providers = getPluginProviders().map((plugin) => plugin.manifest);
2917
2938
  if (providers.length === 0) {
2918
- return "- none";
2939
+ return null;
2919
2940
  }
2920
- const lines = [];
2941
+ const lines = [
2942
+ "Config keys and default targets per provider; use after a skill is loaded."
2943
+ ];
2921
2944
  for (const provider of providers) {
2922
2945
  lines.push(`- provider: ${escapeXml(provider.name)}`);
2923
2946
  lines.push(
@@ -2931,288 +2954,260 @@ function formatProviderCatalogForPrompt() {
2931
2954
  }
2932
2955
  return lines.join("\n");
2933
2956
  }
2934
- function baseSystemPrompt() {
2935
- return [
2936
- "You are a Slack-based helper assistant.",
2937
- "Identity, tone, and domain defaults are defined in the personality block.",
2938
- "",
2939
- "- Be concise, practical, and specific.",
2940
- "- Prefer actionable next steps over generic explanations.",
2941
- "- When the user gives a clear task, execute it immediately in this turn.",
2942
- "- Do not ask for permission to proceed when the request is already clear.",
2943
- "- Keep user-visible progress communication concise and useful.",
2944
- "- In thread follow-ups, answer using prior thread context directly; do not repeat unresolved clarifying questions unless the user asks to refine.",
2945
- "- If the user asks what you just said or means by the previous answer, summarize your prior assistant reply plainly.",
2946
- "- Never ask the user to re-tag or re-invoke for a clear task; continue execution in this turn.",
2947
- "- Never claim you cannot access tools in this turn. If prior results are empty, run tools now.",
2948
- "- If critical input is missing and cannot be discovered with tools, ask one direct clarifying question.",
2949
- "- Always gather evidence from available sources (tools or skills) before answering factual questions.",
2950
- "- When a loaded skill exposes MCP capabilities, those tools are registered as callable tools. Call them directly by name.",
2951
- "- Use `searchTools` only when you need to rediscover or filter active MCP tools.",
2952
- "- Never guess. If you cannot verify with available sources, say it is unverified.",
2953
- "- Never claim a lookup succeeded unless a tool result supports it.",
2954
- "- Do not give up when unsure how to do something; find a viable path, gather evidence, and provide the best actionable way forward.",
2955
- "- When active skills are present, follow their instructions before default behavior."
2956
- ].join("\n");
2957
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
2958
+ if (catalogs.length === 0) {
2959
+ return null;
2960
+ }
2961
+ const lines = [
2962
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`."
2963
+ ];
2964
+ for (const catalog of catalogs) {
2965
+ lines.push(" <catalog>");
2966
+ lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
2967
+ lines.push(
2968
+ ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
2969
+ );
2970
+ lines.push(" </catalog>");
2971
+ }
2972
+ return lines.join("\n");
2957
2973
  }
2958
- function formatReferenceFilesSection() {
2974
+ function formatReferenceFilesLines() {
2959
2975
  const files = listReferenceFiles();
2960
2976
  if (files.length === 0) {
2961
- return [];
2977
+ return null;
2962
2978
  }
2963
- const fileNames = files.map((filePath) => {
2979
+ return files.map((filePath) => {
2964
2980
  const name = path2.basename(filePath);
2965
2981
  return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
2966
2982
  });
2967
- return [
2968
- renderTag(
2969
- "reference-files",
2970
- [
2971
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
2972
- ...fileNames
2973
- ].join("\n")
2974
- )
2975
- ];
2976
2983
  }
2977
- function buildSystemPrompt(params) {
2978
- const {
2979
- availableSkills,
2980
- activeSkills,
2981
- activeTools,
2982
- invocation,
2983
- requester,
2984
- assistant,
2985
- artifactState,
2986
- configuration,
2987
- relevantConfigurationKeys,
2988
- runtimeMetadata,
2989
- threadParticipants
2990
- } = params;
2991
- const assistantSection = renderIdentityBlock("assistant", {
2992
- user_name: assistant?.userName ?? botConfig.userName,
2993
- user_id: assistant?.userId
2994
- });
2995
- const requesterSection = renderIdentityBlock("requester", {
2996
- full_name: requester?.fullName,
2997
- user_name: requester?.userName,
2998
- user_id: requester?.userId
2999
- });
3000
- const availableSkillsSection = [
3001
- "The following skills provide specialized instructions for specific tasks.",
3002
- "Call `loadSkill` when the task matches a skill description.",
3003
- "When a skill references a relative path, resolve it against `skill_dir` and use that path with `bash`.",
3004
- "",
3005
- formatAvailableSkillsForPrompt(availableSkills)
3006
- ].join("\n");
3007
- const activeSkillsSection = [
3008
- "Loaded skills for this turn:",
3009
- formatLoadedSkillsForPrompt(activeSkills)
3010
- ].join("\n");
3011
- const activeToolNames = (activeTools ?? []).map((tool2) => tool2.tool_name);
3012
- const activeToolsSection = activeToolNames.length > 0 ? `Active MCP tools registered for this turn: ${activeToolNames.join(", ")}. Call them directly by name.` : "";
3013
- const configurationKeys = Object.keys(configuration ?? {}).sort(
2984
+ function formatArtifactsLines(artifactState) {
2985
+ if (!artifactState) return null;
2986
+ const lines = [];
2987
+ if (artifactState.lastCanvasId) {
2988
+ lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
2989
+ }
2990
+ if (artifactState.lastCanvasUrl) {
2991
+ lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
2992
+ }
2993
+ if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
2994
+ lines.push("- recent_canvases:");
2995
+ for (const canvas of artifactState.recentCanvases) {
2996
+ lines.push(` - id: ${escapeXml(canvas.id)}`);
2997
+ if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
2998
+ if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
2999
+ if (canvas.createdAt) {
3000
+ lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
3001
+ }
3002
+ }
3003
+ }
3004
+ if (artifactState.lastListId) {
3005
+ lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
3006
+ }
3007
+ if (artifactState.lastListUrl) {
3008
+ lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
3009
+ }
3010
+ return lines.length > 0 ? lines : null;
3011
+ }
3012
+ function formatConfigurationLines(configuration) {
3013
+ const keys = Object.keys(configuration ?? {}).sort(
3014
3014
  (a, b) => a.localeCompare(b)
3015
3015
  );
3016
- const relevantConfigSet = new Set(
3017
- (relevantConfigurationKeys ?? []).filter(
3018
- (key) => Object.prototype.hasOwnProperty.call(configuration ?? {}, key)
3019
- )
3016
+ if (keys.length === 0) return null;
3017
+ return keys.map(
3018
+ (key) => `- ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3019
+ );
3020
+ }
3021
+ function formatThreadParticipantsLines(participants) {
3022
+ if (!participants || participants.length === 0) return null;
3023
+ return participants.map((p) => {
3024
+ const parts = [];
3025
+ if (p.userId) {
3026
+ parts.push(`user_id: ${escapeXml(p.userId)}`);
3027
+ parts.push(`slack_mention: <@${p.userId}>`);
3028
+ }
3029
+ if (p.userName) parts.push(`user_name: ${escapeXml(p.userName)}`);
3030
+ if (p.fullName) parts.push(`full_name: ${escapeXml(p.fullName)}`);
3031
+ return `- ${parts.join(", ")}`;
3032
+ });
3033
+ }
3034
+ var HEADER = "You are a Slack-based helper assistant. The behavior and output blocks below are authoritative; the personality block sets voice only.";
3035
+ var BEHAVIOR_RULES = [
3036
+ "- Load the best-matching skill/tool when relevant, then use it before answering; do not preload multiple skills or claim tool use that did not happen.",
3037
+ "- After `loadSkill`, resolve references under `skill_dir`; for active MCP catalogs, use `searchMcpTools` then `callMcpTool` with exact returned tool names.",
3038
+ "- Default to acting in-turn: use relevant available skills/tools to satisfy the request, continue until done or blocked, and only ask the user when access or required input is missing. If a fact cannot be verified, say so.",
3039
+ "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
3040
+ "- Keep work silent and post one result-focused reply unless blocked or waiting on user input; do not use reactions as progress.",
3041
+ "- Do not claim an attachment, canvas, or channel post succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
3042
+ "- Run authenticated provider commands directly; resolve target defaults first and let the runtime handle auth pauses/resumes.",
3043
+ "- On resumed turns, post a brief continuation notice, then the resumed answer as a separate message.",
3044
+ "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
3045
+ "- Run `jr-rpc config get|set|unset|list` as standalone bash commands for conversation-scoped provider defaults.",
3046
+ "- For explicit channel-post or emoji-reaction requests, skip the text reply."
3047
+ ];
3048
+ function buildOutputSection() {
3049
+ const openTag = `<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
3050
+ return [
3051
+ openTag,
3052
+ "- Use Slack-friendly mrkdwn: bolded section labels instead of headings, no markdown tables or markdown links, and plain URLs.",
3053
+ "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
3054
+ "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to a short summary plus the canvas link.",
3055
+ "- End every turn with a final user-facing markdown response.",
3056
+ "</output>"
3057
+ ].join("\n");
3058
+ }
3059
+ function buildContextSection(params) {
3060
+ const blocks = [];
3061
+ if (JUNIOR_WORLD) {
3062
+ blocks.push(renderTag("world", [JUNIOR_WORLD.trim()]));
3063
+ }
3064
+ const referenceLines = formatReferenceFilesLines();
3065
+ if (referenceLines) {
3066
+ blocks.push(
3067
+ renderTag("reference-files", [
3068
+ "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
3069
+ ...referenceLines
3070
+ ])
3071
+ );
3072
+ }
3073
+ const runtimeVersion = getRuntimeMetadata().version;
3074
+ if (runtimeVersion) {
3075
+ blocks.push([`<runtime version="${escapeXml(runtimeVersion)}" />`]);
3076
+ }
3077
+ blocks.push(
3078
+ renderIdentityBlock("assistant", {
3079
+ user_name: params.assistant?.userName ?? botConfig.userName,
3080
+ user_id: params.assistant?.userId
3081
+ })
3020
3082
  );
3021
- const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map(
3022
- (key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3083
+ blocks.push(
3084
+ renderIdentityBlock("requester", {
3085
+ full_name: params.requester?.fullName,
3086
+ user_name: params.requester?.userName,
3087
+ user_id: params.requester?.userId
3088
+ })
3023
3089
  );
3024
- const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map(
3025
- (key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3090
+ const participantLines = formatThreadParticipantsLines(
3091
+ params.threadParticipants
3026
3092
  );
3027
- const configurationSection = [
3028
- "Use these conversation-scoped defaults when the user has not provided explicit values in this turn.",
3029
- "If explicit user input conflicts with configuration, follow explicit user input.",
3030
- configurationKeys.length === 0 ? "- none" : [
3031
- ...relevantConfigLines.length > 0 ? ["- relevant_for_active_skills:", ...relevantConfigLines] : [],
3032
- ...otherConfigLines.length > 0 ? ["- other_available_keys:", ...otherConfigLines] : []
3033
- ].join("\n")
3034
- ].join("\n");
3093
+ if (participantLines) {
3094
+ blocks.push(
3095
+ renderTag("thread-participants", [
3096
+ "Known participants. When you mention one of these people, use the provided `<@USERID>` token exactly; do not write a bare `@name`.",
3097
+ ...participantLines
3098
+ ])
3099
+ );
3100
+ }
3101
+ const artifactLines = formatArtifactsLines(params.artifactState);
3102
+ if (artifactLines) {
3103
+ blocks.push(renderTag("artifacts", artifactLines));
3104
+ }
3105
+ const configLines = formatConfigurationLines(params.configuration);
3106
+ if (configLines) {
3107
+ blocks.push(
3108
+ renderTag("configuration", [
3109
+ "Conversation-scoped defaults. Follow explicit user input when it conflicts.",
3110
+ ...configLines
3111
+ ])
3112
+ );
3113
+ }
3114
+ if (params.turnState === "resumed") {
3115
+ blocks.push([
3116
+ "<turn-state>resumed</turn-state>",
3117
+ "This turn continues from a prior checkpoint. Prior tool results and assistant messages are already in the conversation history."
3118
+ ]);
3119
+ }
3120
+ if (params.invocation) {
3121
+ blocks.push([
3122
+ `<explicit-skill-trigger>/${escapeXml(params.invocation.skillName)}</explicit-skill-trigger>`
3123
+ ]);
3124
+ }
3125
+ const body = blocks.map((block) => block.join("\n")).join("\n\n");
3126
+ return renderTagBlock("context", body);
3127
+ }
3128
+ function buildCapabilitiesSection(params) {
3129
+ const blocks = [];
3130
+ blocks.push(formatAvailableSkillsForPrompt(params.availableSkills));
3131
+ blocks.push(formatLoadedSkillsForPrompt(params.activeSkills));
3132
+ const activeCatalogs = formatActiveMcpCatalogsForPrompt(
3133
+ params.activeMcpCatalogs
3134
+ );
3135
+ if (activeCatalogs) {
3136
+ blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
3137
+ }
3138
+ const providerCatalog = formatProviderCatalogForPrompt();
3139
+ if (providerCatalog) {
3140
+ blocks.push(renderTagBlock("providers", providerCatalog));
3141
+ }
3142
+ return renderTagBlock("capabilities", blocks.join("\n\n"));
3143
+ }
3144
+ function buildSystemPrompt(params) {
3035
3145
  const sections = [
3036
- baseSystemPrompt(),
3037
- renderTag(
3038
- "personality",
3039
- [
3040
- "Always follow the personality guidance for tone/style unless safety or policy constraints require otherwise.",
3041
- "",
3042
- JUNIOR_PERSONALITY.trim()
3043
- ].join("\n")
3044
- ),
3045
- ...JUNIOR_WORLD ? [
3046
- renderTag(
3047
- "world",
3048
- [
3049
- "Use this as the assistant's operational/domain context.",
3050
- "",
3051
- JUNIOR_WORLD.trim()
3052
- ].join("\n")
3053
- )
3054
- ] : [],
3055
- ...formatReferenceFilesSection(),
3056
- renderTag(
3057
- "identity-context",
3058
- [
3059
- "Use these blocks as authoritative metadata for identity questions.",
3060
- assistantSection,
3061
- requesterSection,
3062
- ...threadParticipants && threadParticipants.length > 0 ? [
3063
- renderTag(
3064
- "thread-participants",
3065
- [
3066
- "Known participants in this thread. When you mention one of these people, use the provided Slack mention token exactly as `<@USERID>` and do not write a bare `@name` form.",
3067
- ...threadParticipants.map((p) => {
3068
- const parts = [];
3069
- if (p.userId) {
3070
- parts.push(`user_id: ${escapeXml(p.userId)}`);
3071
- parts.push(`slack_mention: <@${p.userId}>`);
3072
- }
3073
- if (p.userName)
3074
- parts.push(`user_name: ${escapeXml(p.userName)}`);
3075
- if (p.fullName)
3076
- parts.push(`full_name: ${escapeXml(p.fullName)}`);
3077
- return `- ${parts.join(", ")}`;
3078
- })
3079
- ].join("\n")
3080
- )
3081
- ] : []
3082
- ].join("\n")
3083
- ),
3084
- renderTag(
3085
- "artifact-context",
3086
- [
3087
- "Use this thread-scoped memory for follow-up updates to existing Slack artifacts.",
3088
- artifactState ? [
3089
- artifactState.lastCanvasId ? `- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}` : "- last_canvas_id: none",
3090
- artifactState.lastCanvasUrl ? `- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}` : "- last_canvas_url: none",
3091
- artifactState.recentCanvases && artifactState.recentCanvases.length > 0 ? [
3092
- "- recent_canvases:",
3093
- ...artifactState.recentCanvases.map(
3094
- (canvas) => [
3095
- ` - id: ${escapeXml(canvas.id)}`,
3096
- canvas.title ? ` title: ${escapeXml(canvas.title)}` : " title: [unknown]",
3097
- canvas.url ? ` url: ${escapeXml(canvas.url)}` : " url: [unknown]",
3098
- canvas.createdAt ? ` created_at: ${escapeXml(canvas.createdAt)}` : " created_at: [unknown]"
3099
- ].join("\n")
3100
- )
3101
- ].join("\n") : "- recent_canvases: none",
3102
- artifactState.lastListId ? `- last_list_id: ${escapeXml(artifactState.lastListId)}` : "- last_list_id: none",
3103
- artifactState.lastListUrl ? `- last_list_url: ${escapeXml(artifactState.lastListUrl)}` : "- last_list_url: none"
3104
- ].join("\n") : "- none"
3105
- ].join("\n")
3106
- ),
3107
- renderTag("configuration-context", configurationSection),
3108
- renderTag(
3109
- "runtime-metadata",
3110
- [
3111
- "Use this for runtime version questions about the deployed assistant.",
3112
- `- version: ${escapeXml(runtimeMetadata?.version ?? "unknown")}`
3113
- ].join("\n")
3114
- ),
3115
- renderTag(
3116
- "provider-config",
3117
- [
3118
- "Use this catalog to map already-chosen provider work to valid config keys and provider defaults.",
3119
- "Do not use this catalog by itself to decide which domain skill matches an operational task.",
3120
- "When user intent is to set a provider default, choose a config key from this catalog and use jr-rpc config set.",
3121
- "The `jr-rpc` config command is a built-in bash custom command when conversation config is available; do not claim it is missing just because no `jr-rpc` skill is loaded.",
3122
- formatProviderCatalogForPrompt()
3123
- ].join("\n")
3124
- ),
3125
- renderTag(
3126
- "skill-routing",
3127
- [
3128
- "- Choose the skill that matches the requested operation, not incidental nouns, product names, organization names, or channel context.",
3129
- "- When multiple skills seem adjacent, prefer the one whose description matches the user's requested action most directly.",
3130
- "- If the task needs evidence from a specialized external system or workflow, load the matching skill before drawing conclusions.",
3131
- "- The provider-config catalog is for exact config keys and provider defaults after skill selection. It is not a shortcut for choosing a domain.",
3132
- "- Never start provider auth speculatively. First load the skill that owns the operation, then let runtime-managed credential injection handle authenticated provider commands for that skill."
3133
- ].join("\n")
3134
- ),
3135
- renderTag(
3136
- "tool-usage",
3137
- [
3138
- "- For factual or external questions, run tools/skills first, then answer from evidence.",
3139
- "- Use tool descriptions as the source of truth for when each tool should or should not be called.",
3140
- "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
3141
- "- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
3142
- "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
3143
- "- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
3144
- "- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
3145
- "- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
3146
- "- When the user provides multiple sources, synthesize them explicitly as one combined answer instead of anchoring the reply on a single page or API call.",
3147
- "- When the user provides explicit URLs or named sources, briefly anchor the answer to those provided sources (for example, 'Across the provided Slack docs...') so the summary reads as researched rather than generic memory.",
3148
- "- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
3149
- "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
3150
- "- If `attachFile` fails, explain the failure and do not say the file was shared.",
3151
- "- When you create or update a Slack artifact in this turn (for example a canvas, list, posted message, or attached file), mention it explicitly in the final reply and include its link when the tool returned one.",
3152
- "- For explicit in-channel post requests, prefer no thread text reply after a successful channel post. A reaction-only acknowledgment is acceptable when useful.",
3153
- "- When the user explicitly asks for an emoji reaction instead of text, react and skip the text reply.",
3154
- "- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
3155
- "- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
3156
- "- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
3157
- "- If provider authorization is required, the runtime sends the private authorization link itself and resumes the paused turn after authorization.",
3158
- "- Do not try to manage provider auth directly. Run the real provider command and let the runtime handle authorization, reconnect, and resume behavior.",
3159
- "- Provider-targeted commands may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3160
- "- To persist or read conversation defaults, choose a config key from the provider catalog or active skill metadata and run `jr-rpc config get|set|unset|list ...` as a bash command.",
3161
- "- `jr-rpc` config commands are built into the bash runtime for conversation-scoped config work; they do not require a separate helper binary in the sandbox.",
3162
- "- When your work is complete, provide the exact user-facing markdown response.",
3163
- "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3164
- "- Never call side-effecting tools when the user only asked for analysis or options.",
3165
- "- When the user asks for their conversation ID, trace ID, or a reference for Sentry lookup, use the IDs from `<session-context>` and `<turn-context>` in the user turn."
3166
- ].join("\n")
3167
- ),
3168
- renderTag(
3169
- "skills",
3170
- [
3171
- "- Explicit skill triggers may appear as `/skillname`.",
3172
- "- If explicitly invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
3173
- "- If an explicitly invoked skill is present in <loaded_skills>, never say the skill is unavailable, missing, or unsupported in this environment.",
3174
- "- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
3175
- "- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
3176
- "- When multiple skills appear relevant, prefer the skill whose description best matches the requested action rather than incidental domain nouns in the prompt.",
3177
- "- For explicit config tasks, you may load the helper skill that owns config commands, but do not use helper skills to choose the provider for unrelated operational work.",
3178
- "- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3179
- "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3180
- "- Load only the best matching skill first; do not load multiple skills upfront.",
3181
- "- After `loadSkill`, use `skill_dir` as the root for any referenced files you read via `bash`.",
3182
- "- If a loaded skill exposes MCP tools, they are registered as callable tools after `loadSkill` returns. Call them directly by name (for example `mcp__provider__tool_name`).",
3183
- "- If no skill is a clear fit, continue with normal tool usage."
3184
- ].join("\n")
3185
- ),
3186
- renderTag(
3187
- "output-contract",
3188
- [
3189
- "Always produce output that follows this contract:",
3190
- `<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`,
3191
- "- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
3192
- "- Keep normal responses brief and scannable.",
3193
- "- If depth is needed, start with a concise summary and then provide fuller detail.",
3194
- "- Prefer a single compact thread reply when the full answer comfortably fits within this inline budget.",
3195
- "- When canvas creation is available and a research or document-style answer would likely need continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to a short summary plus the canvas link.",
3196
- "- Typical canvas-first cases include long-form research summaries, timelines, bios or profiles, structured notes, plans, comparisons, and other reusable reference documents.",
3197
- "- Do not create a canvas for short factual answers that fit cleanly in one normal thread reply.",
3198
- "- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
3199
- "- Do not narrate tool execution or repeated status updates in the visible reply.",
3200
- "- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
3201
- "- End every turn with a final user-facing markdown response.",
3202
- "</output>"
3203
- ].join("\n")
3204
- ),
3205
- availableSkillsSection,
3206
- activeSkillsSection,
3207
- ...activeToolsSection ? [activeToolsSection] : [],
3208
- renderTag(
3209
- "invocation-context",
3210
- invocation ? `Explicit skill trigger detected: /${invocation.skillName}` : "No explicit skill trigger detected."
3211
- )
3146
+ HEADER,
3147
+ renderTagBlock("personality", JUNIOR_PERSONALITY.trim()),
3148
+ buildContextSection({
3149
+ assistant: params.assistant,
3150
+ requester: params.requester,
3151
+ artifactState: params.artifactState,
3152
+ configuration: params.configuration,
3153
+ threadParticipants: params.threadParticipants,
3154
+ invocation: params.invocation,
3155
+ turnState: params.turnState
3156
+ }),
3157
+ buildCapabilitiesSection({
3158
+ availableSkills: params.availableSkills,
3159
+ activeSkills: params.activeSkills,
3160
+ activeMcpCatalogs: params.activeMcpCatalogs ?? []
3161
+ }),
3162
+ renderTagBlock("behavior", BEHAVIOR_RULES.join("\n")),
3163
+ buildOutputSection()
3212
3164
  ];
3213
3165
  return sections.join("\n\n");
3214
3166
  }
3215
3167
 
3168
+ // src/chat/capabilities/catalog.ts
3169
+ var cachedCatalog;
3170
+ function getCapabilityCatalog() {
3171
+ const signature = getPluginCatalogSignature();
3172
+ if (cachedCatalog?.signature === signature) return cachedCatalog;
3173
+ const providers = getPluginCapabilityProviders();
3174
+ const capabilityToProvider = /* @__PURE__ */ new Map();
3175
+ for (const provider of providers) {
3176
+ for (const capability of provider.capabilities) {
3177
+ if (capabilityToProvider.has(capability)) {
3178
+ throw new Error(
3179
+ `Duplicate capability registration for "${capability}"`
3180
+ );
3181
+ }
3182
+ capabilityToProvider.set(capability, provider);
3183
+ }
3184
+ }
3185
+ cachedCatalog = { signature, providers, capabilityToProvider };
3186
+ return cachedCatalog;
3187
+ }
3188
+ var catalogLogged = false;
3189
+ function logCapabilityCatalogLoadedOnce() {
3190
+ if (catalogLogged) return;
3191
+ catalogLogged = true;
3192
+ const { providers } = getCapabilityCatalog();
3193
+ const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
3194
+ const configKeys = [
3195
+ ...new Set(providers.flatMap((p) => p.configKeys))
3196
+ ].sort();
3197
+ logInfo(
3198
+ "capability_catalog_loaded",
3199
+ {},
3200
+ {
3201
+ "app.capability.providers": providers.map((p) => p.provider),
3202
+ "app.capability.count": capabilityNames.length,
3203
+ "app.capability.names": capabilityNames,
3204
+ "app.config.key_count": configKeys.length,
3205
+ "app.config.keys": configKeys
3206
+ },
3207
+ "Loaded capability provider catalog"
3208
+ );
3209
+ }
3210
+
3216
3211
  // src/chat/capabilities/router.ts
3217
3212
  var ProviderCredentialRouter = class {
3218
3213
  brokersByProvider;
@@ -3938,6 +3933,27 @@ var MCP_CLIENT_INFO = {
3938
3933
  name: "junior-mcp-client",
3939
3934
  version: "1.0.0"
3940
3935
  };
3936
+ var MCP_TOOLS_CALL_METHOD = "tools/call";
3937
+ function getDefaultPort(url) {
3938
+ if (url.protocol === "https:") {
3939
+ return 443;
3940
+ }
3941
+ if (url.protocol === "http:") {
3942
+ return 80;
3943
+ }
3944
+ return void 0;
3945
+ }
3946
+ function getMcpNetworkAttributes(url) {
3947
+ const port = url.port ? Number(url.port) : getDefaultPort(url);
3948
+ return {
3949
+ "server.address": url.hostname,
3950
+ ...port !== void 0 ? { "server.port": port } : {},
3951
+ ...url.protocol === "http:" || url.protocol === "https:" ? {
3952
+ "network.protocol.name": "http",
3953
+ "network.transport": "tcp"
3954
+ } : {}
3955
+ };
3956
+ }
3941
3957
  var McpAuthorizationRequiredError = class extends Error {
3942
3958
  provider;
3943
3959
  constructor(provider, message) {
@@ -3987,10 +4003,22 @@ var PluginMcpClient = class {
3987
4003
  async callTool(name, args) {
3988
4004
  return await this.withSessionRecovery(async () => {
3989
4005
  const client2 = await this.getClient();
4006
+ const mcp = this.plugin.manifest.mcp;
4007
+ if (mcp) {
4008
+ const url = new URL(mcp.url);
4009
+ setSpanAttributes({
4010
+ "mcp.method.name": MCP_TOOLS_CALL_METHOD,
4011
+ "gen_ai.operation.name": "execute_tool",
4012
+ "gen_ai.tool.name": name,
4013
+ ...this.transport?.sessionId ? { "mcp.session.id": this.transport.sessionId } : {},
4014
+ ...this.transport?.protocolVersion ? { "mcp.protocol.version": this.transport.protocolVersion } : {},
4015
+ ...getMcpNetworkAttributes(url)
4016
+ });
4017
+ }
3990
4018
  const result = await this.wrapAuth(
3991
4019
  client2.callTool({
3992
4020
  name,
3993
- ...args && Object.keys(args).length > 0 ? { arguments: args } : {}
4021
+ arguments: args ?? {}
3994
4022
  })
3995
4023
  );
3996
4024
  await this.syncTransportSessionId();
@@ -4103,13 +4131,24 @@ var PluginMcpClient = class {
4103
4131
  }
4104
4132
  };
4105
4133
 
4106
- // src/chat/mcp/tool-manager.ts
4134
+ // src/chat/mcp/errors.ts
4107
4135
  var McpToolError = class extends Error {
4108
4136
  constructor(message) {
4109
4137
  super(message);
4110
4138
  this.name = "McpToolError";
4111
4139
  }
4112
4140
  };
4141
+ function getMcpAwareErrorType(error, fallback) {
4142
+ if (error instanceof McpToolError) {
4143
+ return "tool_error";
4144
+ }
4145
+ return error instanceof Error ? error.name : fallback;
4146
+ }
4147
+ function getMcpAwareErrorMessage(error) {
4148
+ return error instanceof Error ? error.message : String(error);
4149
+ }
4150
+
4151
+ // src/chat/mcp/tool-manager.ts
4113
4152
  function normalizeMcpToolName(provider, toolName) {
4114
4153
  return `mcp__${provider}__${toolName}`;
4115
4154
  }
@@ -4275,24 +4314,6 @@ var McpToolManager = class {
4275
4314
  (tool2) => this.toToolDescriptor(tool2)
4276
4315
  );
4277
4316
  }
4278
- searchTools(skills, query, options = {}) {
4279
- const resolved = this.getResolvedActiveTools(skills, options);
4280
- const trimmedQuery = query.trim();
4281
- if (!trimmedQuery || trimmedQuery === "*") {
4282
- return resolved.slice(0, Math.max(1, options.limit ?? 8)).map((tool2) => this.toToolDescriptor(tool2));
4283
- }
4284
- const normalizedQuery = trimmedQuery.toLowerCase();
4285
- const queryTokens = normalizedQuery.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
4286
- return resolved.map((tool2) => ({
4287
- tool: tool2,
4288
- score: this.scoreToolMatch(tool2, normalizedQuery, queryTokens)
4289
- })).filter((entry) => entry.score > 0).sort((left, right) => {
4290
- if (right.score !== left.score) {
4291
- return right.score - left.score;
4292
- }
4293
- return left.tool.name.localeCompare(right.tool.name);
4294
- }).slice(0, Math.max(1, options.limit ?? 8)).map((entry) => this.toToolDescriptor(entry.tool));
4295
- }
4296
4317
  filterListedTools(plugin, tools) {
4297
4318
  const allowedTools = plugin.manifest.mcp?.allowedTools;
4298
4319
  if (!allowedTools || allowedTools.length === 0) {
@@ -4324,6 +4345,8 @@ var McpToolManager = class {
4324
4345
  return client2;
4325
4346
  }
4326
4347
  toManagedTool(plugin, client2, tool2) {
4348
+ const outputSchema = toOptionalRecord(tool2.outputSchema);
4349
+ const annotations = toOptionalRecord(tool2.annotations);
4327
4350
  return {
4328
4351
  name: normalizeMcpToolName(plugin.manifest.name, tool2.name),
4329
4352
  description: describeMcpTool(plugin.manifest.name, tool2),
@@ -4331,8 +4354,15 @@ var McpToolManager = class {
4331
4354
  provider: plugin.manifest.name,
4332
4355
  rawName: tool2.name,
4333
4356
  ...tool2.title?.trim() ? { title: tool2.title.trim() } : {},
4357
+ ...outputSchema ? { outputSchema } : {},
4358
+ ...annotations ? { annotations } : {},
4334
4359
  execute: async (args) => {
4335
4360
  const resolvedArgs = typeof args === "object" && args !== null ? args : {};
4361
+ const baseAttributes = {
4362
+ "gen_ai.tool.name": tool2.name,
4363
+ "mcp.method.name": "tools/call"
4364
+ };
4365
+ setSpanAttributes(baseAttributes);
4336
4366
  try {
4337
4367
  const result = await client2.callTool(tool2.name, resolvedArgs);
4338
4368
  if ("isError" in result && result.isError) {
@@ -4368,6 +4398,20 @@ var McpToolManager = class {
4368
4398
  }
4369
4399
  };
4370
4400
  }
4401
+ const errorAttributes = {
4402
+ ...baseAttributes,
4403
+ "error.type": getMcpAwareErrorType(error, "mcp_tool_error"),
4404
+ "error.message": getMcpAwareErrorMessage(error)
4405
+ };
4406
+ setSpanAttributes(errorAttributes);
4407
+ if (error instanceof McpToolError) {
4408
+ logWarn(
4409
+ "mcp_tool_call_failed",
4410
+ {},
4411
+ errorAttributes,
4412
+ "MCP tool call failed"
4413
+ );
4414
+ }
4371
4415
  throw error;
4372
4416
  }
4373
4417
  }
@@ -4414,37 +4458,19 @@ var McpToolManager = class {
4414
4458
  toToolDescriptor(tool2) {
4415
4459
  return {
4416
4460
  name: tool2.name,
4461
+ rawName: tool2.rawName,
4462
+ ...tool2.title ? { title: tool2.title } : {},
4417
4463
  description: tool2.description,
4418
4464
  parameters: tool2.parameters,
4465
+ ...tool2.outputSchema ? { outputSchema: tool2.outputSchema } : {},
4466
+ ...tool2.annotations ? { annotations: tool2.annotations } : {},
4419
4467
  provider: tool2.provider
4420
4468
  };
4421
4469
  }
4422
- scoreToolMatch(tool2, normalizedQuery, queryTokens) {
4423
- const exactCandidates = [tool2.name, tool2.rawName, tool2.title].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
4424
- if (exactCandidates.includes(normalizedQuery)) {
4425
- return 100;
4426
- }
4427
- let score = 0;
4428
- const searchableText = [
4429
- tool2.name,
4430
- tool2.rawName,
4431
- tool2.title,
4432
- tool2.description,
4433
- tool2.provider
4434
- ].filter((value) => Boolean(value)).join(" ").toLowerCase();
4435
- for (const candidate of exactCandidates) {
4436
- if (candidate.startsWith(normalizedQuery)) {
4437
- score = Math.max(score, 60);
4438
- }
4439
- }
4440
- for (const token of queryTokens) {
4441
- if (searchableText.includes(token)) {
4442
- score += 10;
4443
- }
4444
- }
4445
- return score;
4446
- }
4447
4470
  };
4471
+ function toOptionalRecord(value) {
4472
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4473
+ }
4448
4474
 
4449
4475
  // src/chat/tools/definition.ts
4450
4476
  function tool(definition) {
@@ -4757,8 +4783,61 @@ function createImageGenerateTool(hooks, deps = {}) {
4757
4783
  });
4758
4784
  }
4759
4785
 
4760
- // src/chat/tools/skill/load-skill.ts
4786
+ // src/chat/tools/skill/call-mcp-tool.ts
4761
4787
  import { Type as Type4 } from "@sinclair/typebox";
4788
+ function resolveMcpArguments(input) {
4789
+ const extraKeys = Object.keys(input).filter(
4790
+ (key) => key !== "tool_name" && key !== "arguments"
4791
+ );
4792
+ if (extraKeys.length > 0) {
4793
+ throw new Error(
4794
+ `callMcpTool MCP arguments must be nested under arguments, not top-level fields: ${extraKeys.join(", ")}`
4795
+ );
4796
+ }
4797
+ if ("arguments" in input) {
4798
+ const args = input.arguments;
4799
+ if (args === void 0) {
4800
+ return {};
4801
+ }
4802
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
4803
+ throw new Error("callMcpTool arguments must be an object when provided");
4804
+ }
4805
+ return args;
4806
+ }
4807
+ return {};
4808
+ }
4809
+ function createCallMcpToolTool(mcpToolManager, getActiveSkills) {
4810
+ return tool({
4811
+ description: "Call an active MCP tool by exact tool_name. Use loadSkill to activate the provider, then searchMcpTools to discover tool names and schemas; authorization is handled by the runtime when required.",
4812
+ inputSchema: Type4.Object(
4813
+ {
4814
+ tool_name: Type4.String({
4815
+ minLength: 1,
4816
+ description: "Exact MCP tool_name from searchMcpTools."
4817
+ }),
4818
+ arguments: Type4.Optional(
4819
+ Type4.Record(Type4.String(), Type4.Unknown(), {
4820
+ description: "Arguments matching the disclosed MCP tool schema."
4821
+ })
4822
+ )
4823
+ },
4824
+ { additionalProperties: false }
4825
+ ),
4826
+ execute: async (input) => {
4827
+ const { tool_name } = input;
4828
+ const mcpTool = mcpToolManager.getResolvedActiveTools(getActiveSkills()).find((candidate) => candidate.name === tool_name);
4829
+ if (!mcpTool) {
4830
+ throw new Error(`MCP tool is not active for this turn: ${tool_name}`);
4831
+ }
4832
+ return await mcpTool.execute(
4833
+ resolveMcpArguments(input)
4834
+ );
4835
+ }
4836
+ });
4837
+ }
4838
+
4839
+ // src/chat/tools/skill/load-skill.ts
4840
+ import { Type as Type5 } from "@sinclair/typebox";
4762
4841
  function toLoadedSkill(result, availableSkills) {
4763
4842
  if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
4764
4843
  return null;
@@ -4770,7 +4849,6 @@ function toLoadedSkill(result, availableSkills) {
4770
4849
  skillPath: metadata?.skillPath ?? result.skill_dir,
4771
4850
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
4772
4851
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
4773
- ...metadata?.usesConfig ? { usesConfig: metadata.usesConfig } : {},
4774
4852
  body: result.instructions
4775
4853
  };
4776
4854
  }
@@ -4803,9 +4881,9 @@ async function loadSkillFromHost(availableSkills, skillName) {
4803
4881
  }
4804
4882
  function createLoadSkillTool(availableSkills, options) {
4805
4883
  return tool({
4806
- description: "Load a skill by name so its instructions are available for this turn. The result includes `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
4807
- inputSchema: Type4.Object({
4808
- skill_name: Type4.String({
4884
+ description: "Load a skill by name so its instructions and provider tool catalog are available for this turn. When the result includes mcp_provider and available_tool_count, use searchMcpTools to list or search descriptors before callMcpTool. Use when a request clearly matches a known skill.",
4885
+ inputSchema: Type5.Object({
4886
+ skill_name: Type5.String({
4809
4887
  minLength: 1,
4810
4888
  description: "Skill name to load, without the leading slash."
4811
4889
  })
@@ -4824,53 +4902,87 @@ function createLoadSkillTool(availableSkills, options) {
4824
4902
  });
4825
4903
  }
4826
4904
 
4827
- // src/chat/tools/sandbox/read-file.ts
4828
- import { Type as Type5 } from "@sinclair/typebox";
4829
- function createReadFileTool() {
4830
- return tool({
4831
- description: "Read a file from the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Do not use for broad discovery when search tools are better.",
4832
- inputSchema: Type5.Object(
4833
- {
4834
- path: Type5.String({
4835
- minLength: 1,
4836
- description: "Path to the file in the sandbox workspace."
4837
- })
4838
- },
4839
- { additionalProperties: false }
4840
- ),
4841
- execute: async () => {
4842
- throw new Error(
4843
- "readFile can only run when sandbox execution is enabled."
4844
- );
4845
- }
4846
- });
4847
- }
4848
-
4849
- // src/chat/tools/runtime/report-progress.ts
4905
+ // src/chat/tools/skill/search-mcp-tools.ts
4850
4906
  import { Type as Type6 } from "@sinclair/typebox";
4851
- function createReportProgressTool() {
4852
- return tool({
4853
- description: "Update the user-visible assistant loading message with a short progress phase. For every non-trivial turn, call this early with the initial major work phase, then call it again only when the major phase meaningfully changes. Messages must be written in sentence case with a present-participle verb (e.g. 'Searching docs', 'Reviewing results', 'Running checks'). Skip trivial direct answers, generic filler, and minor substeps.",
4854
- inputSchema: Type6.Object({
4855
- message: Type6.String({
4856
- minLength: 1,
4857
- description: "Short user-facing progress message."
4858
- })
4859
- })
4860
- });
4861
- }
4862
-
4863
- // src/chat/tools/skill/search-tools.ts
4864
- import { Type as Type7 } from "@sinclair/typebox";
4865
4907
 
4866
4908
  // src/chat/tools/skill/mcp-tool-summary.ts
4867
- function summarizeInputSchema(schema) {
4868
- const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
4869
- const required = Array.isArray(schema.required) ? new Set(
4909
+ function getSchemaProperties(schema) {
4910
+ return schema.properties && typeof schema.properties === "object" ? schema.properties : {};
4911
+ }
4912
+ function getRequiredFields(schema) {
4913
+ return Array.isArray(schema.required) ? new Set(
4870
4914
  schema.required.filter(
4871
4915
  (value) => typeof value === "string"
4872
4916
  )
4873
4917
  ) : /* @__PURE__ */ new Set();
4918
+ }
4919
+ function formatSchemaType(schema) {
4920
+ if (!schema || typeof schema !== "object") {
4921
+ return "unknown";
4922
+ }
4923
+ const typed = schema;
4924
+ const type = typed.type;
4925
+ if (typeof type === "string") {
4926
+ if (type === "array") {
4927
+ return `${formatSchemaType(typed.items)}[]`;
4928
+ }
4929
+ return type;
4930
+ }
4931
+ if (Array.isArray(type)) {
4932
+ return type.filter((value) => typeof value === "string").join(" | ");
4933
+ }
4934
+ if (Array.isArray(typed.enum) && typed.enum.length > 0) {
4935
+ return typed.enum.map((value) => JSON.stringify(value)).join(" | ");
4936
+ }
4937
+ return "unknown";
4938
+ }
4939
+ function formatArgumentPlaceholder(name, schema) {
4940
+ const type = formatSchemaType(schema);
4941
+ if (type === "string") {
4942
+ return `<${name}>`;
4943
+ }
4944
+ if (type === "number" || type === "integer") {
4945
+ return "<number>";
4946
+ }
4947
+ if (type === "boolean") {
4948
+ return "<boolean>";
4949
+ }
4950
+ if (type.endsWith("[]")) {
4951
+ return "<array>";
4952
+ }
4953
+ if (type === "object") {
4954
+ return "<object>";
4955
+ }
4956
+ return `<${type}>`;
4957
+ }
4958
+ function formatMcpToolSignature(toolName, schema) {
4959
+ const properties = getSchemaProperties(schema);
4960
+ const required = getRequiredFields(schema);
4961
+ const fields = Object.entries(properties).map(([name, propertySchema]) => {
4962
+ const marker = required.has(name) ? "" : "?";
4963
+ return `${name}${marker}: ${formatSchemaType(propertySchema)}`;
4964
+ });
4965
+ if (fields.length === 0) {
4966
+ return `${toolName}()`;
4967
+ }
4968
+ return `${toolName}({ ${fields.join(", ")} })`;
4969
+ }
4970
+ function formatMcpToolCallExample(toolName, schema) {
4971
+ return {
4972
+ tool_name: toolName,
4973
+ arguments: Object.fromEntries(
4974
+ Object.entries(getSchemaProperties(schema)).map(
4975
+ ([name, propertySchema]) => [
4976
+ name,
4977
+ formatArgumentPlaceholder(name, propertySchema)
4978
+ ]
4979
+ )
4980
+ )
4981
+ };
4982
+ }
4983
+ function summarizeInputSchema(schema) {
4984
+ const properties = getSchemaProperties(schema);
4985
+ const required = getRequiredFields(schema);
4874
4986
  const propertyNames = Object.keys(properties);
4875
4987
  if (propertyNames.length === 0) {
4876
4988
  return "No arguments.";
@@ -4880,59 +4992,190 @@ function summarizeInputSchema(schema) {
4880
4992
  function toExposedToolSummary(toolDef) {
4881
4993
  return {
4882
4994
  tool_name: toolDef.name,
4995
+ mcp_tool_name: toolDef.rawName,
4883
4996
  provider: toolDef.provider,
4997
+ ...toolDef.title ? { title: toolDef.title } : {},
4884
4998
  description: toolDef.description,
4999
+ signature: formatMcpToolSignature(toolDef.name, toolDef.parameters),
5000
+ call: formatMcpToolCallExample(toolDef.name, toolDef.parameters),
4885
5001
  input_schema: toolDef.parameters,
4886
- input_schema_summary: summarizeInputSchema(toolDef.parameters)
4887
- };
5002
+ input_schema_summary: summarizeInputSchema(toolDef.parameters),
5003
+ ...toolDef.outputSchema ? { output_schema: toolDef.outputSchema } : {},
5004
+ ...toolDef.annotations ? { annotations: toolDef.annotations } : {}
5005
+ };
5006
+ }
5007
+ function toActiveMcpCatalogSummaries(toolDefs) {
5008
+ const countsByProvider = /* @__PURE__ */ new Map();
5009
+ for (const toolDef of toolDefs) {
5010
+ countsByProvider.set(
5011
+ toolDef.provider,
5012
+ (countsByProvider.get(toolDef.provider) ?? 0) + 1
5013
+ );
5014
+ }
5015
+ return [...countsByProvider.entries()].map(([provider, availableToolCount]) => ({
5016
+ provider,
5017
+ available_tool_count: availableToolCount
5018
+ })).sort((left, right) => left.provider.localeCompare(right.provider));
5019
+ }
5020
+
5021
+ // src/chat/tools/skill/search-mcp-tools.ts
5022
+ var DEFAULT_MAX_RESULTS = 5;
5023
+ var MAX_RESULTS = 20;
5024
+ function normalize(value) {
5025
+ return value.toLowerCase().replace(/[^a-z0-9_]+/g, " ").trim();
5026
+ }
5027
+ function searchableToolText(toolDef) {
5028
+ return normalize(
5029
+ [
5030
+ toolDef.name,
5031
+ toolDef.rawName,
5032
+ toolDef.title,
5033
+ toolDef.provider,
5034
+ toolDef.description,
5035
+ JSON.stringify(toolDef.parameters),
5036
+ JSON.stringify(toolDef.outputSchema),
5037
+ JSON.stringify(toolDef.annotations)
5038
+ ].filter(Boolean).join(" ")
5039
+ );
4888
5040
  }
4889
-
4890
- // src/chat/tools/skill/search-tools.ts
4891
- var DEFAULT_LIMIT = 5;
4892
- var MAX_LIMIT = 20;
4893
- function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5041
+ function scoreTool(toolDef, query) {
5042
+ const normalizedQuery = normalize(query);
5043
+ if (!normalizedQuery) {
5044
+ return 0;
5045
+ }
5046
+ const normalizedName = normalize(toolDef.name);
5047
+ const normalizedRawName = normalize(toolDef.rawName);
5048
+ const text = searchableToolText(toolDef);
5049
+ let score = 0;
5050
+ if (normalizedName === normalizedQuery || normalizedRawName === normalizedQuery) {
5051
+ score += 100;
5052
+ }
5053
+ if (normalizedName.includes(normalizedQuery)) {
5054
+ score += 50;
5055
+ }
5056
+ if (normalizedRawName.includes(normalizedQuery)) {
5057
+ score += 45;
5058
+ }
5059
+ if (text.includes(normalizedQuery)) {
5060
+ score += 25;
5061
+ }
5062
+ for (const term of normalizedQuery.split(/\s+/).filter(Boolean)) {
5063
+ if (normalizedName.includes(term)) {
5064
+ score += 12;
5065
+ }
5066
+ if (normalizedRawName.includes(term)) {
5067
+ score += 10;
5068
+ }
5069
+ if (text.includes(term)) {
5070
+ score += 4;
5071
+ }
5072
+ }
5073
+ return score;
5074
+ }
5075
+ function searchMcpCatalog(tools, query) {
5076
+ if (!normalize(query)) {
5077
+ return [...tools].sort(
5078
+ (left, right) => left.name.localeCompare(right.name)
5079
+ );
5080
+ }
5081
+ return tools.map(
5082
+ (toolDef) => ({
5083
+ tool: toolDef,
5084
+ score: scoreTool(toolDef, query)
5085
+ })
5086
+ ).filter((ranked) => ranked.score > 0).sort((left, right) => {
5087
+ if (right.score !== left.score) {
5088
+ return right.score - left.score;
5089
+ }
5090
+ return left.tool.name.localeCompare(right.tool.name);
5091
+ }).map((ranked) => ranked.tool);
5092
+ }
5093
+ function createSearchMcpToolsTool(mcpToolManager, getActiveSkills) {
4894
5094
  return tool({
4895
- description: "Search active MCP tools exposed by the currently loaded skills. Use when you need to rediscover or filter active tools.",
4896
- inputSchema: Type7.Object(
5095
+ description: "List or search active MCP tools and return full descriptors, including input/output schemas and annotations. Use after loadSkill when choosing a provider tool or when callMcpTool arguments are unclear.",
5096
+ inputSchema: Type6.Object(
4897
5097
  {
4898
- query: Type7.String({
4899
- minLength: 1,
4900
- description: "Search query for matching MCP tool names or descriptions."
4901
- }),
4902
- provider: Type7.Optional(
4903
- Type7.String({
5098
+ query: Type6.Optional(
5099
+ Type6.String({
5100
+ minLength: 1,
5101
+ description: "Optional search terms describing the MCP tool or arguments needed."
5102
+ })
5103
+ ),
5104
+ provider: Type6.Optional(
5105
+ Type6.String({
4904
5106
  minLength: 1,
4905
- description: "Optional MCP provider filter, for example notion or sentry."
5107
+ description: "Optional provider name to list or search within."
4906
5108
  })
4907
5109
  ),
4908
- limit: Type7.Optional(
4909
- Type7.Integer({
5110
+ max_results: Type6.Optional(
5111
+ Type6.Integer({
4910
5112
  minimum: 1,
4911
- maximum: MAX_LIMIT,
4912
- description: "Maximum number of matching tools to return."
5113
+ maximum: MAX_RESULTS,
5114
+ description: "Maximum matching tool descriptors to return."
4913
5115
  })
4914
5116
  )
4915
5117
  },
4916
5118
  { additionalProperties: false }
4917
5119
  ),
4918
- execute: async ({ query, provider, limit }) => {
4919
- const results = mcpToolManager.searchTools(getActiveSkills(), query, {
4920
- ...provider ? { provider } : {},
4921
- limit: limit ?? DEFAULT_LIMIT
4922
- }).map(toExposedToolSummary);
5120
+ execute: async ({ query, provider, max_results }) => {
5121
+ const catalog = mcpToolManager.getActiveToolCatalog(
5122
+ getActiveSkills(),
5123
+ provider ? { provider } : {}
5124
+ );
5125
+ const maxResults = max_results ?? DEFAULT_MAX_RESULTS;
5126
+ const matches = searchMcpCatalog(catalog, query ?? "").slice(
5127
+ 0,
5128
+ maxResults
5129
+ );
4923
5130
  return {
4924
- ok: true,
4925
- query,
4926
- ...provider ? { provider } : {},
4927
- result_count: results.length,
4928
- results
5131
+ query: query ?? null,
5132
+ provider: provider ?? null,
5133
+ total_active_tools: catalog.length,
5134
+ returned_tools: matches.length,
5135
+ tools: matches.map(toExposedToolSummary)
4929
5136
  };
4930
5137
  }
4931
5138
  });
4932
5139
  }
4933
5140
 
4934
- // src/chat/tools/slack/channel-list-messages.ts
5141
+ // src/chat/tools/sandbox/read-file.ts
5142
+ import { Type as Type7 } from "@sinclair/typebox";
5143
+ function createReadFileTool() {
5144
+ return tool({
5145
+ description: "Read a file from the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Do not use for broad discovery when search tools are better.",
5146
+ inputSchema: Type7.Object(
5147
+ {
5148
+ path: Type7.String({
5149
+ minLength: 1,
5150
+ description: "Path to the file in the sandbox workspace."
5151
+ })
5152
+ },
5153
+ { additionalProperties: false }
5154
+ ),
5155
+ execute: async () => {
5156
+ throw new Error(
5157
+ "readFile can only run when sandbox execution is enabled."
5158
+ );
5159
+ }
5160
+ });
5161
+ }
5162
+
5163
+ // src/chat/tools/runtime/report-progress.ts
4935
5164
  import { Type as Type8 } from "@sinclair/typebox";
5165
+ function createReportProgressTool() {
5166
+ return tool({
5167
+ description: "Update the user-visible assistant loading message with a short progress phase. For every non-trivial turn, call this early with the initial major work phase, then call it again only when the major phase meaningfully changes. Messages must be written in sentence case with a present-participle verb (e.g. 'Searching docs', 'Reviewing results', 'Running checks'). Skip trivial direct answers, generic filler, and minor substeps.",
5168
+ inputSchema: Type8.Object({
5169
+ message: Type8.String({
5170
+ minLength: 1,
5171
+ description: "Short user-facing progress message."
5172
+ })
5173
+ })
5174
+ });
5175
+ }
5176
+
5177
+ // src/chat/tools/slack/channel-list-messages.ts
5178
+ import { Type as Type9 } from "@sinclair/typebox";
4936
5179
 
4937
5180
  // src/chat/slack/channel.ts
4938
5181
  async function listChannelMessages(input) {
@@ -5021,39 +5264,39 @@ async function listThreadReplies(input) {
5021
5264
  function createSlackChannelListMessagesTool(context) {
5022
5265
  return tool({
5023
5266
  description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
5024
- inputSchema: Type8.Object({
5025
- limit: Type8.Optional(
5026
- Type8.Integer({
5267
+ inputSchema: Type9.Object({
5268
+ limit: Type9.Optional(
5269
+ Type9.Integer({
5027
5270
  minimum: 1,
5028
5271
  maximum: 1e3,
5029
5272
  description: "Maximum number of messages to return across pages."
5030
5273
  })
5031
5274
  ),
5032
- cursor: Type8.Optional(
5033
- Type8.String({
5275
+ cursor: Type9.Optional(
5276
+ Type9.String({
5034
5277
  minLength: 1,
5035
5278
  description: "Optional cursor to continue from a prior call."
5036
5279
  })
5037
5280
  ),
5038
- oldest: Type8.Optional(
5039
- Type8.String({
5281
+ oldest: Type9.Optional(
5282
+ Type9.String({
5040
5283
  minLength: 1,
5041
5284
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
5042
5285
  })
5043
5286
  ),
5044
- latest: Type8.Optional(
5045
- Type8.String({
5287
+ latest: Type9.Optional(
5288
+ Type9.String({
5046
5289
  minLength: 1,
5047
5290
  description: "Optional latest message timestamp (Slack ts) for range filtering."
5048
5291
  })
5049
5292
  ),
5050
- inclusive: Type8.Optional(
5051
- Type8.Boolean({
5293
+ inclusive: Type9.Optional(
5294
+ Type9.Boolean({
5052
5295
  description: "Whether oldest/latest bounds should be inclusive."
5053
5296
  })
5054
5297
  ),
5055
- max_pages: Type8.Optional(
5056
- Type8.Integer({
5298
+ max_pages: Type9.Optional(
5299
+ Type9.Integer({
5057
5300
  minimum: 1,
5058
5301
  maximum: 10,
5059
5302
  description: "Maximum number of API pages to traverse in a single call."
@@ -5107,7 +5350,7 @@ function createSlackChannelListMessagesTool(context) {
5107
5350
  }
5108
5351
 
5109
5352
  // src/chat/tools/slack/channel-post-message.ts
5110
- import { Type as Type9 } from "@sinclair/typebox";
5353
+ import { Type as Type10 } from "@sinclair/typebox";
5111
5354
 
5112
5355
  // src/chat/tools/idempotency.ts
5113
5356
  function stableSerialize(value) {
@@ -5129,8 +5372,8 @@ function createOperationKey(toolName, input) {
5129
5372
  function createSlackChannelPostMessageTool(context, state) {
5130
5373
  return tool({
5131
5374
  description: "Post a message in the active Slack channel context (outside the thread). Use this only when the user explicitly asks to post/send/share/say something in the current channel. Do not use it for normal thread replies, speculative broadcasts, or requests targeting another named channel; explain that limitation instead. Do not claim a channel message was posted unless this tool succeeds in this turn.",
5132
- inputSchema: Type9.Object({
5133
- text: Type9.String({
5375
+ inputSchema: Type10.Object({
5376
+ text: Type10.String({
5134
5377
  minLength: 1,
5135
5378
  maxLength: 4e4,
5136
5379
  description: "Slack mrkdwn text to post."
@@ -5173,12 +5416,12 @@ function createSlackChannelPostMessageTool(context, state) {
5173
5416
  }
5174
5417
 
5175
5418
  // src/chat/tools/slack/message-add-reaction.ts
5176
- import { Type as Type10 } from "@sinclair/typebox";
5419
+ import { Type as Type11 } from "@sinclair/typebox";
5177
5420
  function createSlackMessageAddReactionTool(context, state) {
5178
5421
  return tool({
5179
5422
  description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
5180
- inputSchema: Type10.Object({
5181
- emoji: Type10.String({
5423
+ inputSchema: Type11.Object({
5424
+ emoji: Type11.String({
5182
5425
  minLength: 1,
5183
5426
  maxLength: 64,
5184
5427
  description: "Slack emoji alias name to react with (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`). Optional surrounding colons are allowed."
@@ -5236,7 +5479,7 @@ function createSlackMessageAddReactionTool(context, state) {
5236
5479
  }
5237
5480
 
5238
5481
  // src/chat/tools/slack/canvas-tools.ts
5239
- import { Type as Type11 } from "@sinclair/typebox";
5482
+ import { Type as Type12 } from "@sinclair/typebox";
5240
5483
 
5241
5484
  // src/chat/tools/slack/canvases.ts
5242
5485
  function normalizeCanvasMarkdown(markdown) {
@@ -5452,13 +5695,13 @@ function mergeRecentCanvases(existing, created) {
5452
5695
  function createSlackCanvasCreateTool(context, state) {
5453
5696
  return tool({
5454
5697
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when the answer is better as a reusable document than a thread reply: long-form research, timelines, bios/profiles, structured notes, plans, comparisons, or anything likely to exceed one compact Slack reply. After creating it, keep the thread reply brief and include the canvas link. Do not use for short answers that fit cleanly in one normal thread reply.",
5455
- inputSchema: Type11.Object({
5456
- title: Type11.String({
5698
+ inputSchema: Type12.Object({
5699
+ title: Type12.String({
5457
5700
  minLength: 1,
5458
5701
  maxLength: 160,
5459
5702
  description: "Canvas title."
5460
5703
  }),
5461
- markdown: Type11.String({
5704
+ markdown: Type12.String({
5462
5705
  minLength: 1,
5463
5706
  description: "Canvas markdown body content."
5464
5707
  })
@@ -5524,29 +5767,29 @@ function createSlackCanvasCreateTool(context, state) {
5524
5767
  function createSlackCanvasUpdateTool(state, _context) {
5525
5768
  return tool({
5526
5769
  description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
5527
- inputSchema: Type11.Object({
5528
- markdown: Type11.String({
5770
+ inputSchema: Type12.Object({
5771
+ markdown: Type12.String({
5529
5772
  minLength: 1,
5530
5773
  description: "Markdown content to insert or use as replacement text."
5531
5774
  }),
5532
- operation: Type11.Optional(
5533
- Type11.Union(
5775
+ operation: Type12.Optional(
5776
+ Type12.Union(
5534
5777
  [
5535
- Type11.Literal("insert_at_end"),
5536
- Type11.Literal("insert_at_start"),
5537
- Type11.Literal("replace")
5778
+ Type12.Literal("insert_at_end"),
5779
+ Type12.Literal("insert_at_start"),
5780
+ Type12.Literal("replace")
5538
5781
  ],
5539
5782
  { description: "Canvas update mode." }
5540
5783
  )
5541
5784
  ),
5542
- section_id: Type11.Optional(
5543
- Type11.String({
5785
+ section_id: Type12.Optional(
5786
+ Type12.String({
5544
5787
  minLength: 1,
5545
5788
  description: "Optional section ID required for targeted replace operations."
5546
5789
  })
5547
5790
  ),
5548
- section_contains_text: Type11.Optional(
5549
- Type11.String({
5791
+ section_contains_text: Type12.Optional(
5792
+ Type12.String({
5550
5793
  minLength: 1,
5551
5794
  description: "Optional helper text used to find the target section when section_id is not provided."
5552
5795
  })
@@ -5612,8 +5855,8 @@ function createSlackCanvasUpdateTool(state, _context) {
5612
5855
  function createSlackCanvasReadTool() {
5613
5856
  return tool({
5614
5857
  description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
5615
- inputSchema: Type11.Object({
5616
- canvas: Type11.String({
5858
+ inputSchema: Type12.Object({
5859
+ canvas: Type12.String({
5617
5860
  minLength: 1,
5618
5861
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
5619
5862
  })
@@ -5663,7 +5906,7 @@ function createSlackCanvasReadTool() {
5663
5906
  }
5664
5907
 
5665
5908
  // src/chat/tools/slack/list-tools.ts
5666
- import { Type as Type12 } from "@sinclair/typebox";
5909
+ import { Type as Type13 } from "@sinclair/typebox";
5667
5910
 
5668
5911
  // src/chat/tools/slack/lists.ts
5669
5912
  function normalizeKey(value) {
@@ -5837,8 +6080,8 @@ async function updateListItem(input) {
5837
6080
  function createSlackListCreateTool(state) {
5838
6081
  return tool({
5839
6082
  description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
5840
- inputSchema: Type12.Object({
5841
- name: Type12.String({
6083
+ inputSchema: Type13.Object({
6084
+ name: Type13.String({
5842
6085
  minLength: 1,
5843
6086
  maxLength: 160,
5844
6087
  description: "Name for the new Slack list."
@@ -5873,20 +6116,20 @@ function createSlackListCreateTool(state) {
5873
6116
  function createSlackListAddItemsTool(state) {
5874
6117
  return tool({
5875
6118
  description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
5876
- inputSchema: Type12.Object({
5877
- items: Type12.Array(Type12.String({ minLength: 1 }), {
6119
+ inputSchema: Type13.Object({
6120
+ items: Type13.Array(Type13.String({ minLength: 1 }), {
5878
6121
  minItems: 1,
5879
6122
  maxItems: 25,
5880
6123
  description: "List item titles to create."
5881
6124
  }),
5882
- assignee_user_id: Type12.Optional(
5883
- Type12.String({
6125
+ assignee_user_id: Type13.Optional(
6126
+ Type13.String({
5884
6127
  minLength: 1,
5885
6128
  description: "Optional Slack user ID assigned to all created items."
5886
6129
  })
5887
6130
  ),
5888
- due_date: Type12.Optional(
5889
- Type12.String({
6131
+ due_date: Type13.Optional(
6132
+ Type13.String({
5890
6133
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
5891
6134
  description: "Optional due date in YYYY-MM-DD format."
5892
6135
  })
@@ -5935,9 +6178,9 @@ function createSlackListAddItemsTool(state) {
5935
6178
  function createSlackListGetItemsTool(state) {
5936
6179
  return tool({
5937
6180
  description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
5938
- inputSchema: Type12.Object({
5939
- limit: Type12.Optional(
5940
- Type12.Integer({
6181
+ inputSchema: Type13.Object({
6182
+ limit: Type13.Optional(
6183
+ Type13.Integer({
5941
6184
  minimum: 1,
5942
6185
  maximum: 200,
5943
6186
  description: "Maximum number of list items to return."
@@ -5962,19 +6205,19 @@ function createSlackListGetItemsTool(state) {
5962
6205
  function createSlackListUpdateItemTool(state) {
5963
6206
  return tool({
5964
6207
  description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
5965
- inputSchema: Type12.Object(
6208
+ inputSchema: Type13.Object(
5966
6209
  {
5967
- item_id: Type12.String({
6210
+ item_id: Type13.String({
5968
6211
  minLength: 1,
5969
6212
  description: "ID of the Slack list item to update."
5970
6213
  }),
5971
- completed: Type12.Optional(
5972
- Type12.Boolean({
6214
+ completed: Type13.Optional(
6215
+ Type13.Boolean({
5973
6216
  description: "Optional completion status update."
5974
6217
  })
5975
6218
  ),
5976
- title: Type12.Optional(
5977
- Type12.String({
6219
+ title: Type13.Optional(
6220
+ Type13.String({
5978
6221
  minLength: 1,
5979
6222
  description: "Optional new item title."
5980
6223
  })
@@ -6024,11 +6267,11 @@ function createSlackListUpdateItemTool(state) {
6024
6267
  }
6025
6268
 
6026
6269
  // src/chat/tools/system-time.ts
6027
- import { Type as Type13 } from "@sinclair/typebox";
6270
+ import { Type as Type14 } from "@sinclair/typebox";
6028
6271
  function createSystemTimeTool() {
6029
6272
  return tool({
6030
6273
  description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
6031
- inputSchema: Type13.Object({}),
6274
+ inputSchema: Type14.Object({}),
6032
6275
  execute: async () => {
6033
6276
  const now = /* @__PURE__ */ new Date();
6034
6277
  return {
@@ -6043,7 +6286,7 @@ function createSystemTimeTool() {
6043
6286
  }
6044
6287
 
6045
6288
  // src/chat/tools/web/fetch-tool.ts
6046
- import { Type as Type14 } from "@sinclair/typebox";
6289
+ import { Type as Type15 } from "@sinclair/typebox";
6047
6290
 
6048
6291
  // src/chat/tools/web/constants.ts
6049
6292
  var USER_AGENT = "junior-bot/0.1";
@@ -6391,13 +6634,13 @@ function extractHttpStatusFromMessage(message) {
6391
6634
  function createWebFetchTool(hooks) {
6392
6635
  return tool({
6393
6636
  description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
6394
- inputSchema: Type14.Object({
6395
- url: Type14.String({
6637
+ inputSchema: Type15.Object({
6638
+ url: Type15.String({
6396
6639
  minLength: 1,
6397
6640
  description: "HTTP(S) URL to fetch."
6398
6641
  }),
6399
- max_chars: Type14.Optional(
6400
- Type14.Integer({
6642
+ max_chars: Type15.Optional(
6643
+ Type15.Integer({
6401
6644
  minimum: 500,
6402
6645
  maximum: MAX_FETCH_CHARS,
6403
6646
  description: "Optional maximum number of extracted characters to return."
@@ -6457,9 +6700,9 @@ function createWebFetchTool(hooks) {
6457
6700
  // src/chat/tools/web/search.ts
6458
6701
  import { generateText } from "ai";
6459
6702
  import { createGatewayProvider } from "@ai-sdk/gateway";
6460
- import { Type as Type15 } from "@sinclair/typebox";
6703
+ import { Type as Type16 } from "@sinclair/typebox";
6461
6704
  var SEARCH_TIMEOUT_MS = 6e4;
6462
- var MAX_RESULTS = 5;
6705
+ var MAX_RESULTS2 = 5;
6463
6706
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
6464
6707
  var SEARCH_TOOL_NAME = "parallelSearch";
6465
6708
  function asString(value) {
@@ -6500,16 +6743,16 @@ function isAuthFailure(message) {
6500
6743
  function createWebSearchTool() {
6501
6744
  return tool({
6502
6745
  description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
6503
- inputSchema: Type15.Object({
6504
- query: Type15.String({
6746
+ inputSchema: Type16.Object({
6747
+ query: Type16.String({
6505
6748
  minLength: 1,
6506
6749
  maxLength: 500,
6507
6750
  description: "Search query."
6508
6751
  }),
6509
- max_results: Type15.Optional(
6510
- Type15.Integer({
6752
+ max_results: Type16.Optional(
6753
+ Type16.Integer({
6511
6754
  minimum: 1,
6512
- maximum: MAX_RESULTS,
6755
+ maximum: MAX_RESULTS2,
6513
6756
  description: "Max results to return."
6514
6757
  })
6515
6758
  )
@@ -6573,17 +6816,17 @@ function createWebSearchTool() {
6573
6816
  }
6574
6817
 
6575
6818
  // src/chat/tools/sandbox/write-file.ts
6576
- import { Type as Type16 } from "@sinclair/typebox";
6819
+ import { Type as Type17 } from "@sinclair/typebox";
6577
6820
  function createWriteFileTool() {
6578
6821
  return tool({
6579
6822
  description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
6580
- inputSchema: Type16.Object(
6823
+ inputSchema: Type17.Object(
6581
6824
  {
6582
- path: Type16.String({
6825
+ path: Type17.String({
6583
6826
  minLength: 1,
6584
6827
  description: "Path to write in the sandbox workspace."
6585
6828
  }),
6586
- content: Type16.String({
6829
+ content: Type17.String({
6587
6830
  description: "UTF-8 file content to write."
6588
6831
  })
6589
6832
  },
@@ -6658,7 +6901,11 @@ function createTools(availableSkills, hooks = {}, context) {
6658
6901
  slackListUpdateItem: createSlackListUpdateItemTool(state)
6659
6902
  };
6660
6903
  if (context.mcpToolManager && context.getActiveSkills) {
6661
- tools.searchTools = createSearchToolsTool(
6904
+ tools.searchMcpTools = createSearchMcpToolsTool(
6905
+ context.mcpToolManager,
6906
+ context.getActiveSkills
6907
+ );
6908
+ tools.callMcpTool = createCallMcpToolTool(
6662
6909
  context.mcpToolManager,
6663
6910
  context.getActiveSkills
6664
6911
  );
@@ -8221,134 +8468,21 @@ function shouldEmitDevAgentTrace() {
8221
8468
  return process.env.NODE_ENV === "development";
8222
8469
  }
8223
8470
 
8224
- // src/chat/credentials/unlink-provider.ts
8225
- async function unlinkProvider(userId, provider, userTokenStore) {
8226
- await Promise.all([
8227
- userTokenStore.delete(userId, provider),
8228
- deleteMcpStoredOAuthCredentials(userId, provider),
8229
- deleteMcpServerSessionId(userId, provider),
8230
- deleteMcpAuthSessionsForUserProvider(userId, provider)
8231
- ]);
8232
- }
8233
-
8234
- // src/chat/services/plugin-auth-orchestration.ts
8235
- var PluginAuthorizationPauseError = class extends Error {
8471
+ // src/chat/services/auth-pause.ts
8472
+ var AuthorizationPauseError = class extends Error {
8473
+ disposition;
8474
+ kind;
8236
8475
  provider;
8237
- constructor(provider) {
8238
- super(`Plugin authorization started for ${provider}`);
8239
- this.name = "PluginAuthorizationPauseError";
8476
+ constructor(kind, provider, disposition) {
8477
+ super(
8478
+ kind === "mcp" ? `MCP authorization started for ${provider}` : `Plugin authorization started for ${provider}`
8479
+ );
8480
+ this.name = kind === "mcp" ? "McpAuthorizationPauseError" : "PluginAuthorizationPauseError";
8481
+ this.disposition = disposition;
8482
+ this.kind = kind;
8240
8483
  this.provider = provider;
8241
8484
  }
8242
8485
  };
8243
- function isCommandAuthFailure(details) {
8244
- if (!details || typeof details !== "object") {
8245
- return false;
8246
- }
8247
- const result = details;
8248
- if (typeof result.exit_code !== "number" || result.exit_code === 0) {
8249
- return false;
8250
- }
8251
- const text = `${typeof result.stdout === "string" ? result.stdout : ""}
8252
- ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
8253
- if (!text.trim()) {
8254
- return false;
8255
- }
8256
- return [
8257
- /\b401\b/,
8258
- /\bunauthorized\b/,
8259
- /\bbad credentials\b/,
8260
- /\binvalid token\b/,
8261
- /\btoken (?:expired|revoked)\b/,
8262
- /\bexpired token\b/,
8263
- /\bmissing scopes?\b/,
8264
- /\binsufficient scope\b/,
8265
- /\binvalid grant\b/,
8266
- /\breauthoriz/
8267
- ].some((pattern) => pattern.test(text));
8268
- }
8269
- function commandTargetsProvider(provider, command, details) {
8270
- const normalizedCommand = command.trim().toLowerCase();
8271
- if (!normalizedCommand) {
8272
- return false;
8273
- }
8274
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
8275
- return true;
8276
- }
8277
- const plugin = getPluginDefinition(provider);
8278
- const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
8279
- const credentials = plugin?.manifest.credentials;
8280
- if (credentials) {
8281
- candidates.add(credentials.authTokenEnv.toLowerCase());
8282
- for (const domain of credentials.apiDomains) {
8283
- candidates.add(domain.toLowerCase());
8284
- }
8285
- }
8286
- const combinedText = `${normalizedCommand}
8287
- ${details.stdout?.toLowerCase() ?? ""}
8288
- ${details.stderr?.toLowerCase() ?? ""}`;
8289
- return [...candidates].some((candidate) => combinedText.includes(candidate));
8290
- }
8291
- function createPluginAuthOrchestration(deps, abortAgent) {
8292
- let pendingPause;
8293
- const startAuthorizationPause = async (provider, activeSkill, options) => {
8294
- if (pendingPause) {
8295
- throw pendingPause;
8296
- }
8297
- if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
8298
- throw new Error(`Cannot start plugin authorization for ${provider}`);
8299
- }
8300
- const providerLabel = formatProviderLabel(provider);
8301
- const oauthResult = await startOAuthFlow(provider, {
8302
- requesterId: deps.requesterId,
8303
- channelId: deps.channelId,
8304
- threadTs: deps.threadTs,
8305
- userMessage: deps.userMessage,
8306
- channelConfiguration: deps.channelConfiguration,
8307
- activeSkillName: activeSkill?.name ?? void 0,
8308
- resumeConversationId: deps.conversationId,
8309
- resumeSessionId: deps.sessionId
8310
- });
8311
- if (!oauthResult.ok) {
8312
- throw new Error(oauthResult.error);
8313
- }
8314
- if (!oauthResult.delivery) {
8315
- throw new Error(
8316
- `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
8317
- );
8318
- }
8319
- if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
8320
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
8321
- }
8322
- pendingPause = new PluginAuthorizationPauseError(provider);
8323
- abortAgent();
8324
- throw pendingPause;
8325
- };
8326
- const handleCredentialUnavailable = async (input) => {
8327
- if (pendingPause) {
8328
- throw pendingPause;
8329
- }
8330
- if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
8331
- throw input.error;
8332
- }
8333
- return await startAuthorizationPause(
8334
- input.error.provider,
8335
- input.activeSkill
8336
- );
8337
- };
8338
- return {
8339
- handleCredentialUnavailable,
8340
- handleCommandFailure: async (input) => {
8341
- const provider = input.activeSkill?.pluginProvider;
8342
- if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
8343
- return;
8344
- }
8345
- await startAuthorizationPause(provider, input.activeSkill, {
8346
- unlinkExistingProvider: true
8347
- });
8348
- },
8349
- getPendingPause: () => pendingPause
8350
- };
8351
- }
8352
8486
 
8353
8487
  // src/chat/runtime/report-progress.ts
8354
8488
  function buildReportedProgressStatus(input) {
@@ -8396,15 +8530,16 @@ function resolveCredentialInjection(toolName, command, capabilityRuntime, sandbo
8396
8530
  const headerDomains = (headerTransforms ?? []).map(
8397
8531
  (transform) => transform.domain
8398
8532
  );
8533
+ const skillName = sandbox.getActiveSkill()?.name;
8399
8534
  logInfo(
8400
8535
  "credential_inject_start",
8401
8536
  {},
8402
8537
  {
8403
- "app.skill.name": sandbox.getActiveSkill()?.name,
8538
+ "app.skill.name": skillName,
8404
8539
  "app.credential.delivery": "header_transform",
8405
8540
  "app.credential.header_domains": headerDomains
8406
8541
  },
8407
- "Injecting scoped credential headers for sandbox command"
8542
+ `Injecting scoped credential headers for sandbox command (${skillName ?? "unknown skill"} \u2192 ${headerDomains.join(", ")})`
8408
8543
  );
8409
8544
  }
8410
8545
  return { headerTransforms, env };
@@ -8460,8 +8595,10 @@ function getToolErrorAttributes(error) {
8460
8595
  };
8461
8596
  }
8462
8597
  function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
8598
+ const errorType = getMcpAwareErrorType(error, "tool_execution_error");
8599
+ const errorMessage = getMcpAwareErrorMessage(error);
8463
8600
  setSpanAttributes({
8464
- "error.type": error instanceof Error ? error.name : "tool_execution_error"
8601
+ "error.type": errorType
8465
8602
  });
8466
8603
  if (shouldTrace) {
8467
8604
  logWarn(
@@ -8472,8 +8609,8 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
8472
8609
  "gen_ai.operation.name": "execute_tool",
8473
8610
  "gen_ai.tool.name": toolName,
8474
8611
  ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
8475
- "error.type": error instanceof Error ? error.name : "tool_execution_error",
8476
- "error.message": error instanceof Error ? error.message : String(error)
8612
+ "error.type": errorType,
8613
+ "error.message": errorMessage
8477
8614
  },
8478
8615
  "Agent tool call failed"
8479
8616
  );
@@ -8507,7 +8644,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8507
8644
  execute: async (toolCallId, params) => {
8508
8645
  const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
8509
8646
  const toolArgumentsAttribute = serializeGenAiAttribute(params);
8510
- onToolCall?.(toolName);
8511
8647
  const traceToolContext = {
8512
8648
  ...spanContext,
8513
8649
  conversationId: spanContext.conversationId,
@@ -8526,6 +8662,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8526
8662
  spanContext,
8527
8663
  async () => {
8528
8664
  const parsed = params;
8665
+ onToolCall?.(toolName, parsed);
8529
8666
  try {
8530
8667
  if (typeof toolDef.execute !== "function") {
8531
8668
  const resultDetails = { ok: true };
@@ -8577,7 +8714,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8577
8714
  }
8578
8715
  return normalized;
8579
8716
  } catch (error) {
8580
- if (error instanceof PluginAuthorizationPauseError) {
8717
+ if (error instanceof AuthorizationPauseError) {
8581
8718
  throw error;
8582
8719
  }
8583
8720
  handleToolExecutionError(
@@ -8788,14 +8925,6 @@ function getTerminalAssistantMessages(messages) {
8788
8925
  }
8789
8926
  return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
8790
8927
  }
8791
- function hasCompletedAssistantTurn(messages) {
8792
- const message = getTerminalAssistantMessages(messages).at(-1);
8793
- if (!message) {
8794
- return false;
8795
- }
8796
- const stopReason = message.stopReason;
8797
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
8798
- }
8799
8928
  function upsertActiveSkill(activeSkills, next) {
8800
8929
  const existing = activeSkills.find((skill) => skill.name === next.name);
8801
8930
  if (existing) {
@@ -8803,24 +8932,11 @@ function upsertActiveSkill(activeSkills, next) {
8803
8932
  existing.description = next.description;
8804
8933
  existing.skillPath = next.skillPath;
8805
8934
  existing.allowedTools = next.allowedTools;
8806
- existing.usesConfig = next.usesConfig;
8807
8935
  existing.pluginProvider = next.pluginProvider;
8808
8936
  return;
8809
8937
  }
8810
8938
  activeSkills.push(next);
8811
8939
  }
8812
- function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
8813
- const keys = /* @__PURE__ */ new Set();
8814
- for (const skill of [
8815
- ...activeSkills,
8816
- ...explicitSkill ? [explicitSkill] : []
8817
- ]) {
8818
- for (const key of skill.usesConfig ?? []) {
8819
- keys.add(key);
8820
- }
8821
- }
8822
- return [...keys].sort((a, b) => a.localeCompare(b));
8823
- }
8824
8940
  function trimTrailingAssistantMessages(messages) {
8825
8941
  let end = messages.length;
8826
8942
  while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
@@ -9023,7 +9139,10 @@ function buildTurnResult(input) {
9023
9139
  // src/chat/services/turn-thinking-level.ts
9024
9140
  import { z } from "zod";
9025
9141
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
9026
- var MAX_ROUTER_CONTEXT_CHARS = 1200;
9142
+ var MAX_ROUTER_CONTEXT_CHARS = 8e3;
9143
+ var ROUTER_CONTEXT_HEAD_CHARS = 3e3;
9144
+ var ROUTER_CONTEXT_TAIL_CHARS = 5e3;
9145
+ var TRUNCATION_MARKER = "\n\u2026[truncated]\u2026\n";
9027
9146
  var TURN_THINKING_LEVELS = ["none", "low", "medium", "high"];
9028
9147
  var turnExecutionProfileSchema = z.object({
9029
9148
  thinking_level: z.enum(TURN_THINKING_LEVELS),
@@ -9034,9 +9153,22 @@ var DEFAULT_THINKING_LEVEL = "low";
9034
9153
  function trimContextForRouter(text) {
9035
9154
  const trimmed = text?.trim();
9036
9155
  if (!trimmed) {
9037
- return void 0;
9156
+ return null;
9157
+ }
9158
+ if (trimmed.length <= MAX_ROUTER_CONTEXT_CHARS) {
9159
+ return {
9160
+ text: trimmed,
9161
+ truncated: false,
9162
+ originalCharCount: trimmed.length
9163
+ };
9038
9164
  }
9039
- return trimmed.length <= MAX_ROUTER_CONTEXT_CHARS ? trimmed : trimmed.slice(-MAX_ROUTER_CONTEXT_CHARS);
9165
+ const head = trimmed.slice(0, ROUTER_CONTEXT_HEAD_CHARS).trimEnd();
9166
+ const tail = trimmed.slice(-ROUTER_CONTEXT_TAIL_CHARS).trimStart();
9167
+ return {
9168
+ text: `${head}${TRUNCATION_MARKER}${tail}`,
9169
+ truncated: true,
9170
+ originalCharCount: trimmed.length
9171
+ };
9040
9172
  }
9041
9173
  function buildClassifierSystemPrompt() {
9042
9174
  return [
@@ -9048,22 +9180,23 @@ function buildClassifierSystemPrompt() {
9048
9180
  "Use medium for investigations, ambiguous asks, multi-step analysis, or likely multi-tool work.",
9049
9181
  "Use high for code changes, debugging/root-cause analysis, research-heavy work, non-trivial drafting, or explicit requests to be thorough.",
9050
9182
  "",
9183
+ "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 the thread-background contains a pending task, classify the pending task \u2014 not the affirmation.",
9184
+ "",
9051
9185
  "Return JSON only with thinking_level, confidence, and reason."
9052
9186
  ].join("\n");
9053
9187
  }
9054
9188
  function buildClassifierPrompt(args) {
9055
9189
  const sections = [];
9056
- const context = trimContextForRouter(args.conversationContext);
9057
- if (context) {
9058
- sections.push("<thread-background>", context, "</thread-background>", "");
9190
+ if (args.conversationContext) {
9191
+ sections.push(
9192
+ "<thread-background>",
9193
+ args.conversationContext.text,
9194
+ "</thread-background>",
9195
+ ""
9196
+ );
9059
9197
  }
9060
9198
  sections.push(
9061
- "<turn-context>",
9062
- `- active_skills: ${args.activeSkillNames.join(", ") || "none"}`,
9063
- `- attachment_count: ${args.attachmentCount}`,
9064
- "</turn-context>",
9065
- "",
9066
- '<current-instruction priority="highest">',
9199
+ "<current-instruction>",
9067
9200
  args.messageText.trim() || "[empty]",
9068
9201
  "</current-instruction>"
9069
9202
  );
@@ -9077,42 +9210,81 @@ function buildClassifierPrompt(args) {
9077
9210
  return sections.join("\n");
9078
9211
  }
9079
9212
  async function selectTurnThinkingLevel(args) {
9080
- const activeSkillNames = [...new Set(args.activeSkillNames ?? [])].sort();
9213
+ const trimmedContext = trimContextForRouter(args.conversationContext);
9214
+ const instructionLength = args.messageText.trim().length;
9215
+ const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
9216
+ (block) => block.trim().length > 0
9217
+ ).length;
9218
+ const prompt = buildClassifierPrompt({
9219
+ conversationContext: trimmedContext,
9220
+ currentTurnBlocks: args.currentTurnBlocks,
9221
+ messageText: args.messageText
9222
+ });
9223
+ const logContext = {
9224
+ slackThreadId: args.context?.threadId,
9225
+ slackChannelId: args.context?.channelId,
9226
+ slackUserId: args.context?.requesterId,
9227
+ runId: args.context?.runId,
9228
+ modelId: args.fastModelId
9229
+ };
9230
+ return withSpan(
9231
+ "chat.route_thinking",
9232
+ "chat.route_thinking",
9233
+ logContext,
9234
+ async () => {
9235
+ setSpanAttributes({
9236
+ "app.ai.router.prompt_char_count": prompt.length,
9237
+ "app.ai.router.instruction_char_count": instructionLength,
9238
+ "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
9239
+ "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
9240
+ "app.ai.router.turn_block_count": turnBlockCount
9241
+ });
9242
+ const selection = await classifyTurn({
9243
+ completeObject: args.completeObject,
9244
+ fastModelId: args.fastModelId,
9245
+ metadata: {
9246
+ modelId: args.fastModelId,
9247
+ threadId: args.context?.threadId ?? "",
9248
+ channelId: args.context?.channelId ?? "",
9249
+ requesterId: args.context?.requesterId ?? "",
9250
+ runId: args.context?.runId ?? ""
9251
+ },
9252
+ prompt
9253
+ });
9254
+ setSpanAttributes({
9255
+ "app.ai.thinking_level": selection.thinkingLevel,
9256
+ "app.ai.thinking_level_reason": selection.reason,
9257
+ ...selection.confidence !== void 0 ? { "app.ai.thinking_level_confidence": selection.confidence } : {}
9258
+ });
9259
+ return selection;
9260
+ }
9261
+ );
9262
+ }
9263
+ async function classifyTurn(args) {
9081
9264
  try {
9082
9265
  const result = await args.completeObject({
9083
9266
  modelId: args.fastModelId,
9084
9267
  schema: turnExecutionProfileSchema,
9085
9268
  maxTokens: 120,
9086
- metadata: {
9087
- modelId: args.fastModelId,
9088
- threadId: args.context?.threadId ?? "",
9089
- channelId: args.context?.channelId ?? "",
9090
- requesterId: args.context?.requesterId ?? "",
9091
- runId: args.context?.runId ?? ""
9092
- },
9093
- prompt: buildClassifierPrompt({
9094
- activeSkillNames,
9095
- attachmentCount: args.attachmentCount ?? 0,
9096
- conversationContext: args.conversationContext,
9097
- currentTurnBlocks: args.currentTurnBlocks,
9098
- messageText: args.messageText
9099
- }),
9269
+ metadata: args.metadata,
9270
+ prompt: args.prompt,
9100
9271
  thinkingLevel: "low",
9101
9272
  system: buildClassifierSystemPrompt(),
9102
9273
  temperature: 0
9103
9274
  });
9104
9275
  const parsed = turnExecutionProfileSchema.parse(result.object);
9276
+ const reason = parsed.reason.trim();
9105
9277
  if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
9106
9278
  return {
9107
9279
  confidence: parsed.confidence,
9108
9280
  thinkingLevel: DEFAULT_THINKING_LEVEL,
9109
- reason: `low_confidence_default:${parsed.reason.trim()}`
9281
+ reason: `low_confidence_default:${reason}`
9110
9282
  };
9111
9283
  }
9112
9284
  return {
9113
9285
  confidence: parsed.confidence,
9114
9286
  thinkingLevel: parsed.thinking_level,
9115
- reason: parsed.reason.trim()
9287
+ reason
9116
9288
  };
9117
9289
  } catch {
9118
9290
  return {
@@ -9150,7 +9322,7 @@ function parseAgentTurnSessionCheckpoint(value) {
9150
9322
  return void 0;
9151
9323
  }
9152
9324
  const status = parsed.state;
9153
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
9325
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
9154
9326
  return void 0;
9155
9327
  }
9156
9328
  const conversationId = parsed.conversationId;
@@ -9222,6 +9394,26 @@ async function upsertAgentTurnSessionCheckpoint(args) {
9222
9394
  );
9223
9395
  return checkpoint;
9224
9396
  }
9397
+ async function supersedeAgentTurnSessionCheckpoint(args) {
9398
+ const existing = await getAgentTurnSessionCheckpoint(
9399
+ args.conversationId,
9400
+ args.sessionId
9401
+ );
9402
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
9403
+ return void 0;
9404
+ }
9405
+ return await upsertAgentTurnSessionCheckpoint({
9406
+ conversationId: existing.conversationId,
9407
+ sessionId: existing.sessionId,
9408
+ sliceId: existing.sliceId,
9409
+ state: "superseded",
9410
+ piMessages: existing.piMessages,
9411
+ loadedSkillNames: existing.loadedSkillNames,
9412
+ resumeReason: existing.resumeReason,
9413
+ resumedFromSliceId: existing.resumedFromSliceId,
9414
+ errorMessage: args.errorMessage ?? existing.errorMessage
9415
+ });
9416
+ }
9225
9417
 
9226
9418
  // src/chat/services/turn-checkpoint.ts
9227
9419
  async function loadTurnCheckpoint(ctx) {
@@ -9334,77 +9526,299 @@ async function persistTimeoutCheckpoint(args) {
9334
9526
  );
9335
9527
  return void 0;
9336
9528
  }
9337
- }
9338
-
9339
- // src/chat/services/mcp-auth-orchestration.ts
9340
- var McpAuthorizationPauseError = class extends Error {
9341
- provider;
9342
- constructor(provider) {
9343
- super(`MCP authorization started for ${provider}`);
9344
- this.name = "McpAuthorizationPauseError";
9345
- this.provider = provider;
9529
+ }
9530
+
9531
+ // src/chat/services/pending-auth.ts
9532
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
9533
+ function canReusePendingAuthLink(args) {
9534
+ const { pendingAuth } = args;
9535
+ if (!pendingAuth) {
9536
+ return false;
9537
+ }
9538
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
9539
+ }
9540
+ function buildAuthPauseReplyText(args) {
9541
+ const providerLabel = args.provider ? formatProviderLabel(args.provider) : "";
9542
+ if (args.disposition === "link_already_sent") {
9543
+ return providerLabel ? `I still need your ${providerLabel} access to continue. I already sent you a private link.` : "I still need additional access to continue. I already sent you a private link.";
9544
+ }
9545
+ return providerLabel ? `I need your ${providerLabel} access to continue. I sent you a private link.` : "I need additional access to continue. I sent you a private link.";
9546
+ }
9547
+ function getConversationPendingAuth(args) {
9548
+ const pendingAuth = args.conversation.processing.pendingAuth;
9549
+ if (!pendingAuth) {
9550
+ return void 0;
9551
+ }
9552
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
9553
+ return void 0;
9554
+ }
9555
+ return pendingAuth;
9556
+ }
9557
+ function clearPendingAuth(conversation, sessionId) {
9558
+ if (!conversation.processing.pendingAuth) {
9559
+ return;
9560
+ }
9561
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
9562
+ return;
9563
+ }
9564
+ conversation.processing.pendingAuth = void 0;
9565
+ }
9566
+ async function applyPendingAuthUpdate(args) {
9567
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
9568
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
9569
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
9570
+ await supersedeAgentTurnSessionCheckpoint({
9571
+ conversationId: args.conversationId,
9572
+ sessionId: previousPendingAuth.sessionId,
9573
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
9574
+ });
9575
+ }
9576
+ }
9577
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
9578
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
9579
+ const message = conversation.messages[index];
9580
+ if (message?.role !== "user") {
9581
+ continue;
9582
+ }
9583
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
9584
+ }
9585
+ return false;
9586
+ }
9587
+
9588
+ // src/chat/services/mcp-auth-orchestration.ts
9589
+ var McpAuthorizationPauseError = class extends AuthorizationPauseError {
9590
+ constructor(provider, disposition) {
9591
+ super("mcp", provider, disposition);
9592
+ }
9593
+ };
9594
+ function createMcpAuthOrchestration(deps, abortAgent) {
9595
+ let pendingPause;
9596
+ const authSessionIdsByProvider = /* @__PURE__ */ new Map();
9597
+ const authProviderFactory = async (plugin) => {
9598
+ if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
9599
+ return void 0;
9600
+ }
9601
+ const provider = await createMcpOAuthClientProvider({
9602
+ provider: plugin.manifest.name,
9603
+ conversationId: deps.conversationId,
9604
+ sessionId: deps.sessionId,
9605
+ userId: deps.requesterId,
9606
+ userMessage: deps.userMessage,
9607
+ ...deps.channelId ? { channelId: deps.channelId } : {},
9608
+ ...deps.threadTs ? { threadTs: deps.threadTs } : {},
9609
+ ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
9610
+ configuration: deps.getConfiguration(),
9611
+ artifactState: deps.getArtifactState()
9612
+ });
9613
+ authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
9614
+ return provider;
9615
+ };
9616
+ const onAuthorizationRequired = async (provider) => {
9617
+ if (pendingPause) {
9618
+ return true;
9619
+ }
9620
+ const authSessionId = authSessionIdsByProvider.get(provider);
9621
+ if (!authSessionId || !deps.requesterId) {
9622
+ throw new Error(
9623
+ `Missing MCP auth session context for plugin "${provider}"`
9624
+ );
9625
+ }
9626
+ const latestArtifactState = deps.getMergedArtifactState();
9627
+ await patchMcpAuthSession(authSessionId, {
9628
+ configuration: { ...deps.getConfiguration() },
9629
+ artifactState: latestArtifactState,
9630
+ toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
9631
+ });
9632
+ const authSession = await getMcpAuthSession(authSessionId);
9633
+ if (!authSession?.authorizationUrl) {
9634
+ throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
9635
+ }
9636
+ const reusingPendingLink = canReusePendingAuthLink({
9637
+ pendingAuth: deps.currentPendingAuth,
9638
+ kind: "mcp",
9639
+ provider,
9640
+ requesterId: deps.requesterId
9641
+ });
9642
+ if (!reusingPendingLink) {
9643
+ const delivery = await deliverPrivateMessage({
9644
+ channelId: authSession.channelId,
9645
+ threadTs: authSession.threadTs,
9646
+ userId: authSession.userId,
9647
+ text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
9648
+ });
9649
+ if (!delivery) {
9650
+ throw new Error(
9651
+ `Unable to deliver MCP authorization link for plugin "${provider}"`
9652
+ );
9653
+ }
9654
+ } else {
9655
+ await deleteMcpAuthSession(authSessionId);
9656
+ }
9657
+ if (deps.sessionId && deps.requesterId) {
9658
+ await deps.onPendingAuth?.({
9659
+ kind: "mcp",
9660
+ provider,
9661
+ requesterId: deps.requesterId,
9662
+ sessionId: deps.sessionId,
9663
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9664
+ });
9665
+ }
9666
+ pendingPause = new McpAuthorizationPauseError(
9667
+ provider,
9668
+ reusingPendingLink ? "link_already_sent" : "link_sent"
9669
+ );
9670
+ abortAgent();
9671
+ return true;
9672
+ };
9673
+ return {
9674
+ authProviderFactory,
9675
+ onAuthorizationRequired,
9676
+ getPendingPause: () => pendingPause
9677
+ };
9678
+ }
9679
+
9680
+ // src/chat/credentials/unlink-provider.ts
9681
+ async function unlinkProvider(userId, provider, userTokenStore) {
9682
+ await Promise.all([
9683
+ userTokenStore.delete(userId, provider),
9684
+ deleteMcpStoredOAuthCredentials(userId, provider),
9685
+ deleteMcpServerSessionId(userId, provider),
9686
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
9687
+ ]);
9688
+ }
9689
+
9690
+ // src/chat/services/plugin-auth-orchestration.ts
9691
+ var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
9692
+ constructor(provider, disposition) {
9693
+ super("plugin", provider, disposition);
9694
+ }
9695
+ };
9696
+ function isCommandAuthFailure(details) {
9697
+ if (!details || typeof details !== "object") {
9698
+ return false;
9699
+ }
9700
+ const result = details;
9701
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
9702
+ return false;
9703
+ }
9704
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
9705
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
9706
+ if (!text.trim()) {
9707
+ return false;
9708
+ }
9709
+ return [
9710
+ /\b401\b/,
9711
+ /\bunauthorized\b/,
9712
+ /\bbad credentials\b/,
9713
+ /\binvalid token\b/,
9714
+ /\btoken (?:expired|revoked)\b/,
9715
+ /\bexpired token\b/,
9716
+ /\bmissing scopes?\b/,
9717
+ /\binsufficient scope\b/,
9718
+ /\binvalid grant\b/,
9719
+ /\breauthoriz/
9720
+ ].some((pattern) => pattern.test(text));
9721
+ }
9722
+ function commandTargetsProvider(provider, command, details) {
9723
+ const normalizedCommand = command.trim().toLowerCase();
9724
+ if (!normalizedCommand) {
9725
+ return false;
9726
+ }
9727
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
9728
+ return true;
9346
9729
  }
9347
- };
9348
- function createMcpAuthOrchestration(deps, abortAgent) {
9349
- let pendingPause;
9350
- const authSessionIdsByProvider = /* @__PURE__ */ new Map();
9351
- const authProviderFactory = async (plugin) => {
9352
- if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
9353
- return void 0;
9730
+ const plugin = getPluginDefinition(provider);
9731
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
9732
+ const credentials = plugin?.manifest.credentials;
9733
+ if (credentials) {
9734
+ candidates.add(credentials.authTokenEnv.toLowerCase());
9735
+ for (const domain of credentials.apiDomains) {
9736
+ candidates.add(domain.toLowerCase());
9354
9737
  }
9355
- const provider = await createMcpOAuthClientProvider({
9356
- provider: plugin.manifest.name,
9357
- conversationId: deps.conversationId,
9358
- sessionId: deps.sessionId,
9359
- userId: deps.requesterId,
9360
- userMessage: deps.userMessage,
9361
- ...deps.channelId ? { channelId: deps.channelId } : {},
9362
- ...deps.threadTs ? { threadTs: deps.threadTs } : {},
9363
- ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
9364
- configuration: deps.getConfiguration(),
9365
- artifactState: deps.getArtifactState()
9366
- });
9367
- authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
9368
- return provider;
9369
- };
9370
- const onAuthorizationRequired = async (provider) => {
9738
+ }
9739
+ const combinedText = `${normalizedCommand}
9740
+ ${details.stdout?.toLowerCase() ?? ""}
9741
+ ${details.stderr?.toLowerCase() ?? ""}`;
9742
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
9743
+ }
9744
+ function createPluginAuthOrchestration(deps, abortAgent) {
9745
+ let pendingPause;
9746
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
9371
9747
  if (pendingPause) {
9372
- return true;
9748
+ throw pendingPause;
9373
9749
  }
9374
- const authSessionId = authSessionIdsByProvider.get(provider);
9375
- if (!authSessionId || !deps.requesterId) {
9376
- throw new Error(
9377
- `Missing MCP auth session context for plugin "${provider}"`
9378
- );
9750
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
9751
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
9379
9752
  }
9380
- const latestArtifactState = deps.getMergedArtifactState();
9381
- await patchMcpAuthSession(authSessionId, {
9382
- configuration: { ...deps.getConfiguration() },
9383
- artifactState: latestArtifactState,
9384
- toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
9753
+ const providerLabel = formatProviderLabel(provider);
9754
+ const reusingPendingLink = canReusePendingAuthLink({
9755
+ pendingAuth: deps.currentPendingAuth,
9756
+ kind: "plugin",
9757
+ provider,
9758
+ requesterId: deps.requesterId
9385
9759
  });
9386
- const authSession = await getMcpAuthSession(authSessionId);
9387
- if (!authSession?.authorizationUrl) {
9388
- throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
9760
+ if (!reusingPendingLink) {
9761
+ const oauthResult = await startOAuthFlow(provider, {
9762
+ requesterId: deps.requesterId,
9763
+ channelId: deps.channelId,
9764
+ threadTs: deps.threadTs,
9765
+ userMessage: deps.userMessage,
9766
+ channelConfiguration: deps.channelConfiguration,
9767
+ activeSkillName: activeSkill?.name ?? void 0,
9768
+ resumeConversationId: deps.conversationId,
9769
+ resumeSessionId: deps.sessionId
9770
+ });
9771
+ if (!oauthResult.ok) {
9772
+ throw new Error(oauthResult.error);
9773
+ }
9774
+ if (!oauthResult.delivery) {
9775
+ throw new Error(
9776
+ `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
9777
+ );
9778
+ }
9389
9779
  }
9390
- const delivery = await deliverPrivateMessage({
9391
- channelId: authSession.channelId,
9392
- threadTs: authSession.threadTs,
9393
- userId: authSession.userId,
9394
- text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
9395
- });
9396
- if (!delivery) {
9397
- throw new Error(
9398
- `Unable to deliver MCP authorization link for plugin "${provider}"`
9399
- );
9780
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
9781
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
9782
+ }
9783
+ if (deps.sessionId) {
9784
+ await deps.onPendingAuth?.({
9785
+ kind: "plugin",
9786
+ provider,
9787
+ requesterId: deps.requesterId,
9788
+ sessionId: deps.sessionId,
9789
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9790
+ });
9400
9791
  }
9401
- pendingPause = new McpAuthorizationPauseError(provider);
9792
+ pendingPause = new PluginAuthorizationPauseError(
9793
+ provider,
9794
+ reusingPendingLink ? "link_already_sent" : "link_sent"
9795
+ );
9402
9796
  abortAgent();
9403
- return true;
9797
+ throw pendingPause;
9798
+ };
9799
+ const handleCredentialUnavailable = async (input) => {
9800
+ if (pendingPause) {
9801
+ throw pendingPause;
9802
+ }
9803
+ if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
9804
+ throw input.error;
9805
+ }
9806
+ return await startAuthorizationPause(
9807
+ input.error.provider,
9808
+ input.activeSkill
9809
+ );
9404
9810
  };
9405
9811
  return {
9406
- authProviderFactory,
9407
- onAuthorizationRequired,
9812
+ handleCredentialUnavailable,
9813
+ handleCommandFailure: async (input) => {
9814
+ const provider = input.activeSkill?.pluginProvider;
9815
+ if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
9816
+ return;
9817
+ }
9818
+ await startAuthorizationPause(provider, input.activeSkill, {
9819
+ unlinkExistingProvider: true
9820
+ });
9821
+ },
9408
9822
  getPendingPause: () => pendingPause
9409
9823
  };
9410
9824
  }
@@ -9505,26 +9919,13 @@ function buildUserTurnInput(args) {
9505
9919
  }
9506
9920
  return { routerBlocks, userContentParts };
9507
9921
  }
9508
- function mcpToolsToDefinitions(mcpTools) {
9509
- const defs = {};
9510
- for (const tool2 of mcpTools) {
9511
- defs[tool2.name] = {
9512
- description: tool2.description,
9513
- // Raw JSON Schema from MCP servers — not a TypeBox TSchema, but
9514
- // pi-agent-core validates with AJV and the Anthropic provider reads
9515
- // .properties/.required, so raw JSON Schema works at runtime.
9516
- inputSchema: tool2.parameters,
9517
- execute: async (args) => tool2.execute(args)
9518
- };
9519
- }
9520
- return defs;
9521
- }
9522
9922
  async function generateAssistantReply(messageText, context = {}) {
9523
9923
  const replyStartedAtMs = Date.now();
9524
9924
  let timeoutResumeConversationId;
9525
9925
  let timeoutResumeSessionId;
9526
9926
  let timeoutResumeSliceId = 1;
9527
9927
  let timeoutResumeMessages = [];
9928
+ let beforeMessageCount = 0;
9528
9929
  let lastKnownSandboxId = context.sandbox?.sandboxId;
9529
9930
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
9530
9931
  let loadedSkillNamesForResume = [];
@@ -9717,8 +10118,6 @@ async function generateAssistantReply(messageText, context = {}) {
9717
10118
  userTurnText
9718
10119
  });
9719
10120
  thinkingSelection = await selectTurnThinkingLevel({
9720
- activeSkillNames: activeSkills.map((skill) => skill.name),
9721
- attachmentCount: context.userAttachments?.length,
9722
10121
  completeObject,
9723
10122
  conversationContext: context.conversationContext,
9724
10123
  context: {
@@ -9754,9 +10153,11 @@ async function generateAssistantReply(messageText, context = {}) {
9754
10153
  threadTs: context.correlation?.threadTs,
9755
10154
  toolChannelId: context.toolChannelId,
9756
10155
  userMessage: userInput,
10156
+ currentPendingAuth: context.pendingAuth,
9757
10157
  getConfiguration: () => configurationValues,
9758
10158
  getArtifactState: () => context.artifactState,
9759
- getMergedArtifactState: () => mergeArtifactsState(context.artifactState ?? {}, artifactStatePatch)
10159
+ getMergedArtifactState: () => mergeArtifactsState(context.artifactState ?? {}, artifactStatePatch),
10160
+ onPendingAuth: context.onAuthPending
9760
10161
  },
9761
10162
  () => agent?.abort()
9762
10163
  );
@@ -9769,6 +10170,8 @@ async function generateAssistantReply(messageText, context = {}) {
9769
10170
  threadTs: context.correlation?.threadTs,
9770
10171
  userMessage: userInput,
9771
10172
  channelConfiguration: context.channelConfiguration,
10173
+ currentPendingAuth: context.pendingAuth,
10174
+ onPendingAuth: context.onAuthPending,
9772
10175
  userTokenStore
9773
10176
  },
9774
10177
  () => agent?.abort()
@@ -9849,11 +10252,18 @@ async function generateAssistantReply(messageText, context = {}) {
9849
10252
  if (!effective.pluginProvider) {
9850
10253
  return void 0;
9851
10254
  }
9852
- syncMcpAgentTools();
9853
- return {
9854
- available_tools: turnMcpToolManager.getActiveToolCatalog(activeSkills, {
10255
+ if (!turnMcpToolManager.getActiveProviders().includes(effective.pluginProvider)) {
10256
+ return void 0;
10257
+ }
10258
+ const availableToolCount = turnMcpToolManager.getActiveToolCatalog(
10259
+ activeSkills,
10260
+ {
9855
10261
  provider: effective.pluginProvider
9856
- }).map(toExposedToolSummary)
10262
+ }
10263
+ ).length;
10264
+ return {
10265
+ mcp_provider: effective.pluginProvider,
10266
+ available_tool_count: availableToolCount
9857
10267
  };
9858
10268
  }
9859
10269
  },
@@ -9883,22 +10293,20 @@ async function generateAssistantReply(messageText, context = {}) {
9883
10293
  await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
9884
10294
  }
9885
10295
  syncResumeState();
9886
- const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
10296
+ const activeMcpCatalogs = toActiveMcpCatalogSummaries(
10297
+ turnMcpToolManager.getActiveToolCatalog(activeSkills)
10298
+ );
9887
10299
  baseInstructions = buildSystemPrompt({
9888
10300
  availableSkills,
9889
10301
  activeSkills,
9890
- activeTools: activeToolSummaries,
10302
+ activeMcpCatalogs,
9891
10303
  invocation: skillInvocation,
9892
10304
  assistant: context.assistant,
9893
10305
  requester: context.requester,
9894
10306
  artifactState: context.artifactState,
9895
10307
  configuration: configurationValues,
9896
- relevantConfigurationKeys: collectRelevantConfigurationKeys(
9897
- activeSkills,
9898
- invokedSkill
9899
- ),
9900
- runtimeMetadata: getRuntimeMetadata(),
9901
- threadParticipants: context.threadParticipants
10308
+ threadParticipants: context.threadParticipants,
10309
+ turnState: resumedFromCheckpoint ? "resumed" : "fresh"
9902
10310
  });
9903
10311
  const inputMessagesAttribute = serializeGenAiAttribute([
9904
10312
  {
@@ -9910,10 +10318,23 @@ async function generateAssistantReply(messageText, context = {}) {
9910
10318
  content: userContentParts.map((part) => toObservablePromptPart(part))
9911
10319
  }
9912
10320
  ]);
9913
- const onToolCall = (toolName) => {
10321
+ const onToolCall = (toolName, params) => {
9914
10322
  toolCalls.push(toolName);
10323
+ try {
10324
+ context.onToolInvocation?.({ toolName, params });
10325
+ } catch (error) {
10326
+ logWarn(
10327
+ "tool_invocation_observer_failed",
10328
+ spanContext,
10329
+ {
10330
+ "gen_ai.tool.name": toolName,
10331
+ "error.message": error instanceof Error ? error.message : String(error)
10332
+ },
10333
+ "Tool invocation observer failed"
10334
+ );
10335
+ }
9915
10336
  };
9916
- const baseAgentTools = createAgentTools(
10337
+ const agentTools = createAgentTools(
9917
10338
  tools,
9918
10339
  skillSandbox,
9919
10340
  spanContext,
@@ -9923,24 +10344,6 @@ async function generateAssistantReply(messageText, context = {}) {
9923
10344
  pluginAuth,
9924
10345
  onToolCall
9925
10346
  );
9926
- const agentTools = [...baseAgentTools];
9927
- const syncMcpAgentTools = () => {
9928
- const mcpTools = turnMcpToolManager.getResolvedActiveTools(activeSkills);
9929
- const mcpDefs = mcpToolsToDefinitions(mcpTools);
9930
- const mcpAgentTools = createAgentTools(
9931
- mcpDefs,
9932
- skillSandbox,
9933
- spanContext,
9934
- context.onStatus,
9935
- sandboxExecutor,
9936
- capabilityRuntime,
9937
- pluginAuth,
9938
- onToolCall
9939
- );
9940
- agentTools.length = 0;
9941
- agentTools.push(...baseAgentTools, ...mcpAgentTools);
9942
- };
9943
- syncMcpAgentTools();
9944
10347
  agent = new Agent({
9945
10348
  getApiKey: () => getPiGatewayApiKeyOverride(),
9946
10349
  initialState: {
@@ -9987,9 +10390,8 @@ async function generateAssistantReply(messageText, context = {}) {
9987
10390
  );
9988
10391
  });
9989
10392
  });
9990
- let beforeMessageCount = agent.state.messages.length;
9991
10393
  let newMessages = [];
9992
- let completedAssistantTurn = false;
10394
+ beforeMessageCount = agent.state.messages.length;
9993
10395
  try {
9994
10396
  if (resumedFromCheckpoint) {
9995
10397
  agent.state.messages = existingCheckpoint.piMessages;
@@ -10056,11 +10458,6 @@ async function generateAssistantReply(messageText, context = {}) {
10056
10458
  }
10057
10459
  }
10058
10460
  newMessages = agent.state.messages.slice(beforeMessageCount);
10059
- completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
10060
- if (getPendingAuthPause() && !completedAssistantTurn) {
10061
- timeoutResumeMessages = [...agent.state.messages];
10062
- throw getPendingAuthPause();
10063
- }
10064
10461
  const outputMessages = newMessages.filter(isAssistantMessage);
10065
10462
  const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
10066
10463
  const usageSummary = extractGenAiUsageSummary(
@@ -10076,6 +10473,10 @@ async function generateAssistantReply(messageText, context = {}) {
10076
10473
  ...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
10077
10474
  ...usageSummary.outputTokens !== void 0 ? { "gen_ai.usage.output_tokens": usageSummary.outputTokens } : {}
10078
10475
  });
10476
+ if (getPendingAuthPause()) {
10477
+ timeoutResumeMessages = [...agent.state.messages];
10478
+ throw getPendingAuthPause();
10479
+ }
10079
10480
  },
10080
10481
  {
10081
10482
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
@@ -10088,9 +10489,6 @@ async function generateAssistantReply(messageText, context = {}) {
10088
10489
  } finally {
10089
10490
  unsubscribe();
10090
10491
  }
10091
- if (getPendingAuthPause() && !completedAssistantTurn) {
10092
- throw getPendingAuthPause();
10093
- }
10094
10492
  if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
10095
10493
  await persistCompletedCheckpoint({
10096
10494
  conversationId: sessionConversationId,
@@ -10148,7 +10546,15 @@ async function generateAssistantReply(messageText, context = {}) {
10148
10546
  );
10149
10547
  }
10150
10548
  }
10151
- if ((error instanceof McpAuthorizationPauseError || error instanceof PluginAuthorizationPauseError) && timeoutResumeConversationId && timeoutResumeSessionId) {
10549
+ if (error instanceof AuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
10550
+ if (!turnUsage && timeoutResumeMessages.length > 0) {
10551
+ const fallbackUsage = extractGenAiUsageSummary(
10552
+ ...timeoutResumeMessages.slice(beforeMessageCount).filter(isAssistantMessage)
10553
+ );
10554
+ turnUsage = Object.values(fallbackUsage).some(
10555
+ (value) => value !== void 0
10556
+ ) ? fallbackUsage : void 0;
10557
+ }
10152
10558
  const nextSliceId = await persistAuthPauseCheckpoint({
10153
10559
  conversationId: timeoutResumeConversationId,
10154
10560
  sessionId: timeoutResumeSessionId,
@@ -10166,9 +10572,15 @@ async function generateAssistantReply(messageText, context = {}) {
10166
10572
  }
10167
10573
  });
10168
10574
  throw new RetryableTurnError(
10169
- error instanceof PluginAuthorizationPauseError ? "plugin_auth_resume" : "mcp_auth_resume",
10575
+ error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
10170
10576
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
10171
10577
  {
10578
+ authDisposition: error.disposition,
10579
+ authDurationMs: Date.now() - replyStartedAtMs,
10580
+ authKind: error.kind,
10581
+ authProvider: error.provider,
10582
+ authThinkingLevel: thinkingSelection?.thinkingLevel,
10583
+ authUsage: turnUsage,
10172
10584
  conversationId: timeoutResumeConversationId,
10173
10585
  sessionId: timeoutResumeSessionId,
10174
10586
  sliceId: nextSliceId
@@ -11041,6 +11453,82 @@ async function resumeAuthorizedRequest(args) {
11041
11453
  });
11042
11454
  }
11043
11455
 
11456
+ // src/chat/runtime/auth-pause-reply.ts
11457
+ function buildAuthPauseSlackMessage(args) {
11458
+ const footer = buildSlackReplyFooter({
11459
+ conversationId: args.conversationId,
11460
+ durationMs: args.durationMs,
11461
+ thinkingLevel: args.thinkingLevel,
11462
+ usage: args.usage
11463
+ });
11464
+ const blocks = buildSlackReplyBlocks(args.text, footer);
11465
+ return blocks ? { text: args.text, blocks } : { text: args.text };
11466
+ }
11467
+ function completeAuthPauseTurn(args) {
11468
+ markConversationMessage(
11469
+ args.conversation,
11470
+ getTurnUserMessageId(args.conversation, args.sessionId),
11471
+ {
11472
+ replied: true,
11473
+ skippedReason: void 0
11474
+ }
11475
+ );
11476
+ upsertConversationMessage(args.conversation, {
11477
+ id: generateConversationId("assistant"),
11478
+ role: "assistant",
11479
+ text: normalizeConversationText(args.text) || "[empty response]",
11480
+ createdAtMs: Date.now(),
11481
+ author: {
11482
+ userName: botConfig.userName,
11483
+ isBot: true
11484
+ },
11485
+ meta: {
11486
+ replied: true
11487
+ }
11488
+ });
11489
+ markTurnCompleted({
11490
+ conversation: args.conversation,
11491
+ nowMs: Date.now(),
11492
+ sessionId: args.sessionId,
11493
+ updateConversationStats
11494
+ });
11495
+ }
11496
+ async function persistAuthPauseReplyState(args) {
11497
+ const currentState = await getPersistedThreadState(args.threadStateId);
11498
+ const conversation = coerceThreadConversationState(currentState);
11499
+ completeAuthPauseTurn({
11500
+ conversation,
11501
+ sessionId: args.sessionId,
11502
+ text: args.text
11503
+ });
11504
+ await persistThreadStateById(args.threadStateId, { conversation });
11505
+ }
11506
+ async function deliverAuthPauseReply(args) {
11507
+ const retryable = isRetryableTurnError(args.error) ? args.error : void 0;
11508
+ const text = retryable ? buildAuthPauseReplyText({
11509
+ disposition: retryable.metadata?.authDisposition,
11510
+ provider: retryable.metadata?.authProvider
11511
+ }) : buildAuthPauseReplyText({ provider: args.fallbackProvider });
11512
+ const message = buildAuthPauseSlackMessage({
11513
+ conversationId: args.conversationId,
11514
+ durationMs: retryable?.metadata?.authDurationMs,
11515
+ text,
11516
+ thinkingLevel: retryable?.metadata?.authThinkingLevel,
11517
+ usage: retryable?.metadata?.authUsage
11518
+ });
11519
+ await postSlackMessage({
11520
+ channelId: args.channelId,
11521
+ threadTs: args.threadTs,
11522
+ text: message.text,
11523
+ ...message.blocks ? { blocks: message.blocks } : {}
11524
+ });
11525
+ await persistAuthPauseReplyState({
11526
+ sessionId: args.sessionId,
11527
+ text,
11528
+ threadStateId: args.threadStateId
11529
+ });
11530
+ }
11531
+
11044
11532
  // src/chat/services/timeout-resume.ts
11045
11533
  import { createHmac, timingSafeEqual } from "crypto";
11046
11534
  var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
@@ -11213,6 +11701,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
11213
11701
  const artifacts = coerceThreadArtifactsState(currentState);
11214
11702
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
11215
11703
  const userMessageId = getTurnUserMessageId(conversation, sessionId);
11704
+ clearPendingAuth(conversation, sessionId);
11216
11705
  markConversationMessage(conversation, userMessageId, {
11217
11706
  replied: true,
11218
11707
  skippedReason: void 0
@@ -11233,6 +11722,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
11233
11722
  markTurnCompleted({
11234
11723
  conversation,
11235
11724
  nowMs: Date.now(),
11725
+ sessionId,
11236
11726
  updateConversationStats
11237
11727
  });
11238
11728
  await persistThreadStateById(threadId, {
@@ -11246,9 +11736,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
11246
11736
  const threadId = `slack:${channelId}:${threadTs}`;
11247
11737
  const currentState = await getPersistedThreadState(threadId);
11248
11738
  const conversation = coerceThreadConversationState(currentState);
11739
+ clearPendingAuth(conversation, sessionId);
11249
11740
  markTurnFailed({
11250
11741
  conversation,
11251
11742
  nowMs: Date.now(),
11743
+ sessionId,
11252
11744
  userMessageId: getTurnUserMessageId(conversation, sessionId),
11253
11745
  markConversationMessage,
11254
11746
  updateConversationStats
@@ -11266,8 +11758,29 @@ async function resumeAuthorizedMcpTurn(args) {
11266
11758
  const currentState = await getPersistedThreadState(threadId);
11267
11759
  const conversation = coerceThreadConversationState(currentState);
11268
11760
  const artifacts = coerceThreadArtifactsState(currentState);
11269
- const userMessage = getTurnUserMessage(conversation, authSession.sessionId);
11270
- if (conversation.processing.activeTurnId !== authSession.sessionId) {
11761
+ const pendingAuth = getConversationPendingAuth({
11762
+ conversation,
11763
+ kind: "mcp",
11764
+ provider,
11765
+ requesterId: authSession.userId
11766
+ });
11767
+ const resolvedSessionId = pendingAuth?.sessionId ?? authSession.sessionId;
11768
+ const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
11769
+ if (pendingAuth) {
11770
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
11771
+ clearPendingAuth(conversation, pendingAuth.sessionId);
11772
+ await persistThreadStateById(threadId, { conversation });
11773
+ await supersedeAgentTurnSessionCheckpoint({
11774
+ conversationId: authSession.conversationId,
11775
+ sessionId: pendingAuth.sessionId,
11776
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
11777
+ });
11778
+ return;
11779
+ }
11780
+ } else if (conversation.processing.activeTurnId !== authSession.sessionId) {
11781
+ return;
11782
+ }
11783
+ if (!userMessage) {
11271
11784
  return;
11272
11785
  }
11273
11786
  const channelConfiguration = getChannelConfigurationServiceById(
@@ -11276,14 +11789,14 @@ async function resumeAuthorizedMcpTurn(args) {
11276
11789
  const conversationContext = await buildResumeConversationContext(
11277
11790
  authSession.channelId,
11278
11791
  authSession.threadTs,
11279
- authSession.sessionId
11792
+ resolvedSessionId
11280
11793
  );
11281
11794
  await resumeAuthorizedRequest({
11282
- messageText: authSession.userMessage,
11795
+ messageText: userMessage.text,
11283
11796
  channelId: authSession.channelId,
11284
11797
  threadTs: authSession.threadTs,
11285
11798
  lockKey: authSession.conversationId,
11286
- connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
11799
+ connectedText: "",
11287
11800
  failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
11288
11801
  replyContext: {
11289
11802
  assistant: { userName: botConfig.userName },
@@ -11294,7 +11807,7 @@ async function resumeAuthorizedMcpTurn(args) {
11294
11807
  },
11295
11808
  correlation: {
11296
11809
  conversationId: authSession.conversationId,
11297
- turnId: authSession.sessionId,
11810
+ turnId: resolvedSessionId,
11298
11811
  channelId: authSession.channelId,
11299
11812
  threadTs: authSession.threadTs,
11300
11813
  requesterId: authSession.userId
@@ -11303,9 +11816,18 @@ async function resumeAuthorizedMcpTurn(args) {
11303
11816
  conversationContext,
11304
11817
  artifactState: artifacts,
11305
11818
  configuration: authSession.configuration,
11819
+ pendingAuth,
11306
11820
  channelConfiguration,
11307
11821
  sandbox: getPersistedSandboxState(currentState),
11308
11822
  threadParticipants: buildThreadParticipants(conversation.messages),
11823
+ onAuthPending: async (nextPendingAuth) => {
11824
+ await applyPendingAuthUpdate({
11825
+ conversation,
11826
+ conversationId: authSession.conversationId,
11827
+ nextPendingAuth
11828
+ });
11829
+ await persistThreadStateById(threadId, { conversation });
11830
+ },
11309
11831
  ...getTurnUserReplyAttachmentContext(userMessage)
11310
11832
  },
11311
11833
  onSuccess: async (reply) => {
@@ -11313,7 +11835,7 @@ async function resumeAuthorizedMcpTurn(args) {
11313
11835
  await persistCompletedReplyState(
11314
11836
  authSession.channelId,
11315
11837
  authSession.threadTs,
11316
- authSession.sessionId,
11838
+ resolvedSessionId,
11317
11839
  reply
11318
11840
  );
11319
11841
  } catch (persistError) {
@@ -11338,7 +11860,7 @@ async function resumeAuthorizedMcpTurn(args) {
11338
11860
  await persistFailedReplyState(
11339
11861
  authSession.channelId,
11340
11862
  authSession.threadTs,
11341
- authSession.sessionId
11863
+ resolvedSessionId
11342
11864
  );
11343
11865
  } catch (persistError) {
11344
11866
  logException(
@@ -11350,7 +11872,16 @@ async function resumeAuthorizedMcpTurn(args) {
11350
11872
  );
11351
11873
  }
11352
11874
  },
11353
- onAuthPause: async () => {
11875
+ onAuthPause: async (error) => {
11876
+ await deliverAuthPauseReply({
11877
+ channelId: authSession.channelId,
11878
+ conversationId: authSession.conversationId,
11879
+ error,
11880
+ fallbackProvider: provider,
11881
+ sessionId: resolvedSessionId,
11882
+ threadStateId: `slack:${authSession.channelId}:${authSession.threadTs}`,
11883
+ threadTs: authSession.threadTs
11884
+ });
11354
11885
  logWarn(
11355
11886
  "mcp_oauth_callback_resume_reparked_for_auth",
11356
11887
  {},
@@ -11385,7 +11916,7 @@ async function resumeAuthorizedMcpTurn(args) {
11385
11916
  }
11386
11917
  await scheduleTurnTimeoutResume({
11387
11918
  conversationId: authSession.conversationId,
11388
- sessionId: authSession.sessionId,
11919
+ sessionId: resolvedSessionId,
11389
11920
  expectedCheckpointVersion: checkpointVersion
11390
11921
  });
11391
11922
  }
@@ -11610,6 +12141,7 @@ async function persistCompletedOAuthReplyState(args) {
11610
12141
  const artifacts = coerceThreadArtifactsState(currentState);
11611
12142
  const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
11612
12143
  const userMessage = getTurnUserMessage(conversation, args.sessionId);
12144
+ clearPendingAuth(conversation, args.sessionId);
11613
12145
  markConversationMessage(conversation, userMessage?.id, {
11614
12146
  replied: true,
11615
12147
  skippedReason: void 0
@@ -11630,6 +12162,7 @@ async function persistCompletedOAuthReplyState(args) {
11630
12162
  markTurnCompleted({
11631
12163
  conversation,
11632
12164
  nowMs: Date.now(),
12165
+ sessionId: args.sessionId,
11633
12166
  updateConversationStats
11634
12167
  });
11635
12168
  await persistThreadStateById(args.conversationId, {
@@ -11642,9 +12175,11 @@ async function persistCompletedOAuthReplyState(args) {
11642
12175
  async function persistFailedOAuthReplyState(args) {
11643
12176
  const currentState = await getPersistedThreadState(args.conversationId);
11644
12177
  const conversation = coerceThreadConversationState(currentState);
12178
+ clearPendingAuth(conversation, args.sessionId);
11645
12179
  markTurnFailed({
11646
12180
  conversation,
11647
12181
  nowMs: Date.now(),
12182
+ sessionId: args.sessionId,
11648
12183
  userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
11649
12184
  markConversationMessage,
11650
12185
  updateConversationStats
@@ -11661,35 +12196,65 @@ async function resumeCheckpointedOAuthTurn(stored) {
11661
12196
  stored.resumeConversationId,
11662
12197
  stored.resumeSessionId
11663
12198
  );
11664
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
12199
+ if (!checkpoint) {
11665
12200
  return false;
11666
12201
  }
12202
+ if (checkpoint.state === "completed" || checkpoint.state === "failed" || checkpoint.state === "superseded") {
12203
+ return true;
12204
+ }
12205
+ if (checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
12206
+ return true;
12207
+ }
11667
12208
  const currentState = await getPersistedThreadState(
11668
12209
  stored.resumeConversationId
11669
12210
  );
11670
12211
  const conversation = coerceThreadConversationState(currentState);
11671
12212
  const artifacts = coerceThreadArtifactsState(currentState);
11672
- const userMessage = getTurnUserMessage(conversation, stored.resumeSessionId);
11673
- if (!userMessage?.author?.userId) {
11674
- return false;
12213
+ const pendingAuth = getConversationPendingAuth({
12214
+ conversation,
12215
+ kind: "plugin",
12216
+ provider: stored.provider,
12217
+ requesterId: stored.userId
12218
+ });
12219
+ const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
12220
+ const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
12221
+ if (pendingAuth) {
12222
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
12223
+ clearPendingAuth(conversation, pendingAuth.sessionId);
12224
+ await persistThreadStateById(stored.resumeConversationId, {
12225
+ conversation
12226
+ });
12227
+ await supersedeAgentTurnSessionCheckpoint({
12228
+ conversationId: stored.resumeConversationId,
12229
+ sessionId: pendingAuth.sessionId,
12230
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
12231
+ });
12232
+ return true;
12233
+ }
12234
+ } else {
12235
+ if (!userMessage?.author?.userId) {
12236
+ return false;
12237
+ }
12238
+ if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
12239
+ return true;
12240
+ }
11675
12241
  }
11676
- if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
11677
- return true;
12242
+ if (!userMessage?.author?.userId || !resolvedSessionId) {
12243
+ return false;
11678
12244
  }
11679
12245
  const conversationContext = await buildCheckpointConversationContext(
11680
12246
  stored.resumeConversationId,
11681
- stored.resumeSessionId
12247
+ resolvedSessionId
11682
12248
  );
11683
12249
  const channelConfiguration = getChannelConfigurationServiceById(
11684
12250
  stored.channelId
11685
12251
  );
11686
- const providerLabel = formatProviderLabel(stored.provider);
11687
12252
  await resumeSlackTurn({
11688
12253
  messageText: stored.pendingMessage ?? userMessage.text,
11689
12254
  channelId: stored.channelId,
11690
12255
  threadTs: stored.threadTs,
11691
12256
  lockKey: stored.resumeConversationId,
11692
- initialText: `Your ${providerLabel} account is now connected. Processing your request...`,
12257
+ initialText: "",
11693
12258
  failureText: "I connected your account but hit an error processing your request. Please try the command again.",
11694
12259
  replyContext: {
11695
12260
  assistant: { userName: botConfig.userName },
@@ -11705,10 +12270,21 @@ async function resumeCheckpointedOAuthTurn(stored) {
11705
12270
  },
11706
12271
  toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
11707
12272
  artifactState: artifacts,
12273
+ pendingAuth,
11708
12274
  conversationContext,
11709
12275
  channelConfiguration,
11710
12276
  sandbox: getPersistedSandboxState(currentState),
11711
12277
  threadParticipants: buildThreadParticipants(conversation.messages),
12278
+ onAuthPending: async (nextPendingAuth) => {
12279
+ await applyPendingAuthUpdate({
12280
+ conversation,
12281
+ conversationId: stored.resumeConversationId,
12282
+ nextPendingAuth
12283
+ });
12284
+ await persistThreadStateById(stored.resumeConversationId, {
12285
+ conversation
12286
+ });
12287
+ },
11712
12288
  ...getTurnUserReplyAttachmentContext(userMessage)
11713
12289
  },
11714
12290
  onSuccess: async (reply) => {
@@ -11720,11 +12296,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
11720
12296
  "app.ai.outcome": reply.diagnostics.outcome,
11721
12297
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
11722
12298
  },
11723
- "Auto-resumed checkpointed turn after OAuth callback"
12299
+ "OAuth callback auto-resumed checkpoint finished replying"
11724
12300
  );
11725
12301
  await persistCompletedOAuthReplyState({
11726
12302
  conversationId: stored.resumeConversationId,
11727
- sessionId: stored.resumeSessionId,
12303
+ sessionId: resolvedSessionId,
11728
12304
  reply
11729
12305
  });
11730
12306
  },
@@ -11738,17 +12314,19 @@ async function resumeCheckpointedOAuthTurn(stored) {
11738
12314
  );
11739
12315
  await persistFailedOAuthReplyState({
11740
12316
  conversationId: stored.resumeConversationId,
11741
- sessionId: stored.resumeSessionId
12317
+ sessionId: resolvedSessionId
11742
12318
  });
11743
12319
  },
11744
12320
  onAuthPause: async (error) => {
11745
- logException(
12321
+ await deliverAuthPauseReply({
12322
+ channelId: stored.channelId,
12323
+ conversationId: stored.resumeConversationId,
11746
12324
  error,
11747
- "oauth_callback_resume_reparked_for_auth",
11748
- {},
11749
- { "app.credential.provider": stored.provider },
11750
- "Resumed OAuth turn requested another authorization flow"
11751
- );
12325
+ fallbackProvider: stored.provider,
12326
+ sessionId: resolvedSessionId,
12327
+ threadStateId: stored.resumeConversationId,
12328
+ threadTs: stored.threadTs
12329
+ });
11752
12330
  },
11753
12331
  onTimeoutPause: async (error) => {
11754
12332
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
@@ -11768,7 +12346,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
11768
12346
  }
11769
12347
  await scheduleTurnTimeoutResume({
11770
12348
  conversationId: stored.resumeConversationId,
11771
- sessionId: stored.resumeSessionId,
12349
+ sessionId: resolvedSessionId,
11772
12350
  expectedCheckpointVersion: checkpointVersion
11773
12351
  });
11774
12352
  }
@@ -11777,7 +12355,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
11777
12355
  }
11778
12356
  async function resumePendingOAuthMessage(stored) {
11779
12357
  if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
11780
- const providerLabel = formatProviderLabel(stored.provider);
11781
12358
  const conversationContext = await buildResumeConversationContext2(
11782
12359
  stored.channelId,
11783
12360
  stored.threadTs
@@ -11786,7 +12363,7 @@ async function resumePendingOAuthMessage(stored) {
11786
12363
  messageText: stored.pendingMessage,
11787
12364
  channelId: stored.channelId,
11788
12365
  threadTs: stored.threadTs,
11789
- connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
12366
+ connectedText: "",
11790
12367
  failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
11791
12368
  replyContext: {
11792
12369
  requester: { userId: stored.userId },
@@ -11802,7 +12379,7 @@ async function resumePendingOAuthMessage(stored) {
11802
12379
  "app.ai.outcome": reply.diagnostics.outcome,
11803
12380
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
11804
12381
  },
11805
- "Auto-resumed pending message after OAuth callback"
12382
+ "OAuth callback auto-resumed pending message finished replying"
11806
12383
  );
11807
12384
  },
11808
12385
  onFailure: async (error) => {
@@ -12051,6 +12628,7 @@ async function persistCompletedReplyState2(args) {
12051
12628
  conversation,
12052
12629
  args.checkpoint.sessionId
12053
12630
  );
12631
+ clearPendingAuth(conversation, args.checkpoint.sessionId);
12054
12632
  markConversationMessage(conversation, userMessage?.id, {
12055
12633
  replied: true,
12056
12634
  skippedReason: void 0
@@ -12071,6 +12649,7 @@ async function persistCompletedReplyState2(args) {
12071
12649
  markTurnCompleted({
12072
12650
  conversation,
12073
12651
  nowMs: Date.now(),
12652
+ sessionId: args.checkpoint.sessionId,
12074
12653
  updateConversationStats
12075
12654
  });
12076
12655
  await persistThreadStateById(args.checkpoint.conversationId, {
@@ -12083,9 +12662,11 @@ async function persistCompletedReplyState2(args) {
12083
12662
  async function persistFailedReplyState2(checkpoint) {
12084
12663
  const currentState = await getPersistedThreadState(checkpoint.conversationId);
12085
12664
  const conversation = coerceThreadConversationState(currentState);
12665
+ clearPendingAuth(conversation, checkpoint.sessionId);
12086
12666
  markTurnFailed({
12087
12667
  conversation,
12088
12668
  nowMs: Date.now(),
12669
+ sessionId: checkpoint.sessionId,
12089
12670
  userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
12090
12671
  markConversationMessage,
12091
12672
  updateConversationStats
@@ -12149,10 +12730,21 @@ async function resumeTimedOutTurn(payload) {
12149
12730
  },
12150
12731
  toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
12151
12732
  artifactState: artifacts,
12733
+ pendingAuth: conversation.processing.pendingAuth,
12152
12734
  conversationContext,
12153
12735
  channelConfiguration,
12154
12736
  sandbox,
12155
12737
  threadParticipants: buildThreadParticipants(conversation.messages),
12738
+ onAuthPending: async (nextPendingAuth) => {
12739
+ await applyPendingAuthUpdate({
12740
+ conversation,
12741
+ conversationId: payload.conversationId,
12742
+ nextPendingAuth
12743
+ });
12744
+ await persistThreadStateById(payload.conversationId, {
12745
+ conversation
12746
+ });
12747
+ },
12156
12748
  ...getTurnUserReplyAttachmentContext(userMessage)
12157
12749
  },
12158
12750
  onSuccess: async (reply) => {
@@ -12184,7 +12776,15 @@ async function resumeTimedOutTurn(payload) {
12184
12776
  );
12185
12777
  await persistFailedReplyState2(checkpoint);
12186
12778
  },
12187
- onAuthPause: async () => {
12779
+ onAuthPause: async (error) => {
12780
+ await deliverAuthPauseReply({
12781
+ channelId: thread.channelId,
12782
+ conversationId: payload.conversationId,
12783
+ error,
12784
+ sessionId: payload.sessionId,
12785
+ threadStateId: payload.conversationId,
12786
+ threadTs: thread.threadTs
12787
+ });
12188
12788
  logWarn(
12189
12789
  "timeout_resume_reparked_for_auth",
12190
12790
  {},
@@ -13923,6 +14523,7 @@ function createReplyToThread(deps) {
13923
14523
  },
13924
14524
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
13925
14525
  artifactState: preparedState.artifacts,
14526
+ pendingAuth: preparedState.conversation.processing.pendingAuth,
13926
14527
  configuration: preparedState.configuration,
13927
14528
  channelConfiguration: preparedState.channelConfiguration,
13928
14529
  inboundAttachmentCount: message.attachments.length,
@@ -13952,6 +14553,16 @@ function createReplyToThread(deps) {
13952
14553
  onArtifactStateUpdated: async (artifacts) => {
13953
14554
  await persistThreadState(thread, { artifacts });
13954
14555
  },
14556
+ onAuthPending: async (pendingAuth) => {
14557
+ await applyPendingAuthUpdate({
14558
+ conversation: preparedState.conversation,
14559
+ conversationId,
14560
+ nextPendingAuth: pendingAuth
14561
+ });
14562
+ await persistThreadState(thread, {
14563
+ conversation: preparedState.conversation
14564
+ });
14565
+ },
13955
14566
  threadParticipants,
13956
14567
  onStatus: (nextStatus) => status.update(nextStatus)
13957
14568
  });
@@ -14129,8 +14740,42 @@ function createReplyToThread(deps) {
14129
14740
  }
14130
14741
  } catch (error) {
14131
14742
  if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
14743
+ const authPauseText = buildAuthPauseReplyText({
14744
+ disposition: error.metadata?.authDisposition,
14745
+ provider: error.metadata?.authProvider
14746
+ });
14747
+ const authPauseFooter = buildSlackReplyFooter({
14748
+ conversationId,
14749
+ durationMs: error.metadata?.authDurationMs,
14750
+ thinkingLevel: error.metadata?.authThinkingLevel,
14751
+ usage: error.metadata?.authUsage
14752
+ });
14753
+ const useSlackFooterForAuthPause = Boolean(authPauseFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
14754
+ if (useSlackFooterForAuthPause && channelId && threadTs) {
14755
+ await beforeFirstResponsePost();
14756
+ await postSlackApiReplyPosts({
14757
+ channelId,
14758
+ threadTs,
14759
+ footer: authPauseFooter,
14760
+ posts: [{ stage: "thread_reply", text: authPauseText }]
14761
+ });
14762
+ } else {
14763
+ await postThreadReply(
14764
+ buildSlackOutputMessage(authPauseText),
14765
+ "thread_reply"
14766
+ );
14767
+ }
14768
+ completeAuthPauseTurn({
14769
+ conversation: preparedState.conversation,
14770
+ sessionId: error.metadata?.sessionId ?? turnId,
14771
+ text: authPauseText
14772
+ });
14773
+ await persistThreadState(thread, {
14774
+ conversation: preparedState.conversation
14775
+ });
14776
+ persistedAtLeastOnce = true;
14132
14777
  shouldPersistFailureState = false;
14133
- throw error;
14778
+ return;
14134
14779
  }
14135
14780
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
14136
14781
  const conversationIdForResume = error.metadata?.conversationId;