Skip to content

Commit 0896917

Browse files
committed
fix subagent dm completion delivery
1 parent 8ff61be commit 0896917

2 files changed

Lines changed: 91 additions & 8 deletions

File tree

src/agents/subagent-announce-delivery.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,56 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
12151215
);
12161216
});
12171217

1218+
it("directly delivers direct-message subagent text when the announce agent omits the result", async () => {
1219+
const callGateway = createGatewayMock({
1220+
result: {
1221+
payloads: [{ text: "TG88042_NO_REOUTPUT" }],
1222+
},
1223+
});
1224+
const sendMessage = createSendMessageMock();
1225+
1226+
const result = await deliverDiscordDirectMessageCompletion({
1227+
callGateway,
1228+
sendMessage,
1229+
internalEvents: [
1230+
{
1231+
type: "task_completion",
1232+
source: "subagent",
1233+
childSessionKey: "agent:worker:subagent:child",
1234+
childSessionId: "child-session-id",
1235+
announceType: "subagent task",
1236+
taskLabel: "direct completion smoke",
1237+
status: "ok",
1238+
statusLabel: "completed successfully",
1239+
result: "TG88042_CHILD",
1240+
replyInstruction: "Summarize the result.",
1241+
},
1242+
],
1243+
});
1244+
1245+
expectRecordFields(result, {
1246+
delivered: true,
1247+
path: "direct",
1248+
});
1249+
expect(sendMessage).toHaveBeenCalledWith(
1250+
expect.objectContaining({
1251+
channel: "discord",
1252+
accountId: "acct-1",
1253+
to: "dm:U123",
1254+
content: "TG88042_CHILD",
1255+
idempotencyKey: "announce-dm-fallback-empty:text-direct",
1256+
}),
1257+
);
1258+
expectGatewayAgentParams(callGateway, {
1259+
deliver: false,
1260+
channel: "discord",
1261+
accountId: "acct-1",
1262+
to: "dm:U123",
1263+
threadId: undefined,
1264+
sourceReplyDeliveryMode: "message_tool_only",
1265+
});
1266+
});
1267+
12181268
it("does not directly deliver failed subagent placeholder output", async () => {
12191269
const callGateway = createGatewayMock({
12201270
result: {
@@ -4176,14 +4226,18 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
41764226
});
41774227
});
41784228

4179-
it("keeps automatic final delivery for direct subagent completions", async () => {
4229+
it("requires message-tool delivery for direct subagent completions", async () => {
41804230
const callGateway = createGatewayMock({
41814231
result: {
4182-
payloads: [{ text: "The subagent is done." }],
4232+
payloads: [{ text: "The subagent is done: child completion output" }],
4233+
didSendViaMessagingTool: true,
4234+
messagingToolSentTexts: ["The subagent is done: child completion output"],
41834235
},
41844236
});
4237+
const sendMessage = createSendMessageMock();
41854238
const result = await deliverDiscordDirectMessageCompletion({
41864239
callGateway,
4240+
sendMessage,
41874241
sourceTool: "subagent_announce",
41884242
internalEvents: [
41894243
{
@@ -4206,12 +4260,14 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
42064260
path: "direct",
42074261
});
42084262
expectGatewayAgentParams(callGateway, {
4209-
deliver: true,
4263+
deliver: false,
42104264
channel: "discord",
42114265
accountId: "acct-1",
42124266
to: "dm:U123",
42134267
threadId: undefined,
4268+
sourceReplyDeliveryMode: "message_tool_only",
42144269
});
4270+
expect(sendMessage).not.toHaveBeenCalled();
42154271
});
42164272

42174273
it("falls back to the external requester route when completion origin is internal", async () => {

src/agents/subagent-announce-delivery.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,14 @@ async function sendSubagentAnnounceDirectly(params: {
12291229
directOrigin: effectiveDirectOrigin,
12301230
requesterSessionOrigin,
12311231
});
1232-
const requiresMessageToolDelivery = completionRouteRequiresMessageToolDelivery;
1232+
const subagentDirectMessageCompletionRequiresMessageTool =
1233+
params.expectsCompletionMessage &&
1234+
isSubagentCompletion &&
1235+
deliveryTarget.deliver &&
1236+
isDirectMessageDeliveryTarget(deliveryTarget, canonicalRequesterSessionKey);
1237+
const requiresMessageToolDelivery =
1238+
completionRouteRequiresMessageToolDelivery ||
1239+
subagentDirectMessageCompletionRequiresMessageTool;
12331240
const requesterActivity = resolveRequesterSessionActivity(canonicalRequesterSessionKey);
12341241
if (
12351242
params.expectsCompletionMessage &&
@@ -1405,7 +1412,7 @@ async function sendSubagentAnnounceDirectly(params: {
14051412
}
14061413
if (
14071414
params.expectsCompletionMessage &&
1408-
shouldDeliverAgentFinal &&
1415+
(shouldDeliverAgentFinal || subagentDirectMessageCompletionRequiresMessageTool) &&
14091416
isSubagentCompletion &&
14101417
isIncompleteAnnounceAgentResultError(err)
14111418
) {
@@ -1456,9 +1463,10 @@ async function sendSubagentAnnounceDirectly(params: {
14561463
};
14571464
}
14581465

1459-
const directDeliveryFailure = shouldDeliverAgentFinal
1460-
? getGatewayAgentCommandDeliveryFailure(directAnnounceResponse)
1461-
: undefined;
1466+
const directDeliveryFailure =
1467+
shouldDeliverAgentFinal || requiresMessageToolDelivery
1468+
? getGatewayAgentCommandDeliveryFailure(directAnnounceResponse)
1469+
: undefined;
14621470
const missingExpectedMediaUrls =
14631471
agentMediatedCompletion && expectedMediaUrls.length > 0
14641472
? resolveGeneratedMediaDirectFallbackUrls({
@@ -1527,6 +1535,25 @@ async function sendSubagentAnnounceDirectly(params: {
15271535
!hasGatewayAgentMessagingToolDeliveryEvidence(directAnnounceResponse) &&
15281536
!hasIntentionalSilentGatewayAgentPayload(directAnnounceResponse)
15291537
) {
1538+
if (hasFailedSubagentNoOutputCompletion(params.internalEvents)) {
1539+
return {
1540+
delivered: false,
1541+
path: "direct",
1542+
error: "completion agent did not produce a visible reply",
1543+
};
1544+
}
1545+
if (subagentDirectMessageCompletionRequiresMessageTool) {
1546+
const textDelivery = await deliverTextCompletionDirect({
1547+
cfg,
1548+
requesterSessionKey: canonicalRequesterSessionKey,
1549+
directIdempotencyKey: params.directIdempotencyKey,
1550+
deliveryTarget,
1551+
internalEvents: params.internalEvents,
1552+
});
1553+
if (textDelivery) {
1554+
return textDelivery;
1555+
}
1556+
}
15301557
return {
15311558
delivered: false,
15321559
path: "direct",

0 commit comments

Comments
 (0)