Skip to content

Commit da6d063

Browse files
fix: 修复 anthropic 煞笔的四个 bug (#352)
* fix: 移除文件编辑前必须先读取的限制 移除 FileEditTool 和 FileWriteTool 中的 "read before edit" 校验, 允许直接编辑未读取过的文件。保留文件修改过期检测。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: 更新 teach-me 自动写 note 笔记的功能 * fix: 修复 DeepSeek V4 reasoning_content 回传导致的 400 错误 - 扩大模型名称检测范围,匹配所有 deepseek 模型(V4、R1 等) - 始终保留 thinking blocks 为 reasoning_content 回传给 API - 移除有 bug 的 turn boundary 剥离逻辑 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: Opus 4.6/4.7 默认推理 effort 从 medium 改为 high Pro 和 Max/Team 订阅者的 Opus 默认 effort 之前被降级为 medium, 导致用户感知模型「变笨」。恢复为 high。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 移除 thinkingClearLatched sticky-on 机制 空闲超过 1 小时后 thinkingClearLatched 会被触发且永不重置, 导致每轮 API 调用都清除 thinking 历史。完整移除该 latch 机制, clearAllThinking 硬编码为 false。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 移除 numeric_length_anchors 系统指令 删除「工具调用间文字 ≤25 词、最终回复 ≤100 词」的硬性限制。 ablation 测试显示该约束使整体智能下降 3%。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: 修复测试中 reasoning_content 类型断言 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8613d55 commit da6d063

13 files changed

Lines changed: 98 additions & 163 deletions

File tree

.claude/skills/teach-me/SKILL.md

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ All teach-me data is stored under `.claude/skills/teach-me/records/`:
4141
.claude/skills/teach-me/records/
4242
├── learner-profile.md # Cross-topic notes (created on first session)
4343
└── {topic-slug}/
44-
└── session.md # Learning state: concepts, status, notes
44+
├── session.md # Learning state: concepts, status, notes
45+
└── {topic-slug}-notes.md # Learner-facing summary notes (generated at session end)
4546
```
4647

4748
**Slug**: Topic in kebab-case, 2-5 words. Example: "Python decorators" → `python-decorators`
@@ -275,7 +276,8 @@ Update `session.md` after each round:
275276
When all concepts mastered or user ends session:
276277

277278
1. Update `session.md` with final state.
278-
2. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
279+
2. **Generate learner-facing notes** — write `{topic-slug}-notes.md` in the topic directory. This is a standalone reference document the learner can review later. See "Notes Generation" below for format.
280+
3. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
279281

280282
```markdown
281283
# Learner Profile
@@ -293,7 +295,48 @@ Updated: {timestamp}
293295
- Python decorators (8/10 concepts, 2025-01-15)
294296
```
295297

296-
3. Give a brief text summary of what was covered, key insights, and areas for further study.
298+
4. Give a brief text summary of what was covered, key insights, and areas for further study.
299+
300+
## Notes Generation
301+
302+
At session end, generate a learner-facing notes file at `{topic-slug}/{topic-slug}-notes.md`. This file is **written for the learner to review later**, not for the tutor. It should be self-contained and organized as a quick-reference.
303+
304+
### Notes Structure
305+
306+
```markdown
307+
# {Topic} 核心笔记
308+
309+
## 1. {Section Name}
310+
{Key concept, mechanism, or principle}
311+
* **One-line summary**: {what it does / why it matters}
312+
* **Detail**: {brief explanation, 2-4 sentences max}
313+
* **Example** (if applicable): {code snippet, command, or concrete scenario}
314+
315+
---
316+
317+
## 2. {Section Name}
318+
...
319+
320+
---
321+
322+
## n. 实战参数 / Cheat Sheet (if applicable)
323+
{Practical commands, config, or quick-reference table}
324+
325+
| Parameter / Concept | What it does | Tuning tip |
326+
|---------------------|-------------|------------|
327+
| ... | ... | ... |
328+
```
329+
330+
### Notes Writing Rules
331+
332+
1. **Start with "what & why"** before "how". Each section should answer: what is this, why does it exist, what problem does it solve.
333+
2. **Use analogies sparingly but effectively**. Only include an analogy if it clarifies a non-obvious mechanism (e.g., "PagedAttention is like OS virtual memory paging").
334+
3. **Include trade-offs**. Every optimization or design choice has a cost. Always state it (e.g., "TP improves throughput but increases communication latency").
335+
4. **Code / command examples should be minimal**. Under 10 lines, self-contained, with comments explaining the key flags.
336+
5. **Organize by concept dependency**, not by chronological teaching order. Foundation concepts first, advanced ones last.
337+
6. **No quiz questions, no misconceptions, no tutor-side notes**. This is a clean reference document.
338+
7. **Language matches the session**. If the session was in Chinese, notes are in Chinese (technical terms can stay in English).
339+
8. **Keep it under 150 lines**. If it gets too long, the learner won't review it. Be ruthless about cutting fluff.
297340

298341
## Resuming Sessions
299342

packages/@ant/model-provider/src/shared/__tests__/openaiConvertMessages.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ describe('anthropicMessagesToOpenAI', () => {
121121
])
122122
})
123123

124-
test('strips thinking blocks', () => {
124+
test('preserves thinking blocks as reasoning_content', () => {
125125
const result = anthropicMessagesToOpenAI(
126126
[
127127
makeAssistantMsg([
@@ -131,7 +131,7 @@ describe('anthropicMessagesToOpenAI', () => {
131131
],
132132
[] as any,
133133
)
134-
expect(result).toEqual([{ role: 'assistant', content: 'visible response' }])
134+
expect(result).toEqual([{ role: 'assistant', content: 'visible response', reasoning_content: 'internal thoughts...' }] as any)
135135
})
136136

137137
test('handles full conversation with tools', () => {
@@ -299,7 +299,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
299299
expect(assistant.reasoning_content).toBe('Let me reason about this...')
300300
})
301301

302-
test('drops thinking block when enableThinking is false (default)', () => {
302+
test('preserves thinking block as reasoning_content even without enableThinking', () => {
303303
const result = anthropicMessagesToOpenAI(
304304
[
305305
makeAssistantMsg([
@@ -311,7 +311,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
311311
)
312312
const assistant = result[0] as any
313313
expect(assistant.content).toBe('visible response')
314-
expect(assistant.reasoning_content).toBeUndefined()
314+
expect(assistant.reasoning_content).toBe('internal thoughts...')
315315
})
316316

317317
test('preserves reasoning_content with tool_calls in same turn', () => {
@@ -352,7 +352,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
352352
expect(assistant.tool_calls[0].function.name).toBe('get_weather')
353353
})
354354

355-
test('strips reasoning_content from previous turns', () => {
355+
test('always preserves reasoning_content from all turns', () => {
356356
const result = anthropicMessagesToOpenAI(
357357
[
358358
// Turn 1: user → assistant (with thinking)
@@ -361,7 +361,8 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
361361
{ type: 'thinking' as const, thinking: 'Turn 1 reasoning...' },
362362
{ type: 'text', text: 'Turn 1 answer' },
363363
]),
364-
// Turn 2: new user message → previous reasoning should be stripped
364+
// Turn 2: new user message → reasoning should still be preserved
365+
// (DeepSeek requires reasoning_content to be passed back when tool calls are involved)
365366
makeUserMsg('question 2'),
366367
makeAssistantMsg([
367368
{ type: 'thinking' as const, thinking: 'Turn 2 reasoning...' },
@@ -373,10 +374,9 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
373374
)
374375

375376
const assistants = result.filter(m => m.role === 'assistant')
376-
// Turn 1 assistant: reasoning should be stripped (previous turn)
377-
expect((assistants[0] as any).reasoning_content).toBeUndefined()
377+
// Both turns preserve reasoning_content (DeepSeek API requires it for tool calls)
378+
expect((assistants[0] as any).reasoning_content).toBe('Turn 1 reasoning...')
378379
expect((assistants[0] as any).content).toBe('Turn 1 answer')
379-
// Turn 2 assistant: reasoning should be preserved (current turn)
380380
expect((assistants[1] as any).reasoning_content).toBe('Turn 2 reasoning...')
381381
expect((assistants[1] as any).content).toBe('Turn 2 answer')
382382
})

packages/@ant/model-provider/src/shared/openaiConvertMessages.ts

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ export interface ConvertMessagesOptions {
2626
* - system prompt → role: "system" message prepended
2727
* - tool_use blocks → tool_calls[] on assistant message
2828
* - tool_result blocks → role: "tool" messages
29-
* - thinking blocks → silently dropped (or preserved as reasoning_content when enableThinking=true)
29+
* - thinking blocks → preserved as reasoning_content (DeepSeek requires passing it back)
3030
* - cache_control → stripped
3131
*/
3232
export function anthropicMessagesToOpenAI(
3333
messages: (UserMessage | AssistantMessage)[],
3434
systemPrompt: SystemPrompt,
35-
options?: ConvertMessagesOptions,
35+
// options retained for API compatibility; thinking blocks are now always preserved
36+
_options?: ConvertMessagesOptions,
3637
): ChatCompletionMessageParam[] {
3738
const result: ChatCompletionMessageParam[] = []
38-
const enableThinking = options?.enableThinking ?? false
3939

4040
// Prepend system prompt as system message
4141
const systemText = systemPromptToText(systemPrompt)
@@ -46,53 +46,13 @@ export function anthropicMessagesToOpenAI(
4646
} satisfies ChatCompletionSystemMessageParam)
4747
}
4848

49-
// When thinking mode is on, detect turn boundaries so that reasoning_content
50-
// from *previous* user turns is stripped (saves bandwidth; DeepSeek ignores it).
51-
// A "new turn" starts when a user text message appears after at least one assistant response.
52-
const turnBoundaries = new Set<number>()
53-
if (enableThinking) {
54-
let hasSeenAssistant = false
55-
for (let i = 0; i < messages.length; i++) {
56-
const msg = messages[i]
57-
if (msg.type === 'assistant') {
58-
hasSeenAssistant = true
59-
}
60-
if (msg.type === 'user' && hasSeenAssistant) {
61-
const content = msg.message.content
62-
// A user message starts a new turn if it contains any non-tool_result content
63-
// (text, image, or other media). Tool results alone do NOT start a new turn
64-
// because they are continuations of the previous assistant tool call.
65-
const startsNewUserTurn =
66-
typeof content === 'string'
67-
? content.length > 0
68-
: Array.isArray(content) &&
69-
content.some(
70-
(b: any) =>
71-
typeof b === 'string' ||
72-
(b &&
73-
typeof b === 'object' &&
74-
'type' in b &&
75-
b.type !== 'tool_result'),
76-
)
77-
if (startsNewUserTurn) {
78-
turnBoundaries.add(i)
79-
}
80-
}
81-
}
82-
}
83-
84-
for (let i = 0; i < messages.length; i++) {
85-
const msg = messages[i]
49+
for (const msg of messages) {
8650
switch (msg.type) {
8751
case 'user':
8852
result.push(...convertInternalUserMessage(msg))
8953
break
9054
case 'assistant':
91-
// Preserve reasoning_content unless we're before a turn boundary
92-
// (i.e., from a previous user Q&A round)
93-
const preserveReasoning =
94-
enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
95-
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
55+
result.push(...convertInternalAssistantMessage(msg))
9656
break
9757
default:
9858
break
@@ -107,17 +67,6 @@ function systemPromptToText(systemPrompt: SystemPrompt): string {
10767
return systemPrompt.filter(Boolean).join('\n\n')
10868
}
10969

110-
/**
111-
* Check if index `i` falls before any turn boundary (i.e. it belongs to a previous turn).
112-
* A message at index i is "before" a boundary if there exists a boundary j where i < j.
113-
*/
114-
function isBeforeAnyTurnBoundary(i: number, boundaries: Set<number>): boolean {
115-
for (const b of boundaries) {
116-
if (i < b) return true
117-
}
118-
return false
119-
}
120-
12170
function convertInternalUserMessage(
12271
msg: UserMessage,
12372
): ChatCompletionMessageParam[] {
@@ -213,7 +162,6 @@ function convertToolResult(
213162

214163
function convertInternalAssistantMessage(
215164
msg: AssistantMessage,
216-
preserveReasoning = false,
217165
): ChatCompletionMessageParam[] {
218166
const content = msg.message.content
219167

@@ -257,8 +205,10 @@ function convertInternalAssistantMessage(
257205
typeof tu.input === 'string' ? tu.input : JSON.stringify(tu.input),
258206
},
259207
})
260-
} else if (block.type === 'thinking' && preserveReasoning) {
261-
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
208+
} else if (block.type === 'thinking') {
209+
// DeepSeek thinking mode: always preserve reasoning_content.
210+
// DeepSeek requires reasoning_content to be passed back in subsequent requests,
211+
// especially when tool calls are involved (returns 400 if missing).
262212
const thinkingText = (block as unknown as Record<string, unknown>)
263213
.thinking
264214
if (typeof thinkingText === 'string' && thinkingText) {

packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,18 +273,6 @@ export const FileEditTool = buildTool({
273273
}
274274

275275
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
276-
if (!readTimestamp || readTimestamp.isPartialView) {
277-
return {
278-
result: false,
279-
behavior: 'ask',
280-
message:
281-
'File has not been read yet. Read it first before writing to it.',
282-
meta: {
283-
isFilePathAbsolute: String(isAbsolute(file_path)),
284-
},
285-
errorCode: 6,
286-
}
287-
}
288276

289277
// Check if file exists and get its last modified time
290278
if (readTimestamp) {

packages/builtin-tools/src/tools/FileEditTool/UI.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,6 @@ export function renderToolUseErrorMessage(
186186
extractTag(result, 'tool_use_error')
187187
) {
188188
const errorMessage = extractTag(result, 'tool_use_error')
189-
// Show a less scary message for intended behavior
190-
if (errorMessage?.includes('File has not been read yet')) {
191-
return (
192-
<MessageResponse>
193-
<Text dimColor>File must be read first</Text>
194-
</MessageResponse>
195-
)
196-
}
197189
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
198190
return (
199191
<MessageResponse>

packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -196,25 +196,18 @@ export const FileWriteTool = buildTool({
196196
}
197197

198198
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)
199-
if (!readTimestamp || readTimestamp.isPartialView) {
200-
return {
201-
result: false,
202-
message:
203-
'File has not been read yet. Read it first before writing to it.',
204-
errorCode: 2,
205-
}
206-
}
207199

208200
// Reuse mtime from the stat above — avoids a redundant statSync via
209-
// getFileModificationTime. The readTimestamp guard above ensures this
210-
// block is always reached when the file exists.
211-
const lastWriteTime = Math.floor(fileMtimeMs)
212-
if (lastWriteTime > readTimestamp.timestamp) {
213-
return {
214-
result: false,
215-
message:
216-
'File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.',
217-
errorCode: 3,
201+
// getFileModificationTime.
202+
if (readTimestamp) {
203+
const lastWriteTime = Math.floor(fileMtimeMs)
204+
if (lastWriteTime > readTimestamp.timestamp) {
205+
return {
206+
result: false,
207+
message:
208+
'File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.',
209+
errorCode: 3,
210+
}
218211
}
219212
}
220213

src/bootstrap/state.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,6 @@ type State = {
235235
// microcompact is first enabled, keep sending the header so mid-session
236236
// GrowthBook/settings toggles don't bust the prompt cache.
237237
cacheEditingHeaderLatched: boolean | null
238-
// Sticky-on latch for clearing thinking from prior tool loops. Triggered
239-
// when >1h since last API call (confirmed cache miss — no cache-hit
240-
// benefit to keeping thinking). Once latched, stays on so the newly-warmed
241-
// thinking-cleared cache isn't busted by flipping back to keep:'all'.
242-
thinkingClearLatched: boolean | null
243238
// Current prompt ID (UUID) correlating a user prompt with subsequent OTel events
244239
promptId: string | null
245240
// Last API requestId for the main conversation chain (not subagents).
@@ -414,7 +409,6 @@ function getInitialState(): State {
414409
afkModeHeaderLatched: null,
415410
fastModeHeaderLatched: null,
416411
cacheEditingHeaderLatched: null,
417-
thinkingClearLatched: null,
418412
// Current prompt ID
419413
promptId: null,
420414
lastMainRequestId: undefined,
@@ -1729,14 +1723,6 @@ export function setCacheEditingHeaderLatched(v: boolean): void {
17291723
STATE.cacheEditingHeaderLatched = v
17301724
}
17311725

1732-
export function getThinkingClearLatched(): boolean | null {
1733-
return STATE.thinkingClearLatched
1734-
}
1735-
1736-
export function setThinkingClearLatched(v: boolean): void {
1737-
STATE.thinkingClearLatched = v
1738-
}
1739-
17401726
/**
17411727
* Reset beta header latches to null. Called on /clear and /compact so a
17421728
* fresh conversation gets fresh header evaluation.
@@ -1745,7 +1731,6 @@ export function clearBetaHeaderLatches(): void {
17451731
STATE.afkModeHeaderLatched = null
17461732
STATE.fastModeHeaderLatched = null
17471733
STATE.cacheEditingHeaderLatched = null
1748-
STATE.thinkingClearLatched = null
17491734
}
17501735

17511736
export function getPromptId(): string | null {

src/constants/prompts.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -614,17 +614,6 @@ ${CYBER_RISK_INSTRUCTION}`,
614614
'summarize_tool_results',
615615
() => SUMMARIZE_TOOL_RESULTS_SECTION,
616616
),
617-
// Numeric length anchors — research shows ~1.2% output token reduction vs
618-
// qualitative "be concise". Ant-only to measure quality impact first.
619-
...(process.env.USER_TYPE === 'ant'
620-
? [
621-
systemPromptSection(
622-
'numeric_length_anchors',
623-
() =>
624-
'Length limits: keep text between tool calls to \u226425 words. Keep final responses to \u2264100 words unless the task requires more detail.',
625-
),
626-
]
627-
: []),
628617
...(feature('TOKEN_BUDGET')
629618
? [
630619
// Cached unconditionally — the "When the user specifies..." phrasing

0 commit comments

Comments
 (0)