fix: emit tool.started immediately on function_call and add duration to tool.completed (#647)

- Emit tool.started on response.output_item.added instead of .done
- Track startedAt timestamp for each tool call to calculate duration
- Include duration (2 decimal places) and error status in tool.completed
- Fix response.completed fallback to emit tool.started before tool.completed
- Update website license from MIT to BSL-1.1

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-12 12:08:12 +08:00
committed by GitHub
parent 13061f8880
commit 96866f36e5
3 changed files with 28 additions and 19 deletions
@@ -1101,8 +1101,21 @@ export class ChatRunSocket {
if (item.type !== 'function_call') return null
const callId = item.call_id || item.id
if (!callId) return null
run.toolCalls.set(callId, responseFunctionCallToToolCall(item))
return null
const toolCall = responseFunctionCallToToolCall(item)
run.toolCalls.set(callId, { ...toolCall, startedAt: Date.now() })
return {
event: 'tool.started',
payload: {
event: 'tool.started',
run_id: run.responseId,
response_id: run.responseId,
tool_call_id: callId,
tool: toolCall.function.name,
name: toolCall.function.name,
arguments: toolCall.function.arguments,
preview: summarizeToolArguments(toolCall.function.arguments),
},
}
}
if (eventType === 'response.output_item.done') {
@@ -1111,7 +1124,8 @@ export class ChatRunSocket {
const callId = item.call_id || item.id
if (!callId) return null
const toolCall = responseFunctionCallToToolCall(item)
run.toolCalls.set(callId, toolCall)
const existing = run.toolCalls.get(callId)
run.toolCalls.set(callId, { ...toolCall, startedAt: existing?.startedAt || Date.now() })
const key = `assistant:${callId}`
if (!run.insertedKeys.has(key)) {
@@ -1127,19 +1141,7 @@ export class ChatRunSocket {
timestamp: now(),
})
}
return {
event: 'tool.started',
payload: {
event: 'tool.started',
run_id: run.responseId,
response_id: run.responseId,
tool_call_id: callId,
tool: toolCall.function.name,
name: toolCall.function.name,
arguments: toolCall.function.arguments,
preview: summarizeToolArguments(toolCall.function.arguments),
},
}
return null
}
if (item.type === 'function_call_output') {
@@ -1147,7 +1149,11 @@ export class ChatRunSocket {
if (!callId) return null
const key = `tool:${callId}`
const output = typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? '')
const toolName = run.toolCalls.get(callId)?.function?.name || null
const toolCallEntry = run.toolCalls.get(callId)
const toolName = toolCallEntry?.function?.name || null
const startedAt = toolCallEntry?.startedAt
const duration = startedAt ? Math.round((Date.now() - startedAt) / 10) / 100 : undefined
const hasError = typeof item.output === 'string' && item.output.startsWith('Error')
if (!run.insertedKeys.has(key)) {
run.insertedKeys.add(key)
state.messages.push({
@@ -1171,6 +1177,8 @@ export class ChatRunSocket {
tool: toolName,
name: toolName,
output,
duration,
error: hasError || undefined,
},
}
}
@@ -1182,6 +1190,7 @@ export class ChatRunSocket {
const output = Array.isArray(response.output) ? response.output : []
for (const item of output) {
if (item.type === 'function_call') {
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.added', { item })
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.done', { item })
} else if (item.type === 'function_call_output') {
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.done', { item })
+1 -1
View File
@@ -101,7 +101,7 @@ export default {
},
footer: {
description: 'Self-hosted AI chat dashboard for Hermes Agent.',
license: 'MIT License',
license: 'BSL-1.1 License',
madeWith: 'Built with Vue 3, Naive UI, and TypeScript.',
},
docs: {
+1 -1
View File
@@ -101,7 +101,7 @@ export default {
},
footer: {
description: 'Hermes Agent 的自托管 AI 聊天仪表板。',
license: 'MIT 开源协议',
license: 'BSL-1.1 开源协议',
madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。',
},
docs: {