feat: langfuse 工具调用显示为嵌套结构

This commit is contained in:
claude-code-best
2026-04-13 18:05:13 +08:00
parent d4b30d32c3
commit 05cabbbd73
6 changed files with 99 additions and 12 deletions

View File

@@ -10,6 +10,8 @@ import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/t
import type { AssistantMessage, Message } from '../../types/message.js'
import { createChildAbortController } from '../../utils/abortController.js'
import { runToolUse } from './toolExecution.js'
import { createToolBatchSpan, endToolBatchSpan } from '../langfuse/index.js'
import type { LangfuseSpan } from '../langfuse/index.js'
type MessageUpdate = {
message?: Message
@@ -42,13 +44,10 @@ export class StreamingToolExecutor {
private toolUseContext: ToolUseContext
private hasErrored = false
private erroredToolDescription = ''
// Child of toolUseContext.abortController. Fires when a Bash tool errors
// so sibling subprocesses die immediately instead of running to completion.
// Aborting this does NOT abort the parent — query.ts won't end the turn.
private siblingAbortController: AbortController
private discarded = false
// Signal to wake up getRemainingResults when progress is available
private progressAvailableResolve?: () => void
private turnSpan: LangfuseSpan | null = null
constructor(
private readonly toolDefinitions: Tools,
@@ -74,6 +73,16 @@ export class StreamingToolExecutor {
* Add a tool to the execution queue. Will start executing immediately if conditions allow.
*/
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
// Create turn span on first tool — will be ended in getRemainingResults
if (this.tools.length === 0 && this.turnSpan === null) {
this.turnSpan = createToolBatchSpan(
this.toolUseContext.langfuseTrace ?? null,
{ toolNames: [block.name], batchIndex: 0 },
)
if (this.turnSpan) {
this.toolUseContext = { ...this.toolUseContext, langfuseBatchSpan: this.turnSpan }
}
}
const toolDefinition = findToolByName(this.toolDefinitions, block.name)
if (!toolDefinition) {
this.tools.push({
@@ -487,6 +496,9 @@ export class StreamingToolExecutor {
for (const result of this.getCompletedResults()) {
yield result
}
endToolBatchSpan(this.turnSpan)
this.turnSpan = null
}
/**

View File

@@ -1309,6 +1309,7 @@ async function checkPermissionsAndCallTool(
output: toolResultStr,
startTime: new Date(startTime),
isError: false,
parentBatchSpan: toolUseContext.langfuseBatchSpan,
})
// Map the tool result to API format once and cache it. This block is reused
@@ -1628,6 +1629,7 @@ async function checkPermissionsAndCallTool(
output: errorMessage(error),
startTime: new Date(startTime),
isError: true,
parentBatchSpan: toolUseContext.langfuseBatchSpan,
})
// Handle MCP auth errors by updating the client status to 'needs-auth'

View File

@@ -4,6 +4,7 @@ import { findToolByName, type ToolUseContext } from '../../Tool.js'
import type { AssistantMessage, Message } from '../../types/message.js'
import { all } from '../../utils/generators.js'
import { type MessageUpdateLazy, runToolUse } from './toolExecution.js'
import { createToolBatchSpan, endToolBatchSpan } from '../langfuse/index.js'
function getMaxToolUseConcurrency(): number {
return (
@@ -22,7 +23,18 @@ export async function* runTools(
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
let currentContext = toolUseContext
// Wrap all tool calls in this turn under a single Langfuse turn span
const turnSpan = toolUseMessages.length > 0
? createToolBatchSpan(toolUseContext.langfuseTrace ?? null, {
toolNames: toolUseMessages.map(b => b.name),
batchIndex: 0,
})
: null
const contextWithTurn = turnSpan
? { ...toolUseContext, langfuseBatchSpan: turnSpan }
: toolUseContext
let currentContext = contextWithTurn
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
toolUseMessages,
currentContext,
@@ -79,6 +91,8 @@ export async function* runTools(
}
}
}
endToolBatchSpan(turnSpan)
}
type Batch = { isConcurrencySafe: boolean; blocks: ToolUseBlock[] }