@yanlinglabs/winter-agent-runtime 0.0.36 → 0.0.38

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.
@@ -934,6 +934,7 @@ function validatePermissionRuleString(raw, direction) {
934
934
  }
935
935
 
936
936
  // src/tools/registry.ts
937
+ var DEFERRED_BUILTINS_CAPABILITY = "winter.deferred-builtins";
937
938
  var registry = new Map;
938
939
  function registerTool2(t) {
939
940
  if (registry.has(t.descriptor.canonicalName)) {
@@ -996,8 +997,9 @@ function onRegistryChange(cb) {
996
997
  registryChangeListeners.delete(cb);
997
998
  };
998
999
  }
999
- function buildMcpToolDescriptor(server, tool, opts) {
1000
- const canonicalName = `mcp__${server}__${tool.name}`;
1000
+ function buildMcpToolDescriptor(server, tool, opts, plainName) {
1001
+ const mcpSpelling = `mcp__${server}__${tool.name}`;
1002
+ const canonicalName = plainName ?? mcpSpelling;
1001
1003
  const requiresInteraction = tool._meta?.["anthropic/requiresUserInteraction"] === true;
1002
1004
  const perToolAlwaysLoad = tool._meta?.["anthropic/alwaysLoad"] === true;
1003
1005
  const alwaysLoadResolved = perToolAlwaysLoad || opts.alwaysLoad === true ? true : opts.alwaysLoad;
@@ -1017,9 +1019,23 @@ function buildMcpToolDescriptor(server, tool, opts) {
1017
1019
  ...alwaysLoadResolved !== undefined ? { alwaysLoad: alwaysLoadResolved } : {},
1018
1020
  deferred: opts.deferredDefault,
1019
1021
  ...tool._meta !== undefined ? { _meta: tool._meta } : {},
1020
- ...requiresInteraction ? { interaction: "required" } : {}
1022
+ ...requiresInteraction ? { interaction: "required" } : {},
1023
+ ...plainName !== undefined ? { mcpName: mcpSpelling } : {}
1021
1024
  };
1022
1025
  }
1026
+ var TOOL_SEARCH_TOOL_NAME = "ToolSearch";
1027
+ var PLAIN_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
1028
+ function mcpSpellingOf(name) {
1029
+ return registry.get(name)?.descriptor.mcpName;
1030
+ }
1031
+ function registeredNameForMcpSpelling(name) {
1032
+ if (registry.has(name))
1033
+ return name;
1034
+ for (const [registered, entry] of registry)
1035
+ if (entry.descriptor.mcpName === name)
1036
+ return registered;
1037
+ return name;
1038
+ }
1023
1039
  var RESERVED_MCP_SERVER_NAMES = new Set([WINTER_BRAND.mcpServerName]);
1024
1040
  var STANDING_REBRANDS = new Map;
1025
1041
  function rebrandStandingServerTools(renames, serverName) {
@@ -1101,13 +1117,36 @@ function registerMcpServerTools(server, incomingTools, opts) {
1101
1117
  throw new Error(`registerMcpServerTools: "${server}" is a RESERVED server name -- the standing Winter server ` + `(its name is defined in mcp/winter-server.ts) owns this identity and is registry-native; it is never installed through ` + `this live-mutation function, and nothing else may register live tools under it either.`);
1102
1118
  }
1103
1119
  const tools = dedupeSameBatch(server, incomingTools);
1104
- const newNames = new Set(tools.map((t) => `mcp__${server}__${t.name}`));
1120
+ const plainNameFor = (tool) => opts.toolNames !== undefined && Object.hasOwn(opts.toolNames, tool.name) ? opts.toolNames[tool.name] : undefined;
1121
+ const registeredName = (tool) => plainNameFor(tool) ?? `mcp__${server}__${tool.name}`;
1122
+ const newNames = new Set(tools.map(registeredName));
1105
1123
  const previouslyOwned = mcpServerOwnedNames.get(server);
1106
1124
  if (tools.length === 0 && (previouslyOwned === undefined || previouslyOwned.size === 0))
1107
1125
  return;
1126
+ const plainNamesSeen = new Set;
1108
1127
  for (const tool of tools) {
1109
- const canonicalName = `mcp__${server}__${tool.name}`;
1128
+ const plain = plainNameFor(tool);
1129
+ if (plain !== undefined) {
1130
+ if (typeof plain !== "string" || !PLAIN_TOOL_NAME.test(plain) || plain.startsWith("mcp__")) {
1131
+ throw new Error(`registerMcpServerTools: server "${server}" names its tool "${tool.name}" ${JSON.stringify(plain)} -- a plain tool name must match ${String(PLAIN_TOOL_NAME)} and must not start with "mcp__"`);
1132
+ }
1133
+ if (plainNamesSeen.has(plain)) {
1134
+ throw new Error(`registerMcpServerTools: server "${server}" gives two of its tools the same plain name "${plain}"`);
1135
+ }
1136
+ plainNamesSeen.add(plain);
1137
+ }
1138
+ const canonicalName = registeredName(tool);
1110
1139
  const existingOwner = mcpToolOwner.get(canonicalName);
1140
+ const spelling = plain !== undefined ? `mcp__${server}__${tool.name}` : undefined;
1141
+ for (const [name, entry] of registry) {
1142
+ const foreign = mcpToolOwner.get(name) !== server;
1143
+ if (foreign && entry.descriptor.mcpName !== undefined && (entry.descriptor.mcpName === canonicalName || entry.descriptor.mcpName === spelling)) {
1144
+ throw new Error(`registerMcpServerTools: "${canonicalName}" collides with the old spelling "${entry.descriptor.mcpName}" that "${name}" keeps -- refusing to register a second tool under it`);
1145
+ }
1146
+ if (foreign && spelling !== undefined && name === spelling) {
1147
+ throw new Error(`registerMcpServerTools: the old spelling "${spelling}" of server "${server}"'s renamed tool "${plain}" is already a registered tool -- refusing to register a second tool under it`);
1148
+ }
1149
+ }
1111
1150
  if (registry.has(canonicalName) && existingOwner === undefined) {
1112
1151
  throw new Error(`registerMcpServerTools: "${canonicalName}" is already registered by a non-live-MCP mechanism ` + `(e.g. a static WS-06 descriptor stub, or the standing winter server's own advisor identity) -- ` + `refusing to overwrite it. If this name is meant to be a live-connected MCP tool, its static ` + `registration must be removed first.`);
1113
1152
  }
@@ -1125,9 +1164,9 @@ function registerMcpServerTools(server, incomingTools, opts) {
1125
1164
  }
1126
1165
  const nowOwned = new Set;
1127
1166
  for (const tool of tools) {
1128
- const canonicalName = `mcp__${server}__${tool.name}`;
1167
+ const canonicalName = registeredName(tool);
1129
1168
  const existingEntry = registry.get(canonicalName);
1130
- const descriptor = buildMcpToolDescriptor(server, tool, opts);
1169
+ const descriptor = buildMcpToolDescriptor(server, tool, opts, plainNameFor(tool));
1131
1170
  registry.set(canonicalName, existingEntry ? { ...existingEntry, descriptor } : { descriptor });
1132
1171
  mcpToolOwner.set(canonicalName, server);
1133
1172
  nowOwned.add(canonicalName);
@@ -1206,12 +1245,15 @@ function resolveDeferral(descriptor, mode, activation) {
1206
1245
  const modes = descriptor.availability.modes;
1207
1246
  if (modes !== undefined && !modes.includes(mode))
1208
1247
  return "hidden";
1209
- if (descriptor.source === "builtin")
1248
+ if (descriptor.canonicalName === TOOL_SEARCH_TOOL_NAME)
1210
1249
  return "eager";
1211
1250
  if (descriptor.alwaysLoad === true)
1212
1251
  return "eager";
1252
+ const hostDeferred = activation.deferTools?.includes(descriptor.canonicalName) === true;
1253
+ if (descriptor.source === "builtin" && !hostDeferred)
1254
+ return "eager";
1213
1255
  const declared = descriptor.deferred;
1214
- const eligible = declared === true ? true : Array.isArray(declared) ? declared.includes(mode) : false;
1256
+ const eligible = hostDeferred || (declared === true ? true : Array.isArray(declared) ? declared.includes(mode) : false);
1215
1257
  if (!eligible)
1216
1258
  return "eager";
1217
1259
  return isDeferralActive(activation) ? "deferred" : "eager";
@@ -1250,6 +1292,8 @@ function isAvailable(descriptor, cfg) {
1250
1292
  return false;
1251
1293
  if (a.insideSubagent === false && cfg.insideSubagent === true)
1252
1294
  return false;
1295
+ if (a.requiresAnyCapability !== undefined && !a.requiresAnyCapability.some((c) => cfg.capabilities?.includes(c) === true))
1296
+ return false;
1253
1297
  if (!descriptor.capabilityRequirements.every((c) => cfg.capabilities?.includes(c) === true))
1254
1298
  return false;
1255
1299
  return true;
@@ -1276,8 +1320,17 @@ function isBareDenied(canonicalName, disallowedTools) {
1276
1320
  });
1277
1321
  }
1278
1322
  function buildAdvertisedSet(cfg) {
1279
- const requested = cfg.tools;
1280
- return listRegisteredTools().map((t) => t.descriptor).filter((d) => d.disposition !== "correctly-absent").filter((d) => d.exposure !== "hidden").filter((d) => requested === undefined || requested.includes(d.canonicalName)).filter((d) => isAvailable(d, cfg)).filter((d) => !isBareDenied(d.canonicalName, cfg.disallowedTools));
1323
+ return listRegisteredTools().map((t) => t.descriptor).filter((d) => d.disposition !== "correctly-absent").filter((d) => d.exposure !== "hidden").filter((d) => isRequestedTool(d, cfg.tools)).filter((d) => isAvailable(d, cfg)).filter((d) => !isDescriptorBareDenied(d, cfg.disallowedTools));
1324
+ }
1325
+ function isRequestedTool(descriptor, tools) {
1326
+ if (descriptor.source !== "builtin")
1327
+ return true;
1328
+ if (tools === undefined)
1329
+ return descriptor.availability.optIn !== true;
1330
+ return tools.includes(descriptor.canonicalName);
1331
+ }
1332
+ function isDescriptorBareDenied(descriptor, disallowedTools) {
1333
+ return isBareDenied(descriptor.canonicalName, disallowedTools) || descriptor.mcpName !== undefined && isBareDenied(descriptor.mcpName, disallowedTools);
1281
1334
  }
1282
1335
  function unknownToolResult(name) {
1283
1336
  return { output: `Error: unknown tool "${name}" -- no descriptor is registered under that name`, isError: true };
@@ -1295,7 +1348,12 @@ function notYetExecutableResult(name) {
1295
1348
  };
1296
1349
  }
1297
1350
  function foldResult(result) {
1298
- return { output: result.output, ...result.blocks !== undefined ? { blocks: result.blocks } : {}, ...result.isError === true ? { isError: true } : {} };
1351
+ return {
1352
+ output: result.output,
1353
+ ...result.blocks !== undefined ? { blocks: result.blocks } : {},
1354
+ ...result.isError === true ? { isError: true } : {},
1355
+ ...result.siteIcons !== undefined && result.siteIcons.length > 0 ? { siteIcons: result.siteIcons } : {}
1356
+ };
1299
1357
  }
1300
1358
  function unavailableResult(name) {
1301
1359
  return {
@@ -2462,6 +2520,8 @@ function renderPlanModeBlock(input) {
2462
2520
  }
2463
2521
 
2464
2522
  // src/context/attachments.ts
2523
+ var DEFERRED_TOOLS_ADDED_HEADER = "The following deferred tools are now available via ToolSearch:";
2524
+ var DEFERRED_TOOLS_REMOVED_HEADER = "The following deferred tools are no longer available (their MCP server disconnected). Do not search for them — ToolSearch will return no match:";
2465
2525
  var AGENT_LISTING_INITIAL_HEADER = "Available agent types for the Agent tool:";
2466
2526
  var AGENT_LISTING_ADDED_HEADER = "New agent types are now available for the Agent tool:";
2467
2527
  var AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
@@ -2511,6 +2571,44 @@ ${removedTypes.map((t) => `- ${t}`).join(`
2511
2571
 
2512
2572
  `));
2513
2573
  });
2574
+ registerAttachmentRenderer("deferred_tools_delta", (a) => {
2575
+ const addedLines = stringArray(a["addedLines"]);
2576
+ const removedNames = stringArray(a["removedNames"]);
2577
+ const parts = [];
2578
+ if (addedLines.length > 0)
2579
+ parts.push(`${DEFERRED_TOOLS_ADDED_HEADER}
2580
+ ${addedLines.join(`
2581
+ `)}`);
2582
+ if (removedNames.length > 0)
2583
+ parts.push(`${DEFERRED_TOOLS_REMOVED_HEADER}
2584
+ ${removedNames.join(`
2585
+ `)}`);
2586
+ return parts.length === 0 ? undefined : neutralizeReminderTags(parts.join(`
2587
+
2588
+ `));
2589
+ });
2590
+ function announcedDeferredTools(messages) {
2591
+ const announced = new Set;
2592
+ for (const a of attachmentsIn(messages)) {
2593
+ if (a.type !== "deferred_tools_delta")
2594
+ continue;
2595
+ if (Array.isArray(a["addedLines"]))
2596
+ for (const name of stringArray(a["addedNames"]))
2597
+ announced.add(name);
2598
+ for (const name of stringArray(a["removedNames"]))
2599
+ announced.delete(name);
2600
+ }
2601
+ return announced;
2602
+ }
2603
+ function computeDeferredToolsDelta(deferred, offered, history) {
2604
+ const announced = announcedDeferredTools(history);
2605
+ const offeredNow = new Set([...offered, ...deferred]);
2606
+ const added = [...new Set(deferred)].filter((name) => !announced.has(name)).sort();
2607
+ const removed = [...announced].filter((name) => !offeredNow.has(name)).sort();
2608
+ if (added.length === 0 && removed.length === 0)
2609
+ return;
2610
+ return { type: "deferred_tools_delta", addedNames: added, addedLines: added, removedNames: removed };
2611
+ }
2514
2612
  registerAttachmentRenderer("skill_listing", (a) => {
2515
2613
  const content = typeof a["content"] === "string" ? a["content"] : "";
2516
2614
  if (content.length === 0)
@@ -6470,6 +6568,7 @@ ${body}
6470
6568
  // src/mcp/control.ts
6471
6569
  var REPLACE_ELIGIBLE_ORIGINS = new Set(["explicit", "dynamic"]);
6472
6570
  function createMcpControlSeam(internals, opts = {}) {
6571
+ const hostReserved = new Set(opts.hostReservedServerNames ?? []);
6473
6572
  const reservedServerName = opts.reservedServerName ?? WINTER_SERVER_NAME;
6474
6573
  return {
6475
6574
  async reconnect(serverName) {
@@ -6498,6 +6597,10 @@ function createMcpControlSeam(internals, opts = {}) {
6498
6597
  errors[name] = `"${reservedServerName}" is a reserved server identity (RULING P4-B, the standing Winter server) -- no source may configure a live MCP server under this name`;
6499
6598
  continue;
6500
6599
  }
6600
+ if (hostReserved.has(name) && raw?.type !== "sdk") {
6601
+ errors[name] = `"${name}" is a server name this session's host reserves for its own in-process server -- only an in-process (type "sdk") server may be configured under it`;
6602
+ continue;
6603
+ }
6501
6604
  const result = validateServerConfig(raw);
6502
6605
  if (!result.ok) {
6503
6606
  errors[name] = result.reason;
@@ -6573,6 +6676,7 @@ function validateServerConfig(raw) {
6573
6676
  }
6574
6677
  function resolveMcpServerSources(sources, opts) {
6575
6678
  const reservedServerName = opts.reservedServerName ?? WINTER_SERVER_NAME;
6679
+ const hostReserved = new Set(opts.hostReservedServerNames ?? []);
6576
6680
  const resolved = [];
6577
6681
  const shadowed = [];
6578
6682
  const rejected = [];
@@ -6597,6 +6701,10 @@ function resolveMcpServerSources(sources, opts) {
6597
6701
  rejected.push({ name, origin, code: "reserved_name", reason: `"${reservedServerName}" is a reserved server identity (RULING P4-B, the standing Winter server) -- no source may configure a live MCP server under this name` });
6598
6702
  continue;
6599
6703
  }
6704
+ if (hostReserved.has(name) && (origin !== "explicit" || raw?.type !== "sdk")) {
6705
+ rejected.push({ name, origin, code: "reserved_name", reason: `"${name}" is a server name this session's host reserves for its own in-process server -- no ${origin} source may configure a server under this name` });
6706
+ continue;
6707
+ }
6600
6708
  const validated = validateServerConfig(raw);
6601
6709
  if (!validated.ok) {
6602
6710
  rejected.push({ name, origin, code: "invalid_config", reason: validated.reason });
@@ -7168,7 +7276,7 @@ function createMcpLifecycle(deps) {
7168
7276
  };
7169
7277
  return {
7170
7278
  stateSource,
7171
- controlSeam: createMcpControlSeam(internals, { ...deps.reservedServerName !== undefined ? { reservedServerName: deps.reservedServerName } : {} }),
7279
+ controlSeam: createMcpControlSeam(internals, { ...deps.reservedServerName !== undefined ? { reservedServerName: deps.reservedServerName } : {}, ...deps.hostReservedServerNames !== undefined ? { hostReservedServerNames: deps.hostReservedServerNames } : {} }),
7172
7280
  start,
7173
7281
  launch,
7174
7282
  dispose,
@@ -11012,13 +11120,15 @@ function capHookText(text, limit = MAX_HOOK_TEXT_CHARS) {
11012
11120
 
11013
11121
  // src/hooks/runner.ts
11014
11122
  function mcpToolProvenance(toolName) {
11015
- const server = mcpServerOwningTool(toolName);
11123
+ const registered = registeredNameForMcpSpelling(toolName);
11124
+ const server = mcpServerOwningTool(registered);
11016
11125
  if (server === undefined)
11017
11126
  return;
11127
+ const spelled = mcpSpellingOf(registered) ?? registered;
11018
11128
  const prefix = `mcp__${server}__`;
11019
- if (!toolName.startsWith(prefix) || toolName.length === prefix.length)
11129
+ if (!spelled.startsWith(prefix) || spelled.length === prefix.length)
11020
11130
  return;
11021
- return { server, tool: toolName.slice(prefix.length) };
11131
+ return { server, tool: spelled.slice(prefix.length) };
11022
11132
  }
11023
11133
  function mcpServerIdentity(toolName, renames) {
11024
11134
  const name = mcpServerOwningTool(toolName);
@@ -14739,6 +14849,26 @@ stub({
14739
14849
  disposition: "implement-now"
14740
14850
  });
14741
14851
 
14852
+ // src/tools/descriptors/search.ts
14853
+ var SEARCH_CANONICAL_NAME = "Search";
14854
+ var SEARCH_ANSWER_CAPABILITY = "winter.search-answer";
14855
+ var SEARCH_DESCRIPTION = "Search the web and get back a written answer with its sources, in a single call. Ask a real question, not keywords — a search engine answers it and the answer comes back already synthesized, followed by the pages it came from. Use it freely whenever a fact might be newer than you are, or when the user asks about something current, and cite the URLs you used.";
14856
+ var descriptor = {
14857
+ canonicalName: SEARCH_CANONICAL_NAME,
14858
+ advertisedName: SEARCH_CANONICAL_NAME,
14859
+ source: "builtin",
14860
+ inputSchema: { type: "object", properties: { query: { type: "string", minLength: 1, description: "The question to answer" } }, required: ["query"] },
14861
+ description: SEARCH_DESCRIPTION,
14862
+ searchHint: "answer a question from the web with sources",
14863
+ annotations: { readOnlyHint: true, openWorldHint: true },
14864
+ exposure: "eager",
14865
+ permissionClass: "network",
14866
+ availability: { optIn: true },
14867
+ capabilityRequirements: [SEARCH_ANSWER_CAPABILITY],
14868
+ disposition: "implement-now"
14869
+ };
14870
+ stub(descriptor);
14871
+
14742
14872
  // src/tools/descriptors/tool-search.ts
14743
14873
  stub({
14744
14874
  canonicalName: "ToolSearch",
@@ -14752,11 +14882,13 @@ stub({
14752
14882
  },
14753
14883
  required: ["query"]
14754
14884
  },
14755
- description: "Fetches full schema definitions for deferred tools so they can be called.",
14885
+ description: `Fetches full schema definitions for deferred tools so they can be called.
14886
+
14887
+ ` + "Deferred tools appear by name in <system-reminder> messages. Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. " + 'Query forms: "select:Read,Edit,Grep" fetches these exact tools by name; "notebook jupyter" is a keyword search over the deferred tools.',
14756
14888
  exposure: "eager",
14757
14889
  permissionClass: "read",
14758
- availability: { requiresToolSearchEnabled: true },
14759
- capabilityRequirements: ["winter.mcp"],
14890
+ availability: { requiresToolSearchEnabled: true, requiresAnyCapability: ["winter.mcp", DEFERRED_BUILTINS_CAPABILITY] },
14891
+ capabilityRequirements: [],
14760
14892
  disposition: "implement-now"
14761
14893
  });
14762
14894
 
@@ -14993,7 +15125,7 @@ var WEB_SEARCH_INPUT_SCHEMA_FIRST_PARTY = {
14993
15125
  function webSearchInputSchemaFor(firstPartyAnthropic) {
14994
15126
  return firstPartyAnthropic ? WEB_SEARCH_INPUT_SCHEMA_FIRST_PARTY : WEB_SEARCH_INPUT_SCHEMA_PORTABLE;
14995
15127
  }
14996
- var descriptor = {
15128
+ var descriptor2 = {
14997
15129
  canonicalName: WEB_SEARCH_CANONICAL_NAME,
14998
15130
  advertisedName: WEB_SEARCH_CANONICAL_NAME,
14999
15131
  source: "builtin",
@@ -15007,14 +15139,14 @@ var descriptor = {
15007
15139
  capabilityRequirements: ["winter.search-backend"],
15008
15140
  disposition: "implement-now"
15009
15141
  };
15010
- Object.defineProperty(descriptor, "description", {
15142
+ Object.defineProperty(descriptor2, "description", {
15011
15143
  enumerable: true,
15012
15144
  configurable: true,
15013
15145
  get() {
15014
15146
  return webSearchDescription(false);
15015
15147
  }
15016
15148
  });
15017
- stub(descriptor);
15149
+ stub(descriptor2);
15018
15150
 
15019
15151
  // src/tools/descriptors/winter-list-agents.ts
15020
15152
  import { WINTER_BRAND as WINTER_BRAND13, mcpToolName } from "@yanlinglabs/winter-agent-sdk";
@@ -19083,6 +19215,7 @@ function computeExposurePartition(query) {
19083
19215
  ...query.platform !== undefined ? { platform: query.platform } : {},
19084
19216
  ...query.capabilities !== undefined ? { capabilities: query.capabilities } : {},
19085
19217
  ...query.disallowedTools !== undefined ? { disallowedTools: query.disallowedTools } : {},
19218
+ ...query.tools !== undefined ? { tools: query.tools } : {},
19086
19219
  ...query.insideSubagent !== undefined ? { insideSubagent: query.insideSubagent } : {},
19087
19220
  ...query.familyMetadata !== undefined ? { familyMetadata: query.familyMetadata } : {}
19088
19221
  };
@@ -19142,6 +19275,7 @@ function exposureQuery(deps) {
19142
19275
  ...deps.platform !== undefined ? { platform: deps.platform } : {},
19143
19276
  ...deps.capabilities !== undefined ? { capabilities: deps.capabilities } : {},
19144
19277
  ...deps.disallowedTools !== undefined ? { disallowedTools: deps.disallowedTools } : {},
19278
+ ...deps.tools !== undefined ? { tools: deps.tools } : {},
19145
19279
  ...deps.insideSubagent !== undefined ? { insideSubagent: deps.insideSubagent } : {},
19146
19280
  ...deps.familyMetadata !== undefined ? { familyMetadata: deps.familyMetadata } : {},
19147
19281
  ...deps.toolAliases !== undefined ? { toolAliases: deps.toolAliases } : {},
@@ -19175,7 +19309,9 @@ async function executeToolSearch(input, deps) {
19175
19309
  const query = rec.query;
19176
19310
  const maxResults = typeof rec.max_results === "number" && Number.isFinite(rec.max_results) && rec.max_results > 0 ? Math.floor(rec.max_results) : DEFAULT_MAX_RESULTS;
19177
19311
  const pendingWaitMs = deps.pendingWaitMs ?? DEFAULT_PENDING_WAIT_MS;
19178
- const selectNames = parseSelect(query);
19312
+ const resolveName = deps.resolveName ?? registeredNameForMcpSpelling;
19313
+ const parsedSelect = parseSelect(query);
19314
+ const selectNames = parsedSelect === undefined ? undefined : [...new Set(parsedSelect.map(resolveName))];
19179
19315
  let attempt = runAttempt(query, selectNames, maxResults, deps);
19180
19316
  const incomplete = selectNames !== undefined ? attempt.matches.length < selectNames.length : attempt.matches.length === 0;
19181
19317
  if (incomplete && deps.stateSource !== undefined) {
@@ -19215,7 +19351,7 @@ function getToolSearchSessionRuntime(sessionId) {
19215
19351
  }
19216
19352
 
19217
19353
  // src/tools/impl/tool-search.ts
19218
- var TOOL_SEARCH_TOOL_NAME = "ToolSearch";
19354
+ var TOOL_SEARCH_TOOL_NAME2 = "ToolSearch";
19219
19355
  function notWiredResult() {
19220
19356
  return {
19221
19357
  output: "Error: ToolSearch has no session runtime registered for this session (WS-09 §8) -- engine.ts must call registerToolSearchSessionRuntime(sessionId, ...) once per run (toolsearch/search.ts); this is a host wiring gap, not a model input error.",
@@ -19233,7 +19369,7 @@ var toolSearchExecutor = {
19233
19369
  return { output: JSON.stringify(outcome.result) };
19234
19370
  }
19235
19371
  };
19236
- replaceExecutor(TOOL_SEARCH_TOOL_NAME, toolSearchExecutor);
19372
+ replaceExecutor(TOOL_SEARCH_TOOL_NAME2, toolSearchExecutor);
19237
19373
 
19238
19374
  // src/tools/impl/wait-for-mcp-servers.ts
19239
19375
  var WAIT_FOR_MCP_SERVERS_TOOL_NAME = "WaitForMcpServers";
@@ -19845,6 +19981,9 @@ function inheritedWebSessionFacts(sessionId) {
19845
19981
  function searchBackendUsable(runtime) {
19846
19982
  return runtime.web.search.enabled;
19847
19983
  }
19984
+ function searchAnswerUsable(runtime) {
19985
+ return runtime.web.search.enabled && runtime.web.search.authRef !== undefined && runtime.resolveToolSecret !== undefined;
19986
+ }
19848
19987
  function digestModelResolves(runtime) {
19849
19988
  const tag = runtime.web.fetch.digestModel;
19850
19989
  if (tag === undefined)
@@ -20937,6 +21076,85 @@ async function runInnerModel(ctx, request, runtime) {
20937
21076
  }
20938
21077
  }
20939
21078
 
21079
+ // src/tools/impl/_site-icons.ts
21080
+ var SITE_ICONS_MAX = 10;
21081
+ var SITE_ICON_URL_MAX_LENGTH = 2048;
21082
+ var ICON_SCAN_CHARS = 256 * 1024;
21083
+ function siteIconHttpsUrl(value) {
21084
+ if (typeof value !== "string" || value.length === 0 || value.length > SITE_ICON_URL_MAX_LENGTH)
21085
+ return;
21086
+ let parsed;
21087
+ try {
21088
+ parsed = new URL(value.trim());
21089
+ } catch {
21090
+ return;
21091
+ }
21092
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || !isPublicName(parsed.hostname))
21093
+ return;
21094
+ const href = parsed.href;
21095
+ return href.length <= SITE_ICON_URL_MAX_LENGTH && /^https:\/\/[!-~]+$/.test(href) ? href : undefined;
21096
+ }
21097
+ var PRIVATE_NAME_SUFFIX = /\.(?:localhost|local|internal|lan|home|home\.arpa|corp|intranet|private)$/;
21098
+ function isPublicName(hostname) {
21099
+ const host = hostname.toLowerCase().replace(/\.$/, "");
21100
+ if (host.length === 0 || host.length > 253 || host.startsWith("[") || !host.includes("."))
21101
+ return false;
21102
+ if (host === "localhost" || PRIVATE_NAME_SUFFIX.test(host))
21103
+ return false;
21104
+ const labels = host.split(".");
21105
+ return /[a-z]/.test(labels[labels.length - 1]) && labels.every((l) => l.length > 0 && l.length <= 63);
21106
+ }
21107
+ function collectSiteIcons(entries) {
21108
+ const out = [];
21109
+ const seen = new Set;
21110
+ for (const e of entries) {
21111
+ if (out.length >= SITE_ICONS_MAX)
21112
+ break;
21113
+ const url = siteIconHttpsUrl(e.url);
21114
+ const iconUrl = siteIconHttpsUrl(e.iconUrl);
21115
+ if (url === undefined || iconUrl === undefined || seen.has(url))
21116
+ continue;
21117
+ seen.add(url);
21118
+ out.push({ url, iconUrl });
21119
+ }
21120
+ return out.length > 0 ? out : undefined;
21121
+ }
21122
+ var LINK_TAG = /<link\b[^>]*>/gi;
21123
+ var ATTRIBUTE = /([A-Za-z][\w:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
21124
+ function pageIconUrl(html, pageUrl) {
21125
+ const page = siteIconHttpsUrl(pageUrl);
21126
+ if (page === undefined)
21127
+ return;
21128
+ const head = html.length > ICON_SCAN_CHARS ? html.slice(0, ICON_SCAN_CHARS) : html;
21129
+ let best;
21130
+ for (const tag of head.matchAll(LINK_TAG)) {
21131
+ const attrs = {};
21132
+ for (const m of tag[0].matchAll(ATTRIBUTE))
21133
+ attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
21134
+ const rel = (attrs["rel"] ?? "").toLowerCase().split(/\s+/).filter((t) => t.length > 0);
21135
+ const href = decodeHtmlEntities(attrs["href"] ?? "").trim();
21136
+ const lowered = href.toLowerCase();
21137
+ if (href.length === 0 || rel.includes("mask-icon") || lowered.startsWith("data:") || /\.svg(?:[?#]|$)/.test(lowered))
21138
+ continue;
21139
+ const rank = rel.includes("apple-touch-icon") || rel.includes("apple-touch-icon-precomposed") ? 3 : rel.includes("icon") ? 2 : 0;
21140
+ if (rank === 0)
21141
+ continue;
21142
+ if (best === undefined || rank > best.rank) {
21143
+ const resolved = resolveIcon(href, page);
21144
+ if (resolved !== undefined)
21145
+ best = { rank, href: resolved };
21146
+ }
21147
+ }
21148
+ return best?.href ?? siteIconHttpsUrl(new URL("/favicon.ico", page).href);
21149
+ }
21150
+ function resolveIcon(href, page) {
21151
+ try {
21152
+ return siteIconHttpsUrl(new URL(href, page).href);
21153
+ } catch {
21154
+ return;
21155
+ }
21156
+ }
21157
+
20940
21158
  // src/tools/impl/web-fetch.ts
20941
21159
  var DIGEST_CONTENT_CAP = 1e5;
20942
21160
  var RESULT_CAP = 50000;
@@ -20993,6 +21211,10 @@ async function raceAgainstAbort(promise, signal) {
20993
21211
  });
20994
21212
  });
20995
21213
  }
21214
+ function siteIconsFor(pageUrl, iconUrl) {
21215
+ const siteIcons = iconUrl !== undefined ? collectSiteIcons([{ url: pageUrl, iconUrl }]) : undefined;
21216
+ return siteIcons !== undefined ? { siteIcons } : {};
21217
+ }
20996
21218
  function capResult(text) {
20997
21219
  if (text.length <= RESULT_CAP)
20998
21220
  return text;
@@ -21156,11 +21378,15 @@ function createWebFetchExecutor(deps = {}) {
21156
21378
  const preapproved = isPreapprovedUrl(originalUrl);
21157
21379
  let content;
21158
21380
  let contentType;
21381
+ let pageUrl;
21382
+ let iconUrl;
21383
+ let addressPublic;
21159
21384
  const cached = cache.get(ctx.sessionId, inputUrlString);
21160
21385
  if (cached !== undefined) {
21161
21386
  const addressVerdict = await raceAgainstAbort(classifyHostname(stripIpv6Brackets(originalUrl.hostname), resolveHost), ctx.signal);
21162
21387
  if (addressVerdict === "aborted")
21163
21388
  return { output: "WebFetch was interrupted.", isError: true };
21389
+ addressPublic = addressVerdict.class !== "private";
21164
21390
  if (addressVerdict.class === "private") {
21165
21391
  const refusal = privateAddressRefusal(originalUrl.hostname, policy, addressVerdict.reason);
21166
21392
  if (refusal !== undefined)
@@ -21173,6 +21399,7 @@ function createWebFetchExecutor(deps = {}) {
21173
21399
  const finalVerdict = await raceAgainstAbort(classifyHostname(stripIpv6Brackets(cachedHost), resolveHost), ctx.signal);
21174
21400
  if (finalVerdict === "aborted")
21175
21401
  return { output: "WebFetch was interrupted.", isError: true };
21402
+ addressPublic &&= finalVerdict.class !== "private";
21176
21403
  if (finalVerdict.class === "private") {
21177
21404
  const refusal = privateAddressRefusal(cachedHost, policy, finalVerdict.reason);
21178
21405
  if (refusal !== undefined)
@@ -21181,6 +21408,8 @@ function createWebFetchExecutor(deps = {}) {
21181
21408
  }
21182
21409
  content = cached.content;
21183
21410
  contentType = cached.contentType;
21411
+ pageUrl = cached.finalUrl;
21412
+ iconUrl = cached.iconUrl;
21184
21413
  } else {
21185
21414
  const outcome = await performWebFetch(inputUrlString, prompt, { blockedDomains, privateAddressPolicy: policy, userAgent: winterUserAgent(), ...ctx.signal !== undefined ? { signal: ctx.signal } : {} }, netDeps);
21186
21415
  switch (outcome.kind) {
@@ -21226,9 +21455,12 @@ The response body was not retrieved. If this URL requires authentication, use an
21226
21455
  }
21227
21456
  const kind = classifyContentType(outcome.contentType);
21228
21457
  if (kind === "html") {
21229
- content = await convertFetchedHtml(new TextDecoder("utf-8", { fatal: false }).decode(outcome.body));
21458
+ const html = new TextDecoder("utf-8", { fatal: false }).decode(outcome.body);
21459
+ iconUrl = pageIconUrl(html, outcome.finalUrl);
21460
+ content = await convertFetchedHtml(html);
21230
21461
  } else if (kind === "text") {
21231
21462
  content = new TextDecoder("utf-8", { fatal: false }).decode(outcome.body);
21463
+ iconUrl = pageIconUrl("", outcome.finalUrl);
21232
21464
  } else {
21233
21465
  const savedPath = await saveBinaryToTemp(ctx, originalUrl, outcome.body);
21234
21466
  const sizeLabel = `${outcome.body.byteLength.toLocaleString("en-US")} bytes`;
@@ -21236,14 +21468,18 @@ The response body was not retrieved. If this URL requires authentication, use an
21236
21468
  return { output: note };
21237
21469
  }
21238
21470
  contentType = outcome.contentType;
21239
- const entry = { bytes: Buffer.byteLength(content, "utf8"), code: outcome.status, codeText: outcome.statusText, content, contentType, finalUrl: outcome.finalUrl };
21471
+ pageUrl = outcome.finalUrl;
21472
+ addressPublic = policy !== "allow";
21473
+ const entry = { bytes: Buffer.byteLength(content, "utf8"), code: outcome.status, codeText: outcome.statusText, content, contentType, finalUrl: outcome.finalUrl, ...iconUrl !== undefined ? { iconUrl } : {} };
21240
21474
  cache.set(ctx.sessionId, inputUrlString, entry);
21241
21475
  }
21476
+ if (!addressPublic)
21477
+ iconUrl = undefined;
21242
21478
  if (preapproved && contentType.toLowerCase().includes("text/markdown") && content.length < 1e5) {
21243
- return { output: capResult(content) };
21479
+ return { output: capResult(content), ...siteIconsFor(pageUrl, iconUrl) };
21244
21480
  }
21245
21481
  const digest = await runDigest(ctx, runtime, content, prompt, preapproved);
21246
- return { output: capResult(digest.output), ...digest.isError ? { isError: true } : {} };
21482
+ return { output: capResult(digest.output), ...digest.isError ? { isError: true } : siteIconsFor(pageUrl, iconUrl) };
21247
21483
  }
21248
21484
  return {
21249
21485
  async execute(rawInput, ctx) {
@@ -21387,7 +21623,8 @@ function hitFromRecord(record) {
21387
21623
  const title = capText(typeof record["title"] === "string" && record["title"].trim().length > 0 ? record["title"].trim() : url, EXA_TITLE_MAX_CHARACTERS);
21388
21624
  const highlight = highlightFrom(record["highlights"]) || highlightFrom(record["summary"]) || highlightFrom(record["text"]);
21389
21625
  const published = typeof record["publishedDate"] === "string" && record["publishedDate"].length > 0 ? record["publishedDate"] : undefined;
21390
- return { title, url, highlight: capText(highlight.trim(), EXA_HIGHLIGHTS_MAX_CHARACTERS), ...published !== undefined ? { publishedDate: published } : {} };
21626
+ const favicon = siteIconHttpsUrl(record["favicon"]);
21627
+ return { title, url, highlight: capText(highlight.trim(), EXA_HIGHLIGHTS_MAX_CHARACTERS), ...published !== undefined ? { publishedDate: published } : {}, ...favicon !== undefined ? { favicon } : {} };
21391
21628
  }
21392
21629
  var FIELD_LINE = /^(Title|URL|Published(?: Date)?|Author|Highlights|Text|Summary|Content):[ \t]*(.*)$/i;
21393
21630
  function hitsFromPlainText(text) {
@@ -21946,6 +22183,7 @@ function createWebSearchExecutor(deps = {}) {
21946
22183
  return { output: `Error: web search failed unexpectedly (${err instanceof Error ? err.name : "unknown error"}).`, isError: true };
21947
22184
  }
21948
22185
  const events = [];
22186
+ const iconEntries = [];
21949
22187
  let successfulSearches = 0;
21950
22188
  let attemptedSearches = 0;
21951
22189
  let lastFailureMessage;
@@ -21970,6 +22208,9 @@ function createWebSearchExecutor(deps = {}) {
21970
22208
  }
21971
22209
  successfulSearches += 1;
21972
22210
  events.push({ type: "search_result", hits: outcome.hits.map((h) => ({ title: h.title, url: h.url })) });
22211
+ for (const h of outcome.hits)
22212
+ if (h.favicon !== undefined)
22213
+ iconEntries.push({ url: h.url, iconUrl: h.favicon });
21973
22214
  }
21974
22215
  if (pass !== undefined && !pass.ok) {
21975
22216
  events.push({ type: "text", text: `
@@ -21981,12 +22222,150 @@ ${innerFailureText(pass)}` });
21981
22222
 
21982
22223
  ${lastFailureMessage}` });
21983
22224
  }
21984
- return { output: assembleWebSearchOutputCapped(query, events, resultCap) };
22225
+ const siteIcons = collectSiteIcons(iconEntries);
22226
+ return { output: assembleWebSearchOutputCapped(query, events, resultCap), ...siteIcons !== undefined ? { siteIcons } : {} };
21985
22227
  }
21986
22228
  return { execute };
21987
22229
  }
21988
22230
  replaceExecutor(WEB_SEARCH_CANONICAL_NAME, createWebSearchExecutor());
21989
22231
 
22232
+ // src/tools/impl/search.ts
22233
+ var SEARCH_REQUEST_TIMEOUT_MS = 45000;
22234
+ var SEARCH_ANSWER_CHARS = 24000;
22235
+ var SEARCH_MAX_CITATIONS = 20;
22236
+ var SEARCH_TOTAL_OUTPUT_CHARS = 30000;
22237
+ var EXA_ANSWER_URL = "https://api.exa.ai/answer";
22238
+ function isValidCitations(value) {
22239
+ if (value === undefined)
22240
+ return true;
22241
+ if (!Array.isArray(value))
22242
+ return false;
22243
+ return value.every((item) => item !== null && typeof item === "object");
22244
+ }
22245
+ function searchStatusMessage(status) {
22246
+ switch (status) {
22247
+ case 401:
22248
+ case 403:
22249
+ return "search failed: the configured Exa API key was rejected — it needs to be replaced before Search can work";
22250
+ case 402:
22251
+ return "search failed: this Exa account is out of credits or over its budget — top it up at exa.ai, or answer from what you already know and say the search was unavailable";
22252
+ case 429:
22253
+ return "search failed: the search service is rate-limiting this key — wait a little before searching again, and do not retry in a loop";
22254
+ case 400:
22255
+ return "search failed: the search service rejected the request as malformed — try a plainer question";
22256
+ default:
22257
+ return `search failed: the search service is unavailable (HTTP ${status})`;
22258
+ }
22259
+ }
22260
+ var SEARCH_NO_KEY_MESSAGE = "Search needs an Exa API key (from exa.ai), and none is configured for this session";
22261
+ function errorResult6(output) {
22262
+ return { output, isError: true };
22263
+ }
22264
+ function createSearchExecutor(deps = {}) {
22265
+ const endpoint = deps.endpoint ?? EXA_ANSWER_URL;
22266
+ const timeoutMs = deps.timeoutMs ?? SEARCH_REQUEST_TIMEOUT_MS;
22267
+ async function execute(rawInput, ctx) {
22268
+ const record = typeof rawInput === "object" && rawInput !== null ? rawInput : {};
22269
+ const query = typeof record["query"] === "string" ? record["query"] : "";
22270
+ if (query.trim().length === 0)
22271
+ return errorResult6("Error: Missing query");
22272
+ const runtime = webSessionRuntimeFor(ctx);
22273
+ if (runtime === undefined)
22274
+ return errorResult6("Error: Search is not available for this session (no search runtime is wired up for it -- a host wiring gap, not an input error).");
22275
+ if (!runtime.web.search.enabled)
22276
+ return { output: "Web search is turned off for this session." };
22277
+ const resolveKey = exaKeyResolverFor(runtime);
22278
+ if (resolveKey === undefined)
22279
+ return errorResult6(SEARCH_NO_KEY_MESSAGE);
22280
+ let key;
22281
+ try {
22282
+ const secret = await resolveKey();
22283
+ if (secret.status === "missing")
22284
+ return errorResult6(SEARCH_NO_KEY_MESSAGE);
22285
+ if (secret.status === "unreadable")
22286
+ return errorResult6(`search failed: an Exa API key is configured but could not be used: ${secret.message}`);
22287
+ key = secret.key;
22288
+ } catch (err) {
22289
+ return errorResult6(`search failed: the key resolver failed with ${err instanceof Error ? err.name : "an unknown error"}`);
22290
+ }
22291
+ if (key.length === 0)
22292
+ return errorResult6(SEARCH_NO_KEY_MESSAGE);
22293
+ const fetchFn = deps.fetchFn ?? fetch;
22294
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
22295
+ const signal = ctx.signal !== undefined ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
22296
+ let res;
22297
+ try {
22298
+ res = await fetchFn(endpoint, {
22299
+ method: "POST",
22300
+ headers: { "x-api-key": key, "content-type": "application/json" },
22301
+ body: JSON.stringify({ query }),
22302
+ redirect: "manual",
22303
+ signal
22304
+ });
22305
+ } catch (err) {
22306
+ const name = err instanceof Error ? err.name : "";
22307
+ if (name === "AbortError" || name === "TimeoutError") {
22308
+ return errorResult6(ctx.signal?.aborted === true ? "Search was interrupted." : `search timed out for ${query}`);
22309
+ }
22310
+ const rawMessage = err instanceof Error ? err.message : String(err);
22311
+ console.error(`Search: network error (${name || "Error"}) — ${rawMessage.replaceAll(key, "<redacted>")}`);
22312
+ return errorResult6("search failed: could not reach the search service");
22313
+ }
22314
+ if (res.status !== 200) {
22315
+ try {
22316
+ await res.body?.cancel();
22317
+ } catch {}
22318
+ return errorResult6(searchStatusMessage(res.status));
22319
+ }
22320
+ let data;
22321
+ try {
22322
+ data = await res.json();
22323
+ } catch (err) {
22324
+ return errorResult6(`search failed: could not parse response (${err instanceof Error ? err.message : String(err)})`);
22325
+ }
22326
+ if (data === null || typeof data !== "object" || !isValidCitations(data.citations))
22327
+ return errorResult6("search failed: malformed response from search service");
22328
+ if (data.answer !== undefined && typeof data.answer !== "string")
22329
+ return errorResult6("search failed: malformed response from search service");
22330
+ const fullAnswer = (data.answer ?? "").trim();
22331
+ const answer = fullAnswer.length > SEARCH_ANSWER_CHARS ? fullAnswer.slice(0, SEARCH_ANSWER_CHARS) + `
22332
+
22333
+ [answer truncated]` : fullAnswer;
22334
+ const hostFloor = Array.isArray(record["blocked_domains"]) ? record["blocked_domains"].filter((d) => typeof d === "string" && d.trim().length > 0).slice(0, 1000) : [];
22335
+ const floor = hostFloor.length > 0 ? [...runtime.web.blockedDomains, ...hostFloor] : runtime.web.blockedDomains;
22336
+ const rawCitations = (data.citations ?? []).slice(0, SEARCH_MAX_CITATIONS);
22337
+ let withheld = 0;
22338
+ const citations = rawCitations.filter((c) => {
22339
+ if (typeof c.url !== "string" || c.url.length === 0 || !isDomainBlocked(c.url, floor))
22340
+ return true;
22341
+ withheld++;
22342
+ return false;
22343
+ });
22344
+ const withheldNote = withheld > 0 ? `
22345
+
22346
+ [${withheld} source${withheld === 1 ? "" : "s"} withheld — matched the dangerous-domain list]` : "";
22347
+ const siteIcons = collectSiteIcons(citations.map((c) => ({ url: c.url, iconUrl: c.favicon })));
22348
+ const withIcons = (output) => ({ output, ...siteIcons !== undefined ? { siteIcons } : {} });
22349
+ if (answer === "")
22350
+ return withIcons(`no answer for ${query}${withheldNote}`);
22351
+ const sources = citations.length === 0 ? `
22352
+
22353
+ [unsourced — ${withheld > 0 ? "every source was withheld by the dangerous-domain list" : "the search service returned no sources"}; say so if you repeat this]` : `
22354
+
22355
+ Sources:
22356
+ ` + citations.map((c, i) => `${i + 1}. ${(typeof c.title === "string" ? c.title.trim() : "") || "-"}
22357
+ ${typeof c.url === "string" && c.url.length > 0 ? c.url : "-"}`).join(`
22358
+ `);
22359
+ const rendered = answer + sources;
22360
+ const capped = rendered.length > SEARCH_TOTAL_OUTPUT_CHARS ? rendered.slice(0, SEARCH_TOTAL_OUTPUT_CHARS) + `
22361
+
22362
+ [truncated]` : rendered;
22363
+ return withIcons(capped + withheldNote);
22364
+ }
22365
+ return { execute };
22366
+ }
22367
+ replaceExecutor(SEARCH_CANONICAL_NAME, createSearchExecutor());
22368
+
21990
22369
  // src/tools/read-state.ts
21991
22370
  import { isAbsolute as isAbsolute6, join as join24, normalize as normalize3 } from "node:path";
21992
22371
  function createSessionReadState(opts) {
@@ -22207,6 +22586,18 @@ function toolResultsForHost(content) {
22207
22586
  };
22208
22587
  return blank(content) ?? content;
22209
22588
  }
22589
+ function withHostSiteIcons(content, icons) {
22590
+ if (icons.size === 0)
22591
+ return content;
22592
+ return content.map((block) => {
22593
+ if (block.type !== "tool_result")
22594
+ return block;
22595
+ const forCall = icons.get(block.tool_use_id);
22596
+ if (forCall === undefined || forCall.length === 0)
22597
+ return block;
22598
+ return { ...block, winter_site_icons: forCall.map((icon) => ({ url: icon.url, icon_url: icon.iconUrl })) };
22599
+ });
22600
+ }
22210
22601
  function asRebuiltContent(content) {
22211
22602
  return content.length === 1 && content[0].type === "text" ? content[0].text : content;
22212
22603
  }
@@ -22349,11 +22740,48 @@ function toolOutputReplacement(value) {
22349
22740
  return capHookText(String(value), cap);
22350
22741
  }
22351
22742
  }
22743
+ function withLegacyToolNameRules(opts) {
22744
+ const legacy = opts.config.legacyToolNames;
22745
+ if (legacy === undefined || Object.keys(legacy).length === 0)
22746
+ return opts;
22747
+ const expand = (rules) => {
22748
+ if (rules === undefined)
22749
+ return;
22750
+ const out = [...rules];
22751
+ for (const raw of rules) {
22752
+ const head = /^([^(]+)(.*)$/s.exec(raw.trim());
22753
+ if (head === null)
22754
+ continue;
22755
+ const target = Object.hasOwn(legacy, head[1]) ? legacy[head[1]] : undefined;
22756
+ if (typeof target === "string" && target.length > 0)
22757
+ out.push(`${target}${head[2]}`);
22758
+ }
22759
+ return [...new Set(out)];
22760
+ };
22761
+ const config = opts.config;
22762
+ const permissions = config.permissions;
22763
+ return {
22764
+ ...opts,
22765
+ config: {
22766
+ ...config,
22767
+ ...config.allowedTools !== undefined ? { allowedTools: expand(config.allowedTools) } : {},
22768
+ ...config.disallowedTools !== undefined ? { disallowedTools: expand(config.disallowedTools) } : {},
22769
+ ...permissions !== undefined ? {
22770
+ permissions: {
22771
+ ...permissions,
22772
+ ...permissions.allow !== undefined ? { allow: expand(permissions.allow) } : {},
22773
+ ...permissions.ask !== undefined ? { ask: expand(permissions.ask) } : {},
22774
+ ...permissions.deny !== undefined ? { deny: expand(permissions.deny) } : {}
22775
+ }
22776
+ } : {}
22777
+ }
22778
+ };
22779
+ }
22352
22780
  async function runEngine2(opts) {
22353
22781
  const facetDisposers = [];
22354
22782
  let sweepBackgroundShellTasksOnExit;
22355
22783
  try {
22356
- return await runEngineBody(opts, facetDisposers, (fn) => {
22784
+ return await runEngineBody(withLegacyToolNameRules(opts), facetDisposers, (fn) => {
22357
22785
  sweepBackgroundShellTasksOnExit = fn;
22358
22786
  });
22359
22787
  } finally {
@@ -22434,6 +22862,16 @@ async function runEngineBody(opts, facetDisposers, registerBackgroundShellSweep)
22434
22862
  resolveToolSecret: resolveToolSecretRoute,
22435
22863
  onPricedGeneration
22436
22864
  } = opts;
22865
+ const legacyToolNames = config.legacyToolNames ?? {};
22866
+ const currentToolName = (name) => {
22867
+ if (getRegisteredTool(name) !== undefined)
22868
+ return name;
22869
+ const legacy = Object.hasOwn(legacyToolNames, name) ? legacyToolNames[name] : undefined;
22870
+ if (typeof legacy === "string" && getRegisteredTool(legacy) !== undefined)
22871
+ return legacy;
22872
+ return name.startsWith("mcp__") ? registeredNameForMcpSpelling(name) : name;
22873
+ };
22874
+ const legacyNameOf = (name) => Object.entries(legacyToolNames).find(([old, current]) => current === name && getRegisteredTool(old) === undefined)?.[0];
22437
22875
  registerBackgroundShellSweep(() => {
22438
22876
  const swept = stopSessionShellTasks({ sessionId: config.sessionId, ...config.agentId !== undefined ? { agentId: config.agentId } : {} });
22439
22877
  if (swept.length > 0) {
@@ -23148,7 +23586,7 @@ ${escapeAttributionText(msg.body)}
23148
23586
  });
23149
23587
  return {
23150
23588
  policy: policyResult,
23151
- tools: req.definition?.tools !== undefined && !req.definition.tools.includes("*") ? req.definition.tools.filter((name) => currentAdvertisedCanonicalNames.includes(name)) : [...currentAdvertisedCanonicalNames],
23589
+ tools: req.definition?.tools !== undefined && !req.definition.tools.includes("*") ? [...new Set(req.definition.tools.map(currentToolName))].filter((name) => currentAdvertisedCanonicalNames.includes(name)) : [...currentAdvertisedCanonicalNames],
23152
23590
  ...childSlot,
23153
23591
  effort: req.definition?.effort !== undefined ? String(req.definition.effort) : "inherit",
23154
23592
  thinking: undefined,
@@ -23323,7 +23761,7 @@ ${escapeAttributionText(msg.body)}
23323
23761
  const denyEntry = findAgentDenyRule(evalCtx.policy.rules, agentType, denyOpts);
23324
23762
  if (denyEntry !== undefined)
23325
23763
  return agentTypeDeniedMessage(agentType, denyEntry);
23326
- if (isBuiltinAllToolsDenied(def, currentAdvertisedCanonicalNames)) {
23764
+ if (isBuiltinAllToolsDenied({ ...def.tools !== undefined ? { tools: def.tools.map(currentToolName) } : {} }, currentAdvertisedCanonicalNames)) {
23327
23765
  return `Agent type '${agentType}' is unavailable because every tool it may use is denied by the current permission settings.`;
23328
23766
  }
23329
23767
  return;
@@ -23343,11 +23781,12 @@ ${escapeAttributionText(msg.body)}
23343
23781
  if (serverCfg.type !== "sdk" || !serverCfg.tools || serverCfg.tools.length === 0)
23344
23782
  continue;
23345
23783
  const toolDefs = serverCfg.tools;
23346
- registerMcpServerTools(serverName, toolDefs, { deferredDefault: true });
23784
+ const toolNames = serverCfg.toolNames;
23785
+ registerMcpServerTools(serverName, toolDefs, { deferredDefault: true, ...toolNames !== undefined ? { toolNames } : {} });
23347
23786
  sdkMcpServerNames.push(serverName);
23348
23787
  const perServerTimeoutMs = serverCfg.timeout;
23349
23788
  for (const tool of toolDefs) {
23350
- const canonicalName = `mcp__${serverName}__${tool.name}`;
23789
+ const canonicalName = toolNames !== undefined && Object.hasOwn(toolNames, tool.name) ? toolNames[tool.name] : `mcp__${serverName}__${tool.name}`;
23351
23790
  replaceExecutor(canonicalName, {
23352
23791
  async execute(input) {
23353
23792
  const timeoutMs = perServerTimeoutMs ?? mcpEnvConfig.toolTimeoutMs ?? 120000;
@@ -23379,13 +23818,15 @@ ${escapeAttributionText(msg.body)}
23379
23818
  const resolvedSources = resolveMcpServerSources(sources, {
23380
23819
  ...config.strictMcpConfig !== undefined ? { strictMcpConfig: config.strictMcpConfig } : {},
23381
23820
  trustedWorkspace,
23382
- reservedServerName: sessionBrand.mcpServerName
23821
+ reservedServerName: sessionBrand.mcpServerName,
23822
+ ...config.reservedMcpServerNames !== undefined ? { hostReservedServerNames: config.reservedMcpServerNames } : {}
23383
23823
  });
23384
23824
  mcpLifecycle = createMcpLifecycle({
23385
23825
  servers: resolvedSources.resolved,
23386
23826
  envConfig: mcpEnvConfig,
23387
23827
  elicitationAsk: createElicitationAsker2(bridge),
23388
23828
  reservedServerName: sessionBrand.mcpServerName,
23829
+ ...config.reservedMcpServerNames !== undefined ? { hostReservedServerNames: config.reservedMcpServerNames } : {},
23389
23830
  cwd: config.cwd,
23390
23831
  ...mcpOAuthStore !== undefined ? { oauth: createSessionMcpOAuth({ store: mcpOAuthStore, sender: bridge, brand: sessionBrand, ...config.hostCredentials === true ? { hostOwnsRefresh: true } : {} }) } : {}
23391
23832
  });
@@ -23395,11 +23836,13 @@ ${escapeAttributionText(msg.body)}
23395
23836
  const effectiveMcpStateSource = mcpServerStateSource ?? mcpLifecycle?.stateSource;
23396
23837
  const effectiveMcpControlSeam = mcpControlSeam ?? mcpLifecycle?.controlSeam;
23397
23838
  const sessionHasMcp = () => (effectiveMcpStateSource?.snapshot().length ?? 0) > 0;
23398
- const enableToolSearch = config.toolSearchEnabled === true ? "true" : config.toolSearchEnabled === false ? "false" : mcpEnvConfig.enableToolSearch;
23839
+ const toolSearchExcludedByTools = config.tools !== undefined && !config.tools.includes(TOOL_SEARCH_TOOL_NAME);
23840
+ const enableToolSearch = toolSearchExcludedByTools ? "false" : config.toolSearchEnabled === true ? "true" : config.toolSearchEnabled === false ? "false" : mcpEnvConfig.enableToolSearch;
23399
23841
  const deferralActivation = {
23400
23842
  enableToolSearch,
23401
23843
  providerSupportsToolSearch: providerSupportsToolSearch ?? true,
23402
- deferrableContextShare: deferrableContextShare ?? 0
23844
+ deferrableContextShare: deferrableContextShare ?? 0,
23845
+ ...config.deferTools !== undefined ? { deferTools: config.deferTools } : {}
23403
23846
  };
23404
23847
  const toolSearchEnabledDerived = isDeferralActive(deferralActivation);
23405
23848
  const enableFileCheckpointing = config.enableFileCheckpointing === true;
@@ -23432,7 +23875,7 @@ ${escapeAttributionText(msg.body)}
23432
23875
  return availableAgentNames(defs, {
23433
23876
  isDenied: (agentType) => findAgentDenyRule(evalCtx.policy.rules, agentType, denyOpts) !== undefined,
23434
23877
  ...config.allowedAgentTypes !== undefined ? { allowedAgentTypes: config.allowedAgentTypes } : {},
23435
- isAllToolsDenied: (def) => isBuiltinAllToolsDenied(def, currentAdvertisedCanonicalNames)
23878
+ isAllToolsDenied: (def) => isBuiltinAllToolsDenied({ ...def.tools !== undefined ? { tools: def.tools.map(currentToolName) } : {} }, currentAdvertisedCanonicalNames)
23436
23879
  });
23437
23880
  }
23438
23881
  function sessionAvailableAgentDefinitions(at = {}) {
@@ -23503,11 +23946,14 @@ ${escapeAttributionText(msg.body)}
23503
23946
  facetDisposers.push(forgetSessionWebState);
23504
23947
  const WEB_DERIVED_CAPABILITIES = [
23505
23948
  { token: "winter.search-backend", probeTool: "WebSearch", fact: () => searchBackendUsable(webSessionRuntime) },
23506
- { token: "winter.fetch-extractor", probeTool: "WebFetch", fact: () => digestModelResolves(webSessionRuntime) }
23949
+ { token: "winter.fetch-extractor", probeTool: "WebFetch", fact: () => digestModelResolves(webSessionRuntime) },
23950
+ { token: SEARCH_ANSWER_CAPABILITY, probeTool: SEARCH_CANONICAL_NAME, fact: () => searchAnswerUsable(webSessionRuntime) }
23507
23951
  ];
23508
23952
  const resolveLiveSessionCapabilities = () => {
23509
23953
  const base = resolveSessionCapabilities(config.capabilities, { hasMcpServers: sessionHasMcp() });
23510
23954
  const derived = reviewerResolves() ? [ADVISOR_CAPABILITY] : [];
23955
+ if ((config.deferTools?.length ?? 0) > 0)
23956
+ derived.push(DEFERRED_BUILTINS_CAPABILITY);
23511
23957
  for (const cap of WEB_DERIVED_CAPABILITIES) {
23512
23958
  if (getRegisteredTool(cap.probeTool)?.executor !== undefined && cap.fact())
23513
23959
  derived.push(cap.token);
@@ -23521,6 +23967,7 @@ ${escapeAttributionText(msg.body)}
23521
23967
  mode: policyStateStore.getState().mode,
23522
23968
  platform: process.platform,
23523
23969
  ...config.disallowedTools !== undefined ? { disallowedTools: config.disallowedTools } : {},
23970
+ ...config.tools !== undefined ? { tools: config.tools } : {},
23524
23971
  capabilities: sessionCapabilities,
23525
23972
  toolSearchEnabled: toolSearchEnabledDerived,
23526
23973
  ...config.insideSubagent !== undefined ? { insideSubagent: config.insideSubagent } : {},
@@ -23606,6 +24053,8 @@ ${escapeAttributionText(msg.body)}
23606
24053
  return visibleServerToolFilter();
23607
24054
  },
23608
24055
  ...config.disallowedTools !== undefined ? { disallowedTools: config.disallowedTools } : {},
24056
+ ...config.tools !== undefined ? { tools: config.tools } : {},
24057
+ resolveName: currentToolName,
23609
24058
  ...config.insideSubagent !== undefined ? { insideSubagent: config.insideSubagent } : {},
23610
24059
  ...config.familyMetadata !== undefined ? { familyMetadata: config.familyMetadata } : {},
23611
24060
  ...config.toolAliases !== undefined ? { toolAliases: config.toolAliases } : {},
@@ -24638,6 +25087,12 @@ ${escapeAttributionText(msg.body)}
24638
25087
  return { system: joinSystemBlocks(systemBlocks), systemBlocks };
24639
25088
  };
24640
25089
  const requestMessages = (context, effort, toolChanges) => buildRequestMessages(messages, context.userContextText, { systemReminders: systemRemindersOnWire(), ...effort !== undefined ? { effort } : {}, ...toolChanges !== undefined ? { toolChanges } : {} });
25090
+ const deferredToolNamesToAnnounce = () => {
25091
+ refreshAdvertisedPartition();
25092
+ const eagerNames = new Set(advertisedPartition.eager.map((d) => d.canonicalName));
25093
+ const twins = new Set(Object.entries(suppressionAliasTable).filter(([source]) => eagerNames.has(source)).map(([, target]) => target));
25094
+ return [...new Set(advertisedPartition.deferred.filter((d) => !twins.has(d.canonicalName)).map((d) => d.advertisedName))];
25095
+ };
24641
25096
  const systemRemindersOnWire = () => currentModelDescription()?.wire?.midConversationSystem === true;
24642
25097
  const currentModelDescription = () => {
24643
25098
  const key = currentProviderIdentity?.modelKey ?? currentModel;
@@ -24793,6 +25248,13 @@ ${escapeAttributionText(msg.body)}
24793
25248
  produced.push(needsAuth);
24794
25249
  }
24795
25250
  }
25251
+ if (exactRequestLayout === undefined) {
25252
+ const deferredNow = deferredToolNamesToAnnounce();
25253
+ const offeredNow = [...advertisedPartition.eager, ...advertisedPartition.deferred].map((d) => d.advertisedName);
25254
+ const deferredDelta = computeDeferredToolsDelta(deferredNow, offeredNow, messages);
25255
+ if (deferredDelta !== undefined)
25256
+ produced.push(deferredDelta);
25257
+ }
24796
25258
  if (phase === "tool-round") {
24797
25259
  const attachment = taskNotificationAttachment(notifications.drainFor(config.agentId, { maxPriority: "next" }), {
24798
25260
  inHumanTurn: config.agentId === undefined && !turnStartedByNotification
@@ -25031,15 +25493,19 @@ ${escapeAttributionText(msg.body)}
25031
25493
  return cached ? { name: identity.name, configName: identity.configName } : identity;
25032
25494
  };
25033
25495
  const declaredMcpIdentity = (toolName) => {
25034
- if (mcpServerRenames === undefined)
25035
- return;
25036
- for (const [actual, declared] of Object.entries(mcpServerRenames)) {
25037
- if (toolName === `mcp__${actual}`)
25038
- return `mcp__${declared}`;
25039
- if (toolName.startsWith(`mcp__${actual}__`))
25040
- return `mcp__${declared}__${toolName.slice(`mcp__${actual}__`.length)}`;
25496
+ const legacy = legacyNameOf(toolName);
25497
+ if (legacy !== undefined)
25498
+ return legacy;
25499
+ const spelled = mcpSpellingOf(toolName) ?? toolName;
25500
+ if (mcpServerRenames !== undefined) {
25501
+ for (const [actual, declared] of Object.entries(mcpServerRenames)) {
25502
+ if (spelled === `mcp__${actual}`)
25503
+ return `mcp__${declared}`;
25504
+ if (spelled.startsWith(`mcp__${actual}__`))
25505
+ return `mcp__${declared}__${spelled.slice(`mcp__${actual}__`.length)}`;
25506
+ }
25041
25507
  }
25042
- return;
25508
+ return spelled !== toolName ? spelled : undefined;
25043
25509
  };
25044
25510
  const postToolHookSubject = (event, toolName) => {
25045
25511
  const declared = declaredMcpIdentity(toolName);
@@ -25612,10 +26078,13 @@ ${reason}` : ""}` : `${promptStop.hookName} hook stopped continuation${reason.le
25612
26078
  ...generationEffort ?? {}
25613
26079
  });
25614
26080
  const resultBlocks = [];
26081
+ const hostSiteIcons = new Map;
25615
26082
  let toolThrowText = null;
25616
26083
  let structuredTerminated = false;
25617
26084
  const truncatedByOutputLimit = turn.stopReason === "max_tokens";
25618
- for (const call of turn.calls) {
26085
+ for (const emitted of turn.calls) {
26086
+ const resolvedName = currentToolName(emitted.name);
26087
+ const call = resolvedName !== emitted.name ? { ...emitted, name: resolvedName } : emitted;
25619
26088
  const stoppedBy = currentTurnStop();
25620
26089
  if (stoppedBy !== undefined) {
25621
26090
  resultBlocks.push({ type: "tool_result", tool_use_id: call.id, content: `[not executed: the ${stoppedBy.hookName} hook stopped the turn]`, error: true });
@@ -25847,6 +26316,8 @@ ${reason}` : ""}` : `${promptStop.hookName} hook stopped continuation${reason.le
25847
26316
  ...loadedTools.length > 0 ? { loadedTools } : {},
25848
26317
  ...loadedToolDefinitions.length > 0 ? { loadedToolDefinitions } : {}
25849
26318
  });
26319
+ if (raced.value.siteIcons !== undefined && raced.value.siteIcons.length > 0)
26320
+ hostSiteIcons.set(call.id, raced.value.siteIcons);
25850
26321
  const postToolUseHookOutcome = raced.value.isError === true ? await fireObservationalHook("PostToolUseFailure", {
25851
26322
  toolUseID: call.id,
25852
26323
  toolName: postToolHookSubject("PostToolUseFailure", call.name),
@@ -25908,7 +26379,7 @@ ${reason}` : ""}` : `${promptStop.hookName} hook stopped continuation${reason.le
25908
26379
  }
25909
26380
  }
25910
26381
  }
25911
- output.write({ type: "data", message: { type: "user", message: { content: toolResultsForHost(resultBlocks.map((b) => b.type === "tool_result" && b.loadedTools !== undefined ? withoutLoadedTools(b) : b)) } } });
26382
+ output.write({ type: "data", message: { type: "user", message: { content: withHostSiteIcons(toolResultsForHost(resultBlocks.map((b) => b.type === "tool_result" && b.loadedTools !== undefined ? withoutLoadedTools(b) : b)), hostSiteIcons) } } });
25912
26383
  messages.push({ role: "tool", content: resultBlocks });
25913
26384
  await recordUser(resultBlocks);
25914
26385
  await flushPendingHookAttachments();
@@ -26168,8 +26639,43 @@ var TEST_PROVIDER_NAMES = new Set([
26168
26639
  "p5skill",
26169
26640
  "p5checkpoint",
26170
26641
  "p5workflow",
26171
- "readimage"
26642
+ "readimage",
26643
+ "calls"
26172
26644
  ]);
26645
+ var CALL_LINE = /^CALL\s+(\S+)(?:\s+(.*))?$/;
26646
+ function promptedCallsProvider() {
26647
+ return {
26648
+ async generate({ messages }) {
26649
+ let at = -1;
26650
+ for (let i = messages.length - 1;i >= 0; i--) {
26651
+ const m = messages[i];
26652
+ if (m.role === "user" && userMessageText(m).split(`
26653
+ `).some((line) => CALL_LINE.test(line.trim()))) {
26654
+ at = i;
26655
+ break;
26656
+ }
26657
+ }
26658
+ if (at < 0)
26659
+ return { kind: "text", text: "[]" };
26660
+ const script = userMessageText(messages[at]).split(`
26661
+ `).map((line) => CALL_LINE.exec(line.trim())).filter((m) => m !== null).map((m) => ({ name: m[1], input: m[2] !== undefined && m[2].trim() !== "" ? JSON.parse(m[2]) : {} }));
26662
+ const after = messages.slice(at + 1);
26663
+ const made = after.filter((m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some((b) => b.type === "tool_use")).length;
26664
+ if (made < script.length) {
26665
+ const next = script[made];
26666
+ return { kind: "tool_use", calls: [{ id: `calls-${at + 1}-${made + 1}`, name: next.name, input: next.input }] };
26667
+ }
26668
+ const results = after.flatMap((m) => Array.isArray(m.content) ? m.content : []).filter((b) => b.type === "tool_result");
26669
+ const named = new Map(after.flatMap((m) => m.role === "assistant" && Array.isArray(m.content) ? m.content : []).flatMap((b) => b.type === "tool_use" ? [[b.id, b.name]] : []));
26670
+ const text = (content) => typeof content === "string" ? content : Array.isArray(content) ? content.map((b) => b !== null && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
26671
+ `) : String(content);
26672
+ return {
26673
+ kind: "text",
26674
+ text: JSON.stringify(results.map((r) => ({ name: named.get(r.tool_use_id) ?? null, isError: r.is_error === true || r.error === true, content: text(r.content) })))
26675
+ };
26676
+ }
26677
+ };
26678
+ }
26173
26679
  function isTestProviderName2(v) {
26174
26680
  return TEST_PROVIDER_NAMES.has(v);
26175
26681
  }
@@ -26334,6 +26840,8 @@ function rawTestProviderByName(name) {
26334
26840
  }
26335
26841
  };
26336
26842
  }
26843
+ case "calls":
26844
+ return promptedCallsProvider();
26337
26845
  case "mcpsdk":
26338
26846
  return scriptedProvider2([
26339
26847
  { kind: "tool_use", calls: [{ id: "mcpsdk-call-1", name: MCP_SDK_TEST_TOOL_NAME2, input: { x: 1 } }] },
@@ -27090,7 +27598,7 @@ class TranscriptWriter2 {
27090
27598
  return entries;
27091
27599
  }
27092
27600
  }
27093
- var RUNTIME_ENGINE_VERSION2 = "0.0.36";
27601
+ var RUNTIME_ENGINE_VERSION2 = "0.0.38";
27094
27602
  function withPermissionJournal(writer, location) {
27095
27603
  return {
27096
27604
  transcriptPath: join26(location.winterHome, "projects", location.projectKey, `${location.sessionId}.jsonl`),