mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
fix(agents): keep grouped subagent completions
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
|
||||
- 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.
|
||||
- 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)
|
||||
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
|
||||
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -722,6 +722,62 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps all grouped child results in direct completion fallback", async () => {
|
||||
const callGateway = createGatewayMock({
|
||||
result: {
|
||||
payloads: [],
|
||||
},
|
||||
});
|
||||
const sendMessage = createSendMessageMock();
|
||||
const result = await deliverSlackThreadAnnouncement({
|
||||
callGateway,
|
||||
sendMessage,
|
||||
sessionId: "requester-session-4",
|
||||
isActive: false,
|
||||
expectsCompletionMessage: true,
|
||||
directIdempotencyKey: "announce-thread-fallback-grouped-results",
|
||||
internalEvents: [
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "subagent",
|
||||
childSessionKey: "agent:worker:subagent:first",
|
||||
childSessionId: "child-session-1",
|
||||
announceType: "subagent task",
|
||||
taskLabel: "first task",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "first child result",
|
||||
replyInstruction: "Summarize the result.",
|
||||
},
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "subagent",
|
||||
childSessionKey: "agent:worker:subagent:second",
|
||||
childSessionId: "child-session-2",
|
||||
announceType: "subagent task",
|
||||
taskLabel: "second task",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "second child result",
|
||||
replyInstruction: "Summarize the result.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
delivered: true,
|
||||
path: "direct-thread-fallback",
|
||||
}),
|
||||
);
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: "first task:\nfirst child result\n\nsecond task:\nsecond child result",
|
||||
idempotencyKey: "announce-thread-fallback-grouped-results",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps concise requester rewrites primary even when child output is long", async () => {
|
||||
const callGateway = createGatewayMock({
|
||||
result: {
|
||||
@@ -1265,4 +1321,33 @@ describe("extractThreadCompletionFallbackText", () => {
|
||||
]),
|
||||
).toBe("sample task");
|
||||
});
|
||||
|
||||
it("combines multiple task completion results for grouped announce fallback", () => {
|
||||
expect(
|
||||
extractThreadCompletionFallbackText([
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "subagent",
|
||||
childSessionKey: "agent:worker:subagent:first",
|
||||
announceType: "subagent task",
|
||||
taskLabel: "first task",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "first child result",
|
||||
replyInstruction: "Summarize the result.",
|
||||
},
|
||||
{
|
||||
type: "task_completion",
|
||||
source: "subagent",
|
||||
childSessionKey: "agent:worker:subagent:second",
|
||||
announceType: "subagent task",
|
||||
taskLabel: "second task",
|
||||
status: "ok",
|
||||
statusLabel: "completed successfully",
|
||||
result: "second child result",
|
||||
replyInstruction: "Summarize the result.",
|
||||
},
|
||||
]),
|
||||
).toBe("first task:\nfirst child result\n\nsecond task:\nsecond child result");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -534,31 +534,65 @@ async function maybeQueueSubagentAnnounce(params: {
|
||||
return "none";
|
||||
}
|
||||
|
||||
function extractTaskCompletionFallbackText(event: AgentInternalEvent): string {
|
||||
const result = event.result.trim();
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const statusLabel = event.statusLabel.trim();
|
||||
const taskLabel = event.taskLabel.trim();
|
||||
if (statusLabel && taskLabel) {
|
||||
return `${taskLabel}: ${statusLabel}`;
|
||||
}
|
||||
if (statusLabel) {
|
||||
return statusLabel;
|
||||
}
|
||||
if (taskLabel) {
|
||||
return taskLabel;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatTaskCompletionFallbackBlock(params: {
|
||||
event: AgentInternalEvent;
|
||||
text: string;
|
||||
includeTaskLabel: boolean;
|
||||
}): string {
|
||||
const taskLabel = params.event.taskLabel.trim();
|
||||
if (!params.includeTaskLabel || !taskLabel || params.text.startsWith(`${taskLabel}:`)) {
|
||||
return params.text;
|
||||
}
|
||||
return `${taskLabel}:\n${params.text}`;
|
||||
}
|
||||
|
||||
export function extractThreadCompletionFallbackText(internalEvents?: AgentInternalEvent[]): string {
|
||||
if (!internalEvents || internalEvents.length === 0) {
|
||||
return "";
|
||||
}
|
||||
for (const event of internalEvents) {
|
||||
if (event.type !== "task_completion") {
|
||||
continue;
|
||||
}
|
||||
const result = event.result.trim();
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const statusLabel = event.statusLabel.trim();
|
||||
const taskLabel = event.taskLabel.trim();
|
||||
if (statusLabel && taskLabel) {
|
||||
return `${taskLabel}: ${statusLabel}`;
|
||||
}
|
||||
if (statusLabel) {
|
||||
return statusLabel;
|
||||
}
|
||||
if (taskLabel) {
|
||||
return taskLabel;
|
||||
}
|
||||
const completions = internalEvents
|
||||
.filter((event) => event.type === "task_completion")
|
||||
.map((event) => ({
|
||||
event,
|
||||
text: extractTaskCompletionFallbackText(event),
|
||||
}))
|
||||
.filter((completion) => completion.text.length > 0);
|
||||
if (completions.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
const onlyCompletion = completions[0];
|
||||
if (completions.length === 1 && onlyCompletion) {
|
||||
return onlyCompletion.text;
|
||||
}
|
||||
return completions
|
||||
.map((completion) =>
|
||||
formatTaskCompletionFallbackBlock({
|
||||
event: completion.event,
|
||||
text: completion.text,
|
||||
includeTaskLabel: true,
|
||||
}),
|
||||
)
|
||||
.join("\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function hasVisibleGatewayAgentPayload(response: unknown): boolean {
|
||||
|
||||
Reference in New Issue
Block a user