Compare commits

..

4 Commits

Author SHA1 Message Date
Vincent Koc
9803ae90b2 docs(agents): align lean message help 2026-05-30 02:42:45 +02:00
Vincent Koc
34cddc295d fix(agents): keep message tool for lean source replies 2026-05-30 02:40:30 +02:00
clawsweeper[bot]
18f94fc83a fix(agents): classify embedded provider business denials for fallback (#84814)
Summary:
- The PR classifies selected embedded agent provider-denial error payloads through the shared failover matcher ... 1/current-ak auth matching, preserves guarded non-fallback cases, and covers fallback progression in tests.
- PR surface: Source +34, Tests +166. Total +200 across 5 files.
- Reproducibility: yes. Current main is source-reproducible: a non-GPT embedded result whose only signal is CE ... returns null from the classifier, and the fallback wrapper treats null classification as candidate success.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): classify embedded provider business denials for fallback
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8304…

Validation:
- ClawSweeper review passed for head e266beac93.
- Required merge gates passed before the squash merge.

Prepared head SHA: e266beac93
Review: https://github.com/openclaw/openclaw/pull/84814#issuecomment-4505010446

Co-authored-by: Stellar鱼 <2182712990@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-30 00:34:28 +00:00
Peter Steinberger
aada44fca5 fix(agents): preserve Codex auth for compaction fallback
Fixes #86820.

Preserve Codex OAuth-backed compaction by selecting and loading the Codex harness before resolving direct or queued compaction models, while keeping OpenAI-compatible custom base URLs on the OpenAI context config path. Also preserves persisted concrete harness pins so compaction does not hot-switch existing sessions just because an explicit Codex fallback exists.

Verification:
- node scripts/run-vitest.mjs src/agents/embedded-agent-runner/compact.hooks.test.ts src/agents/harness/selection.test.ts src/agents/harness/runtime-plugin.test.ts
- pnpm tsgo:prod
- pnpm check:test-types
- pnpm lint --threads=8
- git diff --check origin/main...HEAD
- git diff --check
- autoreview clean: no accepted/actionable findings reported; overall patch is correct (0.82)
- GitHub PR checks green on ac6f93de4a
2026-05-30 02:26:00 +02:00
31 changed files with 1141 additions and 154 deletions

View File

@@ -30,7 +30,7 @@ Treat them differently from normal config:
## Local model lean mode
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` from the agent's tool surface for every turn. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops the `browser` and `cron` tools from the agent's tool surface, and it also drops `message` unless the current turn requires message-tool-only visible replies. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
### Why these three tools
@@ -40,7 +40,7 @@ These three tools have the largest descriptions and the most parameter shapes in
- The model picking the right tool vs. emitting malformed tool calls because there are too many similar-looking schemas.
- The Chat Completions adapter staying inside the server's structured-output limits vs. tripping a 400 on tool-call payload size.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available. When the source reply delivery contract is `message_tool_only`, lean mode keeps `message` so visible replies can still be delivered.
### When to turn it on

View File

@@ -315,7 +315,7 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
openclaw infer model run --gateway --model <provider/model> --prompt "Reply with exactly: pong" --json
```
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) so the prompt shape is smaller and less brittle. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the heaviest default tools (`browser`, `cron`, and usually `message`) so the prompt shape is smaller and less brittle. Lean mode keeps `message` when the current source reply contract requires message-tool delivery. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
4. **Disable tools entirely as a last resort.** If lean mode is not enough, set `models.providers.<provider>.models[].compat.supportsTools: false` for that model entry. The agent will then operate without tool calls on that model.

View File

@@ -679,7 +679,7 @@ Use these as starting points and replace model IDs with the exact names from `ol
```
Use `compat.supportsTools: false` only when the model or server reliably fails on tool schemas. It trades agent capability for stability.
`localModelLean` removes the browser, cron, and message tools from the agent surface, but it does not change Ollama's runtime context or thinking mode. Pair it with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
`localModelLean` removes the browser and cron tools from the agent surface, and it usually removes message unless the current source reply contract requires message-tool delivery. It does not change Ollama's runtime context or thinking mode. Pair it with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
</Accordion>
</AccordionGroup>

View File

@@ -164,6 +164,35 @@ describe("applyModelProviderToolPolicy", () => {
expect(toolNames(filtered)).toEqual(["read", "exec"]);
});
it("keeps message in lean local-model mode when source replies require the message tool", () => {
const filtered = testing.applyModelProviderToolPolicy(
[
{ name: "read" },
{ name: "browser" },
{ name: "cron" },
{ name: "message" },
{ name: "exec" },
] as unknown as AnyAgentTool[],
{
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
},
modelProvider: "openai",
modelApi: "openai-responses",
modelId: "gpt-5.4",
sourceReplyDeliveryMode: "message_tool_only",
},
);
expect(toolNames(filtered)).toEqual(["read", "message", "exec"]);
});
it("drops heavyweight tools when lean local-model mode is enabled for the current agent", () => {
const filtered = testing.applyModelProviderToolPolicy(
[

View File

@@ -245,6 +245,7 @@ function applyModelProviderToolPolicy(
agentDir?: string;
modelCompat?: ModelCompatConfig;
suppressManagedWebSearch?: boolean;
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
},
): AnyAgentTool[] {
tools = filterLocalModelLeanTools({
@@ -252,6 +253,7 @@ function applyModelProviderToolPolicy(
config: params?.config,
agentId: params?.agentId,
sessionKey: params?.sessionKey,
sourceReplyDeliveryMode: params?.sourceReplyDeliveryMode,
});
if (
@@ -1063,6 +1065,7 @@ export function createOpenClawCodingTools(options?: {
agentDir: options?.agentDir,
modelCompat: options?.modelCompat,
suppressManagedWebSearch: options?.suppressManagedWebSearch,
sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode,
});
options?.recordToolPrepStage?.("model-provider-policy");
// Sender identity is carried for command/channel-action auth; tool visibility

View File

@@ -1412,6 +1412,8 @@ describe("classifyFailoverReason provider messages", () => {
// Auth errors
expect(classifyFailoverReason("无权访问该模型")).toBe("auth");
expect(classifyFailoverReason("403 您无权访问glm-5.1。")).toBe("auth");
expect(classifyFailoverReason("当前ak因违规请求被禁止访问该模型")).toBe("auth");
expect(classifyFailoverReason('{"success":false,"code":"CE-011"}')).toBe("auth");
expect(classifyFailoverReason("认证失败")).toBe("auth");
expect(classifyFailoverReason("鉴权失败请检查API Key")).toBe("auth");
expect(classifyFailoverReason("密钥无效")).toBe("auth");

View File

@@ -47,6 +47,8 @@ const CJK_AUTH_ERROR_PATTERNS = [
"鉴权失败",
"密钥无效",
"apikey 无效",
/(?:当前\s*ak|ce-011).*?(?:违规请求|禁止访问)|(?:违规请求|禁止访问).*?(?:当前\s*ak|ce-011)/i,
/\bce-011\b/i,
] as const satisfies readonly ErrorPattern[];
const ZAI_BILLING_CODE_1311_RE = /"code"\s*:\s*1311\b/;

View File

@@ -618,14 +618,6 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
expect(result.ok).toBe(true);
expect(result.result?.summary).toBe("oauth fallback summary");
findMockCall(resolveAgentHarnessPolicyMock, ([arg]) => {
const policyArg = arg as Record<string, unknown>;
return policyArg.provider === "openai" && policyArg.modelId === "gpt-primary";
});
findMockCall(resolveAgentHarnessPolicyMock, ([arg]) => {
const policyArg = arg as Record<string, unknown>;
return policyArg.provider === "openai" && policyArg.modelId === "gpt-fallback";
});
findMockCall(
resolveModelMock,
([provider, modelId]) => provider === "openai-codex" && modelId === "gpt-primary",
@@ -639,7 +631,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
});
it("uses the selected Codex runtime provider for OpenAI compaction context windows", async () => {
it("uses the selected Codex runtime provider for OpenAI compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
@@ -655,6 +647,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
@@ -672,7 +665,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
@@ -680,8 +673,86 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
});
it("preserves direct OpenAI API-key compaction when no Codex auth is configured", async () => {
it("uses explicit Codex runtime policy for direct OpenAI compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
config: {
models: {
providers: {
openai: { models: [{ id: "fake-model", contextWindow: 1_000_000 }] },
"openai-codex": { models: [{ id: "fake-model", contextWindow: 350_000 }] },
},
},
} as never,
});
expect(result.ok).toBe(true);
expect(resolveAgentHarnessPolicyMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "openai", modelId: "fake-model" }),
);
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
modelId: "fake-model",
});
});
it("keeps custom OpenAI-compatible compaction on OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [], contextWindow: 1_000_000 },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }],
},
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
} as never,
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai",
modelId: "gpt-5.5",
});
});
it("preserves direct OpenAI API-key compaction when OpenClaw runtime is active", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
@@ -702,7 +773,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
},
},
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
agents: { defaults: { embeddedHarness: { runtime: "openclaw" } } },
} as never,
});
@@ -711,6 +782,52 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
});
it("routes OpenAI compaction model overrides through Codex OAuth when Codex runtime is active", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }, { id: "gpt-5.4-mini" }],
},
"openai-codex": {
models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini", contextWindow: 350_000 }],
},
},
},
agents: {
defaults: {
embeddedHarness: { runtime: "codex" },
compaction: { model: "openai/gpt-5.4-mini" },
},
},
} as never,
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.4-mini");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
modelId: "gpt-5.4-mini",
});
});
it("uses Codex auth for runtime model loading while preserving OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
@@ -1671,8 +1788,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
}),
);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expect(harnessArg.contextTokenBudget).toBe(32_000);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
tokenBudget?: number;
runtimeContext?: Record<string, unknown>;
@@ -1720,6 +1836,248 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
});
});
it("passes selected Codex runtime to queued context-engine runtime context", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("uses explicit Codex runtime policy for queued native compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("preserves concrete OpenClaw pins over explicit Codex policy for queued compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "openclaw",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("keeps concrete Codex pins when explicit policy is auto for queued compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "auto",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("does not route queued compaction through implicit Codex policy alone", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("keeps queued custom OpenAI-compatible compaction on OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }],
},
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai",
modelId: "gpt-5.5",
});
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("fails deferred budget compaction when background maintenance is not scheduled", async () => {
const dispose = vi.fn(async () => {});
const maintain = vi.fn(async () => ({

View File

@@ -16,16 +16,19 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveUserPath } from "../../utils.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-recovery.js";
import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import {
maybeCompactAgentHarnessSession,
resolveAgentHarnessPolicy,
} from "../harness/selection.js";
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
import { isOpenAICodexProvider, isOpenAIProvider } from "../openai-codex-routing.js";
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
import { DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON } from "./compact-reasons.js";
import type { CompactEmbeddedAgentSessionParams } from "./compact.types.js";
@@ -169,7 +172,12 @@ export async function compactEmbeddedAgentSession(
agentDir,
workspaceDir: resolvedWorkspaceDir,
});
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const policyCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
@@ -177,9 +185,51 @@ export async function compactEmbeddedAgentSession(
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configuredHarnessPolicy = resolveAgentHarnessPolicy({
provider: policyCompactionTarget.provider ?? DEFAULT_PROVIDER,
modelId: policyCompactionTarget.model ?? DEFAULT_MODEL,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
});
const configuredHarnessRuntime =
configuredHarnessPolicy.runtimeSource &&
configuredHarnessPolicy.runtimeSource !== "implicit" &&
!isDefaultAgentRuntimeId(configuredHarnessPolicy.runtime)
? configuredHarnessPolicy.runtime
: undefined;
// The persisted harness id is the runtime contract for this session; config
// changes can supply a runtime only when the session has no concrete pin.
const selectedHarnessRuntime = params.agentHarnessId ?? configuredHarnessRuntime;
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
authProfileId: params.authProfileId,
harnessRuntime: selectedHarnessRuntime,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const ceProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
const ceRuntimeProvider = resolvedCompactionTarget.runtimeProvider ?? ceProvider;
const ceContextConfigProvider = resolvedCompactionTarget.contextProvider ?? ceProvider;
const ceModelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
const attemptNativeHarnessCompaction = shouldAttemptNativeHarnessCompaction({
provider: ceProvider,
contextProvider: resolvedCompactionTarget.contextProvider,
selectedHarnessRuntime,
});
if (attemptNativeHarnessCompaction) {
await ensureSelectedAgentHarnessPlugin({
config: params.config,
provider: ceProvider,
modelId: ceModelId,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride: selectedHarnessRuntime,
workspaceDir: resolvedWorkspaceDir,
});
}
const { model: ceModel } = await resolveModelAsync(
ceRuntimeProvider,
ceModelId,
@@ -187,21 +237,11 @@ export async function compactEmbeddedAgentSession(
params.config,
);
const ceRuntimeModel = ceModel as ProviderRuntimeModel | undefined;
const ceHarnessPolicy = resolveAgentHarnessPolicy({
provider: ceProvider,
modelId: ceModelId,
config: params.config,
agentId: agentIds.sessionAgentId,
sessionKey: params.sessionKey,
});
const resolvedContextTokenBudget =
normalizeContextTokenBudget(
resolveContextWindowInfo({
cfg: params.config,
provider: resolveContextConfigProviderForRuntime({
provider: ceProvider,
runtimeId: params.agentHarnessId ?? ceHarnessPolicy.runtime,
}),
provider: ceContextConfigProvider,
modelId: ceModelId,
modelContextTokens: readAgentModelContextTokens(ceModel),
modelContextWindow: ceRuntimeModel?.contextWindow,
@@ -216,15 +256,18 @@ export async function compactEmbeddedAgentSession(
const contextEngineRuntimeContext = buildCompactionContextEngineRuntimeContext({
params,
agentDir,
harnessRuntime: selectedHarnessRuntime,
contextTokenBudget,
contextEnginePluginId: resolveContextEngineOwnerPluginId(contextEngine),
});
const harnessResult = await maybeCompactAgentHarnessSession({
...params,
contextEngine,
contextTokenBudget,
contextEngineRuntimeContext,
});
const harnessResult = attemptNativeHarnessCompaction
? await maybeCompactAgentHarnessSession({
...params,
contextEngine,
contextTokenBudget,
contextEngineRuntimeContext,
})
: undefined;
if (harnessResult) {
if (!shouldFallbackAfterHarnessCompaction(harnessResult)) {
await contextEngine.dispose?.();
@@ -468,9 +511,25 @@ export async function compactEmbeddedAgentSession(
);
}
function shouldAttemptNativeHarnessCompaction(params: {
provider: string;
contextProvider?: string;
selectedHarnessRuntime?: string | null;
}): boolean {
if (isOpenAICodexProvider(params.provider)) {
return true;
}
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.selectedHarnessRuntime);
if (!selectedRuntime || selectedRuntime === "auto" || selectedRuntime === "openclaw") {
return false;
}
return isOpenAIProvider(params.provider) ? params.contextProvider !== undefined : true;
}
function buildCompactionContextEngineRuntimeContext(params: {
params: CompactEmbeddedAgentSessionParams;
agentDir: string;
harnessRuntime?: string;
contextEnginePluginId?: string;
contextTokenBudget?: number;
}): ContextEngineRuntimeContext {
@@ -499,6 +558,7 @@ function buildCompactionContextEngineRuntimeContext(params: {
senderId: params.params.senderId,
provider: params.params.provider,
modelId: params.params.model,
harnessRuntime: params.harnessRuntime,
modelFallbacksOverride: params.params.modelFallbacksOverride,
thinkLevel: params.params.thinkLevel,
reasoningLevel: params.params.reasoningLevel,

View File

@@ -24,7 +24,11 @@ import {
resolveProviderTextTransforms,
transformProviderSystemPrompt,
} from "../../plugins/provider-runtime.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import {
isCronSessionKey,
isSubagentSessionKey,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import {
@@ -41,6 +45,7 @@ import {
setCompactionSafeguardCancelReason,
} from "../agent-hooks/compaction-safeguard-runtime.js";
import { createPreparedEmbeddedAgentSettingsManager } from "../agent-project-settings.js";
import { isDefaultAgentRuntimeId } from "../agent-runtime-id.js";
import {
resolveAgentDir,
resolveRunModelFallbacksOverride,
@@ -87,7 +92,6 @@ import {
import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.js";
import { supportsModelTools } from "../model-tool-support.js";
import { ensureOpenClawModelsJson } from "../models-config.js";
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js";
import { registerProviderStreamForModel } from "../provider-stream.js";
@@ -489,7 +493,19 @@ async function compactEmbeddedAgentSessionDirectOnce(
workspaceDir: resolvedWorkspace,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
});
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
const earlyAgentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const agentDir =
params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId);
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const policyCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
@@ -497,12 +513,47 @@ async function compactEmbeddedAgentSessionDirectOnce(
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configuredHarnessPolicy = resolveAgentHarnessPolicy({
provider: policyCompactionTarget.provider ?? DEFAULT_PROVIDER,
modelId: policyCompactionTarget.model ?? DEFAULT_MODEL,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
});
const configuredHarnessRuntime =
configuredHarnessPolicy.runtimeSource &&
configuredHarnessPolicy.runtimeSource !== "implicit" &&
!isDefaultAgentRuntimeId(configuredHarnessPolicy.runtime)
? configuredHarnessPolicy.runtime
: undefined;
const selectedHarnessRuntime = params.agentHarnessId ?? configuredHarnessRuntime;
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
authProfileId: params.authProfileId,
harnessRuntime: selectedHarnessRuntime,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
// Keep the configured provider for harness policy, while auth/model loading below can
// route OpenAI compaction through Codex OAuth when that runtime owns the session credentials.
const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
const runtimeProvider = resolvedCompactionTarget.runtimeProvider ?? provider;
const contextConfigProvider = resolvedCompactionTarget.contextProvider ?? provider;
const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
const authProfileId = resolvedCompactionTarget.authProfileId;
if (runtimeProvider !== provider || selectedHarnessRuntime) {
await ensureSelectedAgentHarnessPlugin({
config: params.config,
provider,
modelId,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride: selectedHarnessRuntime,
workspaceDir: resolvedWorkspace,
});
}
let thinkLevel: ThinkLevel = params.thinkLevel ?? "off";
const attemptedThinking = new Set<ThinkLevel>();
const fail = (reason: string, err?: unknown): EmbeddedAgentCompactResult => {
@@ -531,13 +582,6 @@ async function compactEmbeddedAgentSessionDirectOnce(
: undefined,
};
};
const earlyAgentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const agentDir =
params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId);
await ensureOpenClawModelsJson(params.config, agentDir, {
workspaceDir: resolvedWorkspace,
});
@@ -627,11 +671,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
sessionId: params.sessionId,
cwd: effectiveCwd,
});
const { sessionAgentId: effectiveSkillAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const { sessionAgentId: effectiveSkillAgentId } = earlyAgentIds;
let restoreSkillEnv: (() => void) | undefined;
let compactionSessionManager: unknown = null;
@@ -683,19 +723,9 @@ async function compactEmbeddedAgentSessionDirectOnce(
// Apply contextTokens cap to model so session runtime's auto-compaction
// threshold uses the effective limit, not the native context window.
const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel;
const runtimeHarnessPolicy = resolveAgentHarnessPolicy({
provider,
modelId,
config: params.config,
agentId: effectiveSkillAgentId,
sessionKey: params.sessionKey,
});
const ctxInfo = resolveContextWindowInfo({
cfg: params.config,
provider: resolveContextConfigProviderForRuntime({
provider,
runtimeId: params.agentHarnessId ?? runtimeHarnessPolicy.runtime,
}),
provider: contextConfigProvider,
modelId,
modelContextTokens: readAgentModelContextTokens(runtimeModel),
modelContextWindow: runtimeModelWithContext.contextWindow,
@@ -731,7 +761,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
model: effectiveModel,
modelApi: effectiveModel.api,
harnessId: params.agentHarnessId,
harnessRuntime: runtimeHarnessPolicy.runtime,
harnessRuntime: selectedHarnessRuntime,
authProfileProvider: authProfileId?.split(":", 1)[0],
sessionAuthProfileId: authProfileId,
config: params.config,

View File

@@ -25,7 +25,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
workspaceDir: "/tmp/workspace",
cwd: "/tmp/task-repo",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
senderIsOwner: true,
senderId: "user-123",
provider: "openai-codex",
@@ -87,7 +87,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
agentDir: "/tmp/agent",
config: {
agents: { defaults: { compaction: { model: "anthropic/claude-opus-4-6" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "ollama",
modelId: "minimax-m2.7:cloud",
authProfileId: "ollama:default",
@@ -104,7 +104,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
agentDir: "/tmp/agent",
config: {
agents: { defaults: { compaction: { model: "gpt-4o" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-3.5-turbo",
authProfileId: "openai:p1",
@@ -119,7 +119,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
provider: "ollama",
modelId: "minimax-m2.7:cloud",
authProfileId: "ollama:default",
@@ -153,7 +153,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
sessionKey: "agent:main:thread:1",
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
});
try {
@@ -188,7 +188,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
});
expect(result.activeProcessSessions).toBeUndefined();
@@ -199,7 +199,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "anthropic/" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai-codex",
modelId: "gpt-5.4",
authProfileId: "openai:p1",
@@ -223,6 +223,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
@@ -231,7 +232,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
auth: { order: { openai: ["openai-codex:default"] } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
defaultProvider: "openai",
@@ -239,6 +240,90 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("routes Codex-runtime OpenAI compaction through the plugin-backed Codex provider", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBe("openai-codex");
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("carries the selected harness id for delegated runtime compaction", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
});
expect(result.agentHarnessId).toBe("codex");
expect(result.runtimeProvider).toBe("openai-codex");
});
it("preserves direct OpenAI compaction for the OpenClaw runtime", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "openclaw",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBeUndefined();
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("preserves custom OpenAI-compatible compaction providers", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5" }],
},
"openai-codex": { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBeUndefined();
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
@@ -247,7 +332,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "gpt-5.4" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
authProfileId: "openai-codex:default",
@@ -256,6 +341,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
@@ -264,7 +350,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "openai/gpt-5.4" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
authProfileId: "openai-codex:default",
@@ -273,10 +359,35 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
it("routes OpenAI compaction model overrides through Codex runtime auth", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini" }] },
"openai-codex": { models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini" }] },
},
},
agents: { defaults: { compaction: { model: "openai/gpt-5.4-mini" } } },
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBe("openai-codex");
expect(result.model).toBe("gpt-5.4-mini");
expect(result.authProfileId).toBeUndefined();
});
it("leaves non-openai providers unchanged", () => {
const result = resolveEmbeddedCompactionTarget({
provider: "anthropic",

View File

@@ -2,12 +2,16 @@ import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { SkillSnapshot } from "../../skills/types.js";
import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import {
listActiveProcessSessionReferences,
type ActiveProcessSessionReference,
} from "../bash-process-references.js";
import type { ExecElevatedDefaults } from "../bash-tools.js";
import { resolveSelectedOpenAIRuntimeProvider } from "../openai-codex-routing.js";
import {
openAIProviderUsesCodexRuntimeByDefault,
resolveSelectedOpenAIRuntimeProvider,
} from "../openai-codex-routing.js";
export type EmbeddedCompactionRuntimeContext = {
sessionKey?: string;
@@ -18,6 +22,7 @@ export type EmbeddedCompactionRuntimeContext = {
currentThreadTs?: string;
currentMessageId?: string | number;
authProfileId?: string;
agentHarnessId?: string;
workspaceDir: string;
cwd?: string;
agentDir: string;
@@ -47,37 +52,49 @@ export function resolveEmbeddedCompactionTarget(params: {
provider?: string | null;
modelId?: string | null;
authProfileId?: string | null;
harnessRuntime?: string | null;
defaultProvider?: string;
defaultModel?: string;
}): {
provider: string | undefined;
runtimeProvider?: string;
contextProvider?: string;
model: string | undefined;
authProfileId: string | undefined;
} {
const provider = params.provider?.trim() || params.defaultProvider;
const model = params.modelId?.trim() || params.defaultModel;
const override = params.config?.agents?.defaults?.compaction?.model?.trim();
const resolveRuntimeProvider = (
const resolveTargetProviders = (
targetProvider: string | undefined,
authProfileId: string | undefined,
) => {
if (!targetProvider) {
return undefined;
return {};
}
const useCodexHarnessRuntime = shouldUseCodexRuntimeProviderForCompaction({
config: params.config,
provider: targetProvider,
harnessRuntime: params.harnessRuntime,
});
const harnessRuntime = useCodexHarnessRuntime ? params.harnessRuntime : "openclaw";
const runtimeProvider = resolveSelectedOpenAIRuntimeProvider({
provider: targetProvider,
harnessRuntime: "openclaw",
harnessRuntime: harnessRuntime ?? undefined,
authProfileId,
config: params.config,
});
return runtimeProvider === targetProvider ? undefined : runtimeProvider;
const routedRuntimeProvider = runtimeProvider === targetProvider ? undefined : runtimeProvider;
return {
runtimeProvider: routedRuntimeProvider,
contextProvider: useCodexHarnessRuntime ? routedRuntimeProvider : undefined,
};
};
if (!override) {
const authProfileId = params.authProfileId ?? undefined;
return {
provider,
runtimeProvider: resolveRuntimeProvider(provider, authProfileId),
...resolveTargetProviders(provider, authProfileId),
model,
authProfileId,
};
@@ -94,7 +111,7 @@ export function resolveEmbeddedCompactionTarget(params: {
: (params.authProfileId ?? undefined);
return {
provider: overrideProvider,
runtimeProvider: resolveRuntimeProvider(overrideProvider, authProfileId),
...resolveTargetProviders(overrideProvider, authProfileId),
model: overrideModel,
authProfileId,
};
@@ -102,12 +119,26 @@ export function resolveEmbeddedCompactionTarget(params: {
const authProfileId = params.authProfileId ?? undefined;
return {
provider,
runtimeProvider: resolveRuntimeProvider(provider, authProfileId),
...resolveTargetProviders(provider, authProfileId),
model: override,
authProfileId,
};
}
function shouldUseCodexRuntimeProviderForCompaction(params: {
config?: OpenClawConfig;
provider: string;
harnessRuntime?: string | null;
}): boolean {
if (normalizeOptionalAgentRuntimeId(params.harnessRuntime) !== "codex") {
return false;
}
if (!openAIProviderUsesCodexRuntimeByDefault(params)) {
return false;
}
return true;
}
export function buildEmbeddedCompactionRuntimeContext(params: {
sessionKey?: string | null;
messageChannel?: string | null;
@@ -126,6 +157,7 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
senderId?: string | null;
provider?: string | null;
modelId?: string | null;
harnessRuntime?: string | null;
modelFallbacksOverride?: string[];
thinkLevel?: ThinkLevel;
reasoningLevel?: ReasoningLevel;
@@ -140,7 +172,9 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
provider: params.provider,
modelId: params.modelId,
authProfileId: params.authProfileId,
harnessRuntime: params.harnessRuntime,
});
const agentHarnessId = params.harnessRuntime?.trim() || undefined;
const processScopeKey = params.sessionKey?.trim();
const activeProcessSessions =
params.activeProcessSessions ??
@@ -156,6 +190,7 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
currentThreadTs: params.currentThreadTs ?? undefined,
currentMessageId: params.currentMessageId ?? undefined,
authProfileId: resolved.authProfileId,
agentHarnessId,
workspaceDir: params.workspaceDir,
cwd: params.cwd ?? undefined,
agentDir: params.agentDir,

View File

@@ -19,4 +19,119 @@ describe("classifyEmbeddedAgentRunResultForModelFallback", () => {
}),
).toBeNull();
});
it("classifies provider business-denial error payloads as fallback-worthy", () => {
const result = classifyEmbeddedAgentRunResultForModelFallback({
provider: "zai",
model: "glm-5.1",
result: {
payloads: [
{
isError: true,
text: '{"success":false,"code":"CE-011","message":"当前ak因违规请求被禁止访问该模型"}',
},
],
meta: {
durationMs: 42,
},
},
});
expect(result).toEqual({
message:
'zai/glm-5.1 ended with a provider error: {"success":false,"code":"CE-011","message":"当前ak因违规请求被禁止访问该模型"}',
reason: "auth",
code: "embedded_error_payload",
rawError: '{"success":false,"code":"CE-011","message":"当前ak因违规请求被禁止访问该模型"}',
});
});
it("preserves hook block results with auth-like error payload text", () => {
const result = classifyEmbeddedAgentRunResultForModelFallback({
provider: "custom",
model: "gpt-5.5",
result: {
payloads: [
{
isError: true,
text: "Access denied by policy",
},
],
meta: {
durationMs: 42,
error: {
kind: "hook_block",
message: "Access denied by policy",
},
},
},
});
expect(result).toBeNull();
});
it("uses provider-scoped failover matching for business-denial payloads", () => {
const result = classifyEmbeddedAgentRunResultForModelFallback({
provider: "openrouter",
model: "claude-3.5-sonnet",
result: {
payloads: [
{
isError: true,
text: "Key limit exceeded",
},
],
meta: {
durationMs: 42,
},
},
});
expect(result).toEqual({
message: "openrouter/claude-3.5-sonnet ended with a provider error: Key limit exceeded",
reason: "billing",
code: "embedded_error_payload",
rawError: "Key limit exceeded",
});
});
it("does not retry unclassified non-GPT error payloads", () => {
const result = classifyEmbeddedAgentRunResultForModelFallback({
provider: "custom",
model: "llama-3.1",
result: {
payloads: [
{
isError: true,
text: "the model produced an application-level error",
},
],
meta: {
durationMs: 42,
},
},
});
expect(result).toBeNull();
});
it("does not retry non-business transport error payloads", () => {
const result = classifyEmbeddedAgentRunResultForModelFallback({
provider: "custom",
model: "llama-3.1",
result: {
payloads: [
{
isError: true,
text: "HTTP 500: internal server error",
},
],
meta: {
durationMs: 42,
},
},
});
expect(result).toBeNull();
});
});

View File

@@ -1,4 +1,6 @@
import { isSilentReplyPayloadText } from "../../auto-reply/tokens.js";
import { classifyFailoverReason } from "../embedded-agent-helpers/errors.js";
import type { FailoverReason } from "../embedded-agent-helpers/types.js";
import { isGpt5ModelId } from "../gpt5-prompt-overlay.js";
import type { ModelFallbackResultClassification } from "../model-fallback.js";
import { hasOutboundDeliveryEvidence, hasVisibleAgentPayload } from "./delivery-evidence.js";
@@ -55,6 +57,24 @@ function classifyHarnessResult(params: {
}
}
function classifyBusinessDenialErrorPayloadReason(
errorText: string,
provider: string,
): Extract<FailoverReason, "auth" | "auth_permanent" | "billing"> | null {
if (!errorText.trim()) {
return null;
}
const failoverReason = classifyFailoverReason(errorText, { provider });
switch (failoverReason) {
case "auth":
case "auth_permanent":
case "billing":
return failoverReason;
default:
return null;
}
}
export function classifyEmbeddedAgentRunResultForModelFallback(params: {
provider: string;
model: string;
@@ -79,6 +99,9 @@ export function classifyEmbeddedAgentRunResultForModelFallback(params: {
if (hasOutboundDeliveryEvidence(params.result)) {
return null;
}
if (params.result.meta.error?.kind === "hook_block") {
return null;
}
const harnessClassification = classifyHarnessResult({
provider: params.provider,
@@ -101,6 +124,15 @@ export function classifyEmbeddedAgentRunResultForModelFallback(params: {
code: "incomplete_result",
};
}
const failoverReason = classifyBusinessDenialErrorPayloadReason(errorText, params.provider);
if (failoverReason) {
return {
message: `${params.provider}/${params.model} ended with a provider error: ${errorText}`,
reason: failoverReason,
code: "embedded_error_payload",
rawError: errorText,
};
}
if (!isGpt5ModelId(params.model)) {
return null;

View File

@@ -768,6 +768,7 @@ export async function runEmbeddedAgent(
contextConfigProvider: resolveContextConfigProviderForRuntime({
provider: modelConfigProvider,
runtimeId: agentHarness.id,
config: params.config,
}),
modelId,
runtimeModel,
@@ -1905,6 +1906,7 @@ export async function runEmbeddedAgent(
senderId: params.senderId,
provider,
modelId,
harnessRuntime: agentHarness.id,
modelFallbacksOverride: params.modelFallbacksOverride,
thinkLevel,
reasoningLevel: params.reasoningLevel,
@@ -2096,6 +2098,7 @@ export async function runEmbeddedAgent(
senderId: params.senderId,
provider,
modelId,
harnessRuntime: agentHarness.id,
thinkLevel,
reasoningLevel: params.reasoningLevel,
bashElevated: params.bashElevated,

View File

@@ -534,6 +534,7 @@ type AfterTurnRuntimeContextAttempt = Pick<
| "senderId"
| "provider"
| "modelId"
| "agentHarnessId"
| "thinkLevel"
| "reasoningLevel"
| "bashElevated"
@@ -574,6 +575,7 @@ export function buildAfterTurnRuntimeContext(params: {
senderId: params.attempt.senderId,
provider: params.attempt.provider,
modelId: params.attempt.modelId,
harnessRuntime: params.attempt.agentHarnessId,
thinkLevel: params.attempt.thinkLevel,
reasoningLevel: params.attempt.reasoningLevel,
bashElevated: params.attempt.bashElevated,

View File

@@ -1438,6 +1438,7 @@ export async function runEmbeddedAttempt(
tools: [...tools, ...normalizedBundledTools],
config: params.config,
agentId: sessionAgentId,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
});
const uncompactedToolSchemaProjection = filterRuntimeCompatibleTools(
projectedUncompactedEffectiveTools,
@@ -1509,6 +1510,7 @@ export async function runEmbeddedAttempt(
tools: toolSearch.tools,
config: params.config,
agentId: sessionAgentId,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
});
const toolSearchSchemaProjection = filterRuntimeCompatibleTools(projectedToolSearchTools);
logRuntimeToolSchemaQuarantine({

View File

@@ -756,6 +756,35 @@ describe("selectAgentHarness", () => {
).resolves.toBeUndefined();
});
it("honors selected plugin harness pins during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: agentModelRuntimeConfig("openai/gpt-5.5", "openclaw"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("does not compact a selected plugin harness through OpenClaw when the plugin has no compactor", async () => {
registerFailingCodexHarness();
@@ -777,6 +806,95 @@ describe("selectAgentHarness", () => {
});
});
it("uses agent-scoped runtime policy during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:strict:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "strict",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("uses sandbox session key for compaction preflight runtime policy", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sandboxSessionKey: "agent:strict:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "main",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("keeps explicit agent id for non-agent sandbox policy keys during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sandboxSessionKey: "global",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "strict",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it.each([
{ provider: "anthropic", modelId: "sonnet-4.6", alias: "claude-cli" },
{ provider: "google", modelId: "gemini-3-pro-preview", alias: "google-gemini-cli" },

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import {
resolveEffectiveToolPolicy,
@@ -490,21 +491,43 @@ export async function maybeCompactAgentHarnessSession(
if (params.provider && isCliRuntimeProvider(params.provider, { config: params.config })) {
return undefined;
}
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const runtime = resolveConfiguredAgentHarnessPolicy({
provider: params.provider,
modelId: params.model,
config: params.config,
sessionKey: params.sessionKey,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
}).runtime;
if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider, cfg: params.config })) {
return undefined;
}
const harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
sessionKey: params.sessionKey,
});
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const agentHarnessRuntimeOverride =
selectedRuntime && !isDefaultAgentRuntimeId(selectedRuntime) ? selectedRuntime : undefined;
let harness: AgentHarness;
try {
harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride,
});
} catch (err) {
if (agentHarnessRuntimeOverride) {
const message = formatErrorMessage(err);
if (message.includes("does not support")) {
return undefined;
}
}
throw err;
}
if (!harness.compact) {
if (harness.id !== "openclaw") {
return {

View File

@@ -32,6 +32,46 @@ describe("local model lean tool filtering", () => {
).toEqual(["read", "exec"]);
});
it("keeps message when source replies require message-tool delivery", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
};
expect(
filterLocalModelLeanTools({
tools: tools(["read", "browser", "cron", "message", "exec"]),
config: cfg,
sourceReplyDeliveryMode: "message_tool_only",
}).map((tool) => tool.name),
).toEqual(["read", "message", "exec"]);
});
it("drops message when source replies use automatic delivery", () => {
const cfg: OpenClawConfig = {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
};
expect(
filterLocalModelLeanTools({
tools: tools(["read", "browser", "cron", "message", "exec"]),
config: cfg,
sourceReplyDeliveryMode: "automatic",
}).map((tool) => tool.name),
).toEqual(["read", "exec"]);
});
it("lets an agent opt out of an inherited global lean setting", () => {
const cfg: OpenClawConfig = {
agents: {

View File

@@ -1,3 +1,4 @@
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.js";
@@ -43,9 +44,14 @@ export function filterLocalModelLeanTools(params: {
config?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
}): AnyAgentTool[] {
if (!isLocalModelLeanEnabled(params)) {
return params.tools;
}
return params.tools.filter((tool) => !LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name));
return params.tools.filter(
(tool) =>
(tool.name === "message" && params.sourceReplyDeliveryMode === "message_tool_only") ||
!LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name),
);
}

View File

@@ -1223,6 +1223,55 @@ describe("runWithModelFallback", () => {
expect(result.attempts[0]?.code).toBe("empty_result");
});
it("continues fallback after embedded provider business-denial payloads", async () => {
const cfg = makeCfg({
agents: {
defaults: {
model: {
primary: "zai/glm-5.1",
fallbacks: ["openai/gpt-5.5"],
},
},
},
});
const rawError =
'{"success":false,"code":"CE-011","message":"当前ak因违规请求被禁止访问该模型"}';
const run = vi
.fn()
.mockResolvedValueOnce({
payloads: [{ text: rawError, isError: true }],
meta: { durationMs: 1 },
} satisfies EmbeddedAgentRunResult)
.mockResolvedValueOnce({
payloads: [{ text: "fallback ok" }],
meta: { durationMs: 1 },
} satisfies EmbeddedAgentRunResult);
const result = await runWithModelFallback<EmbeddedAgentRunResult>({
cfg,
provider: "zai",
model: "glm-5.1",
run,
classifyResult: ({ provider, model, result }) =>
classifyEmbeddedAgentRunResultForModelFallback({
provider,
model,
result,
}),
});
expect(result.result.payloads).toEqual([{ text: "fallback ok" }]);
expect(run).toHaveBeenCalledTimes(2);
expect(requireMockCall(run, 1, "fallback run")).toEqual(["openai", "gpt-5.5"]);
expect(result.attempts[0]).toMatchObject({
provider: "zai",
model: "glm-5.1",
reason: "auth",
code: "embedded_error_payload",
error: rawError,
});
});
it("surfaces classified terminal results when no fallback remains", async () => {
const cfg = makeCfg({
agents: {

View File

@@ -4,6 +4,7 @@ import {
listOpenAIAuthProfileProvidersForAgentRuntime,
modelSelectionShouldEnsureCodexPlugin,
openAIProviderUsesCodexRuntimeByDefault,
resolveContextConfigProviderForRuntime,
resolveOpenAIRuntimeProvider,
resolveSelectedOpenAIRuntimeProvider,
} from "./openai-codex-routing.js";
@@ -33,6 +34,22 @@ describe("OpenAI Codex routing policy", () => {
expect(openAIProviderUsesCodexRuntimeByDefault({ provider: "openai", config })).toBe(false);
expect(modelSelectionShouldEnsureCodexPlugin({ model: "openai/gpt-5.5", config })).toBe(false);
expect(
resolveContextConfigProviderForRuntime({
provider: "openai",
runtimeId: "codex",
config,
}),
).toBe("openai");
});
it("uses Codex context config for official OpenAI under the Codex runtime", () => {
expect(
resolveContextConfigProviderForRuntime({
provider: "openai",
runtimeId: "codex",
}),
).toBe("openai-codex");
});
it("maps explicit OpenClaw plus Codex auth profile to the OpenClaw Codex-auth transport", () => {

View File

@@ -200,10 +200,15 @@ export function resolveSelectedOpenAIRuntimeProvider(params: {
export function resolveContextConfigProviderForRuntime(params: {
provider: string;
runtimeId?: string;
config?: OpenClawConfig;
}): string {
const provider = normalizeProviderId(params.provider);
const runtimeId = normalizeOptionalAgentRuntimeId(params.runtimeId) ?? OPENCLAW_AGENT_RUNTIME_ID;
if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") {
if (
provider === OPENAI_PROVIDER_ID &&
runtimeId === "codex" &&
openAIProviderUsesCodexRuntimeByDefault({ provider, config: params.config })
) {
return OPENAI_CODEX_PROVIDER_ID;
}
return params.provider;

View File

@@ -247,6 +247,7 @@ function resolveFollowupContextConfigProvider(params: {
return resolveContextConfigProviderForRuntime({
provider,
runtimeId: resolveFollowupAgentRuntimeId(params),
config: params.cfg,
});
}

View File

@@ -134,6 +134,7 @@ function resolveManualCompactContextTokenBudget(params: {
const contextConfigProvider = resolveContextConfigProviderForRuntime({
provider,
runtimeId: harnessPolicy.runtime,
config: params.cfg,
});
const configuredContextTokens = resolveContextTokensForModel({
cfg: params.cfg,

View File

@@ -396,6 +396,7 @@ export async function persistInlineDirectives(params: {
agentId: activeAgentId,
sessionKey,
}).runtime,
config: cfg,
}),
model,
}),

View File

@@ -1127,7 +1127,7 @@ export const FIELD_HELP: Record<string, string> = {
"agents.defaults.experimental":
"Experimental agent-default flags. Keep these off unless you are intentionally testing a preview surface.",
"agents.defaults.experimental.localModelLean":
"Experimental local-model prompt trim. When enabled, OpenClaw drops heavyweight default tools like browser, cron, and message for weaker or smaller local-model backends.",
"Experimental local-model prompt trim. When enabled, OpenClaw drops heavyweight default tools like browser, cron, and message for weaker or smaller local-model backends, but keeps message when source replies require message-tool delivery.",
"agents.defaults.bootstrapPromptTruncationWarning":
'Inject agent-visible warning text when bootstrap files are truncated: "off", "once", or "always" (default).',
"agents.defaults.startupContext":

View File

@@ -205,6 +205,8 @@ export type ContextEngineRuntimeContext = Record<string, unknown> & {
allowDeferredCompactionExecution?: boolean;
/** Runtime-resolved context window budget for the active model call. */
tokenBudget?: number;
/** Selected agent harness id when compaction delegates back to the runtime. */
agentHarnessId?: string;
/** Best-effort current prompt/context token estimate for this turn. */
currentTokenCount?: number;
/** Optional prompt-cache telemetry for cache-aware engines. */

View File

@@ -266,66 +266,6 @@ describe("streamOpenAICodexResponses transport", () => {
expect(payload).toMatchObject({ prompt_cache_key: "stable-cache-key" });
});
it("omits tool controls when no tools are available", async () => {
let payload: Record<string, unknown> | undefined;
const stream = streamOpenAICodexResponses(model, context, {
apiKey: createJwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-1",
},
}),
transport: "sse",
onPayload: (nextPayload) => {
payload = nextPayload as Record<string, unknown>;
throw new Error("stop after payload capture");
},
});
await stream.result();
expect(payload).toBeDefined();
expect(payload).not.toHaveProperty("tools");
expect(payload).not.toHaveProperty("tool_choice");
expect(payload).not.toHaveProperty("parallel_tool_calls");
});
it("keeps tool controls when tools are available", async () => {
let payload: Record<string, unknown> | undefined;
const stream = streamOpenAICodexResponses(
model,
{
...context,
tools: [
{
name: "read",
description: "Read a file",
parameters: { type: "object", properties: { path: { type: "string" } } },
},
],
},
{
apiKey: createJwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct-1",
},
}),
transport: "sse",
onPayload: (nextPayload) => {
payload = nextPayload as Record<string, unknown>;
throw new Error("stop after payload capture");
},
},
);
await stream.result();
expect(payload?.tools).toHaveLength(1);
expect(payload?.tool_choice).toBe("auto");
expect(payload?.parallel_tool_calls).toBe(true);
});
it.each(["1.5", "0x10"])(
"ignores invalid Retry-After header delay values: %s",
async (retryAfter) => {

View File

@@ -491,6 +491,8 @@ function buildRequestBody(
options?.cacheRetention === "none"
? undefined
: clampOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId),
tool_choice: "auto",
parallel_tool_calls: true,
};
if (options?.temperature !== undefined) {
@@ -503,8 +505,6 @@ function buildRequestBody(
if (context.tools && context.tools.length > 0) {
body.tools = convertResponsesTools(context.tools, { strict: null });
body.tool_choice = "auto";
body.parallel_tool_calls = true;
}
if (options?.reasoningEffort !== undefined) {