@sentry/junior 0.30.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,
@@ -2883,18 +2884,21 @@ function formatConfigurationValue(value) {
2883
2884
  function renderIdentityBlock(tag, fields) {
2884
2885
  const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key, value]) => `- ${key}: ${escapeXml(value)}`);
2885
2886
  if (lines.length === 0) {
2886
- return [`<${tag}>`, "none", `</${tag}>`].join("\n");
2887
+ return [`<${tag}>`, "none", `</${tag}>`];
2887
2888
  }
2888
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
2889
+ return [`<${tag}>`, ...lines, `</${tag}>`];
2889
2890
  }
2890
- function renderTag(tag, content) {
2891
+ function renderTag(tag, lines) {
2892
+ return [`<${tag}>`, ...lines, `</${tag}>`];
2893
+ }
2894
+ function renderTagBlock(tag, content) {
2891
2895
  return [`<${tag}>`, content, `</${tag}>`].join("\n");
2892
2896
  }
2893
2897
  function formatAvailableSkillsForPrompt(skills) {
2894
2898
  if (skills.length === 0) {
2895
- return "<available_skills>\n</available_skills>";
2899
+ return "<available-skills>\n</available-skills>";
2896
2900
  }
2897
- const lines = ["<available_skills>"];
2901
+ const lines = ["<available-skills>"];
2898
2902
  for (const skill of skills) {
2899
2903
  const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
2900
2904
  lines.push(" <skill>");
@@ -2906,45 +2910,37 @@ function formatAvailableSkillsForPrompt(skills) {
2906
2910
  if (skill.pluginProvider) {
2907
2911
  lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
2908
2912
  }
2909
- if (skill.usesConfig && skill.usesConfig.length > 0) {
2910
- lines.push(
2911
- ` <uses_config>${escapeXml(skill.usesConfig.join(" "))}</uses_config>`
2912
- );
2913
- }
2914
2913
  lines.push(" </skill>");
2915
2914
  }
2916
- lines.push("</available_skills>");
2915
+ lines.push("</available-skills>");
2917
2916
  return lines.join("\n");
2918
2917
  }
2919
2918
  function formatLoadedSkillsForPrompt(skills) {
2920
2919
  if (skills.length === 0) {
2921
- return "<loaded_skills>\n</loaded_skills>";
2920
+ return "<loaded-skills>\n</loaded-skills>";
2922
2921
  }
2923
- const lines = ["<loaded_skills>"];
2922
+ const lines = ["<loaded-skills>"];
2924
2923
  for (const skill of skills) {
2925
2924
  const skillDir = workspaceSkillDir(skill.name);
2926
2925
  lines.push(
2927
2926
  ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
2928
2927
  );
2929
2928
  lines.push(`References are relative to ${escapeXml(skillDir)}.`);
2930
- if (skill.usesConfig && skill.usesConfig.length > 0) {
2931
- lines.push(
2932
- `Uses config keys: ${escapeXml(skill.usesConfig.join(", "))}.`
2933
- );
2934
- }
2935
2929
  lines.push("");
2936
2930
  lines.push(skill.body);
2937
2931
  lines.push(" </skill>");
2938
2932
  }
2939
- lines.push("</loaded_skills>");
2933
+ lines.push("</loaded-skills>");
2940
2934
  return lines.join("\n");
2941
2935
  }
2942
2936
  function formatProviderCatalogForPrompt() {
2943
2937
  const providers = getPluginProviders().map((plugin) => plugin.manifest);
2944
2938
  if (providers.length === 0) {
2945
- return "- none";
2939
+ return null;
2946
2940
  }
2947
- const lines = [];
2941
+ const lines = [
2942
+ "Config keys and default targets per provider; use after a skill is loaded."
2943
+ ];
2948
2944
  for (const provider of providers) {
2949
2945
  lines.push(`- provider: ${escapeXml(provider.name)}`);
2950
2946
  lines.push(
@@ -2958,288 +2954,260 @@ function formatProviderCatalogForPrompt() {
2958
2954
  }
2959
2955
  return lines.join("\n");
2960
2956
  }
2961
- function baseSystemPrompt() {
2962
- return [
2963
- "You are a Slack-based helper assistant.",
2964
- "Identity, tone, and domain defaults are defined in the personality block.",
2965
- "",
2966
- "- Be concise, practical, and specific.",
2967
- "- Prefer actionable next steps over generic explanations.",
2968
- "- When the user gives a clear task, execute it immediately in this turn.",
2969
- "- Do not ask for permission to proceed when the request is already clear.",
2970
- "- Keep user-visible progress communication concise and useful.",
2971
- "- In thread follow-ups, answer using prior thread context directly; do not repeat unresolved clarifying questions unless the user asks to refine.",
2972
- "- If the user asks what you just said or means by the previous answer, summarize your prior assistant reply plainly.",
2973
- "- Never ask the user to re-tag or re-invoke for a clear task; continue execution in this turn.",
2974
- "- Never claim you cannot access tools in this turn. If prior results are empty, run tools now.",
2975
- "- If critical input is missing and cannot be discovered with tools, ask one direct clarifying question.",
2976
- "- Always gather evidence from available sources (tools or skills) before answering factual questions.",
2977
- "- When a loaded skill exposes MCP capabilities, those tools are registered as callable tools. Call them directly by name.",
2978
- "- Use `searchTools` only when you need to rediscover or filter active MCP tools.",
2979
- "- Never guess. If you cannot verify with available sources, say it is unverified.",
2980
- "- Never claim a lookup succeeded unless a tool result supports it.",
2981
- "- Do not give up when unsure how to do something; find a viable path, gather evidence, and provide the best actionable way forward.",
2982
- "- When active skills are present, follow their instructions before default behavior."
2983
- ].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");
2984
2973
  }
2985
- function formatReferenceFilesSection() {
2974
+ function formatReferenceFilesLines() {
2986
2975
  const files = listReferenceFiles();
2987
2976
  if (files.length === 0) {
2988
- return [];
2977
+ return null;
2989
2978
  }
2990
- const fileNames = files.map((filePath) => {
2979
+ return files.map((filePath) => {
2991
2980
  const name = path2.basename(filePath);
2992
2981
  return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
2993
2982
  });
2994
- return [
2995
- renderTag(
2996
- "reference-files",
2997
- [
2998
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
2999
- ...fileNames
3000
- ].join("\n")
3001
- )
3002
- ];
3003
2983
  }
3004
- function buildSystemPrompt(params) {
3005
- const {
3006
- availableSkills,
3007
- activeSkills,
3008
- activeTools,
3009
- invocation,
3010
- requester,
3011
- assistant,
3012
- artifactState,
3013
- configuration,
3014
- relevantConfigurationKeys,
3015
- runtimeMetadata,
3016
- threadParticipants
3017
- } = params;
3018
- const assistantSection = renderIdentityBlock("assistant", {
3019
- user_name: assistant?.userName ?? botConfig.userName,
3020
- user_id: assistant?.userId
3021
- });
3022
- const requesterSection = renderIdentityBlock("requester", {
3023
- full_name: requester?.fullName,
3024
- user_name: requester?.userName,
3025
- user_id: requester?.userId
3026
- });
3027
- const availableSkillsSection = [
3028
- "The following skills provide specialized instructions for specific tasks.",
3029
- "Call `loadSkill` when the task matches a skill description.",
3030
- "When a skill references a relative path, resolve it against `skill_dir` and use that path with `bash`.",
3031
- "",
3032
- formatAvailableSkillsForPrompt(availableSkills)
3033
- ].join("\n");
3034
- const activeSkillsSection = [
3035
- "Loaded skills for this turn:",
3036
- formatLoadedSkillsForPrompt(activeSkills)
3037
- ].join("\n");
3038
- const activeToolNames = (activeTools ?? []).map((tool2) => tool2.tool_name);
3039
- const activeToolsSection = activeToolNames.length > 0 ? `Active MCP tools registered for this turn: ${activeToolNames.join(", ")}. Call them directly by name.` : "";
3040
- 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(
3041
3014
  (a, b) => a.localeCompare(b)
3042
3015
  );
3043
- const relevantConfigSet = new Set(
3044
- (relevantConfigurationKeys ?? []).filter(
3045
- (key) => Object.prototype.hasOwnProperty.call(configuration ?? {}, key)
3046
- )
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
+ })
3047
3082
  );
3048
- const relevantConfigLines = configurationKeys.filter((key) => relevantConfigSet.has(key)).map(
3049
- (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
+ })
3050
3089
  );
3051
- const otherConfigLines = configurationKeys.filter((key) => !relevantConfigSet.has(key)).map(
3052
- (key) => ` - ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3090
+ const participantLines = formatThreadParticipantsLines(
3091
+ params.threadParticipants
3053
3092
  );
3054
- const configurationSection = [
3055
- "Use these conversation-scoped defaults when the user has not provided explicit values in this turn.",
3056
- "If explicit user input conflicts with configuration, follow explicit user input.",
3057
- configurationKeys.length === 0 ? "- none" : [
3058
- ...relevantConfigLines.length > 0 ? ["- relevant_for_active_skills:", ...relevantConfigLines] : [],
3059
- ...otherConfigLines.length > 0 ? ["- other_available_keys:", ...otherConfigLines] : []
3060
- ].join("\n")
3061
- ].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) {
3062
3145
  const sections = [
3063
- baseSystemPrompt(),
3064
- renderTag(
3065
- "personality",
3066
- [
3067
- "Always follow the personality guidance for tone/style unless safety or policy constraints require otherwise.",
3068
- "",
3069
- JUNIOR_PERSONALITY.trim()
3070
- ].join("\n")
3071
- ),
3072
- ...JUNIOR_WORLD ? [
3073
- renderTag(
3074
- "world",
3075
- [
3076
- "Use this as the assistant's operational/domain context.",
3077
- "",
3078
- JUNIOR_WORLD.trim()
3079
- ].join("\n")
3080
- )
3081
- ] : [],
3082
- ...formatReferenceFilesSection(),
3083
- renderTag(
3084
- "identity-context",
3085
- [
3086
- "Use these blocks as authoritative metadata for identity questions.",
3087
- assistantSection,
3088
- requesterSection,
3089
- ...threadParticipants && threadParticipants.length > 0 ? [
3090
- renderTag(
3091
- "thread-participants",
3092
- [
3093
- "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.",
3094
- ...threadParticipants.map((p) => {
3095
- const parts = [];
3096
- if (p.userId) {
3097
- parts.push(`user_id: ${escapeXml(p.userId)}`);
3098
- parts.push(`slack_mention: <@${p.userId}>`);
3099
- }
3100
- if (p.userName)
3101
- parts.push(`user_name: ${escapeXml(p.userName)}`);
3102
- if (p.fullName)
3103
- parts.push(`full_name: ${escapeXml(p.fullName)}`);
3104
- return `- ${parts.join(", ")}`;
3105
- })
3106
- ].join("\n")
3107
- )
3108
- ] : []
3109
- ].join("\n")
3110
- ),
3111
- renderTag(
3112
- "artifact-context",
3113
- [
3114
- "Use this thread-scoped memory for follow-up updates to existing Slack artifacts.",
3115
- artifactState ? [
3116
- artifactState.lastCanvasId ? `- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}` : "- last_canvas_id: none",
3117
- artifactState.lastCanvasUrl ? `- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}` : "- last_canvas_url: none",
3118
- artifactState.recentCanvases && artifactState.recentCanvases.length > 0 ? [
3119
- "- recent_canvases:",
3120
- ...artifactState.recentCanvases.map(
3121
- (canvas) => [
3122
- ` - id: ${escapeXml(canvas.id)}`,
3123
- canvas.title ? ` title: ${escapeXml(canvas.title)}` : " title: [unknown]",
3124
- canvas.url ? ` url: ${escapeXml(canvas.url)}` : " url: [unknown]",
3125
- canvas.createdAt ? ` created_at: ${escapeXml(canvas.createdAt)}` : " created_at: [unknown]"
3126
- ].join("\n")
3127
- )
3128
- ].join("\n") : "- recent_canvases: none",
3129
- artifactState.lastListId ? `- last_list_id: ${escapeXml(artifactState.lastListId)}` : "- last_list_id: none",
3130
- artifactState.lastListUrl ? `- last_list_url: ${escapeXml(artifactState.lastListUrl)}` : "- last_list_url: none"
3131
- ].join("\n") : "- none"
3132
- ].join("\n")
3133
- ),
3134
- renderTag("configuration-context", configurationSection),
3135
- renderTag(
3136
- "runtime-metadata",
3137
- [
3138
- "Use this for runtime version questions about the deployed assistant.",
3139
- `- version: ${escapeXml(runtimeMetadata?.version ?? "unknown")}`
3140
- ].join("\n")
3141
- ),
3142
- renderTag(
3143
- "provider-config",
3144
- [
3145
- "Use this catalog to map already-chosen provider work to valid config keys and provider defaults.",
3146
- "Do not use this catalog by itself to decide which domain skill matches an operational task.",
3147
- "When user intent is to set a provider default, choose a config key from this catalog and use jr-rpc config set.",
3148
- "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.",
3149
- formatProviderCatalogForPrompt()
3150
- ].join("\n")
3151
- ),
3152
- renderTag(
3153
- "skill-routing",
3154
- [
3155
- "- Choose the skill that matches the requested operation, not incidental nouns, product names, organization names, or channel context.",
3156
- "- When multiple skills seem adjacent, prefer the one whose description matches the user's requested action most directly.",
3157
- "- If the task needs evidence from a specialized external system or workflow, load the matching skill before drawing conclusions.",
3158
- "- The provider-config catalog is for exact config keys and provider defaults after skill selection. It is not a shortcut for choosing a domain.",
3159
- "- 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."
3160
- ].join("\n")
3161
- ),
3162
- renderTag(
3163
- "tool-usage",
3164
- [
3165
- "- For factual or external questions, run tools/skills first, then answer from evidence.",
3166
- "- Use tool descriptions as the source of truth for when each tool should or should not be called.",
3167
- "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
3168
- "- 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.",
3169
- "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
3170
- "- 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.",
3171
- "- 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.",
3172
- "- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
3173
- "- 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.",
3174
- "- 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.",
3175
- "- 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.",
3176
- "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
3177
- "- If `attachFile` fails, explain the failure and do not say the file was shared.",
3178
- "- 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.",
3179
- "- For explicit in-channel post requests, prefer no thread text reply after a successful channel post. A reaction-only acknowledgment is acceptable when useful.",
3180
- "- When the user explicitly asks for an emoji reaction instead of text, react and skip the text reply.",
3181
- "- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
3182
- "- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
3183
- "- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
3184
- "- If provider authorization is required, the runtime sends the private authorization link itself and resumes the paused turn after authorization.",
3185
- "- Do not try to manage provider auth directly. Run the real provider command and let the runtime handle authorization, reconnect, and resume behavior.",
3186
- "- Provider-targeted commands may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3187
- "- 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.",
3188
- "- `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.",
3189
- "- When your work is complete, provide the exact user-facing markdown response.",
3190
- "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3191
- "- Never call side-effecting tools when the user only asked for analysis or options.",
3192
- "- 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."
3193
- ].join("\n")
3194
- ),
3195
- renderTag(
3196
- "skills",
3197
- [
3198
- "- Explicit skill triggers may appear as `/skillname`.",
3199
- "- If explicitly invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
3200
- "- If an explicitly invoked skill is present in <loaded_skills>, never say the skill is unavailable, missing, or unsupported in this environment.",
3201
- "- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
3202
- "- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
3203
- "- When multiple skills appear relevant, prefer the skill whose description best matches the requested action rather than incidental domain nouns in the prompt.",
3204
- "- 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.",
3205
- "- Do not claim to have used a skill unless it is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3206
- "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3207
- "- Load only the best matching skill first; do not load multiple skills upfront.",
3208
- "- After `loadSkill`, use `skill_dir` as the root for any referenced files you read via `bash`.",
3209
- "- 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`).",
3210
- "- If no skill is a clear fit, continue with normal tool usage."
3211
- ].join("\n")
3212
- ),
3213
- renderTag(
3214
- "output-contract",
3215
- [
3216
- "Always produce output that follows this contract:",
3217
- `<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`,
3218
- "- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
3219
- "- Keep normal responses brief and scannable.",
3220
- "- If depth is needed, start with a concise summary and then provide fuller detail.",
3221
- "- Prefer a single compact thread reply when the full answer comfortably fits within this inline budget.",
3222
- "- 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.",
3223
- "- Typical canvas-first cases include long-form research summaries, timelines, bios or profiles, structured notes, plans, comparisons, and other reusable reference documents.",
3224
- "- Do not create a canvas for short factual answers that fit cleanly in one normal thread reply.",
3225
- "- 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.",
3226
- "- Do not narrate tool execution or repeated status updates in the visible reply.",
3227
- "- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
3228
- "- End every turn with a final user-facing markdown response.",
3229
- "</output>"
3230
- ].join("\n")
3231
- ),
3232
- availableSkillsSection,
3233
- activeSkillsSection,
3234
- ...activeToolsSection ? [activeToolsSection] : [],
3235
- renderTag(
3236
- "invocation-context",
3237
- invocation ? `Explicit skill trigger detected: /${invocation.skillName}` : "No explicit skill trigger detected."
3238
- )
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()
3239
3164
  ];
3240
3165
  return sections.join("\n\n");
3241
3166
  }
3242
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
+
3243
3211
  // src/chat/capabilities/router.ts
3244
3212
  var ProviderCredentialRouter = class {
3245
3213
  brokersByProvider;
@@ -3965,6 +3933,27 @@ var MCP_CLIENT_INFO = {
3965
3933
  name: "junior-mcp-client",
3966
3934
  version: "1.0.0"
3967
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
+ }
3968
3957
  var McpAuthorizationRequiredError = class extends Error {
3969
3958
  provider;
3970
3959
  constructor(provider, message) {
@@ -4014,10 +4003,22 @@ var PluginMcpClient = class {
4014
4003
  async callTool(name, args) {
4015
4004
  return await this.withSessionRecovery(async () => {
4016
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
+ }
4017
4018
  const result = await this.wrapAuth(
4018
4019
  client2.callTool({
4019
4020
  name,
4020
- ...args && Object.keys(args).length > 0 ? { arguments: args } : {}
4021
+ arguments: args ?? {}
4021
4022
  })
4022
4023
  );
4023
4024
  await this.syncTransportSessionId();
@@ -4130,13 +4131,24 @@ var PluginMcpClient = class {
4130
4131
  }
4131
4132
  };
4132
4133
 
4133
- // src/chat/mcp/tool-manager.ts
4134
+ // src/chat/mcp/errors.ts
4134
4135
  var McpToolError = class extends Error {
4135
4136
  constructor(message) {
4136
4137
  super(message);
4137
4138
  this.name = "McpToolError";
4138
4139
  }
4139
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
4140
4152
  function normalizeMcpToolName(provider, toolName) {
4141
4153
  return `mcp__${provider}__${toolName}`;
4142
4154
  }
@@ -4302,24 +4314,6 @@ var McpToolManager = class {
4302
4314
  (tool2) => this.toToolDescriptor(tool2)
4303
4315
  );
4304
4316
  }
4305
- searchTools(skills, query, options = {}) {
4306
- const resolved = this.getResolvedActiveTools(skills, options);
4307
- const trimmedQuery = query.trim();
4308
- if (!trimmedQuery || trimmedQuery === "*") {
4309
- return resolved.slice(0, Math.max(1, options.limit ?? 8)).map((tool2) => this.toToolDescriptor(tool2));
4310
- }
4311
- const normalizedQuery = trimmedQuery.toLowerCase();
4312
- const queryTokens = normalizedQuery.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
4313
- return resolved.map((tool2) => ({
4314
- tool: tool2,
4315
- score: this.scoreToolMatch(tool2, normalizedQuery, queryTokens)
4316
- })).filter((entry) => entry.score > 0).sort((left, right) => {
4317
- if (right.score !== left.score) {
4318
- return right.score - left.score;
4319
- }
4320
- return left.tool.name.localeCompare(right.tool.name);
4321
- }).slice(0, Math.max(1, options.limit ?? 8)).map((entry) => this.toToolDescriptor(entry.tool));
4322
- }
4323
4317
  filterListedTools(plugin, tools) {
4324
4318
  const allowedTools = plugin.manifest.mcp?.allowedTools;
4325
4319
  if (!allowedTools || allowedTools.length === 0) {
@@ -4351,6 +4345,8 @@ var McpToolManager = class {
4351
4345
  return client2;
4352
4346
  }
4353
4347
  toManagedTool(plugin, client2, tool2) {
4348
+ const outputSchema = toOptionalRecord(tool2.outputSchema);
4349
+ const annotations = toOptionalRecord(tool2.annotations);
4354
4350
  return {
4355
4351
  name: normalizeMcpToolName(plugin.manifest.name, tool2.name),
4356
4352
  description: describeMcpTool(plugin.manifest.name, tool2),
@@ -4358,8 +4354,15 @@ var McpToolManager = class {
4358
4354
  provider: plugin.manifest.name,
4359
4355
  rawName: tool2.name,
4360
4356
  ...tool2.title?.trim() ? { title: tool2.title.trim() } : {},
4357
+ ...outputSchema ? { outputSchema } : {},
4358
+ ...annotations ? { annotations } : {},
4361
4359
  execute: async (args) => {
4362
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);
4363
4366
  try {
4364
4367
  const result = await client2.callTool(tool2.name, resolvedArgs);
4365
4368
  if ("isError" in result && result.isError) {
@@ -4395,6 +4398,20 @@ var McpToolManager = class {
4395
4398
  }
4396
4399
  };
4397
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
+ }
4398
4415
  throw error;
4399
4416
  }
4400
4417
  }
@@ -4441,37 +4458,19 @@ var McpToolManager = class {
4441
4458
  toToolDescriptor(tool2) {
4442
4459
  return {
4443
4460
  name: tool2.name,
4461
+ rawName: tool2.rawName,
4462
+ ...tool2.title ? { title: tool2.title } : {},
4444
4463
  description: tool2.description,
4445
4464
  parameters: tool2.parameters,
4465
+ ...tool2.outputSchema ? { outputSchema: tool2.outputSchema } : {},
4466
+ ...tool2.annotations ? { annotations: tool2.annotations } : {},
4446
4467
  provider: tool2.provider
4447
4468
  };
4448
4469
  }
4449
- scoreToolMatch(tool2, normalizedQuery, queryTokens) {
4450
- const exactCandidates = [tool2.name, tool2.rawName, tool2.title].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
4451
- if (exactCandidates.includes(normalizedQuery)) {
4452
- return 100;
4453
- }
4454
- let score = 0;
4455
- const searchableText = [
4456
- tool2.name,
4457
- tool2.rawName,
4458
- tool2.title,
4459
- tool2.description,
4460
- tool2.provider
4461
- ].filter((value) => Boolean(value)).join(" ").toLowerCase();
4462
- for (const candidate of exactCandidates) {
4463
- if (candidate.startsWith(normalizedQuery)) {
4464
- score = Math.max(score, 60);
4465
- }
4466
- }
4467
- for (const token of queryTokens) {
4468
- if (searchableText.includes(token)) {
4469
- score += 10;
4470
- }
4471
- }
4472
- return score;
4473
- }
4474
4470
  };
4471
+ function toOptionalRecord(value) {
4472
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4473
+ }
4475
4474
 
4476
4475
  // src/chat/tools/definition.ts
4477
4476
  function tool(definition) {
@@ -4784,8 +4783,61 @@ function createImageGenerateTool(hooks, deps = {}) {
4784
4783
  });
4785
4784
  }
4786
4785
 
4787
- // src/chat/tools/skill/load-skill.ts
4786
+ // src/chat/tools/skill/call-mcp-tool.ts
4788
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";
4789
4841
  function toLoadedSkill(result, availableSkills) {
4790
4842
  if (result.ok !== true || typeof result.skill_name !== "string" || typeof result.description !== "string" || typeof result.skill_dir !== "string" || typeof result.instructions !== "string") {
4791
4843
  return null;
@@ -4797,7 +4849,6 @@ function toLoadedSkill(result, availableSkills) {
4797
4849
  skillPath: metadata?.skillPath ?? result.skill_dir,
4798
4850
  ...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
4799
4851
  ...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
4800
- ...metadata?.usesConfig ? { usesConfig: metadata.usesConfig } : {},
4801
4852
  body: result.instructions
4802
4853
  };
4803
4854
  }
@@ -4830,9 +4881,9 @@ async function loadSkillFromHost(availableSkills, skillName) {
4830
4881
  }
4831
4882
  function createLoadSkillTool(availableSkills, options) {
4832
4883
  return tool({
4833
- 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.",
4834
- inputSchema: Type4.Object({
4835
- 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({
4836
4887
  minLength: 1,
4837
4888
  description: "Skill name to load, without the leading slash."
4838
4889
  })
@@ -4851,53 +4902,87 @@ function createLoadSkillTool(availableSkills, options) {
4851
4902
  });
4852
4903
  }
4853
4904
 
4854
- // src/chat/tools/sandbox/read-file.ts
4855
- import { Type as Type5 } from "@sinclair/typebox";
4856
- function createReadFileTool() {
4857
- return tool({
4858
- 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.",
4859
- inputSchema: Type5.Object(
4860
- {
4861
- path: Type5.String({
4862
- minLength: 1,
4863
- description: "Path to the file in the sandbox workspace."
4864
- })
4865
- },
4866
- { additionalProperties: false }
4867
- ),
4868
- execute: async () => {
4869
- throw new Error(
4870
- "readFile can only run when sandbox execution is enabled."
4871
- );
4872
- }
4873
- });
4874
- }
4875
-
4876
- // src/chat/tools/runtime/report-progress.ts
4905
+ // src/chat/tools/skill/search-mcp-tools.ts
4877
4906
  import { Type as Type6 } from "@sinclair/typebox";
4878
- function createReportProgressTool() {
4879
- return tool({
4880
- 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.",
4881
- inputSchema: Type6.Object({
4882
- message: Type6.String({
4883
- minLength: 1,
4884
- description: "Short user-facing progress message."
4885
- })
4886
- })
4887
- });
4888
- }
4889
-
4890
- // src/chat/tools/skill/search-tools.ts
4891
- import { Type as Type7 } from "@sinclair/typebox";
4892
4907
 
4893
4908
  // src/chat/tools/skill/mcp-tool-summary.ts
4894
- function summarizeInputSchema(schema) {
4895
- const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
4896
- 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(
4897
4914
  schema.required.filter(
4898
4915
  (value) => typeof value === "string"
4899
4916
  )
4900
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);
4901
4986
  const propertyNames = Object.keys(properties);
4902
4987
  if (propertyNames.length === 0) {
4903
4988
  return "No arguments.";
@@ -4907,59 +4992,190 @@ function summarizeInputSchema(schema) {
4907
4992
  function toExposedToolSummary(toolDef) {
4908
4993
  return {
4909
4994
  tool_name: toolDef.name,
4995
+ mcp_tool_name: toolDef.rawName,
4910
4996
  provider: toolDef.provider,
4997
+ ...toolDef.title ? { title: toolDef.title } : {},
4911
4998
  description: toolDef.description,
4999
+ signature: formatMcpToolSignature(toolDef.name, toolDef.parameters),
5000
+ call: formatMcpToolCallExample(toolDef.name, toolDef.parameters),
4912
5001
  input_schema: toolDef.parameters,
4913
- input_schema_summary: summarizeInputSchema(toolDef.parameters)
5002
+ input_schema_summary: summarizeInputSchema(toolDef.parameters),
5003
+ ...toolDef.outputSchema ? { output_schema: toolDef.outputSchema } : {},
5004
+ ...toolDef.annotations ? { annotations: toolDef.annotations } : {}
4914
5005
  };
4915
5006
  }
4916
-
4917
- // src/chat/tools/skill/search-tools.ts
4918
- var DEFAULT_LIMIT = 5;
4919
- var MAX_LIMIT = 20;
4920
- function createSearchToolsTool(mcpToolManager, getActiveSkills) {
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
+ );
5040
+ }
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) {
4921
5094
  return tool({
4922
- description: "Search active MCP tools exposed by the currently loaded skills. Use when you need to rediscover or filter active tools.",
4923
- 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(
4924
5097
  {
4925
- query: Type7.String({
4926
- minLength: 1,
4927
- description: "Search query for matching MCP tool names or descriptions."
4928
- }),
4929
- provider: Type7.Optional(
4930
- 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({
4931
5106
  minLength: 1,
4932
- description: "Optional MCP provider filter, for example notion or sentry."
5107
+ description: "Optional provider name to list or search within."
4933
5108
  })
4934
5109
  ),
4935
- limit: Type7.Optional(
4936
- Type7.Integer({
5110
+ max_results: Type6.Optional(
5111
+ Type6.Integer({
4937
5112
  minimum: 1,
4938
- maximum: MAX_LIMIT,
4939
- description: "Maximum number of matching tools to return."
5113
+ maximum: MAX_RESULTS,
5114
+ description: "Maximum matching tool descriptors to return."
4940
5115
  })
4941
5116
  )
4942
5117
  },
4943
5118
  { additionalProperties: false }
4944
5119
  ),
4945
- execute: async ({ query, provider, limit }) => {
4946
- const results = mcpToolManager.searchTools(getActiveSkills(), query, {
4947
- ...provider ? { provider } : {},
4948
- limit: limit ?? DEFAULT_LIMIT
4949
- }).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
+ );
4950
5130
  return {
4951
- ok: true,
4952
- query,
4953
- ...provider ? { provider } : {},
4954
- result_count: results.length,
4955
- 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)
4956
5136
  };
4957
5137
  }
4958
5138
  });
4959
5139
  }
4960
5140
 
4961
- // 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
4962
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";
4963
5179
 
4964
5180
  // src/chat/slack/channel.ts
4965
5181
  async function listChannelMessages(input) {
@@ -5048,39 +5264,39 @@ async function listThreadReplies(input) {
5048
5264
  function createSlackChannelListMessagesTool(context) {
5049
5265
  return tool({
5050
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.",
5051
- inputSchema: Type8.Object({
5052
- limit: Type8.Optional(
5053
- Type8.Integer({
5267
+ inputSchema: Type9.Object({
5268
+ limit: Type9.Optional(
5269
+ Type9.Integer({
5054
5270
  minimum: 1,
5055
5271
  maximum: 1e3,
5056
5272
  description: "Maximum number of messages to return across pages."
5057
5273
  })
5058
5274
  ),
5059
- cursor: Type8.Optional(
5060
- Type8.String({
5275
+ cursor: Type9.Optional(
5276
+ Type9.String({
5061
5277
  minLength: 1,
5062
5278
  description: "Optional cursor to continue from a prior call."
5063
5279
  })
5064
5280
  ),
5065
- oldest: Type8.Optional(
5066
- Type8.String({
5281
+ oldest: Type9.Optional(
5282
+ Type9.String({
5067
5283
  minLength: 1,
5068
5284
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
5069
5285
  })
5070
5286
  ),
5071
- latest: Type8.Optional(
5072
- Type8.String({
5287
+ latest: Type9.Optional(
5288
+ Type9.String({
5073
5289
  minLength: 1,
5074
5290
  description: "Optional latest message timestamp (Slack ts) for range filtering."
5075
5291
  })
5076
5292
  ),
5077
- inclusive: Type8.Optional(
5078
- Type8.Boolean({
5293
+ inclusive: Type9.Optional(
5294
+ Type9.Boolean({
5079
5295
  description: "Whether oldest/latest bounds should be inclusive."
5080
5296
  })
5081
5297
  ),
5082
- max_pages: Type8.Optional(
5083
- Type8.Integer({
5298
+ max_pages: Type9.Optional(
5299
+ Type9.Integer({
5084
5300
  minimum: 1,
5085
5301
  maximum: 10,
5086
5302
  description: "Maximum number of API pages to traverse in a single call."
@@ -5134,7 +5350,7 @@ function createSlackChannelListMessagesTool(context) {
5134
5350
  }
5135
5351
 
5136
5352
  // src/chat/tools/slack/channel-post-message.ts
5137
- import { Type as Type9 } from "@sinclair/typebox";
5353
+ import { Type as Type10 } from "@sinclair/typebox";
5138
5354
 
5139
5355
  // src/chat/tools/idempotency.ts
5140
5356
  function stableSerialize(value) {
@@ -5156,8 +5372,8 @@ function createOperationKey(toolName, input) {
5156
5372
  function createSlackChannelPostMessageTool(context, state) {
5157
5373
  return tool({
5158
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.",
5159
- inputSchema: Type9.Object({
5160
- text: Type9.String({
5375
+ inputSchema: Type10.Object({
5376
+ text: Type10.String({
5161
5377
  minLength: 1,
5162
5378
  maxLength: 4e4,
5163
5379
  description: "Slack mrkdwn text to post."
@@ -5200,12 +5416,12 @@ function createSlackChannelPostMessageTool(context, state) {
5200
5416
  }
5201
5417
 
5202
5418
  // src/chat/tools/slack/message-add-reaction.ts
5203
- import { Type as Type10 } from "@sinclair/typebox";
5419
+ import { Type as Type11 } from "@sinclair/typebox";
5204
5420
  function createSlackMessageAddReactionTool(context, state) {
5205
5421
  return tool({
5206
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.",
5207
- inputSchema: Type10.Object({
5208
- emoji: Type10.String({
5423
+ inputSchema: Type11.Object({
5424
+ emoji: Type11.String({
5209
5425
  minLength: 1,
5210
5426
  maxLength: 64,
5211
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."
@@ -5263,7 +5479,7 @@ function createSlackMessageAddReactionTool(context, state) {
5263
5479
  }
5264
5480
 
5265
5481
  // src/chat/tools/slack/canvas-tools.ts
5266
- import { Type as Type11 } from "@sinclair/typebox";
5482
+ import { Type as Type12 } from "@sinclair/typebox";
5267
5483
 
5268
5484
  // src/chat/tools/slack/canvases.ts
5269
5485
  function normalizeCanvasMarkdown(markdown) {
@@ -5479,13 +5695,13 @@ function mergeRecentCanvases(existing, created) {
5479
5695
  function createSlackCanvasCreateTool(context, state) {
5480
5696
  return tool({
5481
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.",
5482
- inputSchema: Type11.Object({
5483
- title: Type11.String({
5698
+ inputSchema: Type12.Object({
5699
+ title: Type12.String({
5484
5700
  minLength: 1,
5485
5701
  maxLength: 160,
5486
5702
  description: "Canvas title."
5487
5703
  }),
5488
- markdown: Type11.String({
5704
+ markdown: Type12.String({
5489
5705
  minLength: 1,
5490
5706
  description: "Canvas markdown body content."
5491
5707
  })
@@ -5551,29 +5767,29 @@ function createSlackCanvasCreateTool(context, state) {
5551
5767
  function createSlackCanvasUpdateTool(state, _context) {
5552
5768
  return tool({
5553
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.",
5554
- inputSchema: Type11.Object({
5555
- markdown: Type11.String({
5770
+ inputSchema: Type12.Object({
5771
+ markdown: Type12.String({
5556
5772
  minLength: 1,
5557
5773
  description: "Markdown content to insert or use as replacement text."
5558
5774
  }),
5559
- operation: Type11.Optional(
5560
- Type11.Union(
5775
+ operation: Type12.Optional(
5776
+ Type12.Union(
5561
5777
  [
5562
- Type11.Literal("insert_at_end"),
5563
- Type11.Literal("insert_at_start"),
5564
- Type11.Literal("replace")
5778
+ Type12.Literal("insert_at_end"),
5779
+ Type12.Literal("insert_at_start"),
5780
+ Type12.Literal("replace")
5565
5781
  ],
5566
5782
  { description: "Canvas update mode." }
5567
5783
  )
5568
5784
  ),
5569
- section_id: Type11.Optional(
5570
- Type11.String({
5785
+ section_id: Type12.Optional(
5786
+ Type12.String({
5571
5787
  minLength: 1,
5572
5788
  description: "Optional section ID required for targeted replace operations."
5573
5789
  })
5574
5790
  ),
5575
- section_contains_text: Type11.Optional(
5576
- Type11.String({
5791
+ section_contains_text: Type12.Optional(
5792
+ Type12.String({
5577
5793
  minLength: 1,
5578
5794
  description: "Optional helper text used to find the target section when section_id is not provided."
5579
5795
  })
@@ -5639,8 +5855,8 @@ function createSlackCanvasUpdateTool(state, _context) {
5639
5855
  function createSlackCanvasReadTool() {
5640
5856
  return tool({
5641
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.",
5642
- inputSchema: Type11.Object({
5643
- canvas: Type11.String({
5858
+ inputSchema: Type12.Object({
5859
+ canvas: Type12.String({
5644
5860
  minLength: 1,
5645
5861
  description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
5646
5862
  })
@@ -5690,7 +5906,7 @@ function createSlackCanvasReadTool() {
5690
5906
  }
5691
5907
 
5692
5908
  // src/chat/tools/slack/list-tools.ts
5693
- import { Type as Type12 } from "@sinclair/typebox";
5909
+ import { Type as Type13 } from "@sinclair/typebox";
5694
5910
 
5695
5911
  // src/chat/tools/slack/lists.ts
5696
5912
  function normalizeKey(value) {
@@ -5864,8 +6080,8 @@ async function updateListItem(input) {
5864
6080
  function createSlackListCreateTool(state) {
5865
6081
  return tool({
5866
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.",
5867
- inputSchema: Type12.Object({
5868
- name: Type12.String({
6083
+ inputSchema: Type13.Object({
6084
+ name: Type13.String({
5869
6085
  minLength: 1,
5870
6086
  maxLength: 160,
5871
6087
  description: "Name for the new Slack list."
@@ -5900,20 +6116,20 @@ function createSlackListCreateTool(state) {
5900
6116
  function createSlackListAddItemsTool(state) {
5901
6117
  return tool({
5902
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.",
5903
- inputSchema: Type12.Object({
5904
- items: Type12.Array(Type12.String({ minLength: 1 }), {
6119
+ inputSchema: Type13.Object({
6120
+ items: Type13.Array(Type13.String({ minLength: 1 }), {
5905
6121
  minItems: 1,
5906
6122
  maxItems: 25,
5907
6123
  description: "List item titles to create."
5908
6124
  }),
5909
- assignee_user_id: Type12.Optional(
5910
- Type12.String({
6125
+ assignee_user_id: Type13.Optional(
6126
+ Type13.String({
5911
6127
  minLength: 1,
5912
6128
  description: "Optional Slack user ID assigned to all created items."
5913
6129
  })
5914
6130
  ),
5915
- due_date: Type12.Optional(
5916
- Type12.String({
6131
+ due_date: Type13.Optional(
6132
+ Type13.String({
5917
6133
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
5918
6134
  description: "Optional due date in YYYY-MM-DD format."
5919
6135
  })
@@ -5962,9 +6178,9 @@ function createSlackListAddItemsTool(state) {
5962
6178
  function createSlackListGetItemsTool(state) {
5963
6179
  return tool({
5964
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.",
5965
- inputSchema: Type12.Object({
5966
- limit: Type12.Optional(
5967
- Type12.Integer({
6181
+ inputSchema: Type13.Object({
6182
+ limit: Type13.Optional(
6183
+ Type13.Integer({
5968
6184
  minimum: 1,
5969
6185
  maximum: 200,
5970
6186
  description: "Maximum number of list items to return."
@@ -5989,19 +6205,19 @@ function createSlackListGetItemsTool(state) {
5989
6205
  function createSlackListUpdateItemTool(state) {
5990
6206
  return tool({
5991
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.",
5992
- inputSchema: Type12.Object(
6208
+ inputSchema: Type13.Object(
5993
6209
  {
5994
- item_id: Type12.String({
6210
+ item_id: Type13.String({
5995
6211
  minLength: 1,
5996
6212
  description: "ID of the Slack list item to update."
5997
6213
  }),
5998
- completed: Type12.Optional(
5999
- Type12.Boolean({
6214
+ completed: Type13.Optional(
6215
+ Type13.Boolean({
6000
6216
  description: "Optional completion status update."
6001
6217
  })
6002
6218
  ),
6003
- title: Type12.Optional(
6004
- Type12.String({
6219
+ title: Type13.Optional(
6220
+ Type13.String({
6005
6221
  minLength: 1,
6006
6222
  description: "Optional new item title."
6007
6223
  })
@@ -6051,11 +6267,11 @@ function createSlackListUpdateItemTool(state) {
6051
6267
  }
6052
6268
 
6053
6269
  // src/chat/tools/system-time.ts
6054
- import { Type as Type13 } from "@sinclair/typebox";
6270
+ import { Type as Type14 } from "@sinclair/typebox";
6055
6271
  function createSystemTimeTool() {
6056
6272
  return tool({
6057
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.",
6058
- inputSchema: Type13.Object({}),
6274
+ inputSchema: Type14.Object({}),
6059
6275
  execute: async () => {
6060
6276
  const now = /* @__PURE__ */ new Date();
6061
6277
  return {
@@ -6070,7 +6286,7 @@ function createSystemTimeTool() {
6070
6286
  }
6071
6287
 
6072
6288
  // src/chat/tools/web/fetch-tool.ts
6073
- import { Type as Type14 } from "@sinclair/typebox";
6289
+ import { Type as Type15 } from "@sinclair/typebox";
6074
6290
 
6075
6291
  // src/chat/tools/web/constants.ts
6076
6292
  var USER_AGENT = "junior-bot/0.1";
@@ -6418,13 +6634,13 @@ function extractHttpStatusFromMessage(message) {
6418
6634
  function createWebFetchTool(hooks) {
6419
6635
  return tool({
6420
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.",
6421
- inputSchema: Type14.Object({
6422
- url: Type14.String({
6637
+ inputSchema: Type15.Object({
6638
+ url: Type15.String({
6423
6639
  minLength: 1,
6424
6640
  description: "HTTP(S) URL to fetch."
6425
6641
  }),
6426
- max_chars: Type14.Optional(
6427
- Type14.Integer({
6642
+ max_chars: Type15.Optional(
6643
+ Type15.Integer({
6428
6644
  minimum: 500,
6429
6645
  maximum: MAX_FETCH_CHARS,
6430
6646
  description: "Optional maximum number of extracted characters to return."
@@ -6484,9 +6700,9 @@ function createWebFetchTool(hooks) {
6484
6700
  // src/chat/tools/web/search.ts
6485
6701
  import { generateText } from "ai";
6486
6702
  import { createGatewayProvider } from "@ai-sdk/gateway";
6487
- import { Type as Type15 } from "@sinclair/typebox";
6703
+ import { Type as Type16 } from "@sinclair/typebox";
6488
6704
  var SEARCH_TIMEOUT_MS = 6e4;
6489
- var MAX_RESULTS = 5;
6705
+ var MAX_RESULTS2 = 5;
6490
6706
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
6491
6707
  var SEARCH_TOOL_NAME = "parallelSearch";
6492
6708
  function asString(value) {
@@ -6527,16 +6743,16 @@ function isAuthFailure(message) {
6527
6743
  function createWebSearchTool() {
6528
6744
  return tool({
6529
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.",
6530
- inputSchema: Type15.Object({
6531
- query: Type15.String({
6746
+ inputSchema: Type16.Object({
6747
+ query: Type16.String({
6532
6748
  minLength: 1,
6533
6749
  maxLength: 500,
6534
6750
  description: "Search query."
6535
6751
  }),
6536
- max_results: Type15.Optional(
6537
- Type15.Integer({
6752
+ max_results: Type16.Optional(
6753
+ Type16.Integer({
6538
6754
  minimum: 1,
6539
- maximum: MAX_RESULTS,
6755
+ maximum: MAX_RESULTS2,
6540
6756
  description: "Max results to return."
6541
6757
  })
6542
6758
  )
@@ -6600,17 +6816,17 @@ function createWebSearchTool() {
6600
6816
  }
6601
6817
 
6602
6818
  // src/chat/tools/sandbox/write-file.ts
6603
- import { Type as Type16 } from "@sinclair/typebox";
6819
+ import { Type as Type17 } from "@sinclair/typebox";
6604
6820
  function createWriteFileTool() {
6605
6821
  return tool({
6606
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.",
6607
- inputSchema: Type16.Object(
6823
+ inputSchema: Type17.Object(
6608
6824
  {
6609
- path: Type16.String({
6825
+ path: Type17.String({
6610
6826
  minLength: 1,
6611
6827
  description: "Path to write in the sandbox workspace."
6612
6828
  }),
6613
- content: Type16.String({
6829
+ content: Type17.String({
6614
6830
  description: "UTF-8 file content to write."
6615
6831
  })
6616
6832
  },
@@ -6685,7 +6901,11 @@ function createTools(availableSkills, hooks = {}, context) {
6685
6901
  slackListUpdateItem: createSlackListUpdateItemTool(state)
6686
6902
  };
6687
6903
  if (context.mcpToolManager && context.getActiveSkills) {
6688
- tools.searchTools = createSearchToolsTool(
6904
+ tools.searchMcpTools = createSearchMcpToolsTool(
6905
+ context.mcpToolManager,
6906
+ context.getActiveSkills
6907
+ );
6908
+ tools.callMcpTool = createCallMcpToolTool(
6689
6909
  context.mcpToolManager,
6690
6910
  context.getActiveSkills
6691
6911
  );
@@ -8375,8 +8595,10 @@ function getToolErrorAttributes(error) {
8375
8595
  };
8376
8596
  }
8377
8597
  function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
8598
+ const errorType = getMcpAwareErrorType(error, "tool_execution_error");
8599
+ const errorMessage = getMcpAwareErrorMessage(error);
8378
8600
  setSpanAttributes({
8379
- "error.type": error instanceof Error ? error.name : "tool_execution_error"
8601
+ "error.type": errorType
8380
8602
  });
8381
8603
  if (shouldTrace) {
8382
8604
  logWarn(
@@ -8387,8 +8609,8 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
8387
8609
  "gen_ai.operation.name": "execute_tool",
8388
8610
  "gen_ai.tool.name": toolName,
8389
8611
  ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
8390
- "error.type": error instanceof Error ? error.name : "tool_execution_error",
8391
- "error.message": error instanceof Error ? error.message : String(error)
8612
+ "error.type": errorType,
8613
+ "error.message": errorMessage
8392
8614
  },
8393
8615
  "Agent tool call failed"
8394
8616
  );
@@ -8422,7 +8644,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8422
8644
  execute: async (toolCallId, params) => {
8423
8645
  const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
8424
8646
  const toolArgumentsAttribute = serializeGenAiAttribute(params);
8425
- onToolCall?.(toolName);
8426
8647
  const traceToolContext = {
8427
8648
  ...spanContext,
8428
8649
  conversationId: spanContext.conversationId,
@@ -8441,6 +8662,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8441
8662
  spanContext,
8442
8663
  async () => {
8443
8664
  const parsed = params;
8665
+ onToolCall?.(toolName, parsed);
8444
8666
  try {
8445
8667
  if (typeof toolDef.execute !== "function") {
8446
8668
  const resultDetails = { ok: true };
@@ -8710,24 +8932,11 @@ function upsertActiveSkill(activeSkills, next) {
8710
8932
  existing.description = next.description;
8711
8933
  existing.skillPath = next.skillPath;
8712
8934
  existing.allowedTools = next.allowedTools;
8713
- existing.usesConfig = next.usesConfig;
8714
8935
  existing.pluginProvider = next.pluginProvider;
8715
8936
  return;
8716
8937
  }
8717
8938
  activeSkills.push(next);
8718
8939
  }
8719
- function collectRelevantConfigurationKeys(activeSkills, explicitSkill) {
8720
- const keys = /* @__PURE__ */ new Set();
8721
- for (const skill of [
8722
- ...activeSkills,
8723
- ...explicitSkill ? [explicitSkill] : []
8724
- ]) {
8725
- for (const key of skill.usesConfig ?? []) {
8726
- keys.add(key);
8727
- }
8728
- }
8729
- return [...keys].sort((a, b) => a.localeCompare(b));
8730
- }
8731
8940
  function trimTrailingAssistantMessages(messages) {
8732
8941
  let end = messages.length;
8733
8942
  while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
@@ -9710,20 +9919,6 @@ function buildUserTurnInput(args) {
9710
9919
  }
9711
9920
  return { routerBlocks, userContentParts };
9712
9921
  }
9713
- function mcpToolsToDefinitions(mcpTools) {
9714
- const defs = {};
9715
- for (const tool2 of mcpTools) {
9716
- defs[tool2.name] = {
9717
- description: tool2.description,
9718
- // Raw JSON Schema from MCP servers — not a TypeBox TSchema, but
9719
- // pi-agent-core validates with AJV and the Anthropic provider reads
9720
- // .properties/.required, so raw JSON Schema works at runtime.
9721
- inputSchema: tool2.parameters,
9722
- execute: async (args) => tool2.execute(args)
9723
- };
9724
- }
9725
- return defs;
9726
- }
9727
9922
  async function generateAssistantReply(messageText, context = {}) {
9728
9923
  const replyStartedAtMs = Date.now();
9729
9924
  let timeoutResumeConversationId;
@@ -10057,11 +10252,18 @@ async function generateAssistantReply(messageText, context = {}) {
10057
10252
  if (!effective.pluginProvider) {
10058
10253
  return void 0;
10059
10254
  }
10060
- syncMcpAgentTools();
10061
- return {
10062
- 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
+ {
10063
10261
  provider: effective.pluginProvider
10064
- }).map(toExposedToolSummary)
10262
+ }
10263
+ ).length;
10264
+ return {
10265
+ mcp_provider: effective.pluginProvider,
10266
+ available_tool_count: availableToolCount
10065
10267
  };
10066
10268
  }
10067
10269
  },
@@ -10091,22 +10293,20 @@ async function generateAssistantReply(messageText, context = {}) {
10091
10293
  await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
10092
10294
  }
10093
10295
  syncResumeState();
10094
- const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
10296
+ const activeMcpCatalogs = toActiveMcpCatalogSummaries(
10297
+ turnMcpToolManager.getActiveToolCatalog(activeSkills)
10298
+ );
10095
10299
  baseInstructions = buildSystemPrompt({
10096
10300
  availableSkills,
10097
10301
  activeSkills,
10098
- activeTools: activeToolSummaries,
10302
+ activeMcpCatalogs,
10099
10303
  invocation: skillInvocation,
10100
10304
  assistant: context.assistant,
10101
10305
  requester: context.requester,
10102
10306
  artifactState: context.artifactState,
10103
10307
  configuration: configurationValues,
10104
- relevantConfigurationKeys: collectRelevantConfigurationKeys(
10105
- activeSkills,
10106
- invokedSkill
10107
- ),
10108
- runtimeMetadata: getRuntimeMetadata(),
10109
- threadParticipants: context.threadParticipants
10308
+ threadParticipants: context.threadParticipants,
10309
+ turnState: resumedFromCheckpoint ? "resumed" : "fresh"
10110
10310
  });
10111
10311
  const inputMessagesAttribute = serializeGenAiAttribute([
10112
10312
  {
@@ -10118,10 +10318,23 @@ async function generateAssistantReply(messageText, context = {}) {
10118
10318
  content: userContentParts.map((part) => toObservablePromptPart(part))
10119
10319
  }
10120
10320
  ]);
10121
- const onToolCall = (toolName) => {
10321
+ const onToolCall = (toolName, params) => {
10122
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
+ }
10123
10336
  };
10124
- const baseAgentTools = createAgentTools(
10337
+ const agentTools = createAgentTools(
10125
10338
  tools,
10126
10339
  skillSandbox,
10127
10340
  spanContext,
@@ -10131,24 +10344,6 @@ async function generateAssistantReply(messageText, context = {}) {
10131
10344
  pluginAuth,
10132
10345
  onToolCall
10133
10346
  );
10134
- const agentTools = [...baseAgentTools];
10135
- const syncMcpAgentTools = () => {
10136
- const mcpTools = turnMcpToolManager.getResolvedActiveTools(activeSkills);
10137
- const mcpDefs = mcpToolsToDefinitions(mcpTools);
10138
- const mcpAgentTools = createAgentTools(
10139
- mcpDefs,
10140
- skillSandbox,
10141
- spanContext,
10142
- context.onStatus,
10143
- sandboxExecutor,
10144
- capabilityRuntime,
10145
- pluginAuth,
10146
- onToolCall
10147
- );
10148
- agentTools.length = 0;
10149
- agentTools.push(...baseAgentTools, ...mcpAgentTools);
10150
- };
10151
- syncMcpAgentTools();
10152
10347
  agent = new Agent({
10153
10348
  getApiKey: () => getPiGatewayApiKeyOverride(),
10154
10349
  initialState: {