fix(auto-reply): bind delivery to prepared harness

This commit is contained in:
joshavant
2026-08-10 17:15:09 -05:00
committed by Josh Avant
parent 33a2daf2ba
commit 3cb3f1e426
5 changed files with 75 additions and 63 deletions
@@ -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[];
@@ -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,
@@ -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<typeof createAgentRunEventHandler> | 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:
@@ -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") {
@@ -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;