mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
* refactor: 创建 @anthropic-ai/model-provider 包骨架与类型定义
- 新建 workspace 包 packages/@anthropic-ai/model-provider
- 定义 ModelProviderHooks 接口(依赖注入:分析、成本、日志等)
- 定义 ClientFactories 接口(Anthropic/OpenAI/Gemini/Grok 客户端工厂)
- 搬入核心类型:Message 体系、NonNullableUsage、EMPTY_USAGE、SystemPrompt、错误常量
- 主项目 src/types/message.ts 等改为 re-export,保持向后兼容
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 提升 OpenAI 转换器和模型映射到 model-provider 包
- 搬入 OpenAI 消息转换(convertMessages)、工具转换(convertTools)、流适配(streamAdapter)
- 搬入 OpenAI 和 Grok 模型映射(resolveOpenAIModel、resolveGrokModel)
- 主项目文件改为 thin re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 搬入 Gemini 兼容层到 model-provider 包
- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射
- 主项目 gemini/ 目录下文件改为 thin re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: 搬入 errorUtils 并迁移消费者导入到 model-provider
- 搬入 formatAPIError、extractConnectionErrorDetails 等 errorUtils
- 迁移 10 个消费者文件直接从 @anthropic-ai/model-provider 导入
- 更新 emptyUsage、sdkUtilityTypes、systemPromptType 为 re-export proxy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: 添加 agent-loop 绘图
* Revert "feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)"
This reverts commit e458d6391d.
* docs: 添加简化版 agent loop
* fix: 修复 n 快捷键导致关闭的问题
* fix: 修复 node 下 ws 没打包问题
* docs: 修复链接
* test: 添加测试支持
* fix: 修复类型问题(#267) (#271)
* fix: 修复 Bun 的 polyfill 问题
* fix: 类型修复完成
* feat: 统一所有包的类型文件
* fix: 修复构建问题
* test: 修复类型校验 (#279)
* fix: 修复 Bun 的 polyfill 问题
* fix: 类型修复完成
* feat: 统一所有包的类型文件
* fix: 修复构建问题
* fix(remote-control): harden self-hosted session flows (#278)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
* docs: update contributors
* build: 新增 vite 构建流程
* feat: 添加环境变量支持以覆盖 max_tokens 设置
* feat(langfuse): LLM generation 记录工具定义
将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: 添加对 ACP 协议的支持 (#284)
* feat: 适配 zed acp 协议
* docs: 完善 acp 文档
* chore: 1.4.0
* conflict: 解决冲突
* feat: 添加测试覆盖率上报
* style: 改名加移动文件夹位置
* refactor: 移动测试用例及实现
* test: 修复测试用例完成
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cheng Zi Feng <1154238323@qq.com>
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
Co-authored-by: claude-code-best <272536312+claude-code-best@users.noreply.github.com>
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
/**
|
|
* Side Question ("/btw") feature - allows asking quick questions without
|
|
* interrupting the main agent context.
|
|
*
|
|
* Uses runForkedAgent to leverage prompt caching from the parent context
|
|
* while keeping the side question response separate from main conversation.
|
|
*/
|
|
|
|
import { formatAPIError } from '@ant/model-provider'
|
|
import type { NonNullableUsage } from '@ant/model-provider'
|
|
import type { Message, SystemAPIErrorMessage } from '../types/message.js'
|
|
import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js'
|
|
import { createUserMessage, extractTextContent } from './messages.js'
|
|
|
|
// Pattern to detect "/btw" at start of input (case-insensitive, word boundary)
|
|
const BTW_PATTERN = /^\/btw\b/gi
|
|
|
|
/**
|
|
* Find positions of "/btw" keyword at the start of text for highlighting.
|
|
* Similar to findThinkingTriggerPositions in thinking.ts.
|
|
*/
|
|
export function findBtwTriggerPositions(text: string): Array<{
|
|
word: string
|
|
start: number
|
|
end: number
|
|
}> {
|
|
const positions: Array<{ word: string; start: number; end: number }> = []
|
|
const matches = text.matchAll(BTW_PATTERN)
|
|
|
|
for (const match of matches) {
|
|
if (match.index !== undefined) {
|
|
positions.push({
|
|
word: match[0],
|
|
start: match.index,
|
|
end: match.index + match[0].length,
|
|
})
|
|
}
|
|
}
|
|
|
|
return positions
|
|
}
|
|
|
|
export type SideQuestionResult = {
|
|
response: string | null
|
|
usage: NonNullableUsage
|
|
}
|
|
|
|
/**
|
|
* Run a side question using a forked agent.
|
|
* Shares the parent's prompt cache — no thinking override, no cache write.
|
|
* All tools are blocked and we cap at 1 turn.
|
|
*/
|
|
export async function runSideQuestion({
|
|
question,
|
|
cacheSafeParams,
|
|
}: {
|
|
question: string
|
|
cacheSafeParams: CacheSafeParams
|
|
}): Promise<SideQuestionResult> {
|
|
// Wrap the question with instructions to answer without tools
|
|
const wrappedQuestion = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
|
|
|
|
IMPORTANT CONTEXT:
|
|
- You are a separate, lightweight agent spawned to answer this one question
|
|
- The main agent is NOT interrupted - it continues working independently in the background
|
|
- You share the conversation context but are a completely separate instance
|
|
- Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect
|
|
|
|
CRITICAL CONSTRAINTS:
|
|
- You have NO tools available - you cannot read files, run commands, search, or take any actions
|
|
- This is a one-off response - there will be no follow-up turns
|
|
- You can ONLY provide information based on what you already know from the conversation context
|
|
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
|
|
- If you don't know the answer, say so - do not offer to look it up or investigate
|
|
|
|
Simply answer the question with the information you have.</system-reminder>
|
|
|
|
${question}`
|
|
|
|
const agentResult = await runForkedAgent({
|
|
promptMessages: [createUserMessage({ content: wrappedQuestion })],
|
|
// Do NOT override thinkingConfig — thinking is part of the API cache key,
|
|
// and diverging from the main thread's config busts the prompt cache.
|
|
// Adaptive thinking on a quick Q&A has negligible overhead.
|
|
cacheSafeParams,
|
|
canUseTool: async () => ({
|
|
behavior: 'deny' as const,
|
|
message: 'Side questions cannot use tools',
|
|
decisionReason: { type: 'other' as const, reason: 'side_question' },
|
|
}),
|
|
querySource: 'side_question',
|
|
forkLabel: 'side_question',
|
|
maxTurns: 1, // Single turn only - no tool use loops
|
|
// No future request shares this suffix; skip writing cache entries.
|
|
skipCacheWrite: true,
|
|
})
|
|
|
|
return {
|
|
response: extractSideQuestionResponse(agentResult.messages),
|
|
usage: agentResult.totalUsage,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract a display string from forked agent messages.
|
|
*
|
|
* IMPORTANT: claude.ts yields one AssistantMessage PER CONTENT BLOCK, not one
|
|
* per API response. With adaptive thinking enabled (inherited from the main
|
|
* thread to preserve the cache key), a thinking response arrives as:
|
|
* messages[0] = assistant { content: [thinking_block] }
|
|
* messages[1] = assistant { content: [text_block] }
|
|
*
|
|
* The old code used `.find(m => m.type === 'assistant')` which grabbed the
|
|
* first (thinking-only) message, found no text block, and returned null →
|
|
* "No response received". Repos with large context (many skills, big CLAUDE.md)
|
|
* trigger thinking more often, which is why this reproduced in the monorepo
|
|
* but not here.
|
|
*
|
|
* Secondary failure modes also surfaced as "No response received":
|
|
* - Model attempts tool_use → content = [thinking, tool_use], no text.
|
|
* Rare — the system-reminder usually prevents this, but handled here.
|
|
* - API error exhausts retries → query yields system api_error + user
|
|
* interruption, no assistant message at all.
|
|
*/
|
|
function extractSideQuestionResponse(messages: Message[]): string | null {
|
|
// Flatten all assistant content blocks across the per-block messages.
|
|
const assistantBlocks = messages.flatMap(m =>
|
|
m.type === 'assistant' ? (m.message!.content as unknown as Array<{ type: string; [key: string]: unknown }>) : [],
|
|
)
|
|
|
|
if (assistantBlocks.length > 0) {
|
|
// Concatenate all text blocks (there's normally at most one, but be safe).
|
|
const text = extractTextContent(assistantBlocks, '\n\n').trim()
|
|
if (text) return text
|
|
|
|
// No text — check if the model tried to call a tool despite instructions.
|
|
const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
|
|
if (toolUse) {
|
|
const toolName = 'name' in toolUse ? (toolUse as any).name : 'a tool'
|
|
return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`
|
|
}
|
|
}
|
|
|
|
// No assistant content — likely API error exhausted retries. Surface the
|
|
// first system api_error message so the user sees what happened.
|
|
const apiErr = messages.find(
|
|
(m): m is SystemAPIErrorMessage =>
|
|
m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
|
|
)
|
|
if (apiErr) {
|
|
return `(API error: ${formatAPIError(apiErr.error as any)})`
|
|
}
|
|
|
|
return null
|
|
}
|