Skip to content

Commit 96a4fae

Browse files
Fix webchat media completion handoff
1 parent e498d39 commit 96a4fae

4 files changed

Lines changed: 186 additions & 0 deletions

File tree

src/agents/embedded-agent-runner/delivery-evidence.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type AgentPayloadLike = {
1616
presentation?: unknown;
1717
interactive?: unknown;
1818
channelData?: unknown;
19+
attachments?: unknown;
1920
isError?: unknown;
2021
isReasoning?: unknown;
2122
};
@@ -170,6 +171,7 @@ export function hasVisibleAgentPayload(
170171
hasNonEmptyString(record.text) ||
171172
hasNonEmptyString(record.mediaUrl) ||
172173
hasNonEmptyStringArray(record.mediaUrls) ||
174+
hasNonEmptyArray(record.attachments) ||
173175
record.presentation ||
174176
record.interactive ||
175177
record.channelData,

src/agents/internal-events.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import {
77
formatGeneratedAttachmentLines,
8+
mediaUrlsFromGeneratedAttachments,
89
type AgentGeneratedAttachment,
910
} from "./generated-attachments.js";
1011
import {
@@ -61,6 +62,16 @@ function formatChildResultDataBlock(value: string): string {
6162
);
6263
}
6364

65+
function formatGeneratedMediaDirectiveLines(event: AgentTaskCompletionInternalEvent): string[] {
66+
const mediaUrls = Array.from(
67+
new Set([...(event.mediaUrls ?? []), ...mediaUrlsFromGeneratedAttachments(event.attachments)]),
68+
).filter((value) => value.trim().length > 0);
69+
if (mediaUrls.length === 0) {
70+
return [];
71+
}
72+
return ["Generated media:", ...mediaUrls.map((mediaUrl) => `MEDIA:${mediaUrl}`)];
73+
}
74+
6475
function formatTaskCompletionEvent(event: AgentTaskCompletionInternalEvent): string {
6576
const sessionKey = sanitizeSingleLineField(event.childSessionKey, "unknown");
6677
const sessionId = sanitizeSingleLineField(event.childSessionId ?? "unknown", "unknown");
@@ -69,6 +80,7 @@ function formatTaskCompletionEvent(event: AgentTaskCompletionInternalEvent): str
6980
const statusLabel = sanitizeSingleLineField(event.statusLabel, event.status);
7081
const result = formatChildResultDataBlock(event.result);
7182
const attachmentLines = formatGeneratedAttachmentLines(event.attachments);
83+
const mediaDirectiveLines = formatGeneratedMediaDirectiveLines(event);
7284
const lines = [
7385
"[Internal task completion event]",
7486
`source: ${event.source}`,
@@ -83,6 +95,9 @@ function formatTaskCompletionEvent(event: AgentTaskCompletionInternalEvent): str
8395
if (attachmentLines.length > 0) {
8496
lines.push("", ...attachmentLines);
8597
}
98+
if (mediaDirectiveLines.length > 0) {
99+
lines.push("", ...mediaDirectiveLines);
100+
}
86101
if (event.statsLine?.trim()) {
87102
lines.push("", sanitizeMultilineField(event.statsLine, ""));
88103
}
@@ -98,6 +113,7 @@ function formatTaskCompletionEventForPlainPrompt(event: AgentTaskCompletionInter
98113
const statusLabel = sanitizeSingleLineField(event.statusLabel, event.status);
99114
const result = formatChildResultDataBlock(event.result);
100115
const attachmentLines = formatGeneratedAttachmentLines(event.attachments);
116+
const mediaDirectiveLines = formatGeneratedMediaDirectiveLines(event);
101117
const lines = [
102118
"A background task completed. Use this result to reply to the user in your normal assistant voice.",
103119
"",
@@ -113,6 +129,9 @@ function formatTaskCompletionEventForPlainPrompt(event: AgentTaskCompletionInter
113129
if (attachmentLines.length > 0) {
114130
lines.push("", ...attachmentLines);
115131
}
132+
if (mediaDirectiveLines.length > 0) {
133+
lines.push("", ...mediaDirectiveLines);
134+
}
116135
if (event.statsLine?.trim()) {
117136
lines.push("", sanitizeMultilineField(event.statsLine, ""));
118137
}

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,98 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
16871687
});
16881688
});
16891689

1690+
it("keeps dashboard music completions on session-only handoff with generated media evidence", async () => {
1691+
const dispatchGatewayMethodInProcess = createInProcessGatewayMock({
1692+
result: {
1693+
payloads: [
1694+
{
1695+
text: "The generated music is ready.",
1696+
attachments: [
1697+
{
1698+
type: "audio",
1699+
path: "/tmp/generated-night-drive.mp3",
1700+
mimeType: "audio/mpeg",
1701+
},
1702+
],
1703+
},
1704+
],
1705+
},
1706+
});
1707+
const sendMessage = createSendMessageMock();
1708+
testing.setDepsForTest({
1709+
dispatchGatewayMethodInProcess,
1710+
getRequesterSessionActivity: () => ({
1711+
sessionId: "requester-session-dashboard",
1712+
isActive: false,
1713+
}),
1714+
getRuntimeConfig: () => ({}) as never,
1715+
sendMessage,
1716+
});
1717+
1718+
const result = await deliverSubagentAnnouncement({
1719+
requesterSessionKey: "agent:main:dashboard:music-session",
1720+
targetRequesterSessionKey: "agent:main:dashboard:music-session",
1721+
triggerMessage: "music done\nMEDIA:/tmp/generated-night-drive.mp3",
1722+
steerMessage: "music done\nMEDIA:/tmp/generated-night-drive.mp3",
1723+
requesterOrigin: {
1724+
channel: "webchat",
1725+
to: "session:dashboard",
1726+
accountId: "control-ui",
1727+
},
1728+
requesterSessionOrigin: {
1729+
channel: "webchat",
1730+
to: "session:dashboard",
1731+
accountId: "control-ui",
1732+
},
1733+
completionDirectOrigin: {
1734+
channel: "webchat",
1735+
to: "session:dashboard",
1736+
accountId: "control-ui",
1737+
},
1738+
directOrigin: {
1739+
channel: "webchat",
1740+
to: "session:dashboard",
1741+
accountId: "control-ui",
1742+
},
1743+
requesterIsSubagent: false,
1744+
expectsCompletionMessage: true,
1745+
bestEffortDeliver: true,
1746+
directIdempotencyKey: "announce-dashboard-music-media",
1747+
sourceTool: "music_generate",
1748+
sourceSessionKey: "music_generate:task-123",
1749+
sourceChannel: "internal",
1750+
internalEvents: [
1751+
{
1752+
type: "task_completion",
1753+
source: "music_generation",
1754+
childSessionKey: "music_generate:task-123",
1755+
childSessionId: "task-123",
1756+
announceType: "music generation task",
1757+
taskLabel: "night-drive synthwave",
1758+
status: "ok",
1759+
statusLabel: "completed successfully",
1760+
result: "Generated 1 track.\nMEDIA:/tmp/generated-night-drive.mp3",
1761+
mediaUrls: ["/tmp/generated-night-drive.mp3"],
1762+
replyInstruction: "Tell the user the music is ready and include the generated audio.",
1763+
},
1764+
],
1765+
});
1766+
1767+
expectRecordFields(result, {
1768+
delivered: true,
1769+
path: "direct",
1770+
});
1771+
expectInProcessAgentParams(dispatchGatewayMethodInProcess, {
1772+
sessionKey: "agent:main:dashboard:music-session",
1773+
deliver: false,
1774+
channel: "webchat",
1775+
accountId: "control-ui",
1776+
to: "session:dashboard",
1777+
bestEffortDeliver: true,
1778+
});
1779+
expect(sendMessage).not.toHaveBeenCalled();
1780+
});
1781+
16901782
it("keeps announce-agent delivery primary for dormant completion events with child output", async () => {
16911783
const callGateway = createGatewayMock({
16921784
result: {

src/agents/tools/media-generate-background-shared.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,79 @@ describe("createMediaGenerationTaskLifecycle", () => {
500500
);
501501
});
502502

503+
it("includes MEDIA directives in music completion wake prompts for session-only delivery", async () => {
504+
subagentAnnounceDeliveryMocks.deliverSubagentAnnouncement.mockResolvedValueOnce({
505+
delivered: true,
506+
});
507+
const lifecycle = createMediaGenerationTaskLifecycle({
508+
toolName: "music_generate",
509+
taskKind: "music_generation",
510+
label: "Music generation",
511+
queuedProgressSummary: "Queued music generation",
512+
generatedLabel: "track",
513+
failureProgressSummary: "Music generation failed",
514+
eventSource: "music_generation",
515+
announceType: "music generation task",
516+
completionLabel: "music",
517+
});
518+
519+
await expect(
520+
lifecycle.wakeTaskCompletion({
521+
handle: {
522+
taskId: "task-music-webchat",
523+
runId: "tool:music_generate:webchat",
524+
requesterSessionKey: "agent:main:dashboard:music-session",
525+
taskLabel: "night-drive synthwave",
526+
requesterOrigin: {
527+
channel: "webchat",
528+
to: "session:dashboard",
529+
},
530+
},
531+
status: "ok",
532+
statusLabel: "completed successfully",
533+
result: 'Generated 1 track.\n- path="/tmp/generated-night-drive.mp3"',
534+
attachments: [
535+
{
536+
type: "audio",
537+
path: "/tmp/generated-night-drive.mp3",
538+
mimeType: "audio/mpeg",
539+
name: "generated-night-drive.mp3",
540+
},
541+
],
542+
}),
543+
).resolves.toBe(true);
544+
545+
expect(subagentAnnounceDeliveryMocks.deliverSubagentAnnouncement).toHaveBeenCalledWith(
546+
expect.objectContaining({
547+
requesterSessionKey: "agent:main:dashboard:music-session",
548+
requesterSessionOrigin: {
549+
channel: "webchat",
550+
to: "session:dashboard",
551+
},
552+
completionDirectOrigin: {
553+
channel: "webchat",
554+
to: "session:dashboard",
555+
},
556+
sourceTool: "music_generate",
557+
bestEffortDeliver: true,
558+
}),
559+
);
560+
const announceParams = subagentAnnounceDeliveryMocks.deliverSubagentAnnouncement.mock
561+
.calls[0]?.[0] as { triggerMessage?: string; internalEvents?: unknown[] } | undefined;
562+
expect(announceParams?.triggerMessage).toContain("MEDIA:/tmp/generated-night-drive.mp3");
563+
expect(announceParams?.internalEvents).toEqual([
564+
expect.objectContaining({
565+
mediaUrls: ["/tmp/generated-night-drive.mp3"],
566+
attachments: [
567+
expect.objectContaining({
568+
path: "/tmp/generated-night-drive.mp3",
569+
}),
570+
],
571+
}),
572+
]);
573+
expect(taskRegistryDeliveryRuntimeMocks.sendMessage).not.toHaveBeenCalled();
574+
});
575+
503576
it("does not direct-deliver generated media after requester abandonment", async () => {
504577
// Abandoned requester sessions are terminal; direct delivery would re-open a
505578
// conversation the task lifecycle already decided to stop.

0 commit comments

Comments
 (0)