fix: improve gpt execution flow and visibility

This commit is contained in:
Peter Steinberger
2026-04-05 10:31:36 +01:00
parent 219afbc2cc
commit e468da1040
16 changed files with 399 additions and 21 deletions

View File

@@ -1,4 +1,4 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import type {
@@ -858,7 +858,7 @@ describe("dispatchReplyFromConfig", () => {
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
});
it("suppresses native tool summaries but still forwards tool media", async () => {
it("delivers native tool summaries and tool media", async () => {
setNoAbort();
const cfg = emptyConfig;
const dispatcher = createDispatcher();
@@ -883,13 +883,52 @@ describe("dispatchReplyFromConfig", () => {
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(dispatcher.sendToolResult).toHaveBeenCalledTimes(1);
const sent = firstToolResultPayload(dispatcher);
expect(dispatcher.sendToolResult).toHaveBeenCalledTimes(2);
expect(dispatcher.sendToolResult).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ text: "🔧 tools/sessions_send" }),
);
const sent = (dispatcher.sendToolResult as Mock).mock.calls[1]?.[0] as ReplyPayload | undefined;
expect(sent?.mediaUrl).toBe("https://example.com/tts-native.opus");
expect(sent?.text).toBeUndefined();
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
});
it("emits concise tool-start progress updates for direct sessions", async () => {
setNoAbort();
const cfg = emptyConfig;
const dispatcher = createDispatcher();
const ctx = buildTestCtx({
Provider: "telegram",
ChatType: "direct",
});
const replyResolver = async (
_ctx: MsgContext,
opts?: GetReplyOptions,
_cfg?: OpenClawConfig,
) => {
await opts?.onToolStart?.({ name: "read", phase: "start" });
await opts?.onToolStart?.({ name: "read", phase: "update" });
await opts?.onToolStart?.({ name: "grep", phase: "start" });
await opts?.onToolStart?.({ name: "exec", phase: "start" });
return { text: "done" } satisfies ReplyPayload;
};
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
expect(dispatcher.sendToolResult).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ text: "Working: read" }),
);
expect(dispatcher.sendToolResult).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ text: "Working: grep" }),
);
expect(dispatcher.sendToolResult).toHaveBeenCalledTimes(2);
expect(dispatcher.sendFinalReply).toHaveBeenCalledWith({ text: "done" });
});
it("delivers deterministic exec approval tool payloads for native commands", async () => {
setNoAbort();
const cfg = emptyConfig;

View File

@@ -597,10 +597,12 @@ export async function dispatchReplyFromConfig(params: {
}
}
// Forum topics are threaded conversations within a group — verbose tool
// summaries should be delivered into the topic thread, same as DMs.
const shouldSendToolSummaries =
(ctx.ChatType !== "group" || ctx.IsForum === true) && ctx.CommandSource !== "native";
// Forum topics are threaded conversations within a group — tool visibility
// should be delivered into the topic thread, same as DMs.
const shouldSendToolSummaries = ctx.ChatType !== "group" || ctx.IsForum === true;
const shouldSendToolStartStatuses = ctx.ChatType !== "group" || ctx.IsForum === true;
const toolStartStatusesSent = new Set<string>();
let toolStartStatusCount = 0;
const acpDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
ctx,
cfg,
@@ -699,6 +701,28 @@ export async function dispatchReplyFromConfig(params: {
};
return run();
},
onToolStart: ({ name, phase }) => {
if (!shouldSendToolStartStatuses || phase !== "start") {
return;
}
const normalizedName = typeof name === "string" ? name.trim() : "";
if (
!normalizedName ||
toolStartStatusCount >= 2 ||
toolStartStatusesSent.has(normalizedName)
) {
return;
}
toolStartStatusesSent.add(normalizedName);
toolStartStatusCount += 1;
const payload: ReplyPayload = {
text: `Working: ${normalizedName}`,
};
if (shouldRouteToOriginating) {
return sendPayloadAsync(payload, undefined, false);
}
dispatcher.sendToolResult(payload);
},
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
const run = async () => {
// Suppress reasoning payloads — channels using this generic dispatch