mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 01:06:38 +00:00
fix(subagents): always read latest assistant/tool output on subagent completion
This commit is contained in:
@@ -232,6 +232,36 @@ describe("subagent announce formatting", () => {
|
|||||||
expect(msg).toContain("tool output line 1");
|
expect(msg).toContain("tool output line 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses latest assistant text when it appears after a tool output", async () => {
|
||||||
|
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||||
|
chatHistoryMock.mockResolvedValueOnce({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "tool",
|
||||||
|
content: [{ type: "text", text: "tool output line" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "assistant final line" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
readLatestAssistantReplyMock.mockResolvedValue("");
|
||||||
|
|
||||||
|
await runSubagentAnnounceFlow({
|
||||||
|
childSessionKey: "agent:main:subagent:worker",
|
||||||
|
childRunId: "run-latest-assistant",
|
||||||
|
requesterSessionKey: "agent:main:main",
|
||||||
|
requesterDisplayKey: "main",
|
||||||
|
...defaultOutcomeAnnounce,
|
||||||
|
waitForCompletion: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
|
||||||
|
const msg = call?.params?.message as string;
|
||||||
|
expect(msg).toContain("assistant final line");
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to latest tool output when assistant reply is empty", async () => {
|
it("falls back to latest tool output when assistant reply is empty", async () => {
|
||||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||||
chatHistoryMock.mockResolvedValueOnce({
|
chatHistoryMock.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -29,26 +29,33 @@ import {
|
|||||||
} from "./pi-embedded.js";
|
} from "./pi-embedded.js";
|
||||||
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
|
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
|
||||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||||
import { readLatestAssistantReply } from "./tools/agent-step.js";
|
import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js";
|
||||||
import { sanitizeTextContent } from "./tools/sessions-helpers.js";
|
|
||||||
|
|
||||||
type ToolResultMessage = {
|
type ToolResultMessage = {
|
||||||
role?: unknown;
|
role?: unknown;
|
||||||
content?: unknown;
|
content?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isToolResultMessage(msg: unknown): boolean {
|
|
||||||
if (!msg || typeof msg !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const role = (msg as { role?: unknown }).role;
|
|
||||||
return role === "toolResult" || role === "tool";
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractToolResultText(content: unknown): string {
|
function extractToolResultText(content: unknown): string {
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
return sanitizeTextContent(content);
|
return sanitizeTextContent(content);
|
||||||
}
|
}
|
||||||
|
if (content && typeof content === "object" && !Array.isArray(content)) {
|
||||||
|
const obj = content as {
|
||||||
|
text?: unknown;
|
||||||
|
output?: unknown;
|
||||||
|
content?: unknown;
|
||||||
|
};
|
||||||
|
if (typeof obj.text === "string") {
|
||||||
|
return sanitizeTextContent(obj.text);
|
||||||
|
}
|
||||||
|
if (typeof obj.output === "string") {
|
||||||
|
return sanitizeTextContent(obj.output);
|
||||||
|
}
|
||||||
|
if (typeof obj.content === "string") {
|
||||||
|
return sanitizeTextContent(obj.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!Array.isArray(content)) {
|
if (!Array.isArray(content)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -60,7 +67,21 @@ function extractToolResultText(content: unknown): string {
|
|||||||
return joined?.trim() ?? "";
|
return joined?.trim() ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLatestToolResult(sessionKey: string): Promise<string | undefined> {
|
function extractSubagentOutputText(message: unknown): string {
|
||||||
|
if (!message || typeof message !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const role = (message as { role?: unknown }).role;
|
||||||
|
if (role === "assistant") {
|
||||||
|
return extractAssistantText(message) ?? "";
|
||||||
|
}
|
||||||
|
if (role === "toolResult" || role === "tool") {
|
||||||
|
return extractToolResultText((message as ToolResultMessage).content);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLatestSubagentOutput(sessionKey: string): Promise<string | undefined> {
|
||||||
const history = await callGateway<{ messages?: Array<unknown> }>({
|
const history = await callGateway<{ messages?: Array<unknown> }>({
|
||||||
method: "chat.history",
|
method: "chat.history",
|
||||||
params: { sessionKey, limit: 50 },
|
params: { sessionKey, limit: 50 },
|
||||||
@@ -68,11 +89,7 @@ async function readLatestToolResult(sessionKey: string): Promise<string | undefi
|
|||||||
const messages = Array.isArray(history?.messages) ? history.messages : [];
|
const messages = Array.isArray(history?.messages) ? history.messages : [];
|
||||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
if (!isToolResultMessage(msg)) {
|
const text = extractSubagentOutputText(msg);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const candidate = msg as ToolResultMessage;
|
|
||||||
const text = extractToolResultText(candidate.content);
|
|
||||||
if (text) {
|
if (text) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
@@ -80,7 +97,7 @@ async function readLatestToolResult(sessionKey: string): Promise<string | undefi
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLatestToolResultWithRetry(params: {
|
async function readLatestSubagentOutputWithRetry(params: {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
maxWaitMs: number;
|
maxWaitMs: number;
|
||||||
}): Promise<string | undefined> {
|
}): Promise<string | undefined> {
|
||||||
@@ -88,7 +105,7 @@ async function readLatestToolResultWithRetry(params: {
|
|||||||
const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
|
const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
|
||||||
let result: string | undefined;
|
let result: string | undefined;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
result = await readLatestToolResult(params.sessionKey);
|
result = await readLatestSubagentOutput(params.sessionKey);
|
||||||
if (result?.trim()) {
|
if (result?.trim()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -305,28 +322,6 @@ function loadSessionEntryByKey(sessionKey: string) {
|
|||||||
return store[sessionKey];
|
return store[sessionKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLatestAssistantReplyWithRetry(params: {
|
|
||||||
sessionKey: string;
|
|
||||||
initialReply?: string;
|
|
||||||
maxWaitMs: number;
|
|
||||||
}): Promise<string | undefined> {
|
|
||||||
const RETRY_INTERVAL_MS = 100;
|
|
||||||
let reply = params.initialReply?.trim() ? params.initialReply : undefined;
|
|
||||||
if (reply) {
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS));
|
|
||||||
const latest = await readLatestAssistantReply({ sessionKey: params.sessionKey });
|
|
||||||
if (latest?.trim()) {
|
|
||||||
return latest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildSubagentSystemPrompt(params: {
|
export function buildSubagentSystemPrompt(params: {
|
||||||
requesterSessionKey?: string;
|
requesterSessionKey?: string;
|
||||||
requesterOrigin?: DeliveryContext;
|
requesterOrigin?: DeliveryContext;
|
||||||
@@ -522,23 +517,15 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
outcome = { status: "timeout" };
|
outcome = { status: "timeout" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
|
reply = await readLatestSubagentOutput(params.childSessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reply) {
|
if (!reply) {
|
||||||
reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
|
reply = await readLatestSubagentOutput(params.childSessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reply?.trim()) {
|
if (!reply?.trim()) {
|
||||||
reply = await readLatestAssistantReplyWithRetry({
|
reply = await readLatestSubagentOutputWithRetry({
|
||||||
sessionKey: params.childSessionKey,
|
|
||||||
initialReply: reply,
|
|
||||||
maxWaitMs: params.timeoutMs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reply?.trim()) {
|
|
||||||
reply = await readLatestToolResultWithRetry({
|
|
||||||
sessionKey: params.childSessionKey,
|
sessionKey: params.childSessionKey,
|
||||||
maxWaitMs: params.timeoutMs,
|
maxWaitMs: params.timeoutMs,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user