Skip to content

Commit ed4bdb9

Browse files
feat: 增强 auto mode 的易用性 (#312)
* feat: poor 模式降级 yolo 审阅模型 * feat: 为多模块添加 Langfuse tracing 支持 在 web search、agent creation、away summary、token estimation、 skill improvement 等模块中集成 Langfuse trace,并透传至 compact/apiQueryHook/execPromptHook 等调用链。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 让 auto mode 记录回主 trace * fix: reopen auto mode prompt when classifier is unavailable * fix: 修复 auto mode 情况下, llm 报错导致弹窗也不打开的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e4ce08f commit ed4bdb9

18 files changed

Lines changed: 286 additions & 150 deletions

File tree

packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type {
99
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
1010
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
1111
import { queryModelWithStreaming } from 'src/services/api/claude.js'
12+
import { createTrace, endTrace, isLangfuseEnabled } from 'src/services/langfuse/index.js'
13+
import { getSessionId } from 'src/bootstrap/state.js'
14+
import { getAPIProvider } from 'src/utils/model/providers.js'
1215
import { createUserMessage } from 'src/utils/messages.js'
1316
import { getMainLoopModel, getSmallFastModel } from 'src/utils/model/model.js'
1417
import { jsonParse } from 'src/utils/slowOperations.js'
@@ -38,6 +41,15 @@ export class ApiSearchAdapter implements WebSearchAdapter {
3841
const toolSchema = makeToolSchema({ allowedDomains, blockedDomains })
3942

4043
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE('tengu_plum_vx3', false)
44+
const model = useHaiku ? getSmallFastModel() : getMainLoopModel()
45+
const langfuseTrace = isLangfuseEnabled()
46+
? createTrace({
47+
sessionId: getSessionId(),
48+
model,
49+
provider: getAPIProvider(),
50+
name: 'web-search-tool',
51+
})
52+
: null
4153

4254
const queryStream = queryModelWithStreaming({
4355
messages: [userMessage],
@@ -58,7 +70,7 @@ export class ApiSearchAdapter implements WebSearchAdapter {
5870
alwaysAskRules: {},
5971
isBypassPermissionsModeAvailable: false,
6072
}),
61-
model: useHaiku ? getSmallFastModel() : getMainLoopModel(),
73+
model,
6274
toolChoice: useHaiku ? { type: 'tool' as const, name: 'web_search' } : undefined,
6375
isNonInteractiveSession: false,
6476
hasAppendSystemPrompt: false,
@@ -68,6 +80,7 @@ export class ApiSearchAdapter implements WebSearchAdapter {
6880
mcpTools: [],
6981
agentId: undefined,
7082
effortValue: undefined,
83+
langfuseTrace,
7184
},
7285
})
7386

@@ -148,6 +161,8 @@ export class ApiSearchAdapter implements WebSearchAdapter {
148161
}
149162
}
150163

164+
endTrace(langfuseTrace)
165+
151166
// Extract SearchResult[] from content blocks
152167
return extractSearchResults(allContentBlocks)
153168
}

src/Tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ export type ToolUseContext = {
277277
criticalSystemReminder_EXPERIMENTAL?: string
278278
/** Langfuse root trace span for this query turn. Passed down to tool execution for observability. */
279279
langfuseTrace?: LangfuseSpan | null
280+
/** Langfuse root trace span for the outer/main agent trace. Used when subagents need to nest observations under the parent agent trace. */
281+
langfuseRootTrace?: LangfuseSpan | null
280282
/** Langfuse batch span wrapping a concurrent tool group. When set, tool observations are nested under it. */
281283
langfuseBatchSpan?: LangfuseSpan | null
282284
/** When true, preserve toolUseResult on messages even for subagents.

src/cli/handlers/autoMode.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { errorMessage } from '../../utils/errors.js'
77
import {
88
getMainLoopModel,
9+
getSmallFastModel,
910
parseUserSpecifiedModel,
1011
} from '../../utils/model/model.js'
1112
import {
@@ -14,6 +15,7 @@ import {
1415
getDefaultExternalAutoModeRules,
1516
} from '../../utils/permissions/yoloClassifier.js'
1617
import { getAutoModeConfig } from '../../utils/settings/settings.js'
18+
import { isPoorModeActive } from '../../commands/poor/poorMode.js'
1719
import { sideQuery } from '../../utils/sideQuery.js'
1820
import { jsonStringify } from '../../utils/slowOperations.js'
1921

@@ -90,7 +92,9 @@ export async function autoModeCritiqueHandler(options: {
9092

9193
const model = options.model
9294
? parseUserSpecifiedModel(options.model)
93-
: getMainLoopModel()
95+
: isPoorModeActive()
96+
? getSmallFastModel()
97+
: getMainLoopModel()
9498

9599
const defaults = getDefaultExternalAutoModeRules()
96100
const classifierPrompt = buildDefaultExternalSystemPrompt()

src/components/agents/generateAgent.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
1515
logEvent,
1616
} from '../../services/analytics/index.js'
17+
import { createTrace, endTrace, isLangfuseEnabled } from '../../services/langfuse/index.js'
18+
import { getSessionId } from '../../bootstrap/state.js'
19+
import { getAPIProvider } from '../../utils/model/providers.js'
1720
import { jsonParse } from '../../utils/slowOperations.js'
1821
import { asSystemPrompt } from '../../utils/systemPromptType.js'
1922

@@ -146,6 +149,15 @@ export async function generateAgent(
146149
? AGENT_CREATION_SYSTEM_PROMPT + AGENT_MEMORY_INSTRUCTIONS
147150
: AGENT_CREATION_SYSTEM_PROMPT
148151

152+
const langfuseTrace = isLangfuseEnabled()
153+
? createTrace({
154+
sessionId: getSessionId(),
155+
model,
156+
provider: getAPIProvider(),
157+
name: 'agent-creation',
158+
})
159+
: null
160+
149161
const response = await queryModelWithoutStreaming({
150162
messages: normalizeMessagesForAPI(messagesWithContext),
151163
systemPrompt: asSystemPrompt([systemPrompt]),
@@ -161,9 +173,12 @@ export async function generateAgent(
161173
hasAppendSystemPrompt: false,
162174
querySource: 'agent_creation',
163175
mcpTools: [],
176+
langfuseTrace,
164177
},
165178
})
166179

180+
endTrace(langfuseTrace)
181+
167182
const textBlocks = (Array.isArray(response.message.content) ? response.message.content : []).filter(
168183
(block): block is ContentBlock & { type: 'text' } => block.type === 'text',
169184
)

src/query.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ export async function* query(
235235
// When called as a sub-agent, langfuseTrace is already set by runAgent()
236236
// — reuse it instead of creating an independent trace.
237237
const ownsTrace = !params.toolUseContext.langfuseTrace
238+
logForDebugging(
239+
`[query] ownsTrace=${ownsTrace} incoming langfuseTrace=${params.toolUseContext.langfuseTrace ? 'present' : 'null/undefined'} isLangfuseEnabled=${isLangfuseEnabled()}`,
240+
)
238241
const langfuseTrace = params.toolUseContext.langfuseTrace
239242
?? (isLangfuseEnabled()
240243
? createTrace({

src/services/awaySummary.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { getSmallFastModel } from '../utils/model/model.js'
1010
import { asSystemPrompt } from '../utils/systemPromptType.js'
1111
import { getResolvedLanguage } from '../utils/language.js'
1212
import { queryModelWithoutStreaming } from './api/claude.js'
13+
import { createTrace, endTrace, isLangfuseEnabled } from './langfuse/index.js'
14+
import { getSessionId } from '../bootstrap/state.js'
15+
import { getAPIProvider } from '../utils/model/providers.js'
1316
import { getSessionMemoryContent } from './SessionMemory/sessionMemoryUtils.js'
1417

1518
// Recap only needs recent context — truncate to avoid "prompt too long" on
@@ -42,6 +45,16 @@ export async function generateAwaySummary(
4245
return null
4346
}
4447

48+
const model = getSmallFastModel()
49+
const langfuseTrace = isLangfuseEnabled()
50+
? createTrace({
51+
sessionId: getSessionId(),
52+
model,
53+
provider: getAPIProvider(),
54+
name: 'away-summary',
55+
})
56+
: null
57+
4558
try {
4659
const memory = await getSessionMemoryContent()
4760
const recent = messages.slice(-RECENT_MESSAGE_WINDOW)
@@ -54,29 +67,33 @@ export async function generateAwaySummary(
5467
signal,
5568
options: {
5669
getToolPermissionContext: async () => getEmptyToolPermissionContext(),
57-
model: getSmallFastModel(),
70+
model,
5871
toolChoice: undefined,
5972
isNonInteractiveSession: false,
6073
hasAppendSystemPrompt: false,
6174
agents: [],
6275
querySource: 'away_summary',
6376
mcpTools: [],
6477
skipCacheWrite: true,
78+
langfuseTrace,
6579
},
6680
})
6781

6882
if (response.isApiErrorMessage) {
6983
logForDebugging(
7084
`[awaySummary] API error: ${getAssistantMessageText(response)}`,
7185
)
86+
endTrace(langfuseTrace, undefined, 'error')
7287
return null
7388
}
89+
endTrace(langfuseTrace)
7490
return getAssistantMessageText(response)
7591
} catch (err) {
7692
if (err instanceof APIUserAbortError || signal.aborted) {
7793
return null
7894
}
7995
logForDebugging(`[awaySummary] generation failed: ${err}`)
96+
endTrace(langfuseTrace, undefined, 'error')
8097
return null
8198
}
8299
}

src/services/compact/compact.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,7 @@ async function streamCompactSummary({
13261326
agents: context.options.agentDefinitions.activeAgents,
13271327
mcpTools: [],
13281328
effortValue: appState.effortValue,
1329+
langfuseTrace: context.langfuseTrace,
13291330
},
13301331
})
13311332
const streamIter = streamingGen[Symbol.asyncIterator]()

src/services/tokenEstimation.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { jsonStringify } from '../utils/slowOperations.js'
2525
import { isToolReferenceBlock } from '../utils/toolSearch.js'
2626
import { getAPIMetadata, getExtraBodyParams } from './api/claude.js'
2727
import { getAnthropicClient } from './api/client.js'
28+
import { createTrace, endTrace, isLangfuseEnabled, recordLLMObservation } from './langfuse/index.js'
29+
import { getSessionId } from '../bootstrap/state.js'
2830
import { withTokenCountVCR } from './vcr.js'
2931

3032
// Minimal values for token counting with thinking enabled
@@ -309,6 +311,15 @@ export async function countTokensViaHaikuFallback(
309311
: betas
310312

311313
// biome-ignore lint/plugin: token counting needs specialized parameters (thinking, betas) that sideQuery doesn't support
314+
const apiStart = Date.now()
315+
const langfuseTrace = isLangfuseEnabled()
316+
? createTrace({
317+
sessionId: getSessionId(),
318+
model: normalizeModelStringForAPI(model),
319+
provider: getAPIProvider(),
320+
name: 'token-estimation',
321+
})
322+
: null
312323
const response = await anthropic.beta.messages.create({
313324
model: normalizeModelStringForAPI(model),
314325
max_tokens: containsThinking ? TOKEN_COUNT_MAX_TOKENS : 1,
@@ -331,6 +342,22 @@ export async function countTokensViaHaikuFallback(
331342
const cacheCreationTokens = usage.cache_creation_input_tokens || 0
332343
const cacheReadTokens = usage.cache_read_input_tokens || 0
333344

345+
recordLLMObservation(langfuseTrace, {
346+
model: normalizeModelStringForAPI(model),
347+
provider: getAPIProvider(),
348+
input: messagesToSend,
349+
output: response.content,
350+
usage: {
351+
input_tokens: inputTokens,
352+
output_tokens: usage.output_tokens,
353+
cache_creation_input_tokens: cacheCreationTokens || undefined,
354+
cache_read_input_tokens: cacheReadTokens || undefined,
355+
},
356+
startTime: new Date(apiStart),
357+
endTime: new Date(),
358+
})
359+
endTrace(langfuseTrace)
360+
334361
return inputTokens + cacheCreationTokens + cacheReadTokens
335362
}
336363

src/utils/__tests__/messages.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,14 @@ describe("buildClassifierUnavailableMessage", () => {
457457
expect(msg).toContain("classifier-v1");
458458
expect(msg).toContain("unavailable");
459459
});
460+
461+
test("tells the model to wait and retry later", () => {
462+
const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1");
463+
expect(msg).toContain("Wait briefly and then try this action again.");
464+
expect(msg).toContain("come back to it later");
465+
});
460466
});
461467

462-
// ─── normalizeMessages ──────────────────────────────────────────────────
463468

464469
describe("normalizeMessages", () => {
465470
test("splits multi-block assistant message into individual messages", () => {

src/utils/forkedAgent.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ export function createSubagentContext(
374374
}
375375

376376
return {
377+
// Preserve the parent Langfuse trace separately so nested side queries
378+
// like auto_mode can attach to the main agent trace instead of the
379+
// subagent's own trace.
380+
langfuseRootTrace: parentContext.langfuseTrace,
377381
// Mutable state - cloned by default to maintain isolation
378382
// Clone overrides.readFileState if provided, otherwise clone from parent
379383
readFileState: cloneFileStateCache(

0 commit comments

Comments
 (0)