Skip to content

Commit b6f9b5f

Browse files
committed
fix(agents): keep grouped subagent completions
1 parent cbd9167 commit b6f9b5f

3 files changed

Lines changed: 140 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
1818
- Agents/verbose: use compact explain-mode tool summaries for `/verbose` and progress drafts by default, with `agents.defaults.toolProgressDetail: "raw"` and per-agent overrides for debugging raw command/detail output.
1919
- Agents/commands: add `/steer <message>` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934)
20+
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
2021
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
2122
- Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys.
2223
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,62 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
722722
);
723723
});
724724

725+
it("keeps all grouped child results in direct completion fallback", async () => {
726+
const callGateway = createGatewayMock({
727+
result: {
728+
payloads: [],
729+
},
730+
});
731+
const sendMessage = createSendMessageMock();
732+
const result = await deliverSlackThreadAnnouncement({
733+
callGateway,
734+
sendMessage,
735+
sessionId: "requester-session-4",
736+
isActive: false,
737+
expectsCompletionMessage: true,
738+
directIdempotencyKey: "announce-thread-fallback-grouped-results",
739+
internalEvents: [
740+
{
741+
type: "task_completion",
742+
source: "subagent",
743+
childSessionKey: "agent:worker:subagent:first",
744+
childSessionId: "child-session-1",
745+
announceType: "subagent task",
746+
taskLabel: "first task",
747+
status: "ok",
748+
statusLabel: "completed successfully",
749+
result: "first child result",
750+
replyInstruction: "Summarize the result.",
751+
},
752+
{
753+
type: "task_completion",
754+
source: "subagent",
755+
childSessionKey: "agent:worker:subagent:second",
756+
childSessionId: "child-session-2",
757+
announceType: "subagent task",
758+
taskLabel: "second task",
759+
status: "ok",
760+
statusLabel: "completed successfully",
761+
result: "second child result",
762+
replyInstruction: "Summarize the result.",
763+
},
764+
],
765+
});
766+
767+
expect(result).toEqual(
768+
expect.objectContaining({
769+
delivered: true,
770+
path: "direct-thread-fallback",
771+
}),
772+
);
773+
expect(sendMessage).toHaveBeenCalledWith(
774+
expect.objectContaining({
775+
content: "first task:\nfirst child result\n\nsecond task:\nsecond child result",
776+
idempotencyKey: "announce-thread-fallback-grouped-results",
777+
}),
778+
);
779+
});
780+
725781
it("keeps concise requester rewrites primary even when child output is long", async () => {
726782
const callGateway = createGatewayMock({
727783
result: {
@@ -1265,4 +1321,33 @@ describe("extractThreadCompletionFallbackText", () => {
12651321
]),
12661322
).toBe("sample task");
12671323
});
1324+
1325+
it("combines multiple task completion results for grouped announce fallback", () => {
1326+
expect(
1327+
extractThreadCompletionFallbackText([
1328+
{
1329+
type: "task_completion",
1330+
source: "subagent",
1331+
childSessionKey: "agent:worker:subagent:first",
1332+
announceType: "subagent task",
1333+
taskLabel: "first task",
1334+
status: "ok",
1335+
statusLabel: "completed successfully",
1336+
result: "first child result",
1337+
replyInstruction: "Summarize the result.",
1338+
},
1339+
{
1340+
type: "task_completion",
1341+
source: "subagent",
1342+
childSessionKey: "agent:worker:subagent:second",
1343+
announceType: "subagent task",
1344+
taskLabel: "second task",
1345+
status: "ok",
1346+
statusLabel: "completed successfully",
1347+
result: "second child result",
1348+
replyInstruction: "Summarize the result.",
1349+
},
1350+
]),
1351+
).toBe("first task:\nfirst child result\n\nsecond task:\nsecond child result");
1352+
});
12681353
});

src/agents/subagent-announce-delivery.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -534,31 +534,65 @@ async function maybeQueueSubagentAnnounce(params: {
534534
return "none";
535535
}
536536

537+
function extractTaskCompletionFallbackText(event: AgentInternalEvent): string {
538+
const result = event.result.trim();
539+
if (result) {
540+
return result;
541+
}
542+
const statusLabel = event.statusLabel.trim();
543+
const taskLabel = event.taskLabel.trim();
544+
if (statusLabel && taskLabel) {
545+
return `${taskLabel}: ${statusLabel}`;
546+
}
547+
if (statusLabel) {
548+
return statusLabel;
549+
}
550+
if (taskLabel) {
551+
return taskLabel;
552+
}
553+
return "";
554+
}
555+
556+
function formatTaskCompletionFallbackBlock(params: {
557+
event: AgentInternalEvent;
558+
text: string;
559+
includeTaskLabel: boolean;
560+
}): string {
561+
const taskLabel = params.event.taskLabel.trim();
562+
if (!params.includeTaskLabel || !taskLabel || params.text.startsWith(`${taskLabel}:`)) {
563+
return params.text;
564+
}
565+
return `${taskLabel}:\n${params.text}`;
566+
}
567+
537568
export function extractThreadCompletionFallbackText(internalEvents?: AgentInternalEvent[]): string {
538569
if (!internalEvents || internalEvents.length === 0) {
539570
return "";
540571
}
541-
for (const event of internalEvents) {
542-
if (event.type !== "task_completion") {
543-
continue;
544-
}
545-
const result = event.result.trim();
546-
if (result) {
547-
return result;
548-
}
549-
const statusLabel = event.statusLabel.trim();
550-
const taskLabel = event.taskLabel.trim();
551-
if (statusLabel && taskLabel) {
552-
return `${taskLabel}: ${statusLabel}`;
553-
}
554-
if (statusLabel) {
555-
return statusLabel;
556-
}
557-
if (taskLabel) {
558-
return taskLabel;
559-
}
572+
const completions = internalEvents
573+
.filter((event) => event.type === "task_completion")
574+
.map((event) => ({
575+
event,
576+
text: extractTaskCompletionFallbackText(event),
577+
}))
578+
.filter((completion) => completion.text.length > 0);
579+
if (completions.length === 0) {
580+
return "";
560581
}
561-
return "";
582+
const onlyCompletion = completions[0];
583+
if (completions.length === 1 && onlyCompletion) {
584+
return onlyCompletion.text;
585+
}
586+
return completions
587+
.map((completion) =>
588+
formatTaskCompletionFallbackBlock({
589+
event: completion.event,
590+
text: completion.text,
591+
includeTaskLabel: true,
592+
}),
593+
)
594+
.join("\n\n")
595+
.trim();
562596
}
563597

564598
function hasVisibleGatewayAgentPayload(response: unknown): boolean {

0 commit comments

Comments
 (0)