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:
@@ -1099,34 +1099,10 @@ export class ChatRunSocket {
|
|||||||
if (eventType === 'response.output_item.added') {
|
if (eventType === 'response.output_item.added') {
|
||||||
const item = parsed.item || parsed.output_item || parsed
|
const item = parsed.item || parsed.output_item || parsed
|
||||||
if (item.type !== 'function_call') return null
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'response.output_item.done') {
|
|
||||||
const item = parsed.item || parsed.output_item || parsed
|
|
||||||
if (item.type === 'function_call') {
|
|
||||||
const callId = item.call_id || item.id
|
const callId = item.call_id || item.id
|
||||||
if (!callId) return null
|
if (!callId) return null
|
||||||
const toolCall = responseFunctionCallToToolCall(item)
|
const toolCall = responseFunctionCallToToolCall(item)
|
||||||
run.toolCalls.set(callId, toolCall)
|
run.toolCalls.set(callId, { ...toolCall, startedAt: Date.now() })
|
||||||
|
|
||||||
const key = `assistant:${callId}`
|
|
||||||
if (!run.insertedKeys.has(key)) {
|
|
||||||
run.insertedKeys.add(key)
|
|
||||||
state.messages.push({
|
|
||||||
id: state.messages.length + 1,
|
|
||||||
session_id: sessionId,
|
|
||||||
runMarker,
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
tool_calls: [toolCall],
|
|
||||||
finish_reason: 'tool_calls',
|
|
||||||
timestamp: now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
event: 'tool.started',
|
event: 'tool.started',
|
||||||
payload: {
|
payload: {
|
||||||
@@ -1142,12 +1118,42 @@ export class ChatRunSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType === 'response.output_item.done') {
|
||||||
|
const item = parsed.item || parsed.output_item || parsed
|
||||||
|
if (item.type === 'function_call') {
|
||||||
|
const callId = item.call_id || item.id
|
||||||
|
if (!callId) return null
|
||||||
|
const toolCall = responseFunctionCallToToolCall(item)
|
||||||
|
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)) {
|
||||||
|
run.insertedKeys.add(key)
|
||||||
|
state.messages.push({
|
||||||
|
id: state.messages.length + 1,
|
||||||
|
session_id: sessionId,
|
||||||
|
runMarker,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
tool_calls: [toolCall],
|
||||||
|
finish_reason: 'tool_calls',
|
||||||
|
timestamp: now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (item.type === 'function_call_output') {
|
if (item.type === 'function_call_output') {
|
||||||
const callId = item.call_id || item.id
|
const callId = item.call_id || item.id
|
||||||
if (!callId) return null
|
if (!callId) return null
|
||||||
const key = `tool:${callId}`
|
const key = `tool:${callId}`
|
||||||
const output = typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? '')
|
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)) {
|
if (!run.insertedKeys.has(key)) {
|
||||||
run.insertedKeys.add(key)
|
run.insertedKeys.add(key)
|
||||||
state.messages.push({
|
state.messages.push({
|
||||||
@@ -1171,6 +1177,8 @@ export class ChatRunSocket {
|
|||||||
tool: toolName,
|
tool: toolName,
|
||||||
name: toolName,
|
name: toolName,
|
||||||
output,
|
output,
|
||||||
|
duration,
|
||||||
|
error: hasError || undefined,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1182,6 +1190,7 @@ export class ChatRunSocket {
|
|||||||
const output = Array.isArray(response.output) ? response.output : []
|
const output = Array.isArray(response.output) ? response.output : []
|
||||||
for (const item of output) {
|
for (const item of output) {
|
||||||
if (item.type === 'function_call') {
|
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 })
|
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.done', { item })
|
||||||
} else if (item.type === 'function_call_output') {
|
} else if (item.type === 'function_call_output') {
|
||||||
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.done', { item })
|
this.applyResponseStreamEvent(state, sessionId, runMarker, 'response.output_item.done', { item })
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default {
|
|||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
description: 'Self-hosted AI chat dashboard for Hermes Agent.',
|
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.',
|
madeWith: 'Built with Vue 3, Naive UI, and TypeScript.',
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default {
|
|||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
description: 'Hermes Agent 的自托管 AI 聊天仪表板。',
|
description: 'Hermes Agent 的自托管 AI 聊天仪表板。',
|
||||||
license: 'MIT 开源协议',
|
license: 'BSL-1.1 开源协议',
|
||||||
madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。',
|
madeWith: '使用 Vue 3、Naive UI 和 TypeScript 构建。',
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
|
|||||||
Reference in New Issue
Block a user