Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ All notable changes to this project will be documented in this file.

- Added experimental Codex collaboration profile gates (`runtime.collaborationProfile`, `runtime.orchestratorSubagents`) for plan/orchestrator parity.
- Collaboration features now auto-enable by default in `runtime.mode="codex"` and can be explicitly enabled/disabled in any mode.
- Added `runtime.collaborationToolProfile` (`opencode` | `codex`) to choose OpenCode tool translation guidance vs codex-style tool semantics in injected collaboration instructions.
- Added managed `orchestrator` agent template sync under `~/.config/opencode/agents`, with visibility auto-gated by runtime mode.
- Synced pinned upstream Codex orchestrator + plan templates into a local prompt cache (ETag/304-aware, TTL refreshed) and used the cached plan prompt to populate plan-mode collaboration instructions.
- Added configurable `runtime.promptCacheKeyStrategy` (`default` | `project`) for session-based or project-path-based prompt cache keying.
Expand Down
5 changes: 0 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,6 @@ Known-field type validation is applied on load. If a known field has an invalid
- Experimental: enables Codex-style subagent header hints for helper agents under collaboration profile mode.
- If omitted, inherits `runtime.collaborationProfile` effective value.
- Explicit `true`/`false` works in any mode.
- `runtime.collaborationToolProfile: "opencode" | "codex"`
- Controls tool-language guidance in injected collaboration instructions.
- `opencode` (default): translates Codex semantics to OpenCode tool names.
- `codex`: prefers Codex tool semantics and falls back to OpenCode equivalents.

### Model behavior

Expand Down Expand Up @@ -230,7 +226,6 @@ Advanced path:
- `OPENCODE_OPENAI_MULTI_HEADER_TRANSFORM_DEBUG`: `1|0|true|false`.
- `OPENCODE_OPENAI_MULTI_COLLABORATION_PROFILE`: `1|0|true|false`.
- `OPENCODE_OPENAI_MULTI_ORCHESTRATOR_SUBAGENTS`: `1|0|true|false`.
- `OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE`: `opencode|codex`.

### Debug/OAuth controls

Expand Down
3 changes: 0 additions & 3 deletions docs/development/CONFIG_FIELDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Top-level:
- `runtime.pidOffset: boolean`
- `runtime.collaborationProfile: boolean`
- `runtime.orchestratorSubagents: boolean`
- `runtime.collaborationToolProfile: "opencode" | "codex"`
- `global.personality: string`
- `global.thinkingSummaries: boolean`
- `global.verbosityEnabled: boolean`
Expand Down Expand Up @@ -67,7 +66,6 @@ Default generated values:
- `runtime.pidOffset: false`
- `runtime.collaborationProfile`: mode-derived when unset (`true` in `codex`, `false` in `native`)
- `runtime.orchestratorSubagents`: inherits `runtime.collaborationProfile` effective value when unset
- `runtime.collaborationToolProfile: "opencode"`
- `global.personality: "pragmatic"`
- `global.verbosityEnabled: true`
- `global.verbosity: "default"`
Expand Down Expand Up @@ -115,7 +113,6 @@ Resolved by `resolveConfig`:
- `OPENCODE_OPENAI_MULTI_PROACTIVE_REFRESH_BUFFER_MS`
- `OPENCODE_OPENAI_MULTI_COLLABORATION_PROFILE`
- `OPENCODE_OPENAI_MULTI_ORCHESTRATOR_SUBAGENTS`
- `OPENCODE_OPENAI_MULTI_COLLABORATION_TOOL_PROFILE`

Resolved by auth/runtime code (`lib/codex-native.ts` + helper modules under `lib/codex-native/`):

Expand Down
2 changes: 0 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
getCompatInputSanitizerEnabled,
getCodexCompactionOverrideEnabled,
getBehaviorSettings,
getCollaborationToolProfile,
getCollaborationProfileEnabled,
getDebugEnabled,
getHeaderSnapshotBodiesEnabled,
Expand Down Expand Up @@ -135,7 +134,6 @@ export const OpenAIMultiAuthPlugin: Plugin = async (input) => {
headerTransformDebug: getHeaderTransformDebugEnabled(cfg),
collaborationProfileEnabled,
orchestratorSubagentsEnabled: getOrchestratorSubagentsEnabled(cfg),
collaborationToolProfile: getCollaborationToolProfile(cfg),
behaviorSettings: getBehaviorSettings(cfg)
})

Expand Down
7 changes: 1 addition & 6 deletions lib/codex-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
PluginRuntimeMode,
PromptCacheKeyStrategy
} from "./config"
import type { CollaborationToolProfile } from "./codex-native/collaboration"
import { formatToastMessage } from "./toast"
import type { CodexModelInfo } from "./model-catalog"
import { createRequestSnapshots } from "./request-snapshots"
Expand Down Expand Up @@ -169,7 +168,6 @@ export type CodexAuthPluginOptions = {
headerTransformDebug?: boolean
collaborationProfileEnabled?: boolean
orchestratorSubagentsEnabled?: boolean
collaborationToolProfile?: CollaborationToolProfile
}

export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginOptions = {}): Promise<Hooks> {
Expand All @@ -191,8 +189,6 @@ export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginO
typeof opts.orchestratorSubagentsEnabled === "boolean"
? opts.orchestratorSubagentsEnabled
: collaborationProfileEnabled
const collaborationToolProfile: CollaborationToolProfile =
opts.collaborationToolProfile === "codex" ? "codex" : "opencode"
void refreshCodexClientVersionFromGitHub(opts.log).catch((error) => {
if (error instanceof Error) {
// best-effort background refresh
Expand Down Expand Up @@ -379,8 +375,7 @@ export async function CodexAuthPlugin(input: PluginInput, opts: CodexAuthPluginO
fallbackPersonality: opts.personality,
spoofMode,
collaborationProfileEnabled,
orchestratorSubagentsEnabled,
collaborationToolProfile
orchestratorSubagentsEnabled
})
},
"chat.headers": async (hookInput, output) => {
Expand Down
11 changes: 9 additions & 2 deletions lib/codex-native/acquire-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { extractAccountId, refreshAccessToken, type OAuthTokenRefreshError } fro

const AUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000
const AUTH_REFRESH_LEASE_MS = 30_000
const LAST_USED_WRITE_INTERVAL_MS = 5_000

function isOAuthTokenRefreshError(value: unknown): value is OAuthTokenRefreshError {
return value instanceof Error && ("status" in value || "oauthCode" in value)
Expand Down Expand Up @@ -210,7 +211,10 @@ export async function acquireOpenAIAuth(input: AcquireOpenAIAuthInput): Promise<
plan = selected.plan

if (selected.access && selected.expires && selected.expires > now) {
selected.lastUsed = now
const previousLastUsed = typeof selected.lastUsed === "number" ? selected.lastUsed : undefined
if (previousLastUsed === undefined || now - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) {
selected.lastUsed = now
}
access = selected.access
accountId = selected.accountId
identityKey = selected.identityKey
Expand Down Expand Up @@ -283,7 +287,10 @@ export async function acquireOpenAIAuth(input: AcquireOpenAIAuthInput): Promise<
if (claims?.email) selected.email = normalizeEmail(claims.email)
if (claims?.plan) selected.plan = normalizePlan(claims.plan)
ensureIdentityKey(selected)
selected.lastUsed = now
const previousLastUsed = typeof selected.lastUsed === "number" ? selected.lastUsed : undefined
if (previousLastUsed === undefined || now - previousLastUsed >= LAST_USED_WRITE_INTERVAL_MS) {
selected.lastUsed = now
}
delete selected.refreshLeaseUntil
delete selected.cooldownUntil
if (selected.identityKey) domain.activeIdentityKey = selected.identityKey
Expand Down
41 changes: 21 additions & 20 deletions lib/codex-native/chat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ import {
sessionUsesOpenAIProvider
} from "./session-messages"
import {
CODEX_CODE_MODE_INSTRUCTIONS,
CODEX_ORCHESTRATOR_INSTRUCTIONS,
ensureOpenCodeToolingCompatibility,
getCodexPlanModeInstructions,
isOrchestratorInstructions,
mergeInstructions,
resolveCollaborationInstructions,
replaceCodexToolCallsForOpenCode,
resolveHookAgentName,
resolveCollaborationProfile,
resolveSubagentHeaderValue,
resolveToolingInstructions,
type CollaborationToolProfile
resolveSubagentHeaderValue
} from "./collaboration"

function normalizeVerbositySetting(value: unknown): "default" | "low" | "medium" | "high" | undefined {
Expand Down Expand Up @@ -83,7 +79,6 @@ export async function handleChatParamsHook(input: {
spoofMode: CodexSpoofMode
collaborationProfileEnabled: boolean
orchestratorSubagentsEnabled: boolean
collaborationToolProfile: CollaborationToolProfile
}): Promise<void> {
if (input.hookInput.model.providerID !== "openai") return
const modelOptions = isRecord(input.hookInput.model.options) ? input.hookInput.model.options : {}
Expand Down Expand Up @@ -159,25 +154,31 @@ export async function handleChatParamsHook(input: {
output: input.output
})

input.output.options.instructions = ensureOpenCodeToolingCompatibility(asString(input.output.options.instructions))
if (input.spoofMode !== "codex") return

const normalizedAgentName = resolveHookAgentName(input.hookInput.agent)?.trim().toLowerCase()
if (normalizedAgentName === "build") {
const current = asString(input.output.options.instructions)
const replaced = replaceCodexToolCallsForOpenCode(current)
if (replaced) {
input.output.options.instructions = replaced
}
return
}

if (!input.collaborationProfileEnabled) return

if (!profile.enabled || !profile.kind) return

const collaborationInstructions = resolveCollaborationInstructions(profile.kind, {
plan: getCodexPlanModeInstructions(),
code: CODEX_CODE_MODE_INSTRUCTIONS
})
let mergedInstructions = mergeInstructions(asString(input.output.options.instructions), collaborationInstructions)

if (profile.isOrchestrator && input.orchestratorSubagentsEnabled) {
mergedInstructions = mergeInstructions(mergedInstructions, CODEX_ORCHESTRATOR_INSTRUCTIONS)
if (profile.instructionPreset === "plan") {
const replacedPlan = replaceCodexToolCallsForOpenCode(getCodexPlanModeInstructions()) ?? getCodexPlanModeInstructions()
input.output.options.instructions = mergeInstructions(
asString(input.output.options.instructions),
replacedPlan
)
return
}

mergedInstructions = mergeInstructions(mergedInstructions, resolveToolingInstructions(input.collaborationToolProfile))

input.output.options.instructions = mergedInstructions
}

export async function handleChatHeadersHook(input: {
Expand Down
62 changes: 24 additions & 38 deletions lib/codex-native/collaboration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export type CodexCollaborationModeKind = "plan" | "code"
export type CollaborationToolProfile = "opencode" | "codex"

export type CodexCollaborationProfile = {
enabled: boolean
kind?: CodexCollaborationModeKind
normalizedAgentName?: string
isOrchestrator?: boolean
instructionPreset?: "plan"
}

export type CollaborationInstructionsByKind = {
Expand Down Expand Up @@ -142,7 +142,8 @@ export function getCodexPlanModeInstructions(): string {

export function setCodexPlanModeInstructions(next: string | undefined): void {
const trimmed = next?.trim()
codexPlanModeInstructions = trimmed ? trimmed : CODEX_PLAN_MODE_INSTRUCTIONS_FALLBACK
const source = trimmed ? trimmed : CODEX_PLAN_MODE_INSTRUCTIONS_FALLBACK
codexPlanModeInstructions = replaceCodexToolCallsForOpenCode(source) ?? source
}

export const CODEX_CODE_MODE_INSTRUCTIONS = "you are now in code mode."
Expand All @@ -155,31 +156,21 @@ When subagents are available, delegate independent work in parallel, coordinate

When subagents are active, your primary role is coordination and synthesis; avoid doing worker implementation in parallel with active workers unless needed for unblock/fallback.`

const OPENCODE_TOOLING_TRANSLATION_INSTRUCTIONS = `# Tooling Compatibility (OpenCode)

Translate Codex-style tool intent to OpenCode-native tools:

- exec_command -> bash
- read/search/list -> read, grep, glob
- apply_patch/edit_file -> apply_patch
- write_stdin -> task with existing task_id (or bash fallback)
- spawn_agent -> task (launch a subagent)
- send_input -> task with existing task_id (continue the same subagent)
- wait -> do not return final output until spawned task(s) complete; poll/resume via task tool as needed
- close_agent -> stop reusing task_id (no dedicated close tool in OpenCode)

Always use the available OpenCode tool names and schemas in this runtime.`

const CODEX_STYLE_TOOLING_INSTRUCTIONS = `# Tooling Compatibility (Codex-style)

Prefer Codex-style workflow semantics and naming when reasoning about steps. If an exact Codex tool is unavailable, fall back to the nearest OpenCode equivalent and continue.`

const OPENCODE_TOOLING_HEADER = "# Tooling Compatibility (OpenCode)"
const OPENCODE_TOOLING_HEADER_REGEX = /^\s{0,3}#{1,6}\s*tooling\s+compatibility\s*\(\s*opencode\s*\)\s*$/im

const CODEX_TOOL_NAME_REGEX =
/\b(exec_command|read_file|search_files|list_dir|write_stdin|spawn_agent|send_input|close_agent|edit_file|apply_patch)\b/i

const TOOL_CALL_REPLACEMENTS: Array<{ pattern: RegExp; replacement: string }> = [
{ pattern: /\bexec_command\b/gi, replacement: "bash" },
{ pattern: /\bread_file\b/gi, replacement: "read" },
{ pattern: /\bsearch_files\b/gi, replacement: "grep" },
{ pattern: /\blist_dir\b/gi, replacement: "glob" },
{ pattern: /\bwrite_stdin\b/gi, replacement: "task" },
{ pattern: /\bspawn_agent\b/gi, replacement: "task" },
{ pattern: /\bsend_input\b/gi, replacement: "task" },
{ pattern: /\bclose_agent\b/gi, replacement: "skip_task_reuse" },
{ pattern: /\bedit_file\b/gi, replacement: "apply_patch" }
]

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
Expand Down Expand Up @@ -237,7 +228,8 @@ export function resolveCollaborationProfile(agent: unknown): CodexCollaborationP
enabled: true,
normalizedAgentName,
kind: "plan",
isOrchestrator: false
isOrchestrator: false,
instructionPreset: "plan"
}
}

Expand Down Expand Up @@ -275,27 +267,21 @@ export function resolveCollaborationInstructions(
return instructions.code
}

export function resolveToolingInstructions(profile: CollaborationToolProfile): string {
return profile === "codex" ? CODEX_STYLE_TOOLING_INSTRUCTIONS : OPENCODE_TOOLING_TRANSLATION_INSTRUCTIONS
}

export function hasCodexToolNameMarkers(instructions: string | undefined): boolean {
if (!instructions) return false
return CODEX_TOOL_NAME_REGEX.test(instructions)
}

export function hasOpenCodeToolingCompatibility(instructions: string | undefined): boolean {
if (!instructions) return false
if (instructions.includes(OPENCODE_TOOLING_HEADER)) return true
return OPENCODE_TOOLING_HEADER_REGEX.test(instructions)
}

export function ensureOpenCodeToolingCompatibility(instructions: string | undefined): string | undefined {
export function replaceCodexToolCallsForOpenCode(instructions: string | undefined): string | undefined {
const normalized = instructions?.trim()
if (!normalized) return instructions
if (hasOpenCodeToolingCompatibility(normalized)) return instructions
if (!hasCodexToolNameMarkers(normalized)) return instructions
return mergeInstructions(normalized, resolveToolingInstructions("opencode"))

let out = normalized
for (const replacement of TOOL_CALL_REPLACEMENTS) {
out = out.replace(replacement.pattern, replacement.replacement)
}
return out
}

export function mergeInstructions(base: string | undefined, extra: string): string {
Expand Down
Loading