mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(auto-reply): bind delivery to prepared harness
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user