Remove local context from model requests
This commit is contained in:
@@ -448,9 +448,7 @@ export async function getSystemPrompt(
|
|||||||
mcpClients?: MCPServerConnection[],
|
mcpClients?: MCPServerConnection[],
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
||||||
return [
|
return [`You are Claude Code, Anthropic's official CLI for Claude.`]
|
||||||
`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = getCwd()
|
const cwd = getCwd()
|
||||||
@@ -607,8 +605,6 @@ export async function computeEnvInfo(
|
|||||||
modelId: string,
|
modelId: string,
|
||||||
additionalWorkingDirectories?: string[],
|
additionalWorkingDirectories?: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()])
|
|
||||||
|
|
||||||
// Undercover: keep ALL model names/IDs out of the system prompt so nothing
|
// Undercover: keep ALL model names/IDs out of the system prompt so nothing
|
||||||
// internal can leak into public commits/PRs. This includes the public
|
// internal can leak into public commits/PRs. This includes the public
|
||||||
// FRONTIER_MODEL_* constants — if those ever point at an unannounced model,
|
// FRONTIER_MODEL_* constants — if those ever point at an unannounced model,
|
||||||
@@ -627,33 +623,20 @@ export async function computeEnvInfo(
|
|||||||
: `You are powered by the model ${modelId}.`
|
: `You are powered by the model ${modelId}.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalDirsInfo =
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? `Additional working directories: ${additionalWorkingDirectories.join(', ')}\n`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const cutoff = getKnowledgeCutoff(modelId)
|
const cutoff = getKnowledgeCutoff(modelId)
|
||||||
const knowledgeCutoffMessage = cutoff
|
const knowledgeCutoffMessage = cutoff
|
||||||
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
|
? `\n\nAssistant knowledge cutoff is ${cutoff}.`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
return `Here is useful information about the environment you are running in:
|
return [`# Environment`, `You are Claude Code.`, modelDescription, knowledgeCutoffMessage]
|
||||||
<env>
|
.filter(Boolean)
|
||||||
Working directory: ${getCwd()}
|
.join('\n')
|
||||||
Is directory a git repo: ${isGit ? 'Yes' : 'No'}
|
|
||||||
${additionalDirsInfo}Platform: ${env.platform}
|
|
||||||
${getShellInfoLine()}
|
|
||||||
OS Version: ${unameSR}
|
|
||||||
</env>
|
|
||||||
${modelDescription}${knowledgeCutoffMessage}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function computeSimpleEnvInfo(
|
export async function computeSimpleEnvInfo(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
additionalWorkingDirectories?: string[],
|
additionalWorkingDirectories?: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()])
|
|
||||||
|
|
||||||
// Undercover: strip all model name/ID references. See computeEnvInfo.
|
// Undercover: strip all model name/ID references. See computeEnvInfo.
|
||||||
// DCE: inline the USER_TYPE check at each site — do NOT hoist to a const.
|
// DCE: inline the USER_TYPE check at each site — do NOT hoist to a const.
|
||||||
let modelDescription: string | null = null
|
let modelDescription: string | null = null
|
||||||
@@ -671,42 +654,14 @@ export async function computeSimpleEnvInfo(
|
|||||||
? `Assistant knowledge cutoff is ${cutoff}.`
|
? `Assistant knowledge cutoff is ${cutoff}.`
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cwd = getCwd()
|
|
||||||
const isWorktree = getCurrentWorktreeSession() !== null
|
|
||||||
|
|
||||||
const envItems = [
|
|
||||||
`Primary working directory: ${cwd}`,
|
|
||||||
isWorktree
|
|
||||||
? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.`
|
|
||||||
: null,
|
|
||||||
[`Is a git repository: ${isGit}`],
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? `Additional working directories:`
|
|
||||||
: null,
|
|
||||||
additionalWorkingDirectories && additionalWorkingDirectories.length > 0
|
|
||||||
? additionalWorkingDirectories
|
|
||||||
: null,
|
|
||||||
`Platform: ${env.platform}`,
|
|
||||||
getShellInfoLine(),
|
|
||||||
`OS Version: ${unameSR}`,
|
|
||||||
modelDescription,
|
|
||||||
knowledgeCutoffMessage,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (claude.ai/code), and IDE extensions (VS Code, JetBrains).`,
|
|
||||||
process.env.USER_TYPE === 'ant' && isUndercover()
|
|
||||||
? null
|
|
||||||
: `Fast mode for Claude Code uses the same ${FRONTIER_MODEL_NAME} model with faster output. It does NOT switch to a different model. It can be toggled with /fast.`,
|
|
||||||
].filter(item => item !== null)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`# Environment`,
|
`# Environment`,
|
||||||
`You have been invoked in the following environment: `,
|
`You are Claude Code.`,
|
||||||
...prependBullets(envItems),
|
modelDescription,
|
||||||
].join(`\n`)
|
knowledgeCutoffMessage,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(`\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
|
// @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model.
|
||||||
|
|||||||
177
src/context.ts
177
src/context.ts
@@ -1,25 +1,7 @@
|
|||||||
import { feature } from 'bun:bundle'
|
|
||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import {
|
import { setCachedClaudeMdContent } from './bootstrap/state.js'
|
||||||
getAdditionalDirectoriesForClaudeMd,
|
|
||||||
setCachedClaudeMdContent,
|
|
||||||
} from './bootstrap/state.js'
|
|
||||||
import { getLocalISODate } from './constants/common.js'
|
|
||||||
import {
|
|
||||||
filterInjectedMemoryFiles,
|
|
||||||
getClaudeMds,
|
|
||||||
getMemoryFiles,
|
|
||||||
} from './utils/claudemd.js'
|
|
||||||
import { logForDiagnosticsNoPII } from './utils/diagLogs.js'
|
|
||||||
import { isBareMode, isEnvTruthy } from './utils/envUtils.js'
|
|
||||||
import { execFileNoThrow } from './utils/execFileNoThrow.js'
|
|
||||||
import { getBranch, getDefaultBranch, getIsGit, gitExe } from './utils/git.js'
|
|
||||||
import { shouldIncludeGitInstructions } from './utils/gitSettings.js'
|
|
||||||
import { logError } from './utils/log.js'
|
|
||||||
|
|
||||||
const MAX_STATUS_CHARS = 2000
|
// System prompt injection remains a local cache-busting hook only.
|
||||||
|
|
||||||
// System prompt injection for cache breaking (ant-only, ephemeral debugging state)
|
|
||||||
let systemPromptInjection: string | null = null
|
let systemPromptInjection: string | null = null
|
||||||
|
|
||||||
export function getSystemPromptInjection(): string | null {
|
export function getSystemPromptInjection(): string | null {
|
||||||
@@ -28,162 +10,17 @@ export function getSystemPromptInjection(): string | null {
|
|||||||
|
|
||||||
export function setSystemPromptInjection(value: string | null): void {
|
export function setSystemPromptInjection(value: string | null): void {
|
||||||
systemPromptInjection = value
|
systemPromptInjection = value
|
||||||
// Clear context caches immediately when injection changes
|
|
||||||
getUserContext.cache.clear?.()
|
getUserContext.cache.clear?.()
|
||||||
getSystemContext.cache.clear?.()
|
getSystemContext.cache.clear?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGitStatus = memoize(async (): Promise<string | null> => {
|
export const getGitStatus = memoize(async (): Promise<string | null> => null)
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
// Avoid cycles in tests
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_started')
|
|
||||||
|
|
||||||
const isGitStart = Date.now()
|
|
||||||
const isGit = await getIsGit()
|
|
||||||
logForDiagnosticsNoPII('info', 'git_is_git_check_completed', {
|
|
||||||
duration_ms: Date.now() - isGitStart,
|
|
||||||
is_git: isGit,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isGit) {
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const gitCmdsStart = Date.now()
|
|
||||||
const [branch, mainBranch, status, log, userName] = await Promise.all([
|
|
||||||
getBranch(),
|
|
||||||
getDefaultBranch(),
|
|
||||||
execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], {
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
}).then(({ stdout }) => stdout.trim()),
|
|
||||||
execFileNoThrow(
|
|
||||||
gitExe(),
|
|
||||||
['--no-optional-locks', 'log', '--oneline', '-n', '5'],
|
|
||||||
{
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
},
|
|
||||||
).then(({ stdout }) => stdout.trim()),
|
|
||||||
execFileNoThrow(gitExe(), ['config', 'user.name'], {
|
|
||||||
preserveOutputOnError: false,
|
|
||||||
}).then(({ stdout }) => stdout.trim()),
|
|
||||||
])
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'git_commands_completed', {
|
|
||||||
duration_ms: Date.now() - gitCmdsStart,
|
|
||||||
status_length: status.length,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if status exceeds character limit
|
|
||||||
const truncatedStatus =
|
|
||||||
status.length > MAX_STATUS_CHARS
|
|
||||||
? status.substring(0, MAX_STATUS_CHARS) +
|
|
||||||
'\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)'
|
|
||||||
: status
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'git_status_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
truncated: status.length > MAX_STATUS_CHARS,
|
|
||||||
})
|
|
||||||
|
|
||||||
return [
|
|
||||||
`This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`,
|
|
||||||
`Current branch: ${branch}`,
|
|
||||||
`Main branch (you will usually use this for PRs): ${mainBranch}`,
|
|
||||||
...(userName ? [`Git user: ${userName}`] : []),
|
|
||||||
`Status:\n${truncatedStatus || '(clean)'}`,
|
|
||||||
`Recent commits:\n${log}`,
|
|
||||||
].join('\n\n')
|
|
||||||
} catch (error) {
|
|
||||||
logForDiagnosticsNoPII('error', 'git_status_failed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
})
|
|
||||||
logError(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
|
||||||
*/
|
|
||||||
export const getSystemContext = memoize(
|
export const getSystemContext = memoize(
|
||||||
async (): Promise<{
|
async (): Promise<Record<string, string>> => ({}),
|
||||||
[k: string]: string
|
|
||||||
}> => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'system_context_started')
|
|
||||||
|
|
||||||
// Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled
|
|
||||||
const gitStatus =
|
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
|
|
||||||
!shouldIncludeGitInstructions()
|
|
||||||
? null
|
|
||||||
: await getGitStatus()
|
|
||||||
|
|
||||||
// Include system prompt injection if set (for cache breaking, ant-only)
|
|
||||||
const injection = feature('BREAK_CACHE_COMMAND')
|
|
||||||
? getSystemPromptInjection()
|
|
||||||
: null
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'system_context_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
has_git_status: gitStatus !== null,
|
|
||||||
has_injection: injection !== null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
...(gitStatus && { gitStatus }),
|
|
||||||
...(feature('BREAK_CACHE_COMMAND') && injection
|
|
||||||
? {
|
|
||||||
cacheBreaker: `[CACHE_BREAKER: ${injection}]`,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
export const getUserContext = memoize(async (): Promise<Record<string, string>> => {
|
||||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
setCachedClaudeMdContent(null)
|
||||||
*/
|
return {}
|
||||||
export const getUserContext = memoize(
|
|
||||||
async (): Promise<{
|
|
||||||
[k: string]: string
|
|
||||||
}> => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
logForDiagnosticsNoPII('info', 'user_context_started')
|
|
||||||
|
|
||||||
// CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always.
|
|
||||||
// --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir.
|
|
||||||
// --bare means "skip what I didn't ask for", not "ignore what I asked for".
|
|
||||||
const shouldDisableClaudeMd =
|
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
|
|
||||||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
|
|
||||||
// Await the async I/O (readFile/readdir directory walk) so the event
|
|
||||||
// loop yields naturally at the first fs.readFile.
|
|
||||||
const claudeMd = shouldDisableClaudeMd
|
|
||||||
? null
|
|
||||||
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
|
|
||||||
// Cache for the auto-mode classifier (yoloClassifier.ts reads this
|
|
||||||
// instead of importing claudemd.ts directly, which would create a
|
|
||||||
// cycle through permissions/filesystem → permissions → yoloClassifier).
|
|
||||||
setCachedClaudeMdContent(claudeMd || null)
|
|
||||||
|
|
||||||
logForDiagnosticsNoPII('info', 'user_context_completed', {
|
|
||||||
duration_ms: Date.now() - startTime,
|
|
||||||
claudemd_length: claudeMd?.length ?? 0,
|
|
||||||
claudemd_disabled: Boolean(shouldDisableClaudeMd),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
...(claudeMd && { claudeMd }),
|
|
||||||
currentDate: `Today's date is ${getLocalISODate()}.`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
stripSignatureBlocks,
|
stripSignatureBlocks,
|
||||||
} from './utils/messages.js'
|
} from './utils/messages.js'
|
||||||
import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js'
|
import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js'
|
||||||
import { prependUserContext, appendSystemContext } from './utils/api.js'
|
|
||||||
import {
|
import {
|
||||||
createAttachmentMessage,
|
createAttachmentMessage,
|
||||||
filterDuplicateMemoryAttachments,
|
filterDuplicateMemoryAttachments,
|
||||||
@@ -446,9 +445,7 @@ async function* queryLoop(
|
|||||||
messagesForQuery = collapseResult.messages
|
messagesForQuery = collapseResult.messages
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullSystemPrompt = asSystemPrompt(
|
const fullSystemPrompt = asSystemPrompt(systemPrompt)
|
||||||
appendSystemContext(systemPrompt, systemContext),
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCheckpoint('query_autocompact_start')
|
queryCheckpoint('query_autocompact_start')
|
||||||
const { compactionResult, consecutiveFailures } = await deps.autocompact(
|
const { compactionResult, consecutiveFailures } = await deps.autocompact(
|
||||||
@@ -657,7 +654,7 @@ async function* queryLoop(
|
|||||||
let streamingFallbackOccured = false
|
let streamingFallbackOccured = false
|
||||||
queryCheckpoint('query_api_streaming_start')
|
queryCheckpoint('query_api_streaming_start')
|
||||||
for await (const message of deps.callModel({
|
for await (const message of deps.callModel({
|
||||||
messages: prependUserContext(messagesForQuery, userContext),
|
messages: messagesForQuery,
|
||||||
systemPrompt: fullSystemPrompt,
|
systemPrompt: fullSystemPrompt,
|
||||||
thinkingConfig: toolUseContext.options.thinkingConfig,
|
thinkingConfig: toolUseContext.options.thinkingConfig,
|
||||||
tools: toolUseContext.options.tools,
|
tools: toolUseContext.options.tools,
|
||||||
|
|||||||
106
src/utils/api.ts
106
src/utils/api.ts
@@ -438,41 +438,16 @@ export function appendSystemContext(
|
|||||||
systemPrompt: SystemPrompt,
|
systemPrompt: SystemPrompt,
|
||||||
context: { [k: string]: string },
|
context: { [k: string]: string },
|
||||||
): string[] {
|
): string[] {
|
||||||
return [
|
return systemPrompt
|
||||||
...systemPrompt,
|
|
||||||
Object.entries(context)
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join('\n'),
|
|
||||||
].filter(Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prependUserContext(
|
export function prependUserContext(
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
context: { [k: string]: string },
|
context: { [k: string]: string },
|
||||||
): Message[] {
|
): Message[] {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.entries(context).length === 0) {
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
createUserMessage({
|
|
||||||
content: `<system-reminder>\nAs you answer the user's questions, you can use the following context:\n${Object.entries(
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.map(([key, value]) => `# ${key}\n${value}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n`,
|
|
||||||
isMeta: true,
|
|
||||||
}),
|
|
||||||
...messages,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log metrics about context and system prompt size
|
* Log metrics about context and system prompt size
|
||||||
*/
|
*/
|
||||||
@@ -480,87 +455,8 @@ export async function logContextMetrics(
|
|||||||
mcpConfigs: Record<string, ScopedMcpServerConfig>,
|
mcpConfigs: Record<string, ScopedMcpServerConfig>,
|
||||||
toolPermissionContext: ToolPermissionContext,
|
toolPermissionContext: ToolPermissionContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Early return if logging is disabled
|
|
||||||
if (isAnalyticsDisabled()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const [{ tools: mcpTools }, tools, userContext, systemContext] =
|
|
||||||
await Promise.all([
|
|
||||||
prefetchAllMcpResources(mcpConfigs),
|
|
||||||
getTools(toolPermissionContext),
|
|
||||||
getUserContext(),
|
|
||||||
getSystemContext(),
|
|
||||||
])
|
|
||||||
// Extract individual context sizes and calculate total
|
|
||||||
const gitStatusSize = systemContext.gitStatus?.length ?? 0
|
|
||||||
const claudeMdSize = userContext.claudeMd?.length ?? 0
|
|
||||||
|
|
||||||
// Calculate total context size
|
|
||||||
const totalContextSize = gitStatusSize + claudeMdSize
|
|
||||||
|
|
||||||
// Get file count using ripgrep (rounded to nearest power of 10 for privacy)
|
|
||||||
const currentDir = getCwd()
|
|
||||||
const ignorePatternsByRoot = getFileReadIgnorePatterns(toolPermissionContext)
|
|
||||||
const normalizedIgnorePatterns = normalizePatternsToPath(
|
|
||||||
ignorePatternsByRoot,
|
|
||||||
currentDir,
|
|
||||||
)
|
|
||||||
const fileCount = await countFilesRoundedRg(
|
|
||||||
currentDir,
|
|
||||||
AbortSignal.timeout(1000),
|
|
||||||
normalizedIgnorePatterns,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculate tool metrics
|
|
||||||
let mcpToolsCount = 0
|
|
||||||
let mcpServersCount = 0
|
|
||||||
let mcpToolsTokens = 0
|
|
||||||
let nonMcpToolsCount = 0
|
|
||||||
let nonMcpToolsTokens = 0
|
|
||||||
|
|
||||||
const nonMcpTools = tools.filter(tool => !tool.isMcp)
|
|
||||||
mcpToolsCount = mcpTools.length
|
|
||||||
nonMcpToolsCount = nonMcpTools.length
|
|
||||||
|
|
||||||
// Extract unique server names from MCP tool names (format: mcp__servername__toolname)
|
|
||||||
const serverNames = new Set<string>()
|
|
||||||
for (const tool of mcpTools) {
|
|
||||||
const parts = tool.name.split('__')
|
|
||||||
if (parts.length >= 3 && parts[1]) {
|
|
||||||
serverNames.add(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mcpServersCount = serverNames.size
|
|
||||||
|
|
||||||
// Estimate tool tokens locally for analytics (avoids N API calls per session)
|
|
||||||
// Use inputJSONSchema (plain JSON Schema) when available, otherwise convert Zod schema
|
|
||||||
for (const tool of mcpTools) {
|
|
||||||
const schema =
|
|
||||||
'inputJSONSchema' in tool && tool.inputJSONSchema
|
|
||||||
? tool.inputJSONSchema
|
|
||||||
: zodToJsonSchema(tool.inputSchema)
|
|
||||||
mcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema))
|
|
||||||
}
|
|
||||||
for (const tool of nonMcpTools) {
|
|
||||||
const schema =
|
|
||||||
'inputJSONSchema' in tool && tool.inputJSONSchema
|
|
||||||
? tool.inputJSONSchema
|
|
||||||
: zodToJsonSchema(tool.inputSchema)
|
|
||||||
nonMcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema))
|
|
||||||
}
|
|
||||||
|
|
||||||
logEvent('tengu_context_size', {
|
|
||||||
git_status_size: gitStatusSize,
|
|
||||||
claude_md_size: claudeMdSize,
|
|
||||||
total_context_size: totalContextSize,
|
|
||||||
project_file_count_rounded: fileCount,
|
|
||||||
mcp_tools_count: mcpToolsCount,
|
|
||||||
mcp_servers_count: mcpServersCount,
|
|
||||||
mcp_tools_tokens: mcpToolsTokens,
|
|
||||||
non_mcp_tools_count: nonMcpToolsCount,
|
|
||||||
non_mcp_tools_tokens: nonMcpToolsTokens,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Generalize this to all tools
|
// TODO: Generalize this to all tools
|
||||||
export function normalizeToolInput<T extends Tool>(
|
export function normalizeToolInput<T extends Tool>(
|
||||||
|
|||||||
Reference in New Issue
Block a user