mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +00:00
ACP: simplify stream config to repeatSuppression
This commit is contained in:
@@ -64,7 +64,7 @@ describe("createAcpReplyProjector", () => {
|
||||
expect(deliveries).toEqual([{ kind: "block", text: "What now?" }]);
|
||||
});
|
||||
|
||||
it("suppresses usage_update by default and allows deduped usage when enabled", async () => {
|
||||
it("suppresses usage_update by default and allows deduped usage when tag-visible", async () => {
|
||||
const hidden: Array<{ kind: string; text?: string }> = [];
|
||||
const hiddenProjector = createAcpReplyProjector({
|
||||
cfg: createCfg(),
|
||||
@@ -91,7 +91,6 @@ describe("createAcpReplyProjector", () => {
|
||||
stream: {
|
||||
coalesceIdleMs: 0,
|
||||
maxChunkChars: 64,
|
||||
showUsage: true,
|
||||
tagVisibility: {
|
||||
usage_update: true,
|
||||
},
|
||||
@@ -153,7 +152,7 @@ describe("createAcpReplyProjector", () => {
|
||||
expect(deliveries).toEqual([]);
|
||||
});
|
||||
|
||||
it("dedupes repeated tool lifecycle updates in minimal mode", async () => {
|
||||
it("dedupes repeated tool lifecycle updates when repeatSuppression is enabled", async () => {
|
||||
const deliveries: Array<{ kind: string; text?: string }> = [];
|
||||
const projector = createAcpReplyProjector({
|
||||
cfg: createCfg(),
|
||||
@@ -227,7 +226,7 @@ describe("createAcpReplyProjector", () => {
|
||||
expect(deliveries[0]?.text).not.toContain("call_ABC123 (");
|
||||
});
|
||||
|
||||
it("respects metaMode=off and still streams assistant text", async () => {
|
||||
it("allows repeated status/tool summaries when repeatSuppression is disabled", async () => {
|
||||
const deliveries: Array<{ kind: string; text?: string }> = [];
|
||||
const projector = createAcpReplyProjector({
|
||||
cfg: createCfg({
|
||||
@@ -236,7 +235,10 @@ describe("createAcpReplyProjector", () => {
|
||||
stream: {
|
||||
coalesceIdleMs: 0,
|
||||
maxChunkChars: 256,
|
||||
metaMode: "off",
|
||||
repeatSuppression: false,
|
||||
tagVisibility: {
|
||||
available_commands_update: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -247,6 +249,11 @@ describe("createAcpReplyProjector", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await projector.onEvent({
|
||||
type: "status",
|
||||
text: "available commands updated",
|
||||
tag: "available_commands_update",
|
||||
});
|
||||
await projector.onEvent({
|
||||
type: "status",
|
||||
text: "available commands updated",
|
||||
@@ -259,6 +266,13 @@ describe("createAcpReplyProjector", () => {
|
||||
toolCallId: "x",
|
||||
status: "in_progress",
|
||||
});
|
||||
await projector.onEvent({
|
||||
type: "tool_call",
|
||||
text: "tool call",
|
||||
tag: "tool_call_update",
|
||||
toolCallId: "x",
|
||||
status: "in_progress",
|
||||
});
|
||||
await projector.onEvent({
|
||||
type: "text_delta",
|
||||
text: "hello",
|
||||
@@ -266,10 +280,21 @@ describe("createAcpReplyProjector", () => {
|
||||
});
|
||||
await projector.flush(true);
|
||||
|
||||
expect(deliveries).toEqual([{ kind: "block", text: "hello" }]);
|
||||
expect(deliveries.filter((entry) => entry.kind === "tool").length).toBe(4);
|
||||
expect(deliveries[0]).toEqual({
|
||||
kind: "tool",
|
||||
text: prefixSystemMessage("available commands updated"),
|
||||
});
|
||||
expect(deliveries[1]).toEqual({
|
||||
kind: "tool",
|
||||
text: prefixSystemMessage("available commands updated"),
|
||||
});
|
||||
expect(deliveries[2]?.text).toContain("Tool Call");
|
||||
expect(deliveries[3]?.text).toContain("Tool Call");
|
||||
expect(deliveries[4]).toEqual({ kind: "block", text: "hello" });
|
||||
});
|
||||
|
||||
it("allows non-identical status updates in metaMode=verbose while suppressing exact duplicates", async () => {
|
||||
it("suppresses exact duplicate status updates when repeatSuppression is enabled", async () => {
|
||||
const deliveries: Array<{ kind: string; text?: string }> = [];
|
||||
const projector = createAcpReplyProjector({
|
||||
cfg: createCfg({
|
||||
@@ -278,7 +303,6 @@ describe("createAcpReplyProjector", () => {
|
||||
stream: {
|
||||
coalesceIdleMs: 0,
|
||||
maxChunkChars: 256,
|
||||
metaMode: "verbose",
|
||||
tagVisibility: {
|
||||
available_commands_update: true,
|
||||
},
|
||||
@@ -324,7 +348,6 @@ describe("createAcpReplyProjector", () => {
|
||||
coalesceIdleMs: 0,
|
||||
maxChunkChars: 256,
|
||||
maxTurnChars: 5,
|
||||
metaMode: "minimal",
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -363,7 +386,6 @@ describe("createAcpReplyProjector", () => {
|
||||
coalesceIdleMs: 0,
|
||||
maxChunkChars: 256,
|
||||
maxMetaEventsPerTurn: 1,
|
||||
showUsage: true,
|
||||
tagVisibility: {
|
||||
usage_update: true,
|
||||
},
|
||||
|
||||
@@ -157,16 +157,13 @@ export function createAcpReplyProjector(params: {
|
||||
if (!params.shouldSendToolSummaries) {
|
||||
return;
|
||||
}
|
||||
if (settings.metaMode === "off" && opts?.force !== true) {
|
||||
return;
|
||||
}
|
||||
const bounded = truncateText(text.trim(), settings.maxStatusChars);
|
||||
if (!bounded) {
|
||||
return;
|
||||
}
|
||||
const formatted = prefixSystemMessage(bounded);
|
||||
const hash = hashText(formatted);
|
||||
const shouldDedupe = opts?.dedupe !== false;
|
||||
const shouldDedupe = settings.repeatSuppression && opts?.dedupe !== false;
|
||||
if (shouldDedupe && lastStatusHash === hash) {
|
||||
return;
|
||||
}
|
||||
@@ -184,7 +181,7 @@ export function createAcpReplyProjector(params: {
|
||||
event: Extract<AcpRuntimeEvent, { type: "tool_call" }>,
|
||||
opts?: { force?: boolean },
|
||||
) => {
|
||||
if (!params.shouldSendToolSummaries || settings.metaMode === "off") {
|
||||
if (!params.shouldSendToolSummaries) {
|
||||
return;
|
||||
}
|
||||
if (!isAcpTagVisible(settings, event.tag)) {
|
||||
@@ -198,11 +195,7 @@ export function createAcpReplyProjector(params: {
|
||||
const isTerminal = status ? TERMINAL_TOOL_STATUSES.has(status) : false;
|
||||
const isStart = status === "in_progress" || event.tag === "tool_call";
|
||||
|
||||
if (settings.metaMode === "verbose") {
|
||||
if (lastToolHash === hash) {
|
||||
return;
|
||||
}
|
||||
} else if (settings.metaMode === "minimal") {
|
||||
if (settings.repeatSuppression) {
|
||||
if (toolCallId) {
|
||||
const state = toolLifecycleById.get(toolCallId) ?? {
|
||||
started: false,
|
||||
@@ -299,10 +292,7 @@ export function createAcpReplyProjector(params: {
|
||||
if (!isAcpTagVisible(settings, event.tag)) {
|
||||
return;
|
||||
}
|
||||
if (event.tag === "usage_update") {
|
||||
if (!settings.showUsage) {
|
||||
return;
|
||||
}
|
||||
if (event.tag === "usage_update" && settings.repeatSuppression) {
|
||||
const usageTuple =
|
||||
typeof event.used === "number" && typeof event.size === "number"
|
||||
? `${event.used}/${event.size}`
|
||||
|
||||
@@ -10,8 +10,7 @@ describe("acp stream settings", () => {
|
||||
it("resolves stable defaults", () => {
|
||||
const settings = resolveAcpProjectionSettings(createAcpTestConfig());
|
||||
expect(settings.deliveryMode).toBe("live");
|
||||
expect(settings.metaMode).toBe("minimal");
|
||||
expect(settings.showUsage).toBe(false);
|
||||
expect(settings.repeatSuppression).toBe(true);
|
||||
expect(settings.maxTurnChars).toBe(24_000);
|
||||
expect(settings.maxMetaEventsPerTurn).toBe(64);
|
||||
});
|
||||
@@ -23,8 +22,7 @@ describe("acp stream settings", () => {
|
||||
enabled: true,
|
||||
stream: {
|
||||
deliveryMode: "final_only",
|
||||
metaMode: "off",
|
||||
showUsage: true,
|
||||
repeatSuppression: false,
|
||||
maxTurnChars: 500,
|
||||
maxMetaEventsPerTurn: 7,
|
||||
tagVisibility: {
|
||||
@@ -35,8 +33,7 @@ describe("acp stream settings", () => {
|
||||
}),
|
||||
);
|
||||
expect(settings.deliveryMode).toBe("final_only");
|
||||
expect(settings.metaMode).toBe("off");
|
||||
expect(settings.showUsage).toBe(true);
|
||||
expect(settings.repeatSuppression).toBe(false);
|
||||
expect(settings.maxTurnChars).toBe(500);
|
||||
expect(settings.maxMetaEventsPerTurn).toBe(7);
|
||||
expect(settings.tagVisibility.usage_update).toBe(true);
|
||||
|
||||
@@ -4,8 +4,7 @@ import { resolveEffectiveBlockStreamingConfig } from "./block-streaming.js";
|
||||
|
||||
const DEFAULT_ACP_STREAM_COALESCE_IDLE_MS = 350;
|
||||
const DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS = 1800;
|
||||
const DEFAULT_ACP_META_MODE = "minimal";
|
||||
const DEFAULT_ACP_SHOW_USAGE = false;
|
||||
const DEFAULT_ACP_REPEAT_SUPPRESSION = true;
|
||||
const DEFAULT_ACP_DELIVERY_MODE = "live";
|
||||
const DEFAULT_ACP_MAX_TURN_CHARS = 24_000;
|
||||
const DEFAULT_ACP_MAX_TOOL_SUMMARY_CHARS = 320;
|
||||
@@ -26,12 +25,10 @@ export const ACP_TAG_VISIBILITY_DEFAULTS: Record<AcpSessionUpdateTag, boolean> =
|
||||
};
|
||||
|
||||
export type AcpDeliveryMode = "live" | "final_only";
|
||||
export type AcpMetaMode = "off" | "minimal" | "verbose";
|
||||
|
||||
export type AcpProjectionSettings = {
|
||||
deliveryMode: AcpDeliveryMode;
|
||||
metaMode: AcpMetaMode;
|
||||
showUsage: boolean;
|
||||
repeatSuppression: boolean;
|
||||
maxTurnChars: number;
|
||||
maxToolSummaryChars: number;
|
||||
maxStatusChars: number;
|
||||
@@ -65,13 +62,6 @@ function resolveAcpDeliveryMode(value: unknown): AcpDeliveryMode {
|
||||
return value === "final_only" ? "final_only" : DEFAULT_ACP_DELIVERY_MODE;
|
||||
}
|
||||
|
||||
function resolveAcpMetaMode(value: unknown): AcpMetaMode {
|
||||
if (value === "off" || value === "minimal" || value === "verbose") {
|
||||
return value;
|
||||
}
|
||||
return DEFAULT_ACP_META_MODE;
|
||||
}
|
||||
|
||||
function resolveAcpStreamCoalesceIdleMs(cfg: OpenClawConfig): number {
|
||||
return clampPositiveInteger(
|
||||
cfg.acp?.stream?.coalesceIdleMs,
|
||||
@@ -94,8 +84,7 @@ export function resolveAcpProjectionSettings(cfg: OpenClawConfig): AcpProjection
|
||||
const stream = cfg.acp?.stream;
|
||||
return {
|
||||
deliveryMode: resolveAcpDeliveryMode(stream?.deliveryMode),
|
||||
metaMode: resolveAcpMetaMode(stream?.metaMode),
|
||||
showUsage: clampBoolean(stream?.showUsage, DEFAULT_ACP_SHOW_USAGE),
|
||||
repeatSuppression: clampBoolean(stream?.repeatSuppression, DEFAULT_ACP_REPEAT_SUPPRESSION),
|
||||
maxTurnChars: clampPositiveInteger(stream?.maxTurnChars, DEFAULT_ACP_MAX_TURN_CHARS, {
|
||||
min: 1,
|
||||
max: 500_000,
|
||||
|
||||
@@ -172,10 +172,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Coalescer idle flush window in milliseconds for ACP streamed text before block replies are emitted.",
|
||||
"acp.stream.maxChunkChars":
|
||||
"Maximum chunk size for ACP streamed block projection before splitting into multiple block replies.",
|
||||
"acp.stream.metaMode":
|
||||
"ACP metadata projection mode: off suppresses status/tool lines, minimal dedupes aggressively, verbose streams non-identical updates.",
|
||||
"acp.stream.showUsage":
|
||||
"When true, usage_update events are projected as system lines only when usage values change.",
|
||||
"acp.stream.repeatSuppression":
|
||||
"When true (default), suppress repeated ACP status/tool projection lines in a turn while keeping raw ACP events unchanged.",
|
||||
"acp.stream.deliveryMode":
|
||||
"ACP delivery style: live streams block chunks incrementally, final_only buffers text deltas until terminal turn events.",
|
||||
"acp.stream.maxTurnChars":
|
||||
|
||||
@@ -369,8 +369,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"acp.stream": "ACP Stream",
|
||||
"acp.stream.coalesceIdleMs": "ACP Stream Coalesce Idle (ms)",
|
||||
"acp.stream.maxChunkChars": "ACP Stream Max Chunk Chars",
|
||||
"acp.stream.metaMode": "ACP Stream Meta Mode",
|
||||
"acp.stream.showUsage": "ACP Stream Show Usage",
|
||||
"acp.stream.repeatSuppression": "ACP Stream Repeat Suppression",
|
||||
"acp.stream.deliveryMode": "ACP Stream Delivery Mode",
|
||||
"acp.stream.maxTurnChars": "ACP Stream Max Turn Chars",
|
||||
"acp.stream.maxToolSummaryChars": "ACP Stream Max Tool Summary Chars",
|
||||
|
||||
@@ -10,10 +10,8 @@ export type AcpStreamConfig = {
|
||||
coalesceIdleMs?: number;
|
||||
/** Maximum text size per streamed chunk. */
|
||||
maxChunkChars?: number;
|
||||
/** Controls how ACP meta/system updates are projected to channels. */
|
||||
metaMode?: "off" | "minimal" | "verbose";
|
||||
/** Toggles usage_update projection in channel-facing output. */
|
||||
showUsage?: boolean;
|
||||
/** Suppresses repeated ACP status/tool projection lines within a turn. */
|
||||
repeatSuppression?: boolean;
|
||||
/** Live streams chunks or waits for terminal event before delivery. */
|
||||
deliveryMode?: "live" | "final_only";
|
||||
/** Maximum assistant text characters forwarded per turn. */
|
||||
|
||||
@@ -339,10 +339,7 @@ export const OpenClawSchema = z
|
||||
.object({
|
||||
coalesceIdleMs: z.number().int().nonnegative().optional(),
|
||||
maxChunkChars: z.number().int().positive().optional(),
|
||||
metaMode: z
|
||||
.union([z.literal("off"), z.literal("minimal"), z.literal("verbose")])
|
||||
.optional(),
|
||||
showUsage: z.boolean().optional(),
|
||||
repeatSuppression: z.boolean().optional(),
|
||||
deliveryMode: z.union([z.literal("live"), z.literal("final_only")]).optional(),
|
||||
maxTurnChars: z.number().int().positive().optional(),
|
||||
maxToolSummaryChars: z.number().int().positive().optional(),
|
||||
|
||||
Reference in New Issue
Block a user