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

@@ -157,6 +157,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "whatsapp",
agentTo: "+123",
});
const result = await tool.execute("call2", {
@@ -185,7 +186,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
await waitFor(() => ctx.waitCalls.some((call) => call.runId === child.runId));
await waitFor(() => patchCalls.some((call) => call.label === "my-task"));
await waitFor(() => ctx.calls.filter((c) => c.method === "agent").length >= 2);
await waitFor(() => ctx.calls.filter((c) => c.method === "send").length >= 1);
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
@@ -194,22 +195,24 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
expect(labelPatch?.key).toBe(child.sessionKey);
expect(labelPatch?.label).toBe("my-task");
// Two agent calls: subagent spawn + main agent trigger
// Subagent spawn call plus direct outbound completion send.
const agentCalls = ctx.calls.filter((c) => c.method === "agent");
expect(agentCalls).toHaveLength(2);
expect(agentCalls).toHaveLength(1);
// First call: subagent spawn
const first = agentCalls[0]?.params as { lane?: string } | undefined;
expect(first?.lane).toBe("subagent");
// Second call: main agent trigger (not "Sub-agent announce step." anymore)
const second = agentCalls[1]?.params as { sessionKey?: string; message?: string } | undefined;
expect(second?.sessionKey).toBe("main");
expect(second?.message).toContain("subagent task");
// No direct send to external channel (main agent handles delivery)
// Direct send should route completion to the requester channel/session.
const sendCalls = ctx.calls.filter((c) => c.method === "send");
expect(sendCalls.length).toBe(0);
expect(sendCalls).toHaveLength(1);
const send = sendCalls[0]?.params as
| { sessionKey?: string; channel?: string; to?: string; message?: string }
| undefined;
expect(send?.sessionKey).toBe("agent:main:main");
expect(send?.channel).toBe("whatsapp");
expect(send?.to).toBe("+123");
expect(send?.message).toBe("done");
expect(child.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
});
@@ -232,6 +235,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "discord:group:req",
agentChannel: "discord",
agentTo: "discord:dm:u123",
});
const result = await tool.execute("call1", {
@@ -269,7 +273,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
expect(childWait?.timeoutMs).toBe(1000);
const agentCalls = ctx.calls.filter((call) => call.method === "agent");
expect(agentCalls).toHaveLength(2);
expect(agentCalls).toHaveLength(1);
const first = agentCalls[0]?.params as
| {
@@ -285,19 +289,15 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
expect(child.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
const second = agentCalls[1]?.params as
| {
sessionKey?: string;
message?: string;
deliver?: boolean;
}
| undefined;
expect(second?.sessionKey).toBe("discord:group:req");
expect(second?.deliver).toBe(true);
expect(second?.message).toContain("subagent task");
const sendCalls = ctx.calls.filter((c) => c.method === "send");
expect(sendCalls.length).toBe(0);
expect(sendCalls).toHaveLength(1);
const send = sendCalls[0]?.params as
| { sessionKey?: string; channel?: string; to?: string; message?: string }
| undefined;
expect(send?.sessionKey).toBe("agent:main:discord:group:req");
expect(send?.channel).toBe("discord");
expect(send?.to).toBe("discord:dm:u123");
expect(send?.message).toContain("completed successfully");
expect(deletedKey?.startsWith("agent:main:subagent:")).toBe(true);
});
@@ -323,6 +323,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "discord:group:req",
agentChannel: "discord",
agentTo: "discord:dm:u123",
});
const result = await tool.execute("call1b", {
@@ -340,29 +341,30 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
throw new Error("missing child runId");
}
await waitFor(() => ctx.waitCalls.some((call) => call.runId === child.runId));
await waitFor(() => ctx.calls.filter((call) => call.method === "agent").length >= 2);
await waitFor(() => ctx.calls.filter((call) => call.method === "send").length >= 1);
await waitFor(() => Boolean(deletedKey));
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
expect(childWait?.timeoutMs).toBe(1000);
expect(child.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
// Two agent calls: subagent spawn + main agent trigger
// One agent call for spawn, then direct completion send.
const agentCalls = ctx.calls.filter((call) => call.method === "agent");
expect(agentCalls).toHaveLength(2);
expect(agentCalls).toHaveLength(1);
// First call: subagent spawn
const first = agentCalls[0]?.params as { lane?: string } | undefined;
expect(first?.lane).toBe("subagent");
// Second call: main agent trigger
const second = agentCalls[1]?.params as { sessionKey?: string; deliver?: boolean } | undefined;
expect(second?.sessionKey).toBe("discord:group:req");
expect(second?.deliver).toBe(true);
// No direct send to external channel (main agent handles delivery)
const sendCalls = ctx.calls.filter((c) => c.method === "send");
expect(sendCalls.length).toBe(0);
expect(sendCalls).toHaveLength(1);
const send = sendCalls[0]?.params as
| { sessionKey?: string; channel?: string; to?: string; message?: string }
| undefined;
expect(send?.sessionKey).toBe("agent:main:discord:group:req");
expect(send?.channel).toBe("discord");
expect(send?.to).toBe("discord:dm:u123");
expect(send?.message).toBe("done");
// Session should be deleted
expect(deletedKey?.startsWith("agent:main:subagent:")).toBe(true);