init claude-code
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
import { z } from 'zod/v4'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
buildTool,
|
||||
findToolByName,
|
||||
type Tool,
|
||||
type ToolDef,
|
||||
type Tools,
|
||||
} from '../../Tool.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { escapeRegExp } from '../../utils/stringUtils.js'
|
||||
import { isToolSearchEnabledOptimistic } from '../../utils/toolSearch.js'
|
||||
import { getPrompt, isDeferredTool, TOOL_SEARCH_TOOL_NAME } from './prompt.js'
|
||||
|
||||
export const inputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.',
|
||||
),
|
||||
max_results: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(5)
|
||||
.describe('Maximum number of results to return (default: 5)'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
export const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
matches: z.array(z.string()),
|
||||
query: z.string(),
|
||||
total_deferred_tools: z.number(),
|
||||
pending_mcp_servers: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
// Track deferred tool names to detect when cache should be cleared
|
||||
let cachedDeferredToolNames: string | null = null
|
||||
|
||||
/**
|
||||
* Get a cache key representing the current set of deferred tools.
|
||||
*/
|
||||
function getDeferredToolsCacheKey(deferredTools: Tools): string {
|
||||
return deferredTools
|
||||
.map(t => t.name)
|
||||
.sort()
|
||||
.join(',')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool description, memoized by tool name.
|
||||
* Used for keyword search scoring.
|
||||
*/
|
||||
const getToolDescriptionMemoized = memoize(
|
||||
async (toolName: string, tools: Tools): Promise<string> => {
|
||||
const tool = findToolByName(tools, toolName)
|
||||
if (!tool) {
|
||||
return ''
|
||||
}
|
||||
return tool.prompt({
|
||||
getToolPermissionContext: async () => ({
|
||||
mode: 'default' as const,
|
||||
additionalWorkingDirectories: new Map(),
|
||||
alwaysAllowRules: {},
|
||||
alwaysDenyRules: {},
|
||||
alwaysAskRules: {},
|
||||
isBypassPermissionsModeAvailable: false,
|
||||
}),
|
||||
tools,
|
||||
agents: [],
|
||||
})
|
||||
},
|
||||
(toolName: string) => toolName,
|
||||
)
|
||||
|
||||
/**
|
||||
* Invalidate the description cache if deferred tools have changed.
|
||||
*/
|
||||
function maybeInvalidateCache(deferredTools: Tools): void {
|
||||
const currentKey = getDeferredToolsCacheKey(deferredTools)
|
||||
if (cachedDeferredToolNames !== currentKey) {
|
||||
logForDebugging(
|
||||
`ToolSearchTool: cache invalidated - deferred tools changed`,
|
||||
)
|
||||
getToolDescriptionMemoized.cache.clear?.()
|
||||
cachedDeferredToolNames = currentKey
|
||||
}
|
||||
}
|
||||
|
||||
export function clearToolSearchDescriptionCache(): void {
|
||||
getToolDescriptionMemoized.cache.clear?.()
|
||||
cachedDeferredToolNames = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the search result output structure.
|
||||
*/
|
||||
function buildSearchResult(
|
||||
matches: string[],
|
||||
query: string,
|
||||
totalDeferredTools: number,
|
||||
pendingMcpServers?: string[],
|
||||
): { data: Output } {
|
||||
return {
|
||||
data: {
|
||||
matches,
|
||||
query,
|
||||
total_deferred_tools: totalDeferredTools,
|
||||
...(pendingMcpServers && pendingMcpServers.length > 0
|
||||
? { pending_mcp_servers: pendingMcpServers }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tool name into searchable parts.
|
||||
* Handles both MCP tools (mcp__server__action) and regular tools (CamelCase).
|
||||
*/
|
||||
function parseToolName(name: string): {
|
||||
parts: string[]
|
||||
full: string
|
||||
isMcp: boolean
|
||||
} {
|
||||
// Check if it's an MCP tool
|
||||
if (name.startsWith('mcp__')) {
|
||||
const withoutPrefix = name.replace(/^mcp__/, '').toLowerCase()
|
||||
const parts = withoutPrefix.split('__').flatMap(p => p.split('_'))
|
||||
return {
|
||||
parts: parts.filter(Boolean),
|
||||
full: withoutPrefix.replace(/__/g, ' ').replace(/_/g, ' '),
|
||||
isMcp: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Regular tool - split by CamelCase and underscores
|
||||
const parts = name
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2') // CamelCase to spaces
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
|
||||
return {
|
||||
parts,
|
||||
full: parts.join(' '),
|
||||
isMcp: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compile word-boundary regexes for all search terms.
|
||||
* Called once per search instead of tools×terms×2 times.
|
||||
*/
|
||||
function compileTermPatterns(terms: string[]): Map<string, RegExp> {
|
||||
const patterns = new Map<string, RegExp>()
|
||||
for (const term of terms) {
|
||||
if (!patterns.has(term)) {
|
||||
patterns.set(term, new RegExp(`\\b${escapeRegExp(term)}\\b`))
|
||||
}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyword-based search over tool names and descriptions.
|
||||
* Handles both MCP tools (mcp__server__action) and regular tools (CamelCase).
|
||||
*
|
||||
* The model typically queries with:
|
||||
* - Server names when it knows the integration (e.g., "slack", "github")
|
||||
* - Action words when looking for functionality (e.g., "read", "list", "create")
|
||||
* - Tool-specific terms (e.g., "notebook", "shell", "kill")
|
||||
*/
|
||||
async function searchToolsWithKeywords(
|
||||
query: string,
|
||||
deferredTools: Tools,
|
||||
tools: Tools,
|
||||
maxResults: number,
|
||||
): Promise<string[]> {
|
||||
const queryLower = query.toLowerCase().trim()
|
||||
|
||||
// Fast path: if query matches a tool name exactly, return it directly.
|
||||
// Handles models using a bare tool name instead of select: prefix (seen
|
||||
// from subagents/post-compaction). Checks deferred first, then falls back
|
||||
// to the full tool set — selecting an already-loaded tool is a harmless
|
||||
// no-op that lets the model proceed without retry churn.
|
||||
const exactMatch =
|
||||
deferredTools.find(t => t.name.toLowerCase() === queryLower) ??
|
||||
tools.find(t => t.name.toLowerCase() === queryLower)
|
||||
if (exactMatch) {
|
||||
return [exactMatch.name]
|
||||
}
|
||||
|
||||
// If query looks like an MCP tool prefix (mcp__server), find matching tools.
|
||||
// Handles models searching by server name with mcp__ prefix.
|
||||
if (queryLower.startsWith('mcp__') && queryLower.length > 5) {
|
||||
const prefixMatches = deferredTools
|
||||
.filter(t => t.name.toLowerCase().startsWith(queryLower))
|
||||
.slice(0, maxResults)
|
||||
.map(t => t.name)
|
||||
if (prefixMatches.length > 0) {
|
||||
return prefixMatches
|
||||
}
|
||||
}
|
||||
|
||||
const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0)
|
||||
|
||||
// Partition into required (+prefixed) and optional terms
|
||||
const requiredTerms: string[] = []
|
||||
const optionalTerms: string[] = []
|
||||
for (const term of queryTerms) {
|
||||
if (term.startsWith('+') && term.length > 1) {
|
||||
requiredTerms.push(term.slice(1))
|
||||
} else {
|
||||
optionalTerms.push(term)
|
||||
}
|
||||
}
|
||||
|
||||
const allScoringTerms =
|
||||
requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms
|
||||
const termPatterns = compileTermPatterns(allScoringTerms)
|
||||
|
||||
// Pre-filter to tools matching ALL required terms in name or description
|
||||
let candidateTools = deferredTools
|
||||
if (requiredTerms.length > 0) {
|
||||
const matches = await Promise.all(
|
||||
deferredTools.map(async tool => {
|
||||
const parsed = parseToolName(tool.name)
|
||||
const description = await getToolDescriptionMemoized(tool.name, tools)
|
||||
const descNormalized = description.toLowerCase()
|
||||
const hintNormalized = tool.searchHint?.toLowerCase() ?? ''
|
||||
const matchesAll = requiredTerms.every(term => {
|
||||
const pattern = termPatterns.get(term)!
|
||||
return (
|
||||
parsed.parts.includes(term) ||
|
||||
parsed.parts.some(part => part.includes(term)) ||
|
||||
pattern.test(descNormalized) ||
|
||||
(hintNormalized && pattern.test(hintNormalized))
|
||||
)
|
||||
})
|
||||
return matchesAll ? tool : null
|
||||
}),
|
||||
)
|
||||
candidateTools = matches.filter((t): t is Tool => t !== null)
|
||||
}
|
||||
|
||||
const scored = await Promise.all(
|
||||
candidateTools.map(async tool => {
|
||||
const parsed = parseToolName(tool.name)
|
||||
const description = await getToolDescriptionMemoized(tool.name, tools)
|
||||
const descNormalized = description.toLowerCase()
|
||||
const hintNormalized = tool.searchHint?.toLowerCase() ?? ''
|
||||
|
||||
let score = 0
|
||||
for (const term of allScoringTerms) {
|
||||
const pattern = termPatterns.get(term)!
|
||||
|
||||
// Exact part match (high weight for MCP server names, tool name parts)
|
||||
if (parsed.parts.includes(term)) {
|
||||
score += parsed.isMcp ? 12 : 10
|
||||
} else if (parsed.parts.some(part => part.includes(term))) {
|
||||
score += parsed.isMcp ? 6 : 5
|
||||
}
|
||||
|
||||
// Full name fallback (for edge cases)
|
||||
if (parsed.full.includes(term) && score === 0) {
|
||||
score += 3
|
||||
}
|
||||
|
||||
// searchHint match — curated capability phrase, higher signal than prompt
|
||||
if (hintNormalized && pattern.test(hintNormalized)) {
|
||||
score += 4
|
||||
}
|
||||
|
||||
// Description match - use word boundary to avoid false positives
|
||||
if (pattern.test(descNormalized)) {
|
||||
score += 2
|
||||
}
|
||||
}
|
||||
|
||||
return { name: tool.name, score }
|
||||
}),
|
||||
)
|
||||
|
||||
return scored
|
||||
.filter(item => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, maxResults)
|
||||
.map(item => item.name)
|
||||
}
|
||||
|
||||
export const ToolSearchTool = buildTool({
|
||||
isEnabled() {
|
||||
return isToolSearchEnabledOptimistic()
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
name: TOOL_SEARCH_TOOL_NAME,
|
||||
maxResultSizeChars: 100_000,
|
||||
async description() {
|
||||
return getPrompt()
|
||||
},
|
||||
async prompt() {
|
||||
return getPrompt()
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
async call(input, { options: { tools }, getAppState }) {
|
||||
const { query, max_results = 5 } = input
|
||||
|
||||
const deferredTools = tools.filter(isDeferredTool)
|
||||
maybeInvalidateCache(deferredTools)
|
||||
|
||||
// Check for MCP servers still connecting
|
||||
function getPendingServerNames(): string[] | undefined {
|
||||
const appState = getAppState()
|
||||
const pending = appState.mcp.clients.filter(c => c.type === 'pending')
|
||||
return pending.length > 0 ? pending.map(s => s.name) : undefined
|
||||
}
|
||||
|
||||
// Helper to log search outcome
|
||||
function logSearchOutcome(
|
||||
matches: string[],
|
||||
queryType: 'select' | 'keyword',
|
||||
): void {
|
||||
logEvent('tengu_tool_search_outcome', {
|
||||
query:
|
||||
query as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
queryType:
|
||||
queryType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
matchCount: matches.length,
|
||||
totalDeferredTools: deferredTools.length,
|
||||
maxResults: max_results,
|
||||
hasMatches: matches.length > 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for select: prefix — direct tool selection.
|
||||
// Supports comma-separated multi-select: `select:A,B,C`.
|
||||
// If a name isn't in the deferred set but IS in the full tool set,
|
||||
// we still return it — the tool is already loaded, so "selecting" it
|
||||
// is a harmless no-op that lets the model proceed without retry churn.
|
||||
const selectMatch = query.match(/^select:(.+)$/i)
|
||||
if (selectMatch) {
|
||||
const requested = selectMatch[1]!
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const found: string[] = []
|
||||
const missing: string[] = []
|
||||
for (const toolName of requested) {
|
||||
const tool =
|
||||
findToolByName(deferredTools, toolName) ??
|
||||
findToolByName(tools, toolName)
|
||||
if (tool) {
|
||||
if (!found.includes(tool.name)) found.push(tool.name)
|
||||
} else {
|
||||
missing.push(toolName)
|
||||
}
|
||||
}
|
||||
|
||||
if (found.length === 0) {
|
||||
logForDebugging(
|
||||
`ToolSearchTool: select failed — none found: ${missing.join(', ')}`,
|
||||
)
|
||||
logSearchOutcome([], 'select')
|
||||
const pendingServers = getPendingServerNames()
|
||||
return buildSearchResult(
|
||||
[],
|
||||
query,
|
||||
deferredTools.length,
|
||||
pendingServers,
|
||||
)
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
logForDebugging(
|
||||
`ToolSearchTool: partial select — found: ${found.join(', ')}, missing: ${missing.join(', ')}`,
|
||||
)
|
||||
} else {
|
||||
logForDebugging(`ToolSearchTool: selected ${found.join(', ')}`)
|
||||
}
|
||||
logSearchOutcome(found, 'select')
|
||||
return buildSearchResult(found, query, deferredTools.length)
|
||||
}
|
||||
|
||||
// Keyword search
|
||||
const matches = await searchToolsWithKeywords(
|
||||
query,
|
||||
deferredTools,
|
||||
tools,
|
||||
max_results,
|
||||
)
|
||||
|
||||
logForDebugging(
|
||||
`ToolSearchTool: keyword search for "${query}", found ${matches.length} matches`,
|
||||
)
|
||||
|
||||
logSearchOutcome(matches, 'keyword')
|
||||
|
||||
// Include pending server info when search finds no matches
|
||||
if (matches.length === 0) {
|
||||
const pendingServers = getPendingServerNames()
|
||||
return buildSearchResult(
|
||||
matches,
|
||||
query,
|
||||
deferredTools.length,
|
||||
pendingServers,
|
||||
)
|
||||
}
|
||||
|
||||
return buildSearchResult(matches, query, deferredTools.length)
|
||||
},
|
||||
renderToolUseMessage() {
|
||||
return null
|
||||
},
|
||||
userFacingName: () => '',
|
||||
/**
|
||||
* Returns a tool_result with tool_reference blocks.
|
||||
* This format works on 1P/Foundry. Bedrock/Vertex may not support
|
||||
* client-side tool_reference expansion yet.
|
||||
*/
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: Output,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
if (content.matches.length === 0) {
|
||||
let text = 'No matching deferred tools found'
|
||||
if (
|
||||
content.pending_mcp_servers &&
|
||||
content.pending_mcp_servers.length > 0
|
||||
) {
|
||||
text += `. Some MCP servers are still connecting: ${content.pending_mcp_servers.join(', ')}. Their tools will become available shortly — try searching again.`
|
||||
}
|
||||
return {
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseID,
|
||||
content: text,
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseID,
|
||||
content: content.matches.map(name => ({
|
||||
type: 'tool_reference' as const,
|
||||
tool_name: name,
|
||||
})),
|
||||
} as unknown as ToolResultBlockParam
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
@@ -0,0 +1 @@
|
||||
export const TOOL_SEARCH_TOOL_NAME = 'ToolSearch'
|
||||
@@ -0,0 +1,121 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { isReplBridgeActive } from '../../bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { AGENT_TOOL_NAME } from '../AgentTool/constants.js'
|
||||
|
||||
// Dead code elimination: Brief tool name only needed when KAIROS or KAIROS_BRIEF is on
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const BRIEF_TOOL_NAME: string | null =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? (
|
||||
require('../BriefTool/prompt.js') as typeof import('../BriefTool/prompt.js')
|
||||
).BRIEF_TOOL_NAME
|
||||
: null
|
||||
const SEND_USER_FILE_TOOL_NAME: string | null = feature('KAIROS')
|
||||
? (
|
||||
require('../SendUserFileTool/prompt.js') as typeof import('../SendUserFileTool/prompt.js')
|
||||
).SEND_USER_FILE_TOOL_NAME
|
||||
: null
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
export { TOOL_SEARCH_TOOL_NAME } from './constants.js'
|
||||
|
||||
import { TOOL_SEARCH_TOOL_NAME } from './constants.js'
|
||||
|
||||
const PROMPT_HEAD = `Fetches full schema definitions for deferred tools so they can be called.
|
||||
|
||||
`
|
||||
|
||||
// Matches isDeferredToolsDeltaEnabled in toolSearch.ts (not imported —
|
||||
// toolSearch.ts imports from this file). When enabled: tools announced
|
||||
// via system-reminder attachments. When disabled: prepended
|
||||
// <available-deferred-tools> block (pre-gate behavior).
|
||||
function getToolLocationHint(): string {
|
||||
const deltaEnabled =
|
||||
process.env.USER_TYPE === 'ant' ||
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_glacier_2xr', false)
|
||||
return deltaEnabled
|
||||
? 'Deferred tools appear by name in <system-reminder> messages.'
|
||||
: 'Deferred tools appear by name in <available-deferred-tools> messages.'
|
||||
}
|
||||
|
||||
const PROMPT_TAIL = ` Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. This tool takes a query, matches it against the deferred tool list, and returns the matched tools' complete JSONSchema definitions inside a <functions> block. Once a tool's schema appears in that result, it is callable exactly like any tool defined at the top of the prompt.
|
||||
|
||||
Result format: each matched tool appears as one <function>{"description": "...", "name": "...", "parameters": {...}}</function> line inside the <functions> block — the same encoding as the tool list at the top of this prompt.
|
||||
|
||||
Query forms:
|
||||
- "select:Read,Edit,Grep" — fetch these exact tools by name
|
||||
- "notebook jupyter" — keyword search, up to max_results best matches
|
||||
- "+slack send" — require "slack" in the name, rank by remaining terms`
|
||||
|
||||
/**
|
||||
* Check if a tool should be deferred (requires ToolSearch to load).
|
||||
* A tool is deferred if:
|
||||
* - It's an MCP tool (always deferred - workflow-specific)
|
||||
* - It has shouldDefer: true
|
||||
*
|
||||
* A tool is NEVER deferred if it has alwaysLoad: true (MCP tools set this via
|
||||
* _meta['anthropic/alwaysLoad']). This check runs first, before any other rule.
|
||||
*/
|
||||
export function isDeferredTool(tool: Tool): boolean {
|
||||
// Explicit opt-out via _meta['anthropic/alwaysLoad'] — tool appears in the
|
||||
// initial prompt with full schema. Checked first so MCP tools can opt out.
|
||||
if (tool.alwaysLoad === true) return false
|
||||
|
||||
// MCP tools are always deferred (workflow-specific)
|
||||
if (tool.isMcp === true) return true
|
||||
|
||||
// Never defer ToolSearch itself — the model needs it to load everything else
|
||||
if (tool.name === TOOL_SEARCH_TOOL_NAME) return false
|
||||
|
||||
// Fork-first experiment: Agent must be available turn 1, not behind ToolSearch.
|
||||
// Lazy require: static import of forkSubagent → coordinatorMode creates a cycle
|
||||
// through constants/tools.ts at module init.
|
||||
if (feature('FORK_SUBAGENT') && tool.name === AGENT_TOOL_NAME) {
|
||||
type ForkMod = typeof import('../AgentTool/forkSubagent.js')
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const m = require('../AgentTool/forkSubagent.js') as ForkMod
|
||||
if (m.isForkSubagentEnabled()) return false
|
||||
}
|
||||
|
||||
// Brief is the primary communication channel whenever the tool is present.
|
||||
// Its prompt contains the text-visibility contract, which the model must
|
||||
// see without a ToolSearch round-trip. No runtime gate needed here: this
|
||||
// tool's isEnabled() IS isBriefEnabled(), so being asked about its deferral
|
||||
// status implies the gate already passed.
|
||||
if (
|
||||
(feature('KAIROS') || feature('KAIROS_BRIEF')) &&
|
||||
BRIEF_TOOL_NAME &&
|
||||
tool.name === BRIEF_TOOL_NAME
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// SendUserFile is a file-delivery communication channel (sibling of Brief).
|
||||
// Must be immediately available without a ToolSearch round-trip.
|
||||
if (
|
||||
feature('KAIROS') &&
|
||||
SEND_USER_FILE_TOOL_NAME &&
|
||||
tool.name === SEND_USER_FILE_TOOL_NAME &&
|
||||
isReplBridgeActive()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return tool.shouldDefer === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Format one deferred-tool line for the <available-deferred-tools> user
|
||||
* message. Search hints (tool.searchHint) are not rendered — the
|
||||
* hints A/B (exp_xenhnnmn0smrx4, stopped Mar 21) showed no benefit.
|
||||
*/
|
||||
export function formatDeferredToolLine(tool: Tool): string {
|
||||
return tool.name
|
||||
}
|
||||
|
||||
export function getPrompt(): string {
|
||||
return PROMPT_HEAD + getToolLocationHint() + PROMPT_TAIL
|
||||
}
|
||||
Reference in New Issue
Block a user