Agents: fix subagent completion delivery to origin channel

This commit is contained in:
Gustavo Madeira Santana
2026-02-17 22:35:59 -05:00
parent ca43efa965
commit e8816c554f
5 changed files with 102 additions and 60 deletions

View File

@@ -17,6 +17,7 @@ import {
mergeDeliveryContext,
normalizeDeliveryContext,
} from "../utils/delivery-context.js";
import { isDeliverableMessageChannel } from "../utils/message-channel.js";
import {
buildAnnounceIdFromChildRun,
buildAnnounceIdempotencyKey,
@@ -44,6 +45,19 @@ type SubagentAnnounceDeliveryResult = {
error?: string;
};
function buildCompletionDeliveryMessage(params: {
findings: string;
subagentName: string;
}): string {
const findingsText = params.findings.trim();
const hasFindings = findingsText.length > 0 && findingsText !== "(no output)";
const header = `✅ Subagent ${params.subagentName} finished`;
if (!hasFindings) {
return header;
}
return `${header}\n\n${findingsText}`;
}
function summarizeDeliveryError(error: unknown): string {
if (error instanceof Error) {
return error.message || "error";
@@ -256,10 +270,23 @@ function resolveAnnounceOrigin(
entry?: DeliveryContextSource,
requesterOrigin?: DeliveryContext,
): DeliveryContext | undefined {
const normalizedRequester = normalizeDeliveryContext(requesterOrigin);
const normalizedEntry = deliveryContextFromSession(entry);
if (normalizedRequester?.channel && !isDeliverableMessageChannel(normalizedRequester.channel)) {
// Ignore internal/non-deliverable channel hints (for example webchat)
// so a valid persisted route can still be used for outbound delivery.
return mergeDeliveryContext(
{
accountId: normalizedRequester.accountId,
threadId: normalizedRequester.threadId,
},
normalizedEntry,
);
}
// requesterOrigin (captured at spawn time) reflects the channel the user is
// actually on and must take priority over the session entry, which may carry
// stale lastChannel / lastTo values from a previous channel interaction.
return mergeDeliveryContext(requesterOrigin, deliveryContextFromSession(entry));
return mergeDeliveryContext(normalizedRequester, normalizedEntry);
}
async function sendAnnounce(item: AnnounceQueueItem) {
@@ -411,24 +438,29 @@ async function sendSubagentAnnounceDirectly(params: {
directOrigin?: DeliveryContext;
requesterIsSubagent: boolean;
}): Promise<SubagentAnnounceDeliveryResult> {
const cfg = loadConfig();
const canonicalRequesterSessionKey = resolveRequesterStoreKey(
cfg,
params.targetRequesterSessionKey,
);
try {
const completionDirectOrigin = normalizeDeliveryContext(params.completionDirectOrigin);
const completionChannel =
const completionChannelRaw =
typeof completionDirectOrigin?.channel === "string"
? completionDirectOrigin.channel.trim()
: "";
const completionChannel =
completionChannelRaw && isDeliverableMessageChannel(completionChannelRaw)
? completionChannelRaw
: "";
const completionTo =
typeof completionDirectOrigin?.to === "string" ? completionDirectOrigin.to.trim() : "";
const completionHasThreadHint =
completionDirectOrigin?.threadId != null &&
String(completionDirectOrigin.threadId).trim() !== "";
const hasCompletionDirectTarget =
!params.requesterIsSubagent && Boolean(completionChannel) && Boolean(completionTo);
if (
params.expectsCompletionMessage &&
hasCompletionDirectTarget &&
!completionHasThreadHint &&
params.completionMessage?.trim()
) {
await callGateway({
@@ -437,7 +469,7 @@ async function sendSubagentAnnounceDirectly(params: {
channel: completionChannel,
to: completionTo,
accountId: completionDirectOrigin?.accountId,
sessionKey: params.targetRequesterSessionKey,
sessionKey: canonicalRequesterSessionKey,
message: params.completionMessage,
idempotencyKey: params.directIdempotencyKey,
},
@@ -455,11 +487,10 @@ async function sendSubagentAnnounceDirectly(params: {
directOrigin?.threadId != null && directOrigin.threadId !== ""
? String(directOrigin.threadId)
: undefined;
await callGateway({
method: "agent",
params: {
sessionKey: params.targetRequesterSessionKey,
sessionKey: canonicalRequesterSessionKey,
message: params.triggerMessage,
deliver: !params.requesterIsSubagent,
channel: params.requesterIsSubagent ? undefined : directOrigin?.channel,
@@ -521,11 +552,11 @@ async function deliverSubagentAnnouncement(params: {
targetRequesterSessionKey: params.targetRequesterSessionKey,
triggerMessage: params.triggerMessage,
completionMessage: params.completionMessage,
expectsCompletionMessage: params.expectsCompletionMessage,
directIdempotencyKey: params.directIdempotencyKey,
completionDirectOrigin: params.completionDirectOrigin,
directOrigin: params.directOrigin,
requesterIsSubagent: params.requesterIsSubagent,
expectsCompletionMessage: params.expectsCompletionMessage,
});
if (direct.delivered || !params.expectsCompletionMessage) {
return direct;
@@ -806,6 +837,7 @@ export async function runSubagentAnnounceFlow(params: {
// Build instructional message for main agent
const announceType = params.announceType ?? "subagent task";
const taskLabel = params.label || params.task || "task";
const subagentName = resolveAgentIdFromSessionKey(params.childSessionKey);
const announceSessionId = childSessionId || "unknown";
const findings = reply || "(no output)";
let completionMessage = "";
@@ -872,7 +904,11 @@ export async function runSubagentAnnounceFlow(params: {
startedAt: params.startedAt,
endedAt: params.endedAt,
});
completionMessage = [
completionMessage = buildCompletionDeliveryMessage({
findings,
subagentName,
});
const internalSummaryMessage = [
`[System Message] [sessionId: ${announceSessionId}] A ${announceType} "${taskLabel}" just ${statusLabel}.`,
"",
"Result:",
@@ -880,7 +916,7 @@ export async function runSubagentAnnounceFlow(params: {
"",
statsLine,
].join("\n");
triggerMessage = [completionMessage, "", replyInstruction].join("\n");
triggerMessage = [internalSummaryMessage, "", replyInstruction].join("\n");
const announceId = buildAnnounceIdFromChildRun({
childSessionKey: params.childSessionKey,