mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix: continue code mode with read-only recovery (#128071)
Preserve failed Code Mode mutations without replay while allowing one final-policy-authorized core-read reconciliation attempt to report partial application state. Fixes #128028.
This commit is contained in:
@@ -33,6 +33,7 @@ import { normalizeEmbeddedRunAttempt } from "./run/attempt-normalization.js";
|
||||
import { forgetPromptBuildDrainCacheForRun } from "./run/attempt-prompt-helpers.js";
|
||||
import { recoverEmbeddedRunAttempt } from "./run/attempt-recovery.js";
|
||||
import { createMcpAttemptCarryover } from "./run/attempt-result.js";
|
||||
import { activateCodeModeReconciliation } from "./run/code-mode-reconciliation.js";
|
||||
import { hasCodexAppServerRecoveryRetryBudget } from "./run/codex-app-server-recovery.js";
|
||||
import { createEmbeddedRunCompactionRuntime } from "./run/compaction-runtime.js";
|
||||
import { createEmbeddedRunContextRecoveryState } from "./run/context-recovery-state.js";
|
||||
@@ -522,6 +523,16 @@ export async function runPreparedEmbeddedLoop(
|
||||
if (assistantFailureOutcome.action === "retry") {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
activateCodeModeReconciliation({
|
||||
attempt,
|
||||
hostOwnsToolSurface: !pluginHarnessOwnsTransport,
|
||||
retryState: terminalRetryState,
|
||||
activateInternalPrompt: sessionPromptState.activateInternalPrompt,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let assistantProfileFailureReason = assistantFailureOutcome.assistantProfileFailureReason;
|
||||
const terminalToolPresentationText = terminalToolPresentation.read();
|
||||
const finalizedTerminal = await prepareTerminalWithSettledTurnFinalization({
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { buildEmbeddedRunnerAssistant } from "../test-helpers/embedded-agent-runner-e2e-fixtures.js";
|
||||
import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
|
||||
import {
|
||||
mockedClassifyFailoverReason,
|
||||
mockedRunEmbeddedAttempt,
|
||||
overflowBaseRunParams,
|
||||
resetSharedRunIntegrationHarnessMocks,
|
||||
useOpenAIPlatformAuthFixture,
|
||||
} from "./run.overflow-compaction.harness.js";
|
||||
import { loadSharedRunIntegrationHarness } from "./run.shared-integration-harness.test-support.js";
|
||||
|
||||
let runEmbeddedAgent: Awaited<ReturnType<typeof loadSharedRunIntegrationHarness>>;
|
||||
|
||||
describe("runEmbeddedAgent Code Mode reconciliation", () => {
|
||||
beforeAll(async () => {
|
||||
runEmbeddedAgent = await loadSharedRunIntegrationHarness();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetSharedRunIntegrationHarnessMocks();
|
||||
mockedClassifyFailoverReason.mockReturnValue(null);
|
||||
useOpenAIPlatformAuthFixture();
|
||||
});
|
||||
|
||||
it("continues a settled partial mutation with one read-only attempt", async () => {
|
||||
const mutationAssistant = buildEmbeddedRunnerAssistant({
|
||||
stopReason: "toolUse",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "code-mode-mutation",
|
||||
name: "code_mode",
|
||||
arguments: { action: "exec" },
|
||||
},
|
||||
],
|
||||
});
|
||||
mockedRunEmbeddedAttempt
|
||||
.mockResolvedValueOnce(
|
||||
makeAttemptResult({
|
||||
assistantTexts: [],
|
||||
lastAssistant: mutationAssistant,
|
||||
currentAttemptAssistant: mutationAssistant,
|
||||
currentAttemptCompletedAssistant: mutationAssistant,
|
||||
codeModeReconciliationCandidate: true,
|
||||
itemLifecycle: { startedCount: 1, completedCount: 1, activeCount: 0 },
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(makeAttemptResult({ assistantTexts: ["The first hunk applied."] }));
|
||||
|
||||
await runEmbeddedAgent({
|
||||
...overflowBaseRunParams,
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: { "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
runId: "run-code-mode-reconciliation",
|
||||
});
|
||||
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
mockedRunEmbeddedAttempt.mock.calls[0]?.[0].forceCodeModeReconciliationTools,
|
||||
).toBeFalsy();
|
||||
expect(mockedRunEmbeddedAttempt.mock.calls[1]?.[0]).toMatchObject({
|
||||
forceCodeModeReconciliationTools: true,
|
||||
prompt: expect.stringContaining("may have partially applied"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// The imported scenario modules share one mocked runEmbeddedAgent module graph.
|
||||
import "./run.before-agent-finalize.test-support.js";
|
||||
import "./run.before-agent-reply-cron.test-support.js";
|
||||
import "./run.code-mode-reconciliation.test-support.js";
|
||||
import "./run.codex-app-server-recovery.test-support.js";
|
||||
import "./run.codex-server-error-fallback.test-support.js";
|
||||
import "./run.compaction-loop-guard.test-support.js";
|
||||
|
||||
@@ -70,7 +70,8 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
|
||||
toolsEnabled &&
|
||||
!params.attempt.disableTools &&
|
||||
!params.isRawModelRun &&
|
||||
!params.attempt.forceRestartSafeTools
|
||||
!params.attempt.forceRestartSafeTools &&
|
||||
!params.attempt.forceCodeModeReconciliationTools
|
||||
? params.attempt.clientTools
|
||||
: undefined;
|
||||
// Client functions share the attempt's authority; filter before their names
|
||||
@@ -83,6 +84,7 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
|
||||
: providedClientTools;
|
||||
const bundleMcpEnabled =
|
||||
!params.attempt.forceRestartSafeTools &&
|
||||
!params.attempt.forceCodeModeReconciliationTools &&
|
||||
shouldCreateBundleMcpRuntimeForAttempt({
|
||||
toolsEnabled,
|
||||
disableTools: params.attempt.disableTools || params.isRawModelRun,
|
||||
@@ -125,6 +127,7 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
|
||||
try {
|
||||
const bundleLspEnabled =
|
||||
!params.attempt.forceRestartSafeTools &&
|
||||
!params.attempt.forceCodeModeReconciliationTools &&
|
||||
shouldCreateBundleLspRuntimeForAttempt({
|
||||
toolsEnabled,
|
||||
disableTools: params.attempt.disableTools || params.isRawModelRun,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { setPluginToolMeta } from "../../../plugins/tools.js";
|
||||
import { setChannelAgentToolMeta } from "../../channel-tool-metadata.js";
|
||||
import { createCodeModeCatalogProjection } from "../../code-mode-catalog.js";
|
||||
import { applyCodeModeCatalog, createCodeModeTools } from "../../code-mode.js";
|
||||
import { runUntilCompleted } from "../../code-mode.test-support.js";
|
||||
@@ -99,6 +100,28 @@ function prepare(input: {
|
||||
}
|
||||
|
||||
describe("prepareEmbeddedAttemptClientTools", () => {
|
||||
it("records core read entitlement without plugin or channel shadows", () => {
|
||||
const coreRead = createStubTool("read");
|
||||
const pluginRead = createStubTool("read");
|
||||
const channelRead = createStubTool("read");
|
||||
const catalogRef = createToolSearchCatalogRef();
|
||||
setPluginToolMeta(pluginRead, { pluginId: "example-plugin", optional: false });
|
||||
setChannelAgentToolMeta(channelRead as never, { channelId: "example-channel" });
|
||||
|
||||
expect(
|
||||
[coreRead, pluginRead, channelRead].map(
|
||||
(tool) =>
|
||||
prepare({
|
||||
codeModeControlsEnabledForRun: false,
|
||||
attemptConfig: CATALOGS_DISABLED_CONFIG,
|
||||
toolSearchRuntimeConfig: CATALOGS_DISABLED_CONFIG,
|
||||
catalogRef,
|
||||
uncompactedEffectiveTools: [tool],
|
||||
}).coreReadAuthorized,
|
||||
),
|
||||
).toEqual([true, false, false]);
|
||||
});
|
||||
|
||||
it("hides client tools behind the code-mode catalog when code mode is engaged", () => {
|
||||
const catalogRef = seedCatalog("code-mode", CODE_MODE_CONFIG);
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import {
|
||||
toClientToolDefinitions,
|
||||
} from "../../agent-tool-definition-adapter.js";
|
||||
import { resolveToolLoopDetectionConfig } from "../../agent-tools.js";
|
||||
import { getChannelAgentToolMeta } from "../../channel-tools.js";
|
||||
import { addClientToolsToCodeModeCatalog } from "../../code-mode.js";
|
||||
import type { AgentTool } from "../../runtime/index.js";
|
||||
import { normalizeToolPolicyName } from "../../tool-policy.js";
|
||||
import {
|
||||
collectReplaySafeToolNames,
|
||||
collectSideEffectToolOwners,
|
||||
@@ -68,6 +70,12 @@ export function prepareEmbeddedAttemptClientTools(params: {
|
||||
isPluginTool: (tool) =>
|
||||
Boolean(getPluginToolMeta(tool as Parameters<typeof getPluginToolMeta>[0])),
|
||||
});
|
||||
const coreReadAuthorized = params.uncompactedEffectiveTools.some(
|
||||
(tool) =>
|
||||
normalizeToolPolicyName(tool.name ?? "") === "read" &&
|
||||
!getPluginToolMeta(tool) &&
|
||||
!getChannelAgentToolMeta(tool),
|
||||
);
|
||||
const isReplaySafeTool = (tool: { name?: string }) =>
|
||||
isAgentToolReplaySafe(tool, params.replaySafetyOptions);
|
||||
const replaySafeTools = new Set(params.uncompactedEffectiveTools.filter(isReplaySafeTool));
|
||||
@@ -176,6 +184,7 @@ export function prepareEmbeddedAttemptClientTools(params: {
|
||||
allCustomTools,
|
||||
builtinToolNames,
|
||||
coreBuiltinToolNames,
|
||||
coreReadAuthorized,
|
||||
clientToolCallSlots,
|
||||
clientToolDefs,
|
||||
clientToolLoopDetection,
|
||||
|
||||
@@ -54,7 +54,9 @@ export async function prepareAndDispatchEmbeddedRunAttempt(input: {
|
||||
provider,
|
||||
modelId,
|
||||
} = input;
|
||||
const params = runInput.runParams;
|
||||
const params = input.terminalRetryState.forceCodeModeReconciliationTools
|
||||
? { ...runInput.runParams, forceCodeModeReconciliationTools: true }
|
||||
: runInput.runParams;
|
||||
const {
|
||||
workspaceResolution,
|
||||
workspaceDir,
|
||||
|
||||
@@ -173,8 +173,11 @@ function createFixture() {
|
||||
agentSession: {
|
||||
activeSession,
|
||||
clientToolCallSlots: [],
|
||||
coreReadAuthorized: true,
|
||||
getCodeModeReconciliationCandidate: vi.fn(() => false),
|
||||
hasDeliveredSourceReply: vi.fn(() => true),
|
||||
hookRunner,
|
||||
setCodeModeReconciliationReadAuthorized: vi.fn(),
|
||||
setActiveSessionSystemPrompt: vi.fn(),
|
||||
settingsManager: { getCompactionReserveTokens: vi.fn(() => 1_000) },
|
||||
},
|
||||
|
||||
@@ -64,6 +64,7 @@ import type { submitEmbeddedAttemptPrompt } from "./attempt-prompt-submit.js";
|
||||
type PromptPhaseInput = Parameters<typeof runEmbeddedAttemptPromptPhase>[0];
|
||||
type PromptPhaseState = ReturnType<PromptPhaseInput["lifecycle"]["readState"]>;
|
||||
type AssemblyCall = {
|
||||
applyPromptBuildToolsAllow: (toolsAllow: string[] | undefined) => string[];
|
||||
setLeasedSteering: (lease: { leaseId: string; runIds: string[] }) => void;
|
||||
};
|
||||
type PromptPreflightCall = Parameters<typeof prepareEmbeddedAttemptPromptPreflight>[0];
|
||||
@@ -107,6 +108,7 @@ function createFixture() {
|
||||
prePromptMessageCount = count;
|
||||
});
|
||||
const setPromptCacheChangesForTurn = vi.fn();
|
||||
const setCodeModeReconciliationReadAuthorized = vi.fn();
|
||||
const setFinalPromptText = vi.fn();
|
||||
const markBeforeAgentRunBlocked = vi.fn();
|
||||
const markYieldAborted = vi.fn(() => {
|
||||
@@ -119,6 +121,7 @@ function createFixture() {
|
||||
mocks.preparePromptAssembly.mockImplementation(async (input: AssemblyCall) => {
|
||||
order.push("assembly");
|
||||
const lease = { leaseId: "lease-1", runIds: ["run-1"] };
|
||||
input.applyPromptBuildToolsAllow(undefined);
|
||||
input.setLeasedSteering(lease);
|
||||
return {
|
||||
hookCtx: {},
|
||||
@@ -237,6 +240,14 @@ function createFixture() {
|
||||
transport: "sse",
|
||||
uncompactedEffectiveTools: [],
|
||||
},
|
||||
toolPolicy: {
|
||||
baseline: { activeToolNames: ["read"], catalogEntries: [] },
|
||||
effectiveTools: [{ name: "read" }],
|
||||
uncompactedEffectiveTools: [{ name: "read" }],
|
||||
tools: [{ name: "read" }],
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
},
|
||||
preflight: {
|
||||
contextEngineAssemblySucceeded: false,
|
||||
contextEnginePromptAuthority: "assembled",
|
||||
@@ -258,6 +269,7 @@ function createFixture() {
|
||||
setPrePromptMessageCount,
|
||||
setCurrentUserTimestampOverride: vi.fn(),
|
||||
setPromptCacheChangesForTurn,
|
||||
setCodeModeReconciliationReadAuthorized,
|
||||
setFinalPromptText,
|
||||
markBeforeAgentRunBlocked,
|
||||
markYieldAborted,
|
||||
@@ -275,6 +287,7 @@ function createFixture() {
|
||||
setFinalPromptText,
|
||||
setPrePromptMessageCount,
|
||||
setPromptCacheChangesForTurn,
|
||||
setCodeModeReconciliationReadAuthorized,
|
||||
state,
|
||||
yieldState,
|
||||
};
|
||||
@@ -282,6 +295,13 @@ function createFixture() {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.applyPromptToolsAllow.mockReturnValue({
|
||||
activeToolNames: ["read"],
|
||||
coreReadAuthorized: true,
|
||||
effectiveTools: [{ name: "read" }],
|
||||
uncompactedEffectiveTools: [{ name: "read" }],
|
||||
tools: [{ name: "read" }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("runEmbeddedAttemptPromptPhase", () => {
|
||||
@@ -308,6 +328,7 @@ describe("runEmbeddedAttemptPromptPhase", () => {
|
||||
]);
|
||||
expect(fixture.setPrePromptMessageCount).toHaveBeenCalledWith(2);
|
||||
expect(fixture.setPromptCacheChangesForTurn).toHaveBeenCalledWith([]);
|
||||
expect(fixture.setCodeModeReconciliationReadAuthorized).toHaveBeenCalledWith(true);
|
||||
expect(fixture.setFinalPromptText).toHaveBeenCalledWith("hello");
|
||||
expect(mocks.preparePromptExecution).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -335,6 +356,21 @@ describe("runEmbeddedAttemptPromptPhase", () => {
|
||||
expect(mocks.releasePendingSteering).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("records a final prompt policy that removes core read", async () => {
|
||||
const fixture = createFixture();
|
||||
mocks.applyPromptToolsAllow.mockReturnValueOnce({
|
||||
activeToolNames: [],
|
||||
coreReadAuthorized: false,
|
||||
effectiveTools: [],
|
||||
uncompactedEffectiveTools: [],
|
||||
tools: [],
|
||||
});
|
||||
|
||||
await runEmbeddedAttemptPromptPhase(fixture.input);
|
||||
|
||||
expect(fixture.setCodeModeReconciliationReadAuthorized).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("skips before_agent_run for settled-turn finalization", async () => {
|
||||
const fixture = createFixture();
|
||||
fixture.input.attempt.operation = "settled-tool-finalization";
|
||||
|
||||
@@ -122,6 +122,7 @@ export async function runEmbeddedAttemptPromptPhase(input: {
|
||||
tools: Array<{ name: string }>;
|
||||
toolSearchCatalogRef?: Parameters<typeof applyPromptBuildToolsAllow>[0]["catalogRef"];
|
||||
codeModeControlsEnabled: boolean;
|
||||
coreReadAuthorized: boolean;
|
||||
forceToolNames?: readonly string[];
|
||||
};
|
||||
preflight: PromptPreflightPhaseInput;
|
||||
@@ -137,6 +138,7 @@ export async function runEmbeddedAttemptPromptPhase(input: {
|
||||
setPromptCacheChangesForTurn: (
|
||||
changes: PromptAssemblyResult["promptCacheChangesForTurn"],
|
||||
) => void;
|
||||
setCodeModeReconciliationReadAuthorized: (value: boolean) => void;
|
||||
setFinalPromptText: (prompt: string) => void;
|
||||
markBeforeAgentRunBlocked: (outcome: BeforeAgentRunOutcome) => void;
|
||||
markYieldAborted: () => void;
|
||||
@@ -217,8 +219,10 @@ export async function runEmbeddedAttemptPromptPhase(input: {
|
||||
tools: input.toolPolicy.tools,
|
||||
catalogRef: input.toolPolicy.toolSearchCatalogRef,
|
||||
codeModeControlsEnabled: input.toolPolicy.codeModeControlsEnabled,
|
||||
coreReadAuthorized: input.toolPolicy.coreReadAuthorized,
|
||||
forceToolNames: input.toolPolicy.forceToolNames,
|
||||
});
|
||||
input.lifecycle.setCodeModeReconciliationReadAuthorized(promptToolSurface.coreReadAuthorized);
|
||||
return promptToolSurface.activeToolNames;
|
||||
},
|
||||
setLeasedSteering: (lease) => {
|
||||
|
||||
@@ -76,9 +76,11 @@ export function applyPromptBuildToolsAllow<
|
||||
tools: TTool[];
|
||||
catalogRef?: ToolSearchCatalogRef;
|
||||
codeModeControlsEnabled: boolean;
|
||||
coreReadAuthorized: boolean;
|
||||
forceToolNames?: readonly string[];
|
||||
}): {
|
||||
activeToolNames: string[];
|
||||
coreReadAuthorized: boolean;
|
||||
effectiveTools: TEffectiveTool[];
|
||||
uncompactedEffectiveTools: TUncompactedTool[];
|
||||
tools: TTool[];
|
||||
@@ -122,6 +124,9 @@ export function applyPromptBuildToolsAllow<
|
||||
|
||||
return {
|
||||
activeToolNames,
|
||||
coreReadAuthorized:
|
||||
params.coreReadAuthorized &&
|
||||
allowedUncompactedTools.some((tool) => normalizeToolPolicyName(tool.name) === "read"),
|
||||
effectiveTools: promptPolicy.tools,
|
||||
uncompactedEffectiveTools: allowedUncompactedTools,
|
||||
tools: allowedTools,
|
||||
|
||||
@@ -78,9 +78,11 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
tools: [{ name: "read" }, { name: "write" }, { name: "message" }],
|
||||
catalogRef,
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.activeToolNames).toEqual([]);
|
||||
expect(result.coreReadAuthorized).toBe(false);
|
||||
expect(result.effectiveTools).toEqual([]);
|
||||
expect(result.uncompactedEffectiveTools).toEqual([]);
|
||||
expect(result.tools).toEqual([]);
|
||||
@@ -107,6 +109,7 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
uncompactedEffectiveTools: [{ name: "message" }, { name: "read" }],
|
||||
tools: [{ name: "message" }, { name: "read" }],
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.activeToolNames).toEqual(["message"]);
|
||||
@@ -143,9 +146,11 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
tools: [{ name: "read" }, { name: "write" }, { name: "message" }],
|
||||
catalogRef,
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.activeToolNames).toEqual(["tool_search"]);
|
||||
expect(result.coreReadAuthorized).toBe(true);
|
||||
expect(result.effectiveTools).toEqual([{ name: "tool_search" }]);
|
||||
expect(result.uncompactedEffectiveTools).toEqual([{ name: "read" }]);
|
||||
expect(result.tools).toEqual([{ name: "read" }]);
|
||||
@@ -164,9 +169,11 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
uncompactedEffectiveTools: [{ name: "read" }],
|
||||
tools: [{ name: "read" }],
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
});
|
||||
|
||||
expect(result.activeToolNames).toEqual([]);
|
||||
expect(result.coreReadAuthorized).toBe(false);
|
||||
expect(result.effectiveTools).toEqual([]);
|
||||
expect(result.uncompactedEffectiveTools).toEqual([]);
|
||||
expect(result.tools).toEqual([]);
|
||||
@@ -198,6 +205,7 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
tools: [pluginTool],
|
||||
catalogRef,
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: false,
|
||||
});
|
||||
|
||||
expect(result.activeToolNames).toEqual(["tool_search"]);
|
||||
@@ -225,6 +233,7 @@ describe("applyPromptBuildToolsAllow", () => {
|
||||
tools: [{ name: "read" }, { name: "write" }],
|
||||
catalogRef,
|
||||
codeModeControlsEnabled: false,
|
||||
coreReadAuthorized: true,
|
||||
};
|
||||
|
||||
applyPromptBuildToolsAllow({ ...params, toolsAllow: ["read"] });
|
||||
|
||||
@@ -73,6 +73,7 @@ type EmbeddedAttemptResultState = Pick<
|
||||
| "lastAssistant"
|
||||
| "currentAttemptAssistant"
|
||||
| "currentAttemptCompletedAssistant"
|
||||
| "codeModeReconciliationCandidate"
|
||||
| "successfulNestedToolNames"
|
||||
| "attemptUsage"
|
||||
| "promptCache"
|
||||
@@ -396,6 +397,7 @@ export function completeEmbeddedAttemptResult(
|
||||
...state,
|
||||
replayMetadata,
|
||||
currentAttemptReplayMetadata,
|
||||
codeModeReconciliationCandidate: state.codeModeReconciliationCandidate,
|
||||
itemLifecycle: getItemLifecycle(),
|
||||
assistantTurns: getAssistantTurnCount(),
|
||||
setTerminalLifecycleMeta,
|
||||
|
||||
@@ -222,6 +222,8 @@ export async function prepareEmbeddedAttemptAgentSession(input: {
|
||||
};
|
||||
setActiveSessionSystemPrompt(input.initialSystemPrompt);
|
||||
let didDeliverSourceReplyViaMessageTool = false;
|
||||
let codeModeReconciliationCandidate = false;
|
||||
let codeModeReconciliationReadAuthorized = false;
|
||||
const markSourceReplyDelivered = () => {
|
||||
didDeliverSourceReplyViaMessageTool = true;
|
||||
};
|
||||
@@ -231,7 +233,14 @@ export async function prepareEmbeddedAttemptAgentSession(input: {
|
||||
onDeliveredSourceReply: markSourceReplyDelivered,
|
||||
});
|
||||
if (input.clientToolPreparation.codeModeControlsEnabledForRun) {
|
||||
installCodeModeRepairHook({ agent: activeSession.agent });
|
||||
installCodeModeRepairHook({
|
||||
agent: activeSession.agent,
|
||||
onReconciliationCandidate: () => {
|
||||
if (codeModeReconciliationReadAuthorized) {
|
||||
codeModeReconciliationCandidate = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
input.markStage("agent-session");
|
||||
|
||||
@@ -239,9 +248,13 @@ export async function prepareEmbeddedAttemptAgentSession(input: {
|
||||
activeSession,
|
||||
allCustomTools,
|
||||
...clientToolRuntime,
|
||||
getCodeModeReconciliationCandidate: () => codeModeReconciliationCandidate,
|
||||
hasDeliveredSourceReply: () => didDeliverSourceReplyViaMessageTool,
|
||||
hookRunner,
|
||||
markSourceReplyDelivered,
|
||||
setCodeModeReconciliationReadAuthorized: (value: boolean) => {
|
||||
codeModeReconciliationReadAuthorized = clientToolRuntime.coreReadAuthorized && value;
|
||||
},
|
||||
setActiveSessionSystemPrompt,
|
||||
settingsManager,
|
||||
};
|
||||
|
||||
@@ -90,6 +90,7 @@ const attempt = {
|
||||
function createInput(options?: {
|
||||
activationError?: Error;
|
||||
codeModeControlsEnabledForRun?: boolean;
|
||||
coreReadAllowed?: boolean;
|
||||
}) {
|
||||
const events: string[] = [];
|
||||
const settingsManager = { id: "settings" };
|
||||
@@ -117,6 +118,8 @@ function createInput(options?: {
|
||||
const allCustomTools = [{ name: "custom" }];
|
||||
const clientToolRuntime = {
|
||||
builtinToolNames: new Set(["read"]),
|
||||
coreBuiltinToolNames: new Set(options?.coreReadAllowed === false ? [] : ["read"]),
|
||||
coreReadAuthorized: options?.coreReadAllowed !== false,
|
||||
clientToolCallSlots: [],
|
||||
clientToolDefs: [],
|
||||
clientToolLoopDetection: { enabled: true },
|
||||
@@ -124,6 +127,7 @@ function createInput(options?: {
|
||||
replaySafeTools: new Set(allCustomTools),
|
||||
};
|
||||
let onDeliveredSourceReply: (() => void) | undefined;
|
||||
let onReconciliationCandidate: (() => void) | undefined;
|
||||
|
||||
hoisted.createPreparedEmbeddedAgentSettingsManager.mockReturnValue(settingsManager);
|
||||
hoisted.resolveEffectiveCompactionMode.mockReturnValue("safeguard");
|
||||
@@ -149,9 +153,12 @@ function createInput(options?: {
|
||||
onDeliveredSourceReply = input.onDeliveredSourceReply;
|
||||
},
|
||||
);
|
||||
hoisted.installCodeModeRepairHook.mockImplementation(() => {
|
||||
events.push("install-code-mode-repair");
|
||||
});
|
||||
hoisted.installCodeModeRepairHook.mockImplementation(
|
||||
(input: { onReconciliationCandidate?: () => void }) => {
|
||||
onReconciliationCandidate = input.onReconciliationCandidate;
|
||||
events.push("install-code-mode-repair");
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
activeSession,
|
||||
@@ -184,6 +191,7 @@ function createInput(options?: {
|
||||
transcriptLifecycle: transcriptLifecycle as never,
|
||||
sessionManager: sessionManager as never,
|
||||
},
|
||||
markCodeModeReconciliationCandidate: () => onReconciliationCandidate?.(),
|
||||
onDeliveredSourceReply: () => onDeliveredSourceReply?.(),
|
||||
resourceLoader,
|
||||
setActiveToolsByName,
|
||||
@@ -239,6 +247,10 @@ describe("prepareEmbeddedAttemptAgentSession", () => {
|
||||
expect(result.hasDeliveredSourceReply()).toBe(false);
|
||||
fixture.onDeliveredSourceReply();
|
||||
expect(result.hasDeliveredSourceReply()).toBe(true);
|
||||
expect(result.getCodeModeReconciliationCandidate()).toBe(false);
|
||||
result.setCodeModeReconciliationReadAuthorized(true);
|
||||
fixture.markCodeModeReconciliationCandidate();
|
||||
expect(result.getCodeModeReconciliationCandidate()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not install Code Mode repair when the run kept direct tools", async () => {
|
||||
@@ -250,6 +262,23 @@ describe("prepareEmbeddedAttemptAgentSession", () => {
|
||||
expect(fixture.events).not.toContain("install-code-mode-repair");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["the effective core tools exclude read", false, true],
|
||||
["the final prompt policy removes read", true, false],
|
||||
])("withholds reconciliation when %s", async (_label, coreReadAllowed, finalReadAllowed) => {
|
||||
const fixture = createInput({ coreReadAllowed });
|
||||
|
||||
const result = await prepareEmbeddedAttemptAgentSession(fixture.input);
|
||||
|
||||
expect(hoisted.installCodeModeRepairHook).toHaveBeenCalledWith({
|
||||
agent: fixture.activeSession.agent,
|
||||
onReconciliationCandidate: expect.any(Function),
|
||||
});
|
||||
result.setCodeModeReconciliationReadAuthorized(finalReadAllowed);
|
||||
fixture.markCodeModeReconciliationCandidate();
|
||||
expect(result.getCodeModeReconciliationCandidate()).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves overflow recovery with the session when no model budget was resolved", async () => {
|
||||
const fixture = createInput();
|
||||
fixture.input.attempt = {
|
||||
|
||||
@@ -129,8 +129,11 @@ export async function runEmbeddedAttemptSettledPhase(
|
||||
agentSession: {
|
||||
activeSession,
|
||||
clientToolCallSlots,
|
||||
coreReadAuthorized,
|
||||
getCodeModeReconciliationCandidate,
|
||||
hasDeliveredSourceReply,
|
||||
hookRunner,
|
||||
setCodeModeReconciliationReadAuthorized,
|
||||
setActiveSessionSystemPrompt,
|
||||
settingsManager,
|
||||
},
|
||||
@@ -279,6 +282,7 @@ export async function runEmbeddedAttemptSettledPhase(
|
||||
uncompactedEffectiveTools,
|
||||
tools,
|
||||
codeModeControlsEnabled: toolBase.codeModeControlsEnabledForRun,
|
||||
coreReadAuthorized,
|
||||
toolSearchCatalogRef: toolBase.toolSearchCatalogRef,
|
||||
forceToolNames: [
|
||||
...(toolBase.forceDirectMessageTool ? ["message"] : []),
|
||||
@@ -326,6 +330,7 @@ export async function runEmbeddedAttemptSettledPhase(
|
||||
setPromptCacheChangesForTurn: (changes) => {
|
||||
promptCacheChangesForTurn = changes;
|
||||
},
|
||||
setCodeModeReconciliationReadAuthorized,
|
||||
setFinalPromptText: (prompt) => {
|
||||
finalPromptText = prompt;
|
||||
},
|
||||
@@ -620,6 +625,7 @@ export async function runEmbeddedAttemptSettledPhase(
|
||||
lastAssistant,
|
||||
currentAttemptAssistant,
|
||||
currentAttemptCompletedAssistant,
|
||||
codeModeReconciliationCandidate: getCodeModeReconciliationCandidate(),
|
||||
successfulNestedToolNames,
|
||||
attemptUsage,
|
||||
promptCache: sessionRuntimeState.promptCache,
|
||||
|
||||
@@ -122,8 +122,11 @@ function createFixture(overrides: FixtureOverrides = {}) {
|
||||
agentSession: {
|
||||
activeSession,
|
||||
clientToolCallSlots: [],
|
||||
coreReadAuthorized: true,
|
||||
getCodeModeReconciliationCandidate: vi.fn(() => false),
|
||||
hasDeliveredSourceReply: vi.fn(() => false),
|
||||
hookRunner: {},
|
||||
setCodeModeReconciliationReadAuthorized: vi.fn(),
|
||||
setActiveSessionSystemPrompt: vi.fn(),
|
||||
settingsManager: { getCompactionReserveTokens: vi.fn(() => 1_000) },
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
} from "./attempt-tool-construction-plan.js";
|
||||
import { buildEmbeddedAttemptToolRunContext } from "./attempt-tool-run-context.js";
|
||||
import { TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES } from "./attempt-tool-search-run-plan.js";
|
||||
import { isCodeModeReconciliationTool } from "./code-mode-reconciliation.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
||||
type OpenClawCodingToolsOptions = NonNullable<Parameters<typeof createOpenClawCodingTools>[0]>;
|
||||
@@ -74,15 +75,18 @@ export function prepareEmbeddedAttemptToolBase(params: {
|
||||
toolSearchCatalogExecutor: ToolSearchCatalogToolExecutor;
|
||||
}) {
|
||||
const { attempt } = params;
|
||||
const forceDirectMessageTool = messageToolOwnsVisibleReply(attempt);
|
||||
const toolsAllowWithForcedRuntimeTools = mergeForcedEmbeddedAttemptToolsAllow(
|
||||
attempt.toolsAllow,
|
||||
{
|
||||
forceMessageTool: forceDirectMessageTool,
|
||||
forceToolNames:
|
||||
attempt.swarmCollector && attempt.swarmOutputSchema ? ["structured_output"] : undefined,
|
||||
},
|
||||
);
|
||||
const forceDirectMessageTool =
|
||||
attempt.forceCodeModeReconciliationTools === true
|
||||
? false
|
||||
: messageToolOwnsVisibleReply(attempt);
|
||||
const toolsAllowWithForcedRuntimeTools =
|
||||
attempt.forceCodeModeReconciliationTools === true
|
||||
? ["read"]
|
||||
: mergeForcedEmbeddedAttemptToolsAllow(attempt.toolsAllow, {
|
||||
forceMessageTool: forceDirectMessageTool,
|
||||
forceToolNames:
|
||||
attempt.swarmCollector && attempt.swarmOutputSchema ? ["structured_output"] : undefined,
|
||||
});
|
||||
const toolsEnabled = supportsModelTools(attempt.model);
|
||||
const isRawModelRun = attempt.modelRun === true || attempt.promptMode === "none";
|
||||
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
|
||||
@@ -108,6 +112,7 @@ export function prepareEmbeddedAttemptToolBase(params: {
|
||||
skillWorkshopProposalOnly: attempt.skillWorkshopProposalOnly,
|
||||
toolsAllow: attempt.toolsAllow,
|
||||
forceCodeModeControls: attempt.forceCodeModeTools,
|
||||
forceDirectTools: attempt.forceCodeModeReconciliationTools,
|
||||
});
|
||||
if (isCodeModeDiagnosticEnabled()) {
|
||||
logCodeModeDiagnostic(log, "activation", {
|
||||
@@ -373,9 +378,12 @@ export function prepareEmbeddedAttemptToolBase(params: {
|
||||
params.markCoreToolStage("attempt:tools-allow");
|
||||
return filteredTools;
|
||||
})();
|
||||
const toolsRaw = attempt.forceRestartSafeTools
|
||||
? constructedToolsRaw.filter((tool) => isAgentToolRestartSafe(tool, restartSafetyOptions))
|
||||
: constructedToolsRaw;
|
||||
const toolsRaw =
|
||||
attempt.forceCodeModeReconciliationTools === true
|
||||
? constructedToolsRaw.filter(isCodeModeReconciliationTool)
|
||||
: attempt.forceRestartSafeTools
|
||||
? constructedToolsRaw.filter((tool) => isAgentToolRestartSafe(tool, restartSafetyOptions))
|
||||
: constructedToolsRaw;
|
||||
if (attempt.forceRestartSafeTools) {
|
||||
log.info(
|
||||
`restart-safe recovery tool policy retained ${toolsRaw.length}/${constructedToolsRaw.length} concrete tools`,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { makeEmbeddedRunnerAttempt } from "../../test-helpers/embedded-agent-runner-e2e-fixtures.js";
|
||||
import {
|
||||
activateCodeModeReconciliation,
|
||||
isCodeModeReconciliationTool,
|
||||
} from "./code-mode-reconciliation.js";
|
||||
import { createEmbeddedRunTerminalRetryState } from "./terminal-retry-state.js";
|
||||
|
||||
function eligibleAttempt() {
|
||||
return makeEmbeddedRunnerAttempt({
|
||||
codeModeReconciliationCandidate: true,
|
||||
itemLifecycle: { startedCount: 2, completedCount: 2, activeCount: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
function activates(overrides = {}, hostOwnsToolSurface = true) {
|
||||
return activateCodeModeReconciliation({
|
||||
attempt: { ...eligibleAttempt(), ...overrides } as ReturnType<typeof eligibleAttempt>,
|
||||
hostOwnsToolSurface,
|
||||
retryState: createEmbeddedRunTerminalRetryState(),
|
||||
activateInternalPrompt: () => undefined,
|
||||
});
|
||||
}
|
||||
|
||||
describe("Code Mode reconciliation", () => {
|
||||
it("admits one quiescent candidate", () => {
|
||||
expect(activates()).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["active tool", { itemLifecycle: { startedCount: 2, completedCount: 1, activeCount: 1 } }],
|
||||
["async work", { toolMetas: [{ toolName: "exec", asyncStarted: true }] }],
|
||||
["message delivery", { didSendViaMessagingTool: true }],
|
||||
["child session", { acceptedSessionSpawns: [{ runId: "child" }] }],
|
||||
["approval", { didSendDeterministicApprovalPrompt: true }],
|
||||
["yield", { yieldDetected: true }],
|
||||
["plugin-owned transport", {}, false],
|
||||
])("rejects a candidate with %s", (_label, overrides, hostOwnsToolSurface = true) => {
|
||||
expect(activates(overrides, hostOwnsToolSurface)).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes only the audited core observation tool", () => {
|
||||
expect(
|
||||
[
|
||||
"read",
|
||||
"find",
|
||||
"glob",
|
||||
"grep",
|
||||
"ls",
|
||||
"search",
|
||||
"exec",
|
||||
"write",
|
||||
"apply_patch",
|
||||
"message",
|
||||
"sessions_spawn",
|
||||
"web_fetch",
|
||||
].filter((name) => isCodeModeReconciliationTool({ name })),
|
||||
).toEqual(["read"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { projectAgentRunAttemptTerminal } from "../../agent-run-terminal-outcome.js";
|
||||
import { normalizeToolPolicyName } from "../../tool-policy.js";
|
||||
import type { EmbeddedRunTerminalRetryState } from "./terminal-retry-state.js";
|
||||
import type { EmbeddedRunAttemptResult } from "./types.js";
|
||||
|
||||
const CODE_MODE_RECONCILIATION_PROMPT =
|
||||
"The previous Code Mode mutation may have partially applied. Do not repeat or finish any mutation. Use only the available read-only inspection tools to determine the authoritative current state, then report exactly what applied, what did not, what remains unknown, and what work is still required.";
|
||||
|
||||
const RECONCILIATION_TOOL_NAMES = new Set(["read"]);
|
||||
|
||||
export function isCodeModeReconciliationTool(tool: { name?: string }): boolean {
|
||||
return RECONCILIATION_TOOL_NAMES.has(normalizeToolPolicyName(tool.name ?? ""));
|
||||
}
|
||||
|
||||
function shouldRetryCodeModeReconciliation(params: {
|
||||
attempt: EmbeddedRunAttemptResult;
|
||||
hostOwnsToolSurface: boolean;
|
||||
aborted: boolean;
|
||||
timedOut: boolean;
|
||||
promptError: unknown;
|
||||
}): boolean {
|
||||
const { attempt } = params;
|
||||
return (
|
||||
attempt.codeModeReconciliationCandidate === true &&
|
||||
params.hostOwnsToolSurface &&
|
||||
!params.aborted &&
|
||||
!params.timedOut &&
|
||||
!params.promptError &&
|
||||
attempt.itemLifecycle.activeCount === 0 &&
|
||||
attempt.itemLifecycle.startedCount === attempt.itemLifecycle.completedCount &&
|
||||
!attempt.clientToolCalls &&
|
||||
!attempt.yieldDetected &&
|
||||
!attempt.didSendDeterministicApprovalPrompt &&
|
||||
!attempt.runtimeContinuationStarted &&
|
||||
!attempt.toolMetas.some((entry) => entry.asyncStarted === true) &&
|
||||
(attempt.acceptedSessionSpawns?.length ?? 0) === 0 &&
|
||||
!attempt.didSendViaMessagingTool &&
|
||||
(attempt.successfulCronAdds ?? 0) === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function activateCodeModeReconciliation(params: {
|
||||
attempt: EmbeddedRunAttemptResult;
|
||||
hostOwnsToolSurface: boolean;
|
||||
retryState: EmbeddedRunTerminalRetryState;
|
||||
activateInternalPrompt: (prompt: string) => void;
|
||||
}): boolean {
|
||||
const terminal = projectAgentRunAttemptTerminal(params.attempt.terminal);
|
||||
if (
|
||||
params.retryState.codeModeReconciliationAttempts >= 1 ||
|
||||
!shouldRetryCodeModeReconciliation({
|
||||
attempt: params.attempt,
|
||||
hostOwnsToolSurface: params.hostOwnsToolSurface,
|
||||
...terminal,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
params.retryState.codeModeReconciliationAttempts += 1;
|
||||
params.retryState.forceCodeModeReconciliationTools = true;
|
||||
params.activateInternalPrompt(CODE_MODE_RECONCILIATION_PROMPT);
|
||||
return true;
|
||||
}
|
||||
@@ -66,9 +66,12 @@ function completedResult(): AgentToolResult<unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function createAgent(previous?: Agent["afterToolOutcome"]): Agent {
|
||||
function createAgent(
|
||||
previous?: Agent["afterToolOutcome"],
|
||||
onReconciliationCandidate?: () => void,
|
||||
): Agent {
|
||||
const agent = { afterToolOutcome: previous } as Agent;
|
||||
installCodeModeRepairHook({ agent });
|
||||
installCodeModeRepairHook({ agent, onReconciliationCandidate });
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -199,7 +202,13 @@ describe("installCodeModeRepairHook", () => {
|
||||
});
|
||||
|
||||
it("never offers a retry after bridge dispatch", async () => {
|
||||
const agent = createAgent();
|
||||
const onReconciliationCandidate = vi.fn();
|
||||
const agent = createAgent(undefined, onReconciliationCandidate);
|
||||
const assistantMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call-1", name: "exec", arguments: {} }],
|
||||
timestamp: 1,
|
||||
} as unknown as AfterToolOutcomeContext["assistantMessage"];
|
||||
const failure = failedResult({
|
||||
failurePhase: "bridge",
|
||||
bridgeDispatchStarted: true,
|
||||
@@ -209,6 +218,7 @@ describe("installCodeModeRepairHook", () => {
|
||||
|
||||
const result = await agent.afterToolOutcome?.(
|
||||
outcome({
|
||||
assistantMessage,
|
||||
result: failure,
|
||||
}),
|
||||
);
|
||||
@@ -225,6 +235,7 @@ describe("installCodeModeRepairHook", () => {
|
||||
},
|
||||
});
|
||||
expect(payload.output).toEqual([{ type: "text", text: "before dispatch failure" }]);
|
||||
expect(onReconciliationCandidate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("offers one repair for an authenticated nested no-start bridge failure", async () => {
|
||||
|
||||
@@ -218,7 +218,10 @@ function hookFailure(
|
||||
}
|
||||
|
||||
/** Installs one bounded, side-effect-aware Code Mode repair opportunity. */
|
||||
export function installCodeModeRepairHook(params: { agent: Agent }): void {
|
||||
export function installCodeModeRepairHook(params: {
|
||||
agent: Agent;
|
||||
onReconciliationCandidate?: () => void;
|
||||
}): void {
|
||||
const previousAfterToolOutcome = params.agent.afterToolOutcome?.bind(params.agent);
|
||||
let repairState: RepairState = "ready";
|
||||
let repairOfferedBy: AfterToolOutcomeContext["assistantMessage"] | undefined;
|
||||
@@ -295,6 +298,12 @@ export function installCodeModeRepairHook(params: { agent: Agent }): void {
|
||||
effective.toolCall.name === CODE_MODE_WAIT_TOOL_NAME
|
||||
) {
|
||||
repairState = "consumed";
|
||||
if (
|
||||
effective.toolCall.name === CODE_MODE_EXEC_TOOL_NAME &&
|
||||
effective.assistantMessage.content.filter((entry) => entry.type === "toolCall").length === 1
|
||||
) {
|
||||
params.onReconciliationCandidate?.();
|
||||
}
|
||||
return renderFailure({
|
||||
failure,
|
||||
allowed: false,
|
||||
|
||||
@@ -178,6 +178,8 @@ export type RunEmbeddedAgentParams = {
|
||||
swarmOutputSchema?: Record<string, unknown>;
|
||||
/** Restrict this reconstructed run to restart-safe tools. */
|
||||
forceRestartSafeTools?: boolean;
|
||||
/** Restrict one internal post-mutation recovery attempt to audited core reads. */
|
||||
forceCodeModeReconciliationTools?: boolean;
|
||||
/** Preserve Code Mode controls for a replay-safe restart recovery turn. */
|
||||
forceCodeModeTools?: boolean;
|
||||
/** Internal one-shot model probe mode: no tools, no workspace/chat prompt policy. */
|
||||
|
||||
@@ -489,6 +489,7 @@ export async function dispatchEmbeddedRunAttempt(input: {
|
||||
swarmCollector: params.swarmCollector,
|
||||
swarmOutputSchema: params.swarmOutputSchema,
|
||||
forceRestartSafeTools: params.forceRestartSafeTools,
|
||||
forceCodeModeReconciliationTools: params.forceCodeModeReconciliationTools,
|
||||
forceCodeModeTools: params.forceCodeModeTools,
|
||||
forceMessageTool: params.forceMessageTool,
|
||||
enableHeartbeatTool: params.enableHeartbeatTool,
|
||||
|
||||
@@ -7,6 +7,8 @@ export type EmbeddedRunTerminalRetryState = {
|
||||
compactionContinuationAttempts: number;
|
||||
compactionContinuationInstruction: string | null;
|
||||
beforeFinalizeRevisionAttempts: number;
|
||||
codeModeReconciliationAttempts: number;
|
||||
forceCodeModeReconciliationTools: boolean;
|
||||
};
|
||||
|
||||
export function createEmbeddedRunTerminalRetryState(): EmbeddedRunTerminalRetryState {
|
||||
@@ -17,5 +19,7 @@ export function createEmbeddedRunTerminalRetryState(): EmbeddedRunTerminalRetryS
|
||||
compactionContinuationAttempts: 0,
|
||||
compactionContinuationInstruction: null,
|
||||
beforeFinalizeRevisionAttempts: 0,
|
||||
codeModeReconciliationAttempts: 0,
|
||||
forceCodeModeReconciliationTools: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -346,6 +346,8 @@ export type EmbeddedRunAttemptResult = {
|
||||
* how config-enabled code mode stays visible as a no-op on harness routes.
|
||||
*/
|
||||
codeModeEngaged?: boolean;
|
||||
/** Host-authenticated request for one bounded post-mutation inspection attempt. */
|
||||
codeModeReconciliationCandidate?: boolean;
|
||||
/** Completed assistant round trips observed during this attempt. */
|
||||
assistantTurns?: number;
|
||||
/** Inner bridge call counts from this attempt's tool-search/code-mode catalog. */
|
||||
|
||||
@@ -26,6 +26,7 @@ type AgentToolSurfacePlanParams = {
|
||||
skillWorkshopProposalOnly?: boolean;
|
||||
toolsAllow?: readonly string[];
|
||||
forceCodeModeControls?: boolean;
|
||||
forceDirectTools?: boolean;
|
||||
};
|
||||
|
||||
export function resolveAgentToolSurfacePlan(params: AgentToolSurfacePlanParams) {
|
||||
@@ -55,12 +56,16 @@ export function resolveAgentToolSurfacePlan(params: AgentToolSurfacePlanParams)
|
||||
);
|
||||
const codeModeControlsEnabled =
|
||||
toolsAvailable &&
|
||||
params.forceDirectTools !== true &&
|
||||
// Restart recovery continues one provider turn. Keep its original control
|
||||
// schema even when the reloaded config disables Code Mode for new turns.
|
||||
(params.forceCodeModeControls === true ||
|
||||
isCodeModeEngagedForModel(codeModeConfig, params.model));
|
||||
const toolSearchControlsEnabled =
|
||||
toolsAvailable && !codeModeControlsEnabled && toolSearchConfig.enabled;
|
||||
toolsAvailable &&
|
||||
params.forceDirectTools !== true &&
|
||||
!codeModeControlsEnabled &&
|
||||
toolSearchConfig.enabled;
|
||||
return {
|
||||
codeModeControlsEnabled,
|
||||
toolSearchControlsEnabled,
|
||||
|
||||
Reference in New Issue
Block a user