diff --git a/src/agents/embedded-agent-runner/run/params.ts b/src/agents/embedded-agent-runner/run/params.ts index b293e5d695d7..30921372d3f3 100644 --- a/src/agents/embedded-agent-runner/run/params.ts +++ b/src/agents/embedded-agent-runner/run/params.ts @@ -360,6 +360,8 @@ export type RunEmbeddedAgentParams = { enqueue?: CommandQueueEnqueueFn; extraSystemPrompt?: string; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; + /** Re-resolve a runtime-derived source mode from the authoritative prepared harness. */ + onPreparedHarnessSourceReplyDeliveryMode?: (mode: SourceReplyDeliveryMode) => void; taskSuggestionDeliveryMode?: TaskSuggestionDeliveryMode; silentReplyPromptMode?: SilentReplyPromptMode; internalEvents?: AgentInternalEvent[]; diff --git a/src/agents/embedded-agent-runner/run/runtime-preparation.ts b/src/agents/embedded-agent-runner/run/runtime-preparation.ts index b4b0d936b4c7..0f69902b8603 100644 --- a/src/agents/embedded-agent-runner/run/runtime-preparation.ts +++ b/src/agents/embedded-agent-runner/run/runtime-preparation.ts @@ -496,6 +496,17 @@ export async function prepareEmbeddedRunRuntime(input: { preparedRunAdmission: params.preparedRunAdmission, }); + if (params.onPreparedHarnessSourceReplyDeliveryMode) { + // Route/auth/transport preparation owns the final harness selection. Publishing + // an earlier guess can either suppress a valid final or leak a private one. + const visibleReplies = + agentHarness.deliveryDefaults?.visibleReplies ?? + agentHarness.deliveryDefaults?.sourceVisibleReplies; + const mode = visibleReplies === "message_tool" ? "message_tool_only" : "automatic"; + params.sourceReplyDeliveryMode = mode; + params.forceMessageTool = mode === "message_tool_only"; + params.onPreparedHarnessSourceReplyDeliveryMode(mode); + } return { admittedRunContext, provider, diff --git a/src/auto-reply/reply/agent-runner-embedded-candidate.ts b/src/auto-reply/reply/agent-runner-embedded-candidate.ts index 0dbac82a35d9..973356baeb8e 100644 --- a/src/auto-reply/reply/agent-runner-embedded-candidate.ts +++ b/src/auto-reply/reply/agent-runner-embedded-candidate.ts @@ -89,7 +89,9 @@ export async function runEmbeddedFallbackCandidate(params: { >; notifyAgentRunStart: () => void; notifyUserAboutCompaction: boolean; - sourceRepliesAreToolOnly: boolean; + onPreparedHarnessSourceReplyDeliveryMode?: NonNullable< + RunEmbeddedAgentParams["onPreparedHarnessSourceReplyDeliveryMode"] + >; messageToolDeliveryState: MessageToolDeliveryState; preserveProgressCallbackStartOrder: boolean; presentation: EmbeddedPresentation; @@ -195,6 +197,7 @@ export async function runEmbeddedFallbackCandidate(params: { sessionKey: turn.sessionKey, milestone: "before_embedded_run", }); + let eventHandler: ReturnType | undefined; const result = await params.timing.measure("embedded_run", () => runEmbeddedAgent({ preparedRunAdmission: params.preparedRunAdmission, @@ -231,6 +234,7 @@ export async function runEmbeddedFallbackCandidate(params: { extraSystemPrompt: turn.followupRun.run.extraSystemPrompt, sourceReplyDeliveryMode: turn.followupRun.run.sourceReplyDeliveryMode, forceMessageTool: turn.followupRun.run.sourceReplyDeliveryMode === "message_tool_only", + onPreparedHarnessSourceReplyDeliveryMode: params.onPreparedHarnessSourceReplyDeliveryMode, silentReplyPromptMode: turn.followupRun.run.silentReplyPromptMode, suppressNextUserMessagePersistence: params.suppressQueuedUserPersistenceForCandidate, onUserMessagePersisted: params.notifyUserMessagePersisted, @@ -355,22 +359,26 @@ export async function runEmbeddedFallbackCandidate(params: { await turn.opts?.onReasoningEnd?.(); } : undefined, - onAgentEvent: createAgentRunEventHandler({ - turn, - lifecycleBackstop, - notifyAgentRunStart: params.notifyAgentRunStart, - sourceRepliesAreToolOnly: params.sourceRepliesAreToolOnly, - messageToolDeliveryState: params.messageToolDeliveryState, - provider: params.provider, - model: params.model, - runId: params.runId, - effectiveSessionId: params.effectiveRun.sessionId, - notifyUserAboutCompaction: params.notifyUserAboutCompaction, - onCompactionCompleted: () => { - attemptCompactionCount += 1; - return attemptCompactionCount; - }, - }), + onAgentEvent: (event) => { + eventHandler ??= createAgentRunEventHandler({ + turn, + lifecycleBackstop, + notifyAgentRunStart: params.notifyAgentRunStart, + sourceRepliesAreToolOnly: + turn.followupRun.run.sourceReplyDeliveryMode === "message_tool_only", + messageToolDeliveryState: params.messageToolDeliveryState, + provider: params.provider, + model: params.model, + runId: params.runId, + effectiveSessionId: params.effectiveRun.sessionId, + notifyUserAboutCompaction: params.notifyUserAboutCompaction, + onCompactionCompleted: () => { + attemptCompactionCount += 1; + return attemptCompactionCount; + }, + }); + return eventHandler(event); + }, // Flush-before-tool requires a handler even when regular block streaming is off. onBlockReply: params.presentation.blockReplyHandler, onBlockReplyFlush: diff --git a/src/auto-reply/reply/agent-runner-execution-message-tools.test.ts b/src/auto-reply/reply/agent-runner-execution-message-tools.test.ts index c57c9443b1aa..bfe3a032874f 100644 --- a/src/auto-reply/reply/agent-runner-execution-message-tools.test.ts +++ b/src/auto-reply/reply/agent-runner-execution-message-tools.test.ts @@ -219,21 +219,32 @@ describe("executeAgentTurn: message tool progress", () => { state.isCliProviderMock.mockImplementation((provider: unknown) => provider === "anthropic"); state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => { await params.run("anthropic", "primary").catch(() => undefined); - await params.run("custom", "plugin-fallback").catch(() => undefined); return { - result: await params.run("openai", "fallback"), - provider: "openai", - model: "fallback", + result: await params.run("custom", "plugin-fallback"), + provider: "custom", + model: "plugin-fallback", attempts: [], }; }); state.runCliAgentMock.mockRejectedValueOnce(new Error("cli failed")); - state.runEmbeddedAgentMock - .mockRejectedValueOnce(new Error("plugin fallback failed")) - .mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + const prepared = params as EmbeddedAgentParams & { + sourceReplyDeliveryMode?: "automatic" | "message_tool_only"; + forceMessageTool?: boolean; + onPreparedHarnessSourceReplyDeliveryMode?: ( + mode: "automatic" | "message_tool_only", + ) => void; + }; + // Preliminary custom-harness selection is tool-owned, but the prepared + // route/auth/transport facts authoritatively select the automatic owner. + prepared.sourceReplyDeliveryMode = "automatic"; + prepared.forceMessageTool = false; + prepared.onPreparedHarnessSourceReplyDeliveryMode?.("automatic"); + return { payloads: [{ text: "Short fallback final" }], meta: {}, - }); + }; + }); const executeAgentTurn = await getExecuteAgentTurnForTest(); const followupRun = createFollowupRun(); @@ -264,27 +275,15 @@ describe("executeAgentTurn: message tool progress", () => { resolvedVerboseLevel: "off", }); - const pluginParams = state.runEmbeddedAgentMock.mock.calls[0]?.[0] as { + const embeddedParams = state.runEmbeddedAgentMock.mock.calls[0]?.[0] as { sourceReplyDeliveryMode?: "automatic" | "message_tool_only"; forceMessageTool?: boolean; }; - const embeddedParams = state.runEmbeddedAgentMock.mock.calls[1]?.[0] as { - sourceReplyDeliveryMode?: "automatic" | "message_tool_only"; - forceMessageTool?: boolean; - }; - expect(pluginParams).toMatchObject({ - sourceReplyDeliveryMode: "message_tool_only", - forceMessageTool: true, - }); expect(embeddedParams).toMatchObject({ sourceReplyDeliveryMode: "automatic", forceMessageTool: false, }); - expect(onCandidateMode.mock.calls).toEqual([ - ["message_tool_only"], - ["message_tool_only"], - ["automatic"], - ]); + expect(onCandidateMode.mock.calls).toEqual([["message_tool_only"], ["automatic"]]); expect(execution.kind).toBe("success"); if (execution.kind !== "success") { diff --git a/src/auto-reply/reply/agent-runner-fallback-candidate.ts b/src/auto-reply/reply/agent-runner-fallback-candidate.ts index 0010657e367e..d671b7579ede 100644 --- a/src/auto-reply/reply/agent-runner-fallback-candidate.ts +++ b/src/auto-reply/reply/agent-runner-fallback-candidate.ts @@ -3,7 +3,6 @@ import { markAutoFallbackPrimaryProbe } from "../../agents/agent-scope.js"; import { resolveCliBackendConfig } from "../../agents/cli-backends.js"; import { runEmbeddedAgentEntry } from "../../agents/embedded-agent-runner/run-entry.js"; import type { FastModeAutoProgressState } from "../../agents/fast-mode.js"; -import { selectAgentHarness } from "../../agents/harness/selection.js"; import { resolveCliRuntimeExecutionProvider } from "../../agents/model-runtime-aliases.js"; import { isCliProvider } from "../../agents/model-selection.js"; import { resolveSessionRuntimeOverrideForProvider } from "../../agents/session-runtime-compat.js"; @@ -96,30 +95,11 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam const useCliExecution = pinnedCliRuntime !== undefined || (!sessionRuntimeOverride && isCliProvider(cliExecutionProvider, params.runtimeConfig)); - const embeddedHarness = useCliExecution - ? undefined - : selectAgentHarness({ - provider, - modelId: model, - config: params.runtimeConfig, - agentId: turn.followupRun.run.agentId, - sessionKey: turn.followupRun.run.runtimePolicySessionKey ?? turn.sessionKey, - agentHarnessId: - activeEntry?.modelSelectionLocked === true ? activeEntry.agentHarnessId : undefined, - agentHarnessRuntimeOverride: sessionRuntimeOverride, - }); - const harnessVisibleReplies = - embeddedHarness?.deliveryDefaults?.visibleReplies ?? - embeddedHarness?.deliveryDefaults?.sourceVisibleReplies; return { candidateRun, sessionRuntimeOverride, cliExecutionProvider, useCliExecution, - runtimeDefaultSourceReplyDeliveryMode: - useCliExecution || harnessVisibleReplies === "message_tool" - ? ("message_tool_only" as const) - : ("automatic" as const), }; }; return params.timing.measure("model_fallback", () => @@ -199,10 +179,12 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam ); const candidateRun = runtime.candidateRun; const candidateSourceReplyDeliveryMode = - sourceReplyDeliveryModeOrigin === "runtime_default" - ? runtime.runtimeDefaultSourceReplyDeliveryMode + sourceReplyDeliveryModeOrigin === "runtime_default" && runtime.useCliExecution + ? "message_tool_only" : turn.followupRun.run.sourceReplyDeliveryMode; - if (candidateSourceReplyDeliveryMode) { + const applySourceReplyDeliveryModeBeforeInvocation = + sourceReplyDeliveryModeOrigin !== "runtime_default" || runtime.useCliExecution; + if (candidateSourceReplyDeliveryMode && applySourceReplyDeliveryModeBeforeInvocation) { candidateRun.sourceReplyDeliveryMode = candidateSourceReplyDeliveryMode; turn.followupRun.run.sourceReplyDeliveryMode = candidateSourceReplyDeliveryMode; if (turn.opts) { @@ -294,7 +276,17 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam assistantErrorPersistedAcrossFallback = true; }, notifyUserAboutCompaction: params.notifyUserAboutCompaction, - sourceRepliesAreToolOnly: candidateSourceReplyDeliveryMode === "message_tool_only", + onPreparedHarnessSourceReplyDeliveryMode: + sourceReplyDeliveryModeOrigin === "runtime_default" + ? (mode) => { + candidateRun.sourceReplyDeliveryMode = mode; + turn.followupRun.run.sourceReplyDeliveryMode = mode; + if (turn.opts) { + turn.opts.sourceReplyDeliveryMode = mode; + } + sourceReplyDeliveryRuntimeOptions?.onSourceReplyDeliveryModeResolved?.(mode); + } + : undefined, messageToolDeliveryState, onCompactionCount: (count) => { params.state.autoCompactionCount += count;