diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index f86ecb8a9..70c8bb0a3 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -48,6 +48,8 @@ export type AgentRunLoopResult = autoCompactionCompleted: boolean; /** Payload keys sent directly (not via pipeline) during tool flush. */ directlySentBlockKeys?: Set; + /** Tool call IDs that were executed successfully (phase === "result" without error). */ + executedToolCallIds?: Set; } | { kind: "final"; payload: ReplyPayload }; @@ -82,6 +84,8 @@ export async function runAgentTurnWithFallback(params: { let autoCompactionCompleted = false; // Track payloads sent directly (not via pipeline) during tool flush to avoid duplicates. const directlySentBlockKeys = new Set(); + // Track tool calls that were executed successfully (completed with result phase). + const executedToolCallIds = new Set(); const runId = params.opts?.runId ?? crypto.randomUUID(); params.opts?.onAgentRunStart?.(runId); @@ -307,6 +311,14 @@ export async function runAgentTurnWithFallback(params: { if (phase === "start" || phase === "update") { await params.typingSignals.signalToolStart(); } + // Track successful tool execution (result phase without error). + if (phase === "result") { + const toolCallId = typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : ""; + const isError = Boolean(evt.data.isError); + if (toolCallId && !isError) { + executedToolCallIds.add(toolCallId); + } + } } // Track auto-compaction completion if (evt.stream === "compaction") { @@ -554,5 +566,6 @@ export async function runAgentTurnWithFallback(params: { didLogHeartbeatStrip, autoCompactionCompleted, directlySentBlockKeys: directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined, + executedToolCallIds: executedToolCallIds.size > 0 ? executedToolCallIds : undefined, }; } diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 227e6f17e..30de62225 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -328,7 +328,13 @@ export async function runReplyAgent(params: { return finalizeWithFollowup(runOutcome.payload, queueKey, runFollowupTurn); } - const { runResult, fallbackProvider, fallbackModel, directlySentBlockKeys } = runOutcome; + const { + runResult, + fallbackProvider, + fallbackModel, + directlySentBlockKeys, + executedToolCallIds, + } = runOutcome; let { didLogHeartbeatStrip, autoCompactionCompleted } = runOutcome; if ( @@ -391,8 +397,33 @@ export async function runReplyAgent(params: { // Drain any late tool/block deliveries before deciding there's "nothing to send". // Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and // keep the typing indicator stuck. - if (payloadArray.length === 0) + if (payloadArray.length === 0) { + // If tools were executed but no final response was generated, send a confirmation. + // Skip if verbose is enabled (onToolResult already sent notifications via tool callbacks) + // or if it's a heartbeat (which should be silent). + const hasExecutedTools = + executedToolCallIds && executedToolCallIds.size > 0; + const verboseEnabled = shouldEmitToolResult(); + // Only send confirmation if: + // 1. Tools were executed successfully + // 2. Verbose is disabled (onToolResult wasn't called, so no notifications were sent) + // 3. Not a heartbeat (heartbeats should be silent) + // 4. All pending tool tasks completed (to avoid race conditions) + if ( + hasExecutedTools && + !verboseEnabled && + !isHeartbeat && + pendingToolTasks.size === 0 + ) { + const confirmationPayload: ReplyPayload = { + text: "✅ Concluído", + }; + const taggedConfirmation = applyReplyToMode(confirmationPayload); + await signalTypingIfNeeded([taggedConfirmation], typingSignals); + return finalizeWithFollowup(taggedConfirmation, queueKey, runFollowupTurn); + } return finalizeWithFollowup(undefined, queueKey, runFollowupTurn); + } const payloadResult = buildReplyPayloads({ payloads: payloadArray,