Skip to content

Commit 20874c1

Browse files
committed
fix(auto-reply): count message tool sends as delivery
1 parent d4867ec commit 20874c1

15 files changed

Lines changed: 130 additions & 6 deletions

extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,31 @@ describe("whatsapp inbound dispatch", () => {
11391139
expect(rememberSentText).toHaveBeenCalledTimes(1);
11401140
});
11411141

1142+
it("returns success when shared dispatch observes message-tool delivery", async () => {
1143+
const deliverReply = vi.fn(async () => acceptedDeliveryResult());
1144+
const rememberSentText = vi.fn();
1145+
dispatchReplyWithBufferedBlockDispatcherMock.mockImplementationOnce(
1146+
async (params: CapturedDispatchParams) => {
1147+
capturedDispatchParams = params;
1148+
return {
1149+
queuedFinal: false,
1150+
counts: { tool: 0, block: 0, final: 0 },
1151+
observedReplyDelivery: true,
1152+
};
1153+
},
1154+
);
1155+
1156+
await expect(
1157+
dispatchBufferedReply({
1158+
deliverReply,
1159+
rememberSentText,
1160+
}),
1161+
).resolves.toBe(true);
1162+
1163+
expect(deliverReply).not.toHaveBeenCalled();
1164+
expect(rememberSentText).not.toHaveBeenCalled();
1165+
});
1166+
11421167
it("does not treat generated WhatsApp text as sent when the provider did not accept it", async () => {
11431168
const deliverReply = vi.fn(async () => unacceptedDeliveryResult());
11441169
const rememberSentText = vi.fn();

extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
654654
void statusReactionController.setThinking();
655655
}
656656

657-
const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({
657+
const dispatchResult = await dispatchReplyWithBufferedBlockDispatcher({
658658
ctx: params.context,
659659
cfg: params.cfg,
660660
replyResolver: params.replyResolver,
@@ -798,7 +798,8 @@ export async function dispatchWhatsAppBufferedReply(params: {
798798
: {}),
799799
},
800800
});
801-
const didQueueVisibleReply = hasVisibleInboundReplyDispatch({ queuedFinal, counts });
801+
const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
802+
const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
802803
if (!didQueueVisibleReply) {
803804
if (statusReactionController) {
804805
void finalizeWhatsAppStatusReaction({
@@ -819,8 +820,8 @@ export async function dispatchWhatsAppBufferedReply(params: {
819820
if (statusReactionController) {
820821
void finalizeWhatsAppStatusReaction({
821822
controller: statusReactionController,
822-
outcome: didSendReply ? "done" : "error",
823-
hasFinalResponse: didSendReply,
823+
outcome: didDeliverVisibleReply ? "done" : "error",
824+
hasFinalResponse: didDeliverVisibleReply,
824825
removeAckAfterReply,
825826
timing: statusReactionTiming,
826827
});
@@ -830,7 +831,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
830831
params.groupHistories.set(params.groupHistoryKey, []);
831832
}
832833

833-
return didSendReply;
834+
return didDeliverVisibleReply;
834835
}
835836

836837
async function finalizeWhatsAppStatusReaction(params: {

src/agents/embedded-agent-runner/run.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ function normalizeEmbeddedRunAttemptResult(
322322
messagingToolSourceReplyPayloads?:
323323
| EmbeddedRunAttemptForRunner["messagingToolSourceReplyPayloads"]
324324
| null;
325+
didDeliverSourceReplyViaMessageTool?: boolean | null;
325326
itemLifecycle?: EmbeddedRunAttemptForRunner["itemLifecycle"] | null;
326327
};
327328
return {
@@ -334,6 +335,7 @@ function normalizeEmbeddedRunAttemptResult(
334335
messagingToolSentMediaUrls: raw.messagingToolSentMediaUrls ?? [],
335336
messagingToolSentTargets: raw.messagingToolSentTargets ?? [],
336337
messagingToolSourceReplyPayloads: raw.messagingToolSourceReplyPayloads ?? [],
338+
didDeliverSourceReplyViaMessageTool: raw.didDeliverSourceReplyViaMessageTool === true,
337339
itemLifecycle: raw.itemLifecycle ?? {
338340
startedCount: 0,
339341
completedCount: 0,
@@ -3061,6 +3063,8 @@ export async function runEmbeddedAgent(
30613063
agentHarnessResultClassification: attempt.agentHarnessResultClassification,
30623064
},
30633065
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
3066+
didDeliverSourceReplyViaMessageTool:
3067+
attempt.didDeliverSourceReplyViaMessageTool === true,
30643068
didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt,
30653069
messagingToolSentTexts: attempt.messagingToolSentTexts,
30663070
messagingToolSentMediaUrls: attempt.messagingToolSentMediaUrls,
@@ -3306,6 +3310,8 @@ export async function runEmbeddedAgent(
33063310
agentHarnessResultClassification: attempt.agentHarnessResultClassification,
33073311
},
33083312
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
3313+
didDeliverSourceReplyViaMessageTool:
3314+
attempt.didDeliverSourceReplyViaMessageTool === true,
33093315
didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt,
33103316
messagingToolSentTexts: attempt.messagingToolSentTexts,
33113317
messagingToolSentMediaUrls: attempt.messagingToolSentMediaUrls,
@@ -3360,6 +3366,8 @@ export async function runEmbeddedAgent(
33603366
agentHarnessResultClassification: attempt.agentHarnessResultClassification,
33613367
},
33623368
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
3369+
didDeliverSourceReplyViaMessageTool:
3370+
attempt.didDeliverSourceReplyViaMessageTool === true,
33633371
didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt,
33643372
messagingToolSentTexts: attempt.messagingToolSentTexts,
33653373
messagingToolSentMediaUrls: attempt.messagingToolSentMediaUrls,
@@ -3481,6 +3489,8 @@ export async function runEmbeddedAgent(
34813489
agentHarnessResultClassification: attempt.agentHarnessResultClassification,
34823490
},
34833491
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
3492+
didDeliverSourceReplyViaMessageTool:
3493+
attempt.didDeliverSourceReplyViaMessageTool === true,
34843494
didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt,
34853495
messagingToolSentTexts: attempt.messagingToolSentTexts,
34863496
messagingToolSentMediaUrls: attempt.messagingToolSentMediaUrls,
@@ -3624,6 +3634,8 @@ export async function runEmbeddedAgent(
36243634
autoCompactionCount > 0 ? { lastTurnCompactions: autoCompactionCount } : undefined,
36253635
},
36263636
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
3637+
didDeliverSourceReplyViaMessageTool:
3638+
attempt.didDeliverSourceReplyViaMessageTool === true,
36273639
didSendDeterministicApprovalPrompt: attempt.didSendDeterministicApprovalPrompt,
36283640
messagingToolSentTexts: attempt.messagingToolSentTexts,
36293641
messagingToolSentMediaUrls: attempt.messagingToolSentMediaUrls,

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,9 +2268,13 @@ export async function runEmbeddedAttempt(
22682268
applySystemPromptToSession(activeSession, nextSystemPrompt);
22692269
};
22702270
setActiveSessionSystemPrompt(systemPromptText);
2271+
let didDeliverSourceReplyViaMessageTool = false;
22712272
installMessageToolOnlyTerminalHook({
22722273
agent: activeSession.agent,
22732274
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
2275+
onDeliveredSourceReply: () => {
2276+
didDeliverSourceReplyViaMessageTool = true;
2277+
},
22742278
});
22752279
prepStages.mark("agent-session");
22762280
if (isRawModelRun) {
@@ -5130,6 +5134,7 @@ export async function runEmbeddedAttempt(
51305134
currentAttemptAssistant,
51315135
lastToolError,
51325136
didSendViaMessagingTool: didSendViaMessagingTool(),
5137+
didDeliverSourceReplyViaMessageTool,
51335138
didSendDeterministicApprovalPrompt: didSendDeterministicApprovalPromptNow,
51345139
messagingToolSentTexts: getMessagingToolSentTexts(),
51355140
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),

src/agents/embedded-agent-runner/run/message-tool-terminal.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,11 @@ describe("message-tool-only terminal sends", () => {
160160
details: { rewritten: true },
161161
}));
162162
const agent = { afterToolCall: previousAfterToolCall } as unknown as Agent;
163+
const onDeliveredSourceReply = vi.fn();
163164
installMessageToolOnlyTerminalHook({
164165
agent,
165166
sourceReplyDeliveryMode: "message_tool_only",
167+
onDeliveredSourceReply,
166168
});
167169

168170
await expect(
@@ -178,6 +180,7 @@ describe("message-tool-only terminal sends", () => {
178180
terminate: true,
179181
});
180182
expect(previousAfterToolCall).toHaveBeenCalledTimes(1);
183+
expect(onDeliveredSourceReply).toHaveBeenCalledTimes(1);
181184
});
182185

183186
it("leaves existing after-tool-call output alone when the send failed", async () => {
@@ -187,9 +190,11 @@ describe("message-tool-only terminal sends", () => {
187190
isError: true,
188191
}));
189192
const agent = { afterToolCall: previousAfterToolCall } as unknown as Agent;
193+
const onDeliveredSourceReply = vi.fn();
190194
installMessageToolOnlyTerminalHook({
191195
agent,
192196
sourceReplyDeliveryMode: "message_tool_only",
197+
onDeliveredSourceReply,
193198
});
194199

195200
await expect(
@@ -205,6 +210,7 @@ describe("message-tool-only terminal sends", () => {
205210
isError: true,
206211
});
207212
expect(previousAfterToolCall).toHaveBeenCalledTimes(1);
213+
expect(onDeliveredSourceReply).not.toHaveBeenCalled();
208214
});
209215

210216
it("does not install a wrapper for non-message-tool-only delivery", async () => {

src/agents/embedded-agent-runner/run/message-tool-terminal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export function shouldTerminateAfterMessageToolOnlySend(params: {
192192
export function installMessageToolOnlyTerminalHook(params: {
193193
agent: Agent;
194194
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
195+
onDeliveredSourceReply?: () => void;
195196
}): void {
196197
if (params.sourceReplyDeliveryMode !== "message_tool_only") {
197198
return;
@@ -206,6 +207,7 @@ export function installMessageToolOnlyTerminalHook(params: {
206207
hookResult,
207208
})
208209
) {
210+
params.onDeliveredSourceReply?.();
209211
return { ...hookResult, terminate: true };
210212
}
211213
return hookResult;

src/agents/embedded-agent-runner/run/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export type EmbeddedRunAttemptResult = {
157157
currentAttemptAssistant?: AssistantMessage | undefined;
158158
lastToolError?: ToolErrorSummary;
159159
didSendViaMessagingTool: boolean;
160+
didDeliverSourceReplyViaMessageTool?: boolean;
160161
didSendDeterministicApprovalPrompt?: boolean;
161162
messagingToolSentTexts: string[];
162163
messagingToolSentMediaUrls: string[];

src/agents/embedded-agent-runner/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export type EmbeddedAgentRunResult = {
193193
// True if a messaging tool successfully sent a message.
194194
// Used to suppress agent's confirmation text.
195195
didSendViaMessagingTool?: boolean;
196+
// True if message_tool_only delivered a visible reply to the current source conversation.
197+
didDeliverSourceReplyViaMessageTool?: boolean;
196198
// True if a deterministic approval prompt was sent through the tool-result channel.
197199
didSendDeterministicApprovalPrompt?: boolean;
198200
// Texts successfully sent via messaging tools during the run.

src/auto-reply/get-reply-options.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ export type GetReplyOptions = {
197197
queuedFollowupLifecycle?: QueuedReplyLifecycle;
198198
/** Allow channel-owned progress UI while final/source reply delivery remains message-tool-only. */
199199
allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean;
200+
/** Called when a suppressed source reply mode observes visible delivery through another path. */
201+
onObservedReplyDelivery?: () => Promise<void> | void;
200202
disableBlockStreaming?: boolean;
201203
/** Timeout for block reply delivery (ms). */
202204
blockReplyTimeoutMs?: number;

src/auto-reply/reply/agent-runner.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../../agents/agent-scope.js";
1010
import { resolveContextTokensForModel } from "../../agents/context.js";
1111
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
12+
import { hasVisibleAgentPayload } from "../../agents/embedded-agent-runner/delivery-evidence.js";
1213
import {
1314
formatEmbeddedAgentQueueFailureSummary,
1415
queueEmbeddedAgentMessageWithOutcomeAsync,
@@ -1818,6 +1819,15 @@ export async function runReplyAgent(params: {
18181819
messagingToolSentMediaUrls: runResult.messagingToolSentMediaUrls,
18191820
messagingToolSentTargets: runResult.messagingToolSentTargets,
18201821
});
1822+
const committedMessagingToolSourceReplyDelivery =
1823+
runResult.didDeliverSourceReplyViaMessageTool === true ||
1824+
hasVisibleAgentPayload({ payloads: runResult.messagingToolSourceReplyPayloads });
1825+
if (
1826+
opts?.sourceReplyDeliveryMode === "message_tool_only" &&
1827+
committedMessagingToolSourceReplyDelivery
1828+
) {
1829+
await opts.onObservedReplyDelivery?.();
1830+
}
18211831
const returnSilentFallbackFailureIfNeeded = async (): Promise<ReplyPayload | undefined> => {
18221832
const silentFallbackFailurePayload = buildSilentFallbackFailurePayload({
18231833
fallbackTransition,

0 commit comments

Comments
 (0)