diff --git a/docs/configurations.md b/docs/configurations.md index 622b60a8..f5961631 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -1133,6 +1133,7 @@ Opt-in experimental features that may change or be removed in future versions. U "truncate_all_tool_outputs": true, "aggressive_truncation": true, "auto_resume": true, + "disable_omo_env": false, "dynamic_context_pruning": { "enabled": false, "notification": "detailed", @@ -1164,6 +1165,7 @@ Opt-in experimental features that may change or be removed in future versions. U | `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. | | `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. | | `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts last user message and continues. | +| `disable_omo_env` | `false` | When `true`, disables auto-injected `` block generation (date, time, timezone, locale). When unset or `false`, current behavior is preserved. Setting this to `true` will improve the cache hit rate and reduce the API cost. | | `dynamic_context_pruning` | See below | Dynamic context pruning configuration for managing context window usage automatically. See [Dynamic Context Pruning](#dynamic-context-pruning) below. | ### Dynamic Context Pruning diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts index 2a4d651b..57e859c7 100644 --- a/src/agents/builtin-agents.ts +++ b/src/agents/builtin-agents.ts @@ -69,8 +69,10 @@ export async function createBuiltinAgents( browserProvider?: BrowserAutomationProvider, uiSelectedModel?: string, disabledSkills?: Set, - useTaskSystem = false + useTaskSystem = false, + disableOmoEnv = false ): Promise> { + const connectedProviders = readConnectedProvidersCache() const providerModelsConnected = connectedProviders ? (readProviderModelsCache()?.connected ?? []) @@ -112,6 +114,7 @@ export async function createBuiltinAgents( uiSelectedModel, availableModels, disabledSkills, + disableOmoEnv, }) const registeredAgents = parseRegisteredAgentSummaries(customAgentSummaries) @@ -145,6 +148,7 @@ export async function createBuiltinAgents( directory, userCategories: categories, useTaskSystem, + disableOmoEnv, }) if (sisyphusConfig) { result["sisyphus"] = sisyphusConfig @@ -162,6 +166,7 @@ export async function createBuiltinAgents( mergedCategories, directory, useTaskSystem, + disableOmoEnv, }) if (hephaestusConfig) { result["hephaestus"] = hephaestusConfig diff --git a/src/agents/builtin-agents/environment-context.ts b/src/agents/builtin-agents/environment-context.ts index cf309b85..ac740dcf 100644 --- a/src/agents/builtin-agents/environment-context.ts +++ b/src/agents/builtin-agents/environment-context.ts @@ -1,8 +1,16 @@ import type { AgentConfig } from "@opencode-ai/sdk" import { createEnvContext } from "../env-context" -export function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig { - if (!directory || !config.prompt) return config +type ApplyEnvironmentContextOptions = { + disableOmoEnv?: boolean +} + +export function applyEnvironmentContext( + config: AgentConfig, + directory?: string, + options: ApplyEnvironmentContextOptions = {} +): AgentConfig { + if (options.disableOmoEnv || !directory || !config.prompt) return config const envContext = createEnvContext() return { ...config, prompt: config.prompt + envContext } } diff --git a/src/agents/builtin-agents/general-agents.ts b/src/agents/builtin-agents/general-agents.ts index c84dea2e..54f2ae3f 100644 --- a/src/agents/builtin-agents/general-agents.ts +++ b/src/agents/builtin-agents/general-agents.ts @@ -23,6 +23,7 @@ export function collectPendingBuiltinAgents(input: { availableModels: Set disabledSkills?: Set useTaskSystem?: boolean + disableOmoEnv?: boolean }): { pendingAgentConfigs: Map; availableAgents: AvailableAgent[] } { const { agentSources, @@ -37,6 +38,7 @@ export function collectPendingBuiltinAgents(input: { uiSelectedModel, availableModels, disabledSkills, + disableOmoEnv = false, } = input const availableAgents: AvailableAgent[] = [] @@ -81,7 +83,7 @@ export function collectPendingBuiltinAgents(input: { } if (agentName === "librarian") { - config = applyEnvironmentContext(config, directory) + config = applyEnvironmentContext(config, directory, { disableOmoEnv }) } config = applyOverrides(config, override, mergedCategories, directory) diff --git a/src/agents/builtin-agents/hephaestus-agent.ts b/src/agents/builtin-agents/hephaestus-agent.ts index dc1ac25d..a4f0a801 100644 --- a/src/agents/builtin-agents/hephaestus-agent.ts +++ b/src/agents/builtin-agents/hephaestus-agent.ts @@ -4,7 +4,7 @@ import type { CategoryConfig } from "../../config/schema" import type { AvailableAgent, AvailableCategory, AvailableSkill } from "../dynamic-agent-prompt-builder" import { AGENT_MODEL_REQUIREMENTS, isAnyProviderConnected } from "../../shared" import { createHephaestusAgent } from "../hephaestus" -import { createEnvContext } from "../env-context" +import { applyEnvironmentContext } from "./environment-context" import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" @@ -20,6 +20,7 @@ export function maybeCreateHephaestusConfig(input: { mergedCategories: Record directory?: string useTaskSystem: boolean + disableOmoEnv?: boolean }): AgentConfig | undefined { const { disabledAgents, @@ -33,6 +34,7 @@ export function maybeCreateHephaestusConfig(input: { mergedCategories, directory, useTaskSystem, + disableOmoEnv = false, } = input if (disabledAgents.includes("hephaestus")) return undefined @@ -79,10 +81,7 @@ export function maybeCreateHephaestusConfig(input: { hephaestusConfig = applyCategoryOverride(hephaestusConfig, hepOverrideCategory, mergedCategories) } - if (directory && hephaestusConfig.prompt) { - const envContext = createEnvContext() - hephaestusConfig = { ...hephaestusConfig, prompt: hephaestusConfig.prompt + envContext } - } + hephaestusConfig = applyEnvironmentContext(hephaestusConfig, directory, { disableOmoEnv }) if (hephaestusOverride) { hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride, directory) diff --git a/src/agents/builtin-agents/sisyphus-agent.ts b/src/agents/builtin-agents/sisyphus-agent.ts index 11b313e2..a28879b7 100644 --- a/src/agents/builtin-agents/sisyphus-agent.ts +++ b/src/agents/builtin-agents/sisyphus-agent.ts @@ -22,6 +22,7 @@ export function maybeCreateSisyphusConfig(input: { directory?: string userCategories?: CategoriesConfig useTaskSystem: boolean + disableOmoEnv?: boolean }): AgentConfig | undefined { const { disabledAgents, @@ -36,6 +37,7 @@ export function maybeCreateSisyphusConfig(input: { mergedCategories, directory, useTaskSystem, + disableOmoEnv = false, } = input const sisyphusOverride = agentOverrides["sisyphus"] @@ -78,7 +80,9 @@ export function maybeCreateSisyphusConfig(input: { } sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory) - sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory) + sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory, { + disableOmoEnv, + }) return sisyphusConfig } diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index b64bffce..b179a47a 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -662,6 +662,178 @@ describe("createBuiltinAgents with requiresProvider gating (hephaestus)", () => }) }) +describe("Hephaestus environment context toggle", () => { + let fetchSpy: ReturnType + + beforeEach(() => { + fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["openai/gpt-5.3-codex"]) + ) + }) + + afterEach(() => { + fetchSpy.mockRestore() + }) + + async function buildAgents(disableFlag?: boolean) { + return createBuiltinAgents( + [], + {}, + "/tmp/work", + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + undefined, + disableFlag + ) + } + + test("includes tag when disable flag is unset", async () => { + // #when + const agents = await buildAgents(undefined) + + // #then + expect(agents.hephaestus).toBeDefined() + expect(agents.hephaestus.prompt).toContain("") + }) + + test("includes tag when disable flag is false", async () => { + // #when + const agents = await buildAgents(false) + + // #then + expect(agents.hephaestus).toBeDefined() + expect(agents.hephaestus.prompt).toContain("") + }) + + test("omits tag when disable flag is true", async () => { + // #when + const agents = await buildAgents(true) + + // #then + expect(agents.hephaestus).toBeDefined() + expect(agents.hephaestus.prompt).not.toContain("") + }) +}) + +describe("Sisyphus and Librarian environment context toggle", () => { + let fetchSpy: ReturnType + + beforeEach(() => { + fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "google/gemini-3-flash"]) + ) + }) + + afterEach(() => { + fetchSpy.mockRestore() + }) + + async function buildAgents(disableFlag?: boolean) { + return createBuiltinAgents( + [], + {}, + "/tmp/work", + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + undefined, + disableFlag + ) + } + + test("includes for sisyphus and librarian when disable flag is unset", async () => { + const agents = await buildAgents(undefined) + + expect(agents.sisyphus).toBeDefined() + expect(agents.librarian).toBeDefined() + expect(agents.sisyphus.prompt).toContain("") + expect(agents.librarian.prompt).toContain("") + }) + + test("includes for sisyphus and librarian when disable flag is false", async () => { + const agents = await buildAgents(false) + + expect(agents.sisyphus).toBeDefined() + expect(agents.librarian).toBeDefined() + expect(agents.sisyphus.prompt).toContain("") + expect(agents.librarian.prompt).toContain("") + }) + + test("omits for sisyphus and librarian when disable flag is true", async () => { + const agents = await buildAgents(true) + + expect(agents.sisyphus).toBeDefined() + expect(agents.librarian).toBeDefined() + expect(agents.sisyphus.prompt).not.toContain("") + expect(agents.librarian.prompt).not.toContain("") + }) +}) + +describe("Atlas is unaffected by environment context toggle", () => { + let fetchSpy: ReturnType + + beforeEach(() => { + fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"]) + ) + }) + + afterEach(() => { + fetchSpy.mockRestore() + }) + + test("atlas prompt is unchanged and never contains ", async () => { + const agentsDefault = await createBuiltinAgents( + [], + {}, + "/tmp/work", + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + undefined, + false + ) + + const agentsDisabled = await createBuiltinAgents( + [], + {}, + "/tmp/work", + TEST_DEFAULT_MODEL, + undefined, + undefined, + [], + undefined, + undefined, + undefined, + undefined, + undefined, + true + ) + + expect(agentsDefault.atlas).toBeDefined() + expect(agentsDisabled.atlas).toBeDefined() + expect(agentsDefault.atlas.prompt).not.toContain("") + expect(agentsDisabled.atlas.prompt).not.toContain("") + expect(agentsDisabled.atlas.prompt).toBe(agentsDefault.atlas.prompt) + }) +}) + describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => { test("sisyphus is created when at least one fallback model is available", async () => { // #given diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 3543a45a..53d10c6d 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -741,6 +741,59 @@ describe("ExperimentalConfigSchema feature flags", () => { } }) + test("accepts disable_omo_env as true", () => { + //#given + const config = { disable_omo_env: true } + + //#when + const result = ExperimentalConfigSchema.safeParse(config) + + //#then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.disable_omo_env).toBe(true) + } + }) + + test("accepts disable_omo_env as false", () => { + //#given + const config = { disable_omo_env: false } + + //#when + const result = ExperimentalConfigSchema.safeParse(config) + + //#then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.disable_omo_env).toBe(false) + } + }) + + test("disable_omo_env is optional", () => { + //#given + const config = { safe_hook_creation: true } + + //#when + const result = ExperimentalConfigSchema.safeParse(config) + + //#then + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.disable_omo_env).toBeUndefined() + } + }) + + test("rejects non-boolean disable_omo_env", () => { + //#given + const config = { disable_omo_env: "true" } + + //#when + const result = ExperimentalConfigSchema.safeParse(config) + + //#then + expect(result.success).toBe(false) + }) + test("rejects non-boolean hashline_edit", () => { //#given const config = { hashline_edit: "true" } diff --git a/src/config/schema/experimental.ts b/src/config/schema/experimental.ts index 927a13a2..4fbcdf18 100644 --- a/src/config/schema/experimental.ts +++ b/src/config/schema/experimental.ts @@ -15,6 +15,8 @@ export const ExperimentalConfigSchema = z.object({ plugin_load_timeout_ms: z.number().min(1000).optional(), /** Wrap hook creation in try/catch to prevent one failing hook from crashing the plugin (default: true at call site) */ safe_hook_creation: z.boolean().optional(), + /** Disable auto-injected context in prompts (experimental) */ + disable_omo_env: z.boolean().optional(), /** Enable hashline_edit tool for improved file editing with hash-based line anchors */ hashline_edit: z.boolean().optional(), }) diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index ca2e8d2c..067ee83f 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -76,6 +76,7 @@ export async function applyAgentConfig(params: { const currentModel = params.config.model as string | undefined; const disabledSkills = new Set(params.pluginConfig.disabled_skills ?? []); const useTaskSystem = params.pluginConfig.experimental?.task_system ?? false; + const disableOmoEnv = params.pluginConfig.experimental?.disable_omo_env ?? false; const builtinAgents = await createBuiltinAgents( migratedDisabledAgents, @@ -90,6 +91,7 @@ export async function applyAgentConfig(params: { currentModel, disabledSkills, useTaskSystem, + disableOmoEnv, ); const includeClaudeAgents = params.pluginConfig.claude_code?.agents ?? true; diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index ebba28ea..a3a81f92 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -1275,3 +1275,69 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => { expect(agentResult[getAgentDisplayName("sisyphus")]?.permission?.todoread).toBeUndefined() }) }) + +describe("disable_omo_env pass-through", () => { + test("omits in generated sisyphus prompt when disable_omo_env is true", async () => { + //#given + ;(agents.createBuiltinAgents as any)?.mockRestore?.() + ;(shared.fetchAvailableModels as any).mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "google/gemini-3-flash"]) + ) + + const pluginConfig: OhMyOpenCodeConfig = { + experimental: { disable_omo_env: true }, + } + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then + const agentResult = config.agent as Record + const sisyphusPrompt = agentResult[getAgentDisplayName("sisyphus")]?.prompt + expect(sisyphusPrompt).toBeDefined() + expect(sisyphusPrompt).not.toContain("") + }) + + test("keeps in generated sisyphus prompt when disable_omo_env is omitted", async () => { + //#given + ;(agents.createBuiltinAgents as any)?.mockRestore?.() + ;(shared.fetchAvailableModels as any).mockResolvedValue( + new Set(["anthropic/claude-opus-4-6", "google/gemini-3-flash"]) + ) + + const pluginConfig: OhMyOpenCodeConfig = {} + const config: Record = { + model: "anthropic/claude-opus-4-6", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + //#when + await handler(config) + + //#then + const agentResult = config.agent as Record + const sisyphusPrompt = agentResult[getAgentDisplayName("sisyphus")]?.prompt + expect(sisyphusPrompt).toBeDefined() + expect(sisyphusPrompt).toContain("") + }) +})