fix(agent): align hook respond path with normal tool execution flow
The hook respond code path was missing several critical behaviors that existed in normal tool execution: - Add logging for tool calls with arguments preview - Add is_tool_call metadata to user-facing messages - Handle attachment delivery failures by setting error state and notifying LLM - Set ResponseHandled=false when using bus for media delivery - Check for steering messages and graceful interrupts after tool execution, skipping remaining tools when appropriate - Poll for SubTurn results that arrived during tool execution This ensures consistent behavior between hook-responded tool calls and normally executed tool calls.
This commit is contained in:
parent
ebedbebc7c
commit
bbcda9a15c
1 changed files with 83 additions and 1 deletions
|
|
@ -2358,6 +2358,15 @@ turnLoop:
|
||||||
if toolReq != nil && toolReq.HookResult != nil {
|
if toolReq != nil && toolReq.HookResult != nil {
|
||||||
hookResult := toolReq.HookResult
|
hookResult := toolReq.HookResult
|
||||||
|
|
||||||
|
argsJSON, _ := json.Marshal(toolArgs)
|
||||||
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
|
logger.InfoCF("agent", fmt.Sprintf("Tool call (hook respond): %s(%s)", toolName, argsPreview),
|
||||||
|
map[string]any{
|
||||||
|
"agent_id": ts.agent.ID,
|
||||||
|
"tool": toolName,
|
||||||
|
"iteration": iteration,
|
||||||
|
})
|
||||||
|
|
||||||
// Emit ToolExecStart event (same as normal tool execution)
|
// Emit ToolExecStart event (same as normal tool execution)
|
||||||
al.emitEvent(
|
al.emitEvent(
|
||||||
EventKindToolExecStart,
|
EventKindToolExecStart,
|
||||||
|
|
@ -2399,6 +2408,9 @@ turnLoop:
|
||||||
Channel: ts.channel,
|
Channel: ts.channel,
|
||||||
ChatID: ts.chatID,
|
ChatID: ts.chatID,
|
||||||
Content: hookResult.ForUser,
|
Content: hookResult.ForUser,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"is_tool_call": "true",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2431,9 +2443,14 @@ turnLoop:
|
||||||
"chat_id": ts.chatID,
|
"chat_id": ts.chatID,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
// Same as normal tool execution: notify LLM about delivery failure
|
||||||
|
hookResult.IsError = true
|
||||||
|
hookResult.ForLLM = fmt.Sprintf("failed to deliver attachment: %v", err)
|
||||||
}
|
}
|
||||||
} else if al.bus != nil {
|
} else if al.bus != nil {
|
||||||
al.bus.PublishOutboundMedia(ctx, outboundMedia)
|
al.bus.PublishOutboundMedia(ctx, outboundMedia)
|
||||||
|
// Same as normal tool execution: bus only queues, media not yet delivered
|
||||||
|
hookResult.ResponseHandled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2484,9 +2501,74 @@ turnLoop:
|
||||||
if !ts.opts.NoHistory {
|
if !ts.opts.NoHistory {
|
||||||
ts.agent.Sessions.AddFullMessage(ts.sessionKey, toolResultMsg)
|
ts.agent.Sessions.AddFullMessage(ts.sessionKey, toolResultMsg)
|
||||||
ts.recordPersistedMessage(toolResultMsg)
|
ts.recordPersistedMessage(toolResultMsg)
|
||||||
|
ts.ingestMessage(turnCtx, al, toolResultMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as normal tool execution: check for steering/interrupt/SubTurn after each tool
|
||||||
|
if steerMsgs := al.dequeueSteeringMessagesForScope(ts.sessionKey); len(steerMsgs) > 0 {
|
||||||
|
pendingMessages = append(pendingMessages, steerMsgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
skipReason := ""
|
||||||
|
skipMessage := ""
|
||||||
|
if len(pendingMessages) > 0 {
|
||||||
|
skipReason = "queued user steering message"
|
||||||
|
skipMessage = "Skipped due to queued user message."
|
||||||
|
} else if gracefulPending, _ := ts.gracefulInterruptRequested(); gracefulPending {
|
||||||
|
skipReason = "graceful interrupt requested"
|
||||||
|
skipMessage = "Skipped due to graceful interrupt."
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipReason != "" {
|
||||||
|
remaining := len(normalizedToolCalls) - i - 1
|
||||||
|
if remaining > 0 {
|
||||||
|
logger.InfoCF("agent", "Turn checkpoint: skipping remaining tools after hook respond",
|
||||||
|
map[string]any{
|
||||||
|
"agent_id": ts.agent.ID,
|
||||||
|
"completed": i + 1,
|
||||||
|
"skipped": remaining,
|
||||||
|
"reason": skipReason,
|
||||||
|
})
|
||||||
|
for j := i + 1; j < len(normalizedToolCalls); j++ {
|
||||||
|
skippedTC := normalizedToolCalls[j]
|
||||||
|
al.emitEvent(
|
||||||
|
EventKindToolExecSkipped,
|
||||||
|
ts.eventMeta("runTurn", "turn.tool.skipped"),
|
||||||
|
ToolExecSkippedPayload{
|
||||||
|
Tool: skippedTC.Name,
|
||||||
|
Reason: skipReason,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
skippedMsg := providers.Message{
|
||||||
|
Role: "tool",
|
||||||
|
Content: skipMessage,
|
||||||
|
ToolCallID: skippedTC.ID,
|
||||||
|
}
|
||||||
|
messages = append(messages, skippedMsg)
|
||||||
|
if !ts.opts.NoHistory {
|
||||||
|
ts.agent.Sessions.AddFullMessage(ts.sessionKey, skippedMsg)
|
||||||
|
ts.recordPersistedMessage(skippedMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also poll for any SubTurn results that arrived during tool execution.
|
||||||
|
if ts.pendingResults != nil {
|
||||||
|
select {
|
||||||
|
case result, ok := <-ts.pendingResults:
|
||||||
|
if ok && result != nil && result.ForLLM != "" {
|
||||||
|
content := al.cfg.FilterSensitiveData(result.ForLLM)
|
||||||
|
msg := providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)}
|
||||||
|
messages = append(messages, msg)
|
||||||
|
ts.agent.Sessions.AddFullMessage(ts.sessionKey, msg)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// No results available
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip subsequent tool execution flow
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// If no HookResult, fall back to continue with warning
|
// If no HookResult, fall back to continue with warning
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue