mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-18 20:32:25 +08:00
Compare commits
1 Commits
codex/loca
...
codex/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d46cd809b |
@@ -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 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.
|
||||
`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.
|
||||
|
||||
### 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. When the source reply delivery contract is `message_tool_only`, lean mode keeps `message` so visible replies can still be delivered.
|
||||
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 to turn it on
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
`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.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -164,35 +164,6 @@ 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(
|
||||
[
|
||||
|
||||
@@ -245,7 +245,6 @@ function applyModelProviderToolPolicy(
|
||||
agentDir?: string;
|
||||
modelCompat?: ModelCompatConfig;
|
||||
suppressManagedWebSearch?: boolean;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
},
|
||||
): AnyAgentTool[] {
|
||||
tools = filterLocalModelLeanTools({
|
||||
@@ -253,7 +252,6 @@ function applyModelProviderToolPolicy(
|
||||
config: params?.config,
|
||||
agentId: params?.agentId,
|
||||
sessionKey: params?.sessionKey,
|
||||
sourceReplyDeliveryMode: params?.sourceReplyDeliveryMode,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -1065,7 +1063,6 @@ 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
|
||||
|
||||
@@ -1412,8 +1412,6 @@ 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");
|
||||
|
||||
@@ -47,8 +47,6 @@ 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/;
|
||||
|
||||
@@ -618,6 +618,14 @@ 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",
|
||||
@@ -631,7 +639,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the selected Codex runtime provider for OpenAI compaction", async () => {
|
||||
it("uses the selected Codex runtime provider for OpenAI compaction context windows", async () => {
|
||||
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
|
||||
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
|
||||
model: { provider, api: "responses", id: modelId, input: [] },
|
||||
@@ -647,7 +655,6 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
agentHarnessId: "codex",
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
@@ -664,95 +671,17 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
|
||||
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
|
||||
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves direct OpenAI API-key compaction when OpenClaw runtime is active", async () => {
|
||||
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" });
|
||||
it("preserves direct OpenAI API-key compaction when no Codex auth is configured", async () => {
|
||||
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
|
||||
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
|
||||
model: { provider, api: "responses", id: modelId, input: [] },
|
||||
error: null,
|
||||
@@ -773,7 +702,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
|
||||
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
|
||||
},
|
||||
},
|
||||
agents: { defaults: { embeddedHarness: { runtime: "openclaw" } } },
|
||||
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
|
||||
} as never,
|
||||
});
|
||||
|
||||
@@ -782,52 +711,6 @@ 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") => ({
|
||||
@@ -1788,7 +1671,8 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
|
||||
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
|
||||
expect(harnessArg.contextTokenBudget).toBe(32_000);
|
||||
const compactArg = mockCallArg(contextEngineCompactMock) as {
|
||||
tokenBudget?: number;
|
||||
runtimeContext?: Record<string, unknown>;
|
||||
@@ -1836,248 +1720,6 @@ 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 () => ({
|
||||
|
||||
@@ -16,19 +16,16 @@ 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 { isOpenAICodexProvider, isOpenAIProvider } from "../openai-codex-routing.js";
|
||||
import { resolveContextConfigProviderForRuntime } 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";
|
||||
@@ -172,64 +169,17 @@ export async function compactEmbeddedAgentSession(
|
||||
agentDir,
|
||||
workspaceDir: resolvedWorkspaceDir,
|
||||
});
|
||||
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,
|
||||
authProfileId: params.authProfileId,
|
||||
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,
|
||||
@@ -237,11 +187,21 @@ 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: ceContextConfigProvider,
|
||||
provider: resolveContextConfigProviderForRuntime({
|
||||
provider: ceProvider,
|
||||
runtimeId: params.agentHarnessId ?? ceHarnessPolicy.runtime,
|
||||
}),
|
||||
modelId: ceModelId,
|
||||
modelContextTokens: readAgentModelContextTokens(ceModel),
|
||||
modelContextWindow: ceRuntimeModel?.contextWindow,
|
||||
@@ -256,18 +216,15 @@ export async function compactEmbeddedAgentSession(
|
||||
const contextEngineRuntimeContext = buildCompactionContextEngineRuntimeContext({
|
||||
params,
|
||||
agentDir,
|
||||
harnessRuntime: selectedHarnessRuntime,
|
||||
contextTokenBudget,
|
||||
contextEnginePluginId: resolveContextEngineOwnerPluginId(contextEngine),
|
||||
});
|
||||
const harnessResult = attemptNativeHarnessCompaction
|
||||
? await maybeCompactAgentHarnessSession({
|
||||
...params,
|
||||
contextEngine,
|
||||
contextTokenBudget,
|
||||
contextEngineRuntimeContext,
|
||||
})
|
||||
: undefined;
|
||||
const harnessResult = await maybeCompactAgentHarnessSession({
|
||||
...params,
|
||||
contextEngine,
|
||||
contextTokenBudget,
|
||||
contextEngineRuntimeContext,
|
||||
});
|
||||
if (harnessResult) {
|
||||
if (!shouldFallbackAfterHarnessCompaction(harnessResult)) {
|
||||
await contextEngine.dispose?.();
|
||||
@@ -511,25 +468,9 @@ 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 {
|
||||
@@ -558,7 +499,6 @@ 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,
|
||||
|
||||
@@ -24,11 +24,7 @@ import {
|
||||
resolveProviderTextTransforms,
|
||||
transformProviderSystemPrompt,
|
||||
} from "../../plugins/provider-runtime.js";
|
||||
import {
|
||||
isCronSessionKey,
|
||||
isSubagentSessionKey,
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
|
||||
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
|
||||
import {
|
||||
@@ -45,7 +41,6 @@ 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,
|
||||
@@ -92,6 +87,7 @@ 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";
|
||||
@@ -493,46 +489,11 @@ async function compactEmbeddedAgentSessionDirectOnce(
|
||||
workspaceDir: resolvedWorkspace,
|
||||
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
||||
});
|
||||
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,
|
||||
authProfileId: params.authProfileId,
|
||||
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,
|
||||
});
|
||||
@@ -540,20 +501,8 @@ async function compactEmbeddedAgentSessionDirectOnce(
|
||||
// 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 => {
|
||||
@@ -582,6 +531,13 @@ 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,
|
||||
});
|
||||
@@ -671,7 +627,11 @@ async function compactEmbeddedAgentSessionDirectOnce(
|
||||
sessionId: params.sessionId,
|
||||
cwd: effectiveCwd,
|
||||
});
|
||||
const { sessionAgentId: effectiveSkillAgentId } = earlyAgentIds;
|
||||
const { sessionAgentId: effectiveSkillAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
|
||||
let restoreSkillEnv: (() => void) | undefined;
|
||||
let compactionSessionManager: unknown = null;
|
||||
@@ -723,9 +683,19 @@ 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: contextConfigProvider,
|
||||
provider: resolveContextConfigProviderForRuntime({
|
||||
provider,
|
||||
runtimeId: params.agentHarnessId ?? runtimeHarnessPolicy.runtime,
|
||||
}),
|
||||
modelId,
|
||||
modelContextTokens: readAgentModelContextTokens(runtimeModel),
|
||||
modelContextWindow: runtimeModelWithContext.contextWindow,
|
||||
@@ -761,7 +731,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
|
||||
model: effectiveModel,
|
||||
modelApi: effectiveModel.api,
|
||||
harnessId: params.agentHarnessId,
|
||||
harnessRuntime: selectedHarnessRuntime,
|
||||
harnessRuntime: runtimeHarnessPolicy.runtime,
|
||||
authProfileProvider: authProfileId?.split(":", 1)[0],
|
||||
sessionAuthProfileId: authProfileId,
|
||||
config: params.config,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
cwd: "/tmp/task-repo",
|
||||
agentDir: "/tmp/agent",
|
||||
config: {} as unknown as OpenClawConfig,
|
||||
config: {} 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 unknown as OpenClawConfig,
|
||||
} 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 unknown as OpenClawConfig,
|
||||
} 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 unknown as OpenClawConfig,
|
||||
config: {} 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 unknown as OpenClawConfig,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -188,7 +188,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
const result = buildEmbeddedCompactionRuntimeContext({
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
config: {} as unknown as OpenClawConfig,
|
||||
config: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(result.activeProcessSessions).toBeUndefined();
|
||||
@@ -199,7 +199,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
resolveEmbeddedCompactionTarget({
|
||||
config: {
|
||||
agents: { defaults: { compaction: { model: "anthropic/" } } },
|
||||
} as unknown as OpenClawConfig,
|
||||
} as OpenClawConfig,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
authProfileId: "openai:p1",
|
||||
@@ -223,7 +223,6 @@ 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");
|
||||
});
|
||||
@@ -232,7 +231,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
const result = resolveEmbeddedCompactionTarget({
|
||||
config: {
|
||||
auth: { order: { openai: ["openai-codex:default"] } },
|
||||
} as unknown as OpenClawConfig,
|
||||
} as OpenClawConfig,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
defaultProvider: "openai",
|
||||
@@ -240,90 +239,6 @@ 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();
|
||||
});
|
||||
@@ -332,7 +247,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
const result = resolveEmbeddedCompactionTarget({
|
||||
config: {
|
||||
agents: { defaults: { compaction: { model: "gpt-5.4" } } },
|
||||
} as unknown as OpenClawConfig,
|
||||
} as OpenClawConfig,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
authProfileId: "openai-codex:default",
|
||||
@@ -341,7 +256,6 @@ 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");
|
||||
});
|
||||
@@ -350,7 +264,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
const result = resolveEmbeddedCompactionTarget({
|
||||
config: {
|
||||
agents: { defaults: { compaction: { model: "openai/gpt-5.4" } } },
|
||||
} as unknown as OpenClawConfig,
|
||||
} as OpenClawConfig,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.5",
|
||||
authProfileId: "openai-codex:default",
|
||||
@@ -359,35 +273,10 @@ 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",
|
||||
|
||||
@@ -2,16 +2,12 @@ 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 {
|
||||
openAIProviderUsesCodexRuntimeByDefault,
|
||||
resolveSelectedOpenAIRuntimeProvider,
|
||||
} from "../openai-codex-routing.js";
|
||||
import { resolveSelectedOpenAIRuntimeProvider } from "../openai-codex-routing.js";
|
||||
|
||||
export type EmbeddedCompactionRuntimeContext = {
|
||||
sessionKey?: string;
|
||||
@@ -22,7 +18,6 @@ export type EmbeddedCompactionRuntimeContext = {
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
authProfileId?: string;
|
||||
agentHarnessId?: string;
|
||||
workspaceDir: string;
|
||||
cwd?: string;
|
||||
agentDir: string;
|
||||
@@ -52,49 +47,37 @@ 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 resolveTargetProviders = (
|
||||
const resolveRuntimeProvider = (
|
||||
targetProvider: string | undefined,
|
||||
authProfileId: string | undefined,
|
||||
) => {
|
||||
if (!targetProvider) {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
const useCodexHarnessRuntime = shouldUseCodexRuntimeProviderForCompaction({
|
||||
config: params.config,
|
||||
provider: targetProvider,
|
||||
harnessRuntime: params.harnessRuntime,
|
||||
});
|
||||
const harnessRuntime = useCodexHarnessRuntime ? params.harnessRuntime : "openclaw";
|
||||
const runtimeProvider = resolveSelectedOpenAIRuntimeProvider({
|
||||
provider: targetProvider,
|
||||
harnessRuntime: harnessRuntime ?? undefined,
|
||||
harnessRuntime: "openclaw",
|
||||
authProfileId,
|
||||
config: params.config,
|
||||
});
|
||||
const routedRuntimeProvider = runtimeProvider === targetProvider ? undefined : runtimeProvider;
|
||||
return {
|
||||
runtimeProvider: routedRuntimeProvider,
|
||||
contextProvider: useCodexHarnessRuntime ? routedRuntimeProvider : undefined,
|
||||
};
|
||||
return runtimeProvider === targetProvider ? undefined : runtimeProvider;
|
||||
};
|
||||
if (!override) {
|
||||
const authProfileId = params.authProfileId ?? undefined;
|
||||
return {
|
||||
provider,
|
||||
...resolveTargetProviders(provider, authProfileId),
|
||||
runtimeProvider: resolveRuntimeProvider(provider, authProfileId),
|
||||
model,
|
||||
authProfileId,
|
||||
};
|
||||
@@ -111,7 +94,7 @@ export function resolveEmbeddedCompactionTarget(params: {
|
||||
: (params.authProfileId ?? undefined);
|
||||
return {
|
||||
provider: overrideProvider,
|
||||
...resolveTargetProviders(overrideProvider, authProfileId),
|
||||
runtimeProvider: resolveRuntimeProvider(overrideProvider, authProfileId),
|
||||
model: overrideModel,
|
||||
authProfileId,
|
||||
};
|
||||
@@ -119,26 +102,12 @@ export function resolveEmbeddedCompactionTarget(params: {
|
||||
const authProfileId = params.authProfileId ?? undefined;
|
||||
return {
|
||||
provider,
|
||||
...resolveTargetProviders(provider, authProfileId),
|
||||
runtimeProvider: resolveRuntimeProvider(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;
|
||||
@@ -157,7 +126,6 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
|
||||
senderId?: string | null;
|
||||
provider?: string | null;
|
||||
modelId?: string | null;
|
||||
harnessRuntime?: string | null;
|
||||
modelFallbacksOverride?: string[];
|
||||
thinkLevel?: ThinkLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
@@ -172,9 +140,7 @@ 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 ??
|
||||
@@ -190,7 +156,6 @@ 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,
|
||||
|
||||
@@ -19,119 +19,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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";
|
||||
@@ -57,24 +55,6 @@ 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;
|
||||
@@ -99,9 +79,6 @@ 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,
|
||||
@@ -124,15 +101,6 @@ 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;
|
||||
|
||||
@@ -768,7 +768,6 @@ export async function runEmbeddedAgent(
|
||||
contextConfigProvider: resolveContextConfigProviderForRuntime({
|
||||
provider: modelConfigProvider,
|
||||
runtimeId: agentHarness.id,
|
||||
config: params.config,
|
||||
}),
|
||||
modelId,
|
||||
runtimeModel,
|
||||
@@ -1906,7 +1905,6 @@ export async function runEmbeddedAgent(
|
||||
senderId: params.senderId,
|
||||
provider,
|
||||
modelId,
|
||||
harnessRuntime: agentHarness.id,
|
||||
modelFallbacksOverride: params.modelFallbacksOverride,
|
||||
thinkLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
@@ -2098,7 +2096,6 @@ export async function runEmbeddedAgent(
|
||||
senderId: params.senderId,
|
||||
provider,
|
||||
modelId,
|
||||
harnessRuntime: agentHarness.id,
|
||||
thinkLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
bashElevated: params.bashElevated,
|
||||
|
||||
@@ -534,7 +534,6 @@ type AfterTurnRuntimeContextAttempt = Pick<
|
||||
| "senderId"
|
||||
| "provider"
|
||||
| "modelId"
|
||||
| "agentHarnessId"
|
||||
| "thinkLevel"
|
||||
| "reasoningLevel"
|
||||
| "bashElevated"
|
||||
@@ -575,7 +574,6 @@ 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,
|
||||
|
||||
@@ -1438,7 +1438,6 @@ export async function runEmbeddedAttempt(
|
||||
tools: [...tools, ...normalizedBundledTools],
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
});
|
||||
const uncompactedToolSchemaProjection = filterRuntimeCompatibleTools(
|
||||
projectedUncompactedEffectiveTools,
|
||||
@@ -1510,7 +1509,6 @@ export async function runEmbeddedAttempt(
|
||||
tools: toolSearch.tools,
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
});
|
||||
const toolSearchSchemaProjection = filterRuntimeCompatibleTools(projectedToolSearchTools);
|
||||
logRuntimeToolSchemaQuarantine({
|
||||
|
||||
@@ -756,35 +756,6 @@ 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();
|
||||
|
||||
@@ -806,95 +777,6 @@ 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" },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
@@ -491,43 +490,21 @@ 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,
|
||||
agentId: runtimePolicyAgentId,
|
||||
sessionKey: runtimePolicySessionKey,
|
||||
sessionKey: params.sessionKey,
|
||||
}).runtime;
|
||||
if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider, cfg: params.config })) {
|
||||
return undefined;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const harness = selectAgentHarness({
|
||||
provider: params.provider ?? "",
|
||||
modelId: params.model,
|
||||
config: params.config,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
if (!harness.compact) {
|
||||
if (harness.id !== "openclaw") {
|
||||
return {
|
||||
|
||||
@@ -32,46 +32,6 @@ 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: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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";
|
||||
@@ -44,14 +43,9 @@ export function filterLocalModelLeanTools(params: {
|
||||
config?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
}): AnyAgentTool[] {
|
||||
if (!isLocalModelLeanEnabled(params)) {
|
||||
return params.tools;
|
||||
}
|
||||
return params.tools.filter(
|
||||
(tool) =>
|
||||
(tool.name === "message" && params.sourceReplyDeliveryMode === "message_tool_only") ||
|
||||
!LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name),
|
||||
);
|
||||
return params.tools.filter((tool) => !LOCAL_MODEL_LEAN_DENY_TOOL_NAMES.has(tool.name));
|
||||
}
|
||||
|
||||
@@ -1223,55 +1223,6 @@ 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: {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
listOpenAIAuthProfileProvidersForAgentRuntime,
|
||||
modelSelectionShouldEnsureCodexPlugin,
|
||||
openAIProviderUsesCodexRuntimeByDefault,
|
||||
resolveContextConfigProviderForRuntime,
|
||||
resolveOpenAIRuntimeProvider,
|
||||
resolveSelectedOpenAIRuntimeProvider,
|
||||
} from "./openai-codex-routing.js";
|
||||
@@ -34,22 +33,6 @@ 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", () => {
|
||||
|
||||
@@ -200,15 +200,10 @@ 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" &&
|
||||
openAIProviderUsesCodexRuntimeByDefault({ provider, config: params.config })
|
||||
) {
|
||||
if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") {
|
||||
return OPENAI_CODEX_PROVIDER_ID;
|
||||
}
|
||||
return params.provider;
|
||||
|
||||
@@ -247,7 +247,6 @@ function resolveFollowupContextConfigProvider(params: {
|
||||
return resolveContextConfigProviderForRuntime({
|
||||
provider,
|
||||
runtimeId: resolveFollowupAgentRuntimeId(params),
|
||||
config: params.cfg,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ function resolveManualCompactContextTokenBudget(params: {
|
||||
const contextConfigProvider = resolveContextConfigProviderForRuntime({
|
||||
provider,
|
||||
runtimeId: harnessPolicy.runtime,
|
||||
config: params.cfg,
|
||||
});
|
||||
const configuredContextTokens = resolveContextTokensForModel({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -396,7 +396,6 @@ export async function persistInlineDirectives(params: {
|
||||
agentId: activeAgentId,
|
||||
sessionKey,
|
||||
}).runtime,
|
||||
config: cfg,
|
||||
}),
|
||||
model,
|
||||
}),
|
||||
|
||||
@@ -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, but keeps message when source replies require message-tool delivery.",
|
||||
"Experimental local-model prompt trim. When enabled, OpenClaw drops heavyweight default tools like browser, cron, and message for weaker or smaller local-model backends.",
|
||||
"agents.defaults.bootstrapPromptTruncationWarning":
|
||||
'Inject agent-visible warning text when bootstrap files are truncated: "off", "once", or "always" (default).',
|
||||
"agents.defaults.startupContext":
|
||||
|
||||
@@ -205,8 +205,6 @@ 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. */
|
||||
|
||||
@@ -266,6 +266,66 @@ 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) => {
|
||||
|
||||
@@ -491,8 +491,6 @@ function buildRequestBody(
|
||||
options?.cacheRetention === "none"
|
||||
? undefined
|
||||
: clampOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId),
|
||||
tool_choice: "auto",
|
||||
parallel_tool_calls: true,
|
||||
};
|
||||
|
||||
if (options?.temperature !== undefined) {
|
||||
@@ -505,6 +503,8 @@ 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) {
|
||||
|
||||
Reference in New Issue
Block a user