mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-07 06:03:34 +00:00
fix(discord): hide internal Code Mode wait progress (#99893)
Hide invocation-specific Code Mode wait controls from channel progress while preserving telemetry and visible non-internal tools.
This commit is contained in:
committed by
GitHub
parent
3d6a2216ea
commit
3d404478b8
@@ -730,6 +730,65 @@ describe("agentLoop tool termination", () => {
|
||||
};
|
||||
}
|
||||
|
||||
it("marks lifecycle events from the concrete hidden tool instance", async () => {
|
||||
let turn = 0;
|
||||
const streamFn: StreamFn = () => {
|
||||
turn += 1;
|
||||
const stream = createAssistantMessageEventStream();
|
||||
queueMicrotask(() => {
|
||||
const message =
|
||||
turn === 1
|
||||
? makeAssistantMessage([
|
||||
{ type: "toolCall", id: "call-wait", name: "wait", arguments: {} },
|
||||
])
|
||||
: makeAssistantMessage([{ type: "text", text: "done" }]);
|
||||
stream.push({
|
||||
type: "done",
|
||||
reason: message.stopReason === "toolUse" ? "toolUse" : "stop",
|
||||
message,
|
||||
});
|
||||
stream.end();
|
||||
});
|
||||
return stream;
|
||||
};
|
||||
const hiddenTool: AgentTool = {
|
||||
...makeTool("wait", []),
|
||||
hideFromChannelProgress: true,
|
||||
execute: async (_toolCallId, _args, _signal, onUpdate) => {
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: "still waiting" }],
|
||||
details: { status: "waiting" },
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: "resumed" }],
|
||||
details: { status: "completed" },
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const events = await collectEvents(
|
||||
agentLoop(
|
||||
[{ role: "user", content: "resume", timestamp: 1 }],
|
||||
{ systemPrompt: "", messages: [], tools: [hiddenTool] },
|
||||
{ ...config, toolExecution: "sequential" },
|
||||
undefined,
|
||||
streamFn,
|
||||
),
|
||||
);
|
||||
const lifecycleEvents = events.filter((event) => event.type.startsWith("tool_execution_"));
|
||||
|
||||
expect(lifecycleEvents.map((event) => event.type)).toEqual([
|
||||
"tool_execution_start",
|
||||
"tool_execution_update",
|
||||
"tool_execution_end",
|
||||
]);
|
||||
expect(
|
||||
lifecycleEvents.every(
|
||||
(event) => "hideFromChannelProgress" in event && event.hideFromChannelProgress === true,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("continues after a side-effect tool result when afterToolCall records it without terminate", async () => {
|
||||
const executed: string[] = [];
|
||||
let turn = 0;
|
||||
|
||||
@@ -605,6 +605,19 @@ type ResolvedToolCallOutcome =
|
||||
| { kind: "resolved"; tool?: AgentTool }
|
||||
| { kind: "error"; error: unknown };
|
||||
|
||||
function hidesToolCallFromChannelProgress(
|
||||
context: AgentContext,
|
||||
toolCall: AgentToolCall,
|
||||
resolvedToolCalls: Map<AgentToolCall, ResolvedToolCallOutcome>,
|
||||
): boolean {
|
||||
const resolution = resolvedToolCalls.get(toolCall);
|
||||
const tool =
|
||||
resolution?.kind === "resolved"
|
||||
? resolution.tool
|
||||
: context.tools?.find((candidate) => candidate.name === toolCall.name);
|
||||
return tool?.hideFromChannelProgress === true;
|
||||
}
|
||||
|
||||
async function executeToolCallsSequential(
|
||||
currentContext: AgentContext,
|
||||
assistantMessage: AssistantMessage,
|
||||
@@ -618,11 +631,17 @@ async function executeToolCallsSequential(
|
||||
const messages: ToolResultMessage[] = [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
const hideFromChannelProgress = hidesToolCallFromChannelProgress(
|
||||
currentContext,
|
||||
toolCall,
|
||||
resolvedToolCalls,
|
||||
);
|
||||
await emit({
|
||||
type: "tool_execution_start",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
});
|
||||
|
||||
const preparation = await prepareToolCall(
|
||||
@@ -640,6 +659,7 @@ async function executeToolCallsSequential(
|
||||
result: preparation.result,
|
||||
isError: preparation.isError,
|
||||
executionStarted: false,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
};
|
||||
} else {
|
||||
const executed = await executePreparedToolCall(preparation, signal, emit);
|
||||
@@ -682,11 +702,17 @@ async function executeToolCallsParallel(
|
||||
const finalizedCalls: FinalizedToolCallEntry[] = [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
const hideFromChannelProgress = hidesToolCallFromChannelProgress(
|
||||
currentContext,
|
||||
toolCall,
|
||||
resolvedToolCalls,
|
||||
);
|
||||
await emit({
|
||||
type: "tool_execution_start",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.name,
|
||||
args: toolCall.arguments,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
});
|
||||
|
||||
const preparation = await prepareToolCall(
|
||||
@@ -703,6 +729,7 @@ async function executeToolCallsParallel(
|
||||
result: preparation.result,
|
||||
isError: preparation.isError,
|
||||
executionStarted: false,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
} satisfies FinalizedToolCallOutcome;
|
||||
await emitToolExecutionEnd(finalized, emit);
|
||||
finalizedCalls.push(finalized);
|
||||
@@ -769,6 +796,7 @@ type FinalizedToolCallOutcome = {
|
||||
result: AgentToolResult<unknown>;
|
||||
isError: boolean;
|
||||
executionStarted: boolean;
|
||||
hideFromChannelProgress?: boolean;
|
||||
};
|
||||
|
||||
type FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);
|
||||
@@ -947,6 +975,9 @@ async function executePreparedToolCall(
|
||||
toolName: prepared.toolCall.name,
|
||||
args: prepared.toolCall.arguments,
|
||||
partialResult,
|
||||
...(prepared.tool.hideFromChannelProgress === true
|
||||
? { hideFromChannelProgress: true }
|
||||
: {}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -1006,6 +1037,7 @@ async function finalizeExecutedToolCall(
|
||||
result,
|
||||
isError,
|
||||
executionStarted: true,
|
||||
...(prepared.tool.hideFromChannelProgress === true ? { hideFromChannelProgress: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1027,6 +1059,7 @@ async function emitToolExecutionEnd(
|
||||
result: finalized.result,
|
||||
isError: finalized.isError,
|
||||
executionStarted: finalized.executionStarted,
|
||||
...(finalized.hideFromChannelProgress === true ? { hideFromChannelProgress: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,8 @@ export interface AgentTool<
|
||||
> extends Tool<TParameters> {
|
||||
/** Human-readable label for UI display. */
|
||||
label: string;
|
||||
/** Preserve lifecycle telemetry without rendering transient channel progress. */
|
||||
hideFromChannelProgress?: boolean;
|
||||
/**
|
||||
* Optional compatibility shim for raw tool-call arguments before schema validation.
|
||||
* Must return an object that matches `TParameters`.
|
||||
@@ -514,13 +516,20 @@ export type AgentEvent =
|
||||
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
|
||||
| { type: "message_end"; message: AgentMessage }
|
||||
// Tool execution lifecycle
|
||||
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown }
|
||||
| {
|
||||
type: "tool_execution_start";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
hideFromChannelProgress?: boolean;
|
||||
}
|
||||
| {
|
||||
type: "tool_execution_update";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
partialResult: unknown;
|
||||
hideFromChannelProgress?: boolean;
|
||||
}
|
||||
| {
|
||||
type: "tool_execution_end";
|
||||
@@ -530,4 +539,5 @@ export type AgentEvent =
|
||||
isError: boolean;
|
||||
/** False when resolution, argument preparation, validation, or policy blocked execution. */
|
||||
executionStarted?: boolean;
|
||||
hideFromChannelProgress?: boolean;
|
||||
};
|
||||
|
||||
@@ -269,6 +269,13 @@ describe("Code Mode", () => {
|
||||
expect(compacted.catalogToolCount).toBe(2);
|
||||
});
|
||||
|
||||
it("marks only the internal wait control as hidden from channel progress", () => {
|
||||
const { tools } = createCodeModeHarness();
|
||||
|
||||
expect(tools[0].hideFromChannelProgress).toBeUndefined();
|
||||
expect(tools[1].hideFromChannelProgress).toBe(true);
|
||||
});
|
||||
|
||||
it("tells models to return the final code value", () => {
|
||||
const { config, catalogRef, tools: codeModeTools } = createCodeModeHarness();
|
||||
const compacted = applyCodeModeCatalog({
|
||||
|
||||
@@ -1172,6 +1172,7 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] {
|
||||
const waitTool = markCodeModeControlTool({
|
||||
name: CODE_MODE_WAIT_TOOL_NAME,
|
||||
label: "wait",
|
||||
hideFromChannelProgress: true,
|
||||
description: "Resume a suspended OpenClaw code mode run returned by exec.",
|
||||
parameters: Type.Object({
|
||||
runId: Type.String({ description: "Code mode run id returned by exec." }),
|
||||
|
||||
@@ -3756,6 +3756,9 @@ export async function runEmbeddedAttempt(
|
||||
toolCallId: toolParams.toolCallId,
|
||||
args: toolParams.input,
|
||||
replaySafe: replaySafeTools.has(toolParams.tool as never),
|
||||
hideFromChannelProgress:
|
||||
"hideFromChannelProgress" in toolParams.tool &&
|
||||
toolParams.tool.hideFromChannelProgress === true,
|
||||
execute: async () =>
|
||||
await toolParams.tool.execute(
|
||||
toolParams.toolCallId,
|
||||
|
||||
@@ -393,6 +393,68 @@ describe("handleToolExecutionStart read path checks", () => {
|
||||
expect(ctx.state.toolMetaById.has("tool-callback-rejects")).toBe(true);
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining("tool agent event callback failed"));
|
||||
});
|
||||
|
||||
it("preserves hidden tool telemetry while marking its channel progress private", async () => {
|
||||
const { ctx, onAgentEvent } = createTestContext();
|
||||
|
||||
await handleToolExecutionStart(ctx, {
|
||||
type: "tool_execution_start",
|
||||
toolName: "wait",
|
||||
toolCallId: "tool-code-wait",
|
||||
args: { runId: "cm_1" },
|
||||
hideFromChannelProgress: true,
|
||||
});
|
||||
handleToolExecutionUpdate(ctx, {
|
||||
type: "tool_execution_update",
|
||||
toolName: "wait",
|
||||
toolCallId: "tool-code-wait",
|
||||
args: { runId: "cm_1" },
|
||||
partialResult: { status: "waiting" },
|
||||
hideFromChannelProgress: true,
|
||||
});
|
||||
await handleToolExecutionEnd(ctx, {
|
||||
type: "tool_execution_end",
|
||||
toolName: "wait",
|
||||
toolCallId: "tool-code-wait",
|
||||
isError: false,
|
||||
result: { details: { status: "completed" } },
|
||||
hideFromChannelProgress: true,
|
||||
});
|
||||
|
||||
const lifecycleEvents = onAgentEvent.mock.calls
|
||||
.map((call) => call[0] as CapturedAgentEvent)
|
||||
.filter((event) => event.data?.name === "wait");
|
||||
expect(lifecycleEvents).not.toHaveLength(0);
|
||||
expect(lifecycleEvents.every((event) => event.data?.hideFromChannelProgress === true)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an unmarked catalog tool named wait visible", async () => {
|
||||
const { ctx, onAgentEvent } = createTestContext();
|
||||
|
||||
await handleToolExecutionStart(ctx, {
|
||||
type: "tool_execution_start",
|
||||
toolName: "wait",
|
||||
toolCallId: "tool-catalog-wait",
|
||||
args: {},
|
||||
});
|
||||
await handleToolExecutionEnd(ctx, {
|
||||
type: "tool_execution_end",
|
||||
toolName: "wait",
|
||||
toolCallId: "tool-catalog-wait",
|
||||
isError: false,
|
||||
result: { details: { status: "completed" } },
|
||||
});
|
||||
|
||||
const lifecycleEvents = onAgentEvent.mock.calls
|
||||
.map((call) => call[0] as CapturedAgentEvent)
|
||||
.filter((event) => event.data?.name === "wait");
|
||||
expect(lifecycleEvents).not.toHaveLength(0);
|
||||
expect(lifecycleEvents.every((event) => event.data?.hideFromChannelProgress !== true)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleToolExecutionEnd cron mutation tracking", () => {
|
||||
|
||||
@@ -788,6 +788,7 @@ export function handleToolExecutionStart(
|
||||
toolCallId: string;
|
||||
args: unknown;
|
||||
replaySafe?: boolean;
|
||||
hideFromChannelProgress?: boolean;
|
||||
},
|
||||
): void | Promise<void> {
|
||||
const continueAfterBlockReplyFlush = (): void | Promise<void> => {
|
||||
@@ -804,6 +805,7 @@ export function handleToolExecutionStart(
|
||||
const continueToolExecutionStart = () => {
|
||||
const rawToolName = evt.toolName;
|
||||
const toolName = normalizeToolName(rawToolName);
|
||||
const hideFromChannelProgress = evt.hideFromChannelProgress === true;
|
||||
const toolCallId = evt.toolCallId;
|
||||
const args = evt.args;
|
||||
const runId = ctx.params.runId;
|
||||
@@ -911,6 +913,7 @@ export function handleToolExecutionStart(
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
args: sanitizeToolArgs(args) as Record<string, unknown>,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
const itemData: AgentItemEventData = {
|
||||
@@ -923,6 +926,7 @@ export function handleToolExecutionStart(
|
||||
meta,
|
||||
toolCallId,
|
||||
startedAt,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
};
|
||||
emitTrackedItemEvent(ctx, itemData);
|
||||
// Best-effort typing signal; do not block tool summaries on slow emitters.
|
||||
@@ -933,6 +937,7 @@ export function handleToolExecutionStart(
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
args: sanitizeToolArgs(args) as Record<string, unknown>,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1026,10 +1031,12 @@ export function handleToolExecutionUpdate(
|
||||
toolName: string;
|
||||
toolCallId: string;
|
||||
partialResult?: unknown;
|
||||
hideFromChannelProgress?: boolean;
|
||||
},
|
||||
) {
|
||||
const toolName = normalizeToolName(evt.toolName);
|
||||
const toolCallId = evt.toolCallId;
|
||||
const hideFromChannelProgress = evt.hideFromChannelProgress === true;
|
||||
const partial = evt.partialResult;
|
||||
const sanitized = sanitizeToolResult(partial);
|
||||
const isExecTool = isExecToolName(toolName);
|
||||
@@ -1048,6 +1055,7 @@ export function handleToolExecutionUpdate(
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
partialResult: liveResult,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1059,6 +1067,7 @@ export function handleToolExecutionUpdate(
|
||||
status: "running",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
...(toolProgress
|
||||
? { progressText: toolProgress.text }
|
||||
: { meta: ctx.state.toolMetaById.get(toolCallId)?.meta }),
|
||||
@@ -1071,6 +1080,7 @@ export function handleToolExecutionUpdate(
|
||||
phase: "update",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1118,6 +1128,7 @@ export async function handleToolExecutionEnd(
|
||||
) {
|
||||
const rawToolName = evt.toolName;
|
||||
const toolName = normalizeToolName(rawToolName);
|
||||
const hideFromChannelProgress = evt.hideFromChannelProgress === true;
|
||||
const toolCallId = evt.toolCallId;
|
||||
const runId = ctx.params.runId;
|
||||
const isError = evt.isError;
|
||||
@@ -1330,6 +1341,7 @@ export async function handleToolExecutionEnd(
|
||||
meta,
|
||||
isError: isToolError,
|
||||
result: eventResult,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
const endedAt = Date.now();
|
||||
@@ -1345,6 +1357,7 @@ export async function handleToolExecutionEnd(
|
||||
toolCallId,
|
||||
startedAt: startData?.startTime,
|
||||
endedAt,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
...(isToolError && extractToolErrorMessage(sanitizedResult)
|
||||
? { error: extractToolErrorMessage(sanitizedResult) }
|
||||
: {}),
|
||||
@@ -1358,6 +1371,7 @@ export async function handleToolExecutionEnd(
|
||||
toolCallId,
|
||||
meta,
|
||||
isError: isToolError,
|
||||
...(hideFromChannelProgress ? { hideFromChannelProgress: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1350,6 +1350,7 @@ export function subscribeEmbeddedAgentSession(params: SubscribeEmbeddedAgentSess
|
||||
toolCallId: string;
|
||||
args: unknown;
|
||||
replaySafe?: boolean;
|
||||
hideFromChannelProgress?: boolean;
|
||||
execute: () => Promise<T>;
|
||||
}): Promise<T> => {
|
||||
await handleToolExecutionStart(ctx, {
|
||||
@@ -1358,6 +1359,7 @@ export function subscribeEmbeddedAgentSession(params: SubscribeEmbeddedAgentSess
|
||||
toolCallId: toolParams.toolCallId,
|
||||
args: toolParams.args,
|
||||
replaySafe: toolParams.replaySafe,
|
||||
hideFromChannelProgress: toolParams.hideFromChannelProgress,
|
||||
} as never);
|
||||
try {
|
||||
const result = await toolParams.execute();
|
||||
@@ -1368,6 +1370,7 @@ export function subscribeEmbeddedAgentSession(params: SubscribeEmbeddedAgentSess
|
||||
isError: false,
|
||||
executionStarted: true,
|
||||
result,
|
||||
hideFromChannelProgress: toolParams.hideFromChannelProgress,
|
||||
} as never);
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -1378,6 +1381,7 @@ export function subscribeEmbeddedAgentSession(params: SubscribeEmbeddedAgentSess
|
||||
isError: true,
|
||||
executionStarted: true,
|
||||
result: buildToolLifecycleErrorResult(error),
|
||||
hideFromChannelProgress: toolParams.hideFromChannelProgress,
|
||||
} as never);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -4566,6 +4566,82 @@ describe("runAgentTurnWithFallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides internal lifecycle events while preserving visible tool progress", async () => {
|
||||
const onItemEvent = vi.fn();
|
||||
const onToolStart = vi.fn();
|
||||
state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
|
||||
await params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
name: "exec",
|
||||
phase: "start",
|
||||
args: { command: "pwd" },
|
||||
},
|
||||
});
|
||||
await params.onAgentEvent?.({
|
||||
stream: "item",
|
||||
data: {
|
||||
itemId: "tool:exec-1",
|
||||
kind: "tool",
|
||||
title: "exec pwd",
|
||||
name: "exec",
|
||||
phase: "start",
|
||||
status: "running",
|
||||
},
|
||||
});
|
||||
await params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
name: "wait",
|
||||
phase: "start",
|
||||
args: { runId: "ordinary_wait" },
|
||||
},
|
||||
});
|
||||
await params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
name: "wait",
|
||||
phase: "start",
|
||||
args: { runId: "cm_1" },
|
||||
hideFromChannelProgress: true,
|
||||
},
|
||||
});
|
||||
await params.onAgentEvent?.({
|
||||
stream: "item",
|
||||
data: {
|
||||
itemId: "tool:wait-1",
|
||||
kind: "tool",
|
||||
title: "wait",
|
||||
name: "wait",
|
||||
phase: "start",
|
||||
status: "running",
|
||||
hideFromChannelProgress: true,
|
||||
},
|
||||
});
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback({
|
||||
...createMinimalRunAgentTurnParams({
|
||||
opts: { onItemEvent, onToolStart } satisfies GetReplyOptions,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("success");
|
||||
expect(onToolStart).toHaveBeenCalledTimes(2);
|
||||
expect(onToolStart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "exec", phase: "start" }),
|
||||
);
|
||||
expect(onToolStart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "wait", phase: "start" }),
|
||||
);
|
||||
expect(onItemEvent).toHaveBeenCalledTimes(1);
|
||||
expect(onItemEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "exec", phase: "start" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards raw tool progress detail mode to tool-start reply options", async () => {
|
||||
const onToolStart = vi.fn();
|
||||
state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
|
||||
|
||||
@@ -2697,7 +2697,7 @@ async function runAgentTurnWithFallbackInternal(
|
||||
}
|
||||
// Trigger typing when tools start executing.
|
||||
// Must await to ensure typing indicator starts before tool summaries are emitted.
|
||||
if (evt.stream === "tool") {
|
||||
if (evt.stream === "tool" && evt.data.hideFromChannelProgress !== true) {
|
||||
const phase = readStringValue(evt.data.phase) ?? "";
|
||||
const name = readStringValue(evt.data.name);
|
||||
const toolCallId = readStringValue(evt.data.toolCallId) ?? "";
|
||||
@@ -2741,6 +2741,8 @@ async function runAgentTurnWithFallbackInternal(
|
||||
evt.stream === "item" &&
|
||||
evt.data.suppressChannelProgress === true &&
|
||||
Boolean(params.opts?.onToolStart);
|
||||
const hideItemFromChannelProgress =
|
||||
evt.stream === "item" && evt.data.hideFromChannelProgress === true;
|
||||
const itemPhase =
|
||||
evt.stream === "item" ? readStringValue(evt.data.phase) : "";
|
||||
const itemName =
|
||||
@@ -2793,6 +2795,7 @@ async function runAgentTurnWithFallbackInternal(
|
||||
}
|
||||
if (
|
||||
evt.stream === "item" &&
|
||||
!hideItemFromChannelProgress &&
|
||||
!suppressItemChannelProgress &&
|
||||
(!suppressProgressAfterMessageToolDelivery ||
|
||||
completedMessageToolDelivery)
|
||||
|
||||
@@ -3165,6 +3165,60 @@ describe("createFollowupRunner progress forwarding", () => {
|
||||
expect(onCommandOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps internal tool lifecycle events out of queued channel progress", async () => {
|
||||
const onToolStart = vi.fn(async () => {});
|
||||
const onItemEvent = vi.fn(async () => {});
|
||||
|
||||
runEmbeddedAgentMock.mockImplementationOnce(
|
||||
async (args: {
|
||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => Promise<void>;
|
||||
}) => {
|
||||
await args.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "start",
|
||||
name: "wait",
|
||||
hideFromChannelProgress: true,
|
||||
},
|
||||
});
|
||||
await args.onAgentEvent?.({
|
||||
stream: "item",
|
||||
data: {
|
||||
phase: "start",
|
||||
itemId: "tool:wait-1",
|
||||
title: "wait",
|
||||
hideFromChannelProgress: true,
|
||||
},
|
||||
});
|
||||
return { payloads: [{ text: "final" }], meta: { agentMeta: {} } };
|
||||
},
|
||||
);
|
||||
|
||||
const runner = createFollowupRunner({
|
||||
opts: {
|
||||
allowToolLifecycleWhenProgressHidden: true,
|
||||
onToolStart,
|
||||
onItemEvent,
|
||||
},
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
defaultModel: "claude",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
run: {
|
||||
messageProvider: "discord",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
verboseLevel: "off",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onToolStart).not.toHaveBeenCalled();
|
||||
expect(onItemEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps queued follow-up progress quiet when verbose state is missing", async () => {
|
||||
const onToolStart = vi.fn(async () => {});
|
||||
const onCommandOutput = vi.fn(async () => {});
|
||||
|
||||
@@ -167,7 +167,7 @@ async function forwardFollowupProgressEvent(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.stream === "tool") {
|
||||
if (evt.stream === "tool" && evt.data.hideFromChannelProgress !== true) {
|
||||
const phase = readStringValue(evt.data.phase) ?? "";
|
||||
const name = readStringValue(evt.data.name);
|
||||
if (phase === "start" || phase === "update") {
|
||||
@@ -193,7 +193,9 @@ async function forwardFollowupProgressEvent(params: {
|
||||
evt.stream === "item" &&
|
||||
evt.data.suppressChannelProgress === true &&
|
||||
Boolean(opts?.onToolStart);
|
||||
if (evt.stream === "item" && !suppressItemChannelProgress) {
|
||||
const hideItemFromChannelProgress =
|
||||
evt.stream === "item" && evt.data.hideFromChannelProgress === true;
|
||||
if (evt.stream === "item" && !suppressItemChannelProgress && !hideItemFromChannelProgress) {
|
||||
await opts?.onItemEvent?.({
|
||||
itemId: readStringValue(evt.data.itemId),
|
||||
toolCallId: readStringValue(evt.data.toolCallId),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { createAbortError } from "./abort-signal.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { notifyListeners, registerListener } from "../shared/listeners.js";
|
||||
import { createAbortError } from "./abort-signal.js";
|
||||
|
||||
/** Stream name for agent events delivered to gateway listeners and plugin host hooks. */
|
||||
export type AgentEventStream =
|
||||
@@ -51,6 +51,8 @@ export type AgentItemEventData = {
|
||||
progressText?: string;
|
||||
/** Preserve item telemetry while letting channel progress render a sibling tool event instead. */
|
||||
suppressChannelProgress?: boolean;
|
||||
/** Preserve activity telemetry without rendering this internal item in channel progress. */
|
||||
hideFromChannelProgress?: boolean;
|
||||
approvalId?: string;
|
||||
approvalSlug?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user