Respect source channel for agent event surfacing (#36030)

This commit is contained in:
Octane
2026-03-06 14:14:00 +08:00
committed by GitHub
parent dfe23b9cc4
commit 777af476cb
6 changed files with 71 additions and 5 deletions

View File

@@ -26,6 +26,7 @@ import {
isMarkdownCapableMessageChannel,
resolveMessageChannel,
} from "../../utils/message-channel.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import { stripHeartbeatToken } from "../heartbeat.js";
import type { TemplateContext } from "../templating.js";
import type { VerboseLevel } from "../thinking.js";
@@ -113,11 +114,17 @@ export async function runAgentTurnWithFallback(params: {
didNotifyAgentRunStart = true;
params.opts?.onAgentRunStart?.(runId);
};
const shouldSurfaceToControlUi = isInternalMessageChannel(
params.followupRun.run.messageProvider ??
params.sessionCtx.Surface ??
params.sessionCtx.Provider,
);
if (params.sessionKey) {
registerAgentRunContext(runId, {
sessionKey: params.sessionKey,
verboseLevel: params.resolvedVerboseLevel,
isHeartbeat: params.isHeartbeat,
isControlUiVisible: shouldSurfaceToControlUi,
});
}
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;

View File

@@ -10,6 +10,7 @@ import type { TypingMode } from "../../config/types.js";
import { logVerbose } from "../../globals.js";
import { registerAgentRunContext } from "../../infra/agent-events.js";
import { defaultRuntime } from "../../runtime.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import { stripHeartbeatToken } from "../heartbeat.js";
import type { OriginatingChannelType } from "../templating.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
@@ -131,10 +132,17 @@ export function createFollowupRunner(params: {
return async (queued: FollowupRun) => {
try {
const runId = crypto.randomUUID();
const shouldSurfaceToControlUi = isInternalMessageChannel(
resolveOriginMessageProvider({
originatingChannel: queued.originatingChannel,
provider: queued.run.messageProvider,
}),
);
if (queued.run.sessionKey) {
registerAgentRunContext(runId, {
sessionKey: queued.run.sessionKey,
verboseLevel: queued.run.verboseLevel,
isControlUiVisible: shouldSurfaceToControlUi,
});
}
let autoCompactionCompleted = false;

View File

@@ -612,6 +612,29 @@ describe("agent event handler", () => {
expect(nodePayload.runId).toBe("run-fallback-client");
});
it("suppresses chat and node session events for non-control-UI-visible runs", () => {
const { broadcast, nodeSendToSession, handler } = createHarness({
resolveSessionKeyForRun: () => "session-hidden",
});
registerAgentRunContext("run-hidden", {
sessionKey: "session-hidden",
isControlUiVisible: false,
verboseLevel: "off",
});
handler({
runId: "run-hidden",
seq: 1,
stream: "assistant",
ts: Date.now(),
data: { text: "Reply from imessage" },
});
emitLifecycleEnd(handler, "run-hidden", 2);
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
expect(nodeSendToSession).not.toHaveBeenCalled();
});
it("uses agent event sessionKey when run-context lookup cannot resolve", () => {
const { broadcast, handler } = createHarness({
resolveSessionKeyForRun: () => undefined,

View File

@@ -502,6 +502,7 @@ export function createAgentEventHandler({
const chatLink = chatRunState.registry.peek(evt.runId);
const eventSessionKey =
typeof evt.sessionKey === "string" && evt.sessionKey.trim() ? evt.sessionKey : undefined;
const isControlUiVisible = getAgentRunContext(evt.runId)?.isControlUiVisible ?? true;
const sessionKey =
chatLink?.sessionKey ?? eventSessionKey ?? resolveSessionKeyForRun(evt.runId);
const clientRunId = chatLink?.clientRunId ?? evt.runId;
@@ -556,7 +557,7 @@ export function createAgentEventHandler({
const lifecyclePhase =
evt.stream === "lifecycle" && typeof evt.data?.phase === "string" ? evt.data.phase : null;
if (sessionKey) {
if (isControlUiVisible && sessionKey) {
// Send tool events to node/channel subscribers only when verbose is enabled;
// WS clients already received the event above via broadcastToConnIds.
if (!isToolEvent || toolVerbose !== "off") {

View File

@@ -61,4 +61,26 @@ describe("agent-events sequencing", () => {
expect(phases).toEqual(["start", "end"]);
});
test("omits sessionKey for runs hidden from Control UI", async () => {
resetAgentRunContextForTest();
registerAgentRunContext("run-hidden", {
sessionKey: "session-imessage",
isControlUiVisible: false,
});
let receivedSessionKey: string | undefined;
const stop = onAgentEvent((evt) => {
receivedSessionKey = evt.sessionKey;
});
emitAgentEvent({
runId: "run-hidden",
stream: "assistant",
data: { text: "hi" },
sessionKey: "session-imessage",
});
stop();
expect(receivedSessionKey).toBeUndefined();
});
});

View File

@@ -15,6 +15,8 @@ export type AgentRunContext = {
sessionKey?: string;
verboseLevel?: VerboseLevel;
isHeartbeat?: boolean;
/** Whether control UI clients should receive chat/agent updates for this run. */
isControlUiVisible?: boolean;
};
// Keep per-run counters so streams stay strictly monotonic per runId.
@@ -37,6 +39,9 @@ export function registerAgentRunContext(runId: string, context: AgentRunContext)
if (context.verboseLevel && existing.verboseLevel !== context.verboseLevel) {
existing.verboseLevel = context.verboseLevel;
}
if (context.isControlUiVisible !== undefined) {
existing.isControlUiVisible = context.isControlUiVisible;
}
if (context.isHeartbeat !== undefined && existing.isHeartbeat !== context.isHeartbeat) {
existing.isHeartbeat = context.isHeartbeat;
}
@@ -58,10 +63,10 @@ export function emitAgentEvent(event: Omit<AgentEventPayload, "seq" | "ts">) {
const nextSeq = (seqByRun.get(event.runId) ?? 0) + 1;
seqByRun.set(event.runId, nextSeq);
const context = runContextById.get(event.runId);
const sessionKey =
typeof event.sessionKey === "string" && event.sessionKey.trim()
? event.sessionKey
: context?.sessionKey;
const isControlUiVisible = context?.isControlUiVisible ?? true;
const eventSessionKey =
typeof event.sessionKey === "string" && event.sessionKey.trim() ? event.sessionKey : undefined;
const sessionKey = isControlUiVisible ? (eventSessionKey ?? context?.sessionKey) : undefined;
const enriched: AgentEventPayload = {
...event,
sessionKey,