Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ configs/**/*.local.yaml
.secrets/
.codex_azure*/

# Local MCP server config — references the machine's GITHUB_PAT, never commit
.mcp.json

# Internal docs (not for open-source release)
docs/ablation_plan.md
docs/ablation_paper_tables.md
Expand Down
1 change: 1 addition & 0 deletions configs/_base_/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ model:
copilot_chat_target_model: ""
copilot_chat_timeout: null # preserves COPILOT_CHAT_TIMEOUT or the built-in default
codex_trace_to_optimizer: true
claude_trace_to_optimizer: true
azure_openai_endpoint: "" # e.g. "https://your-resource.openai.azure.com/"
azure_openai_api_version: "2024-12-01-preview"
azure_openai_api_key: "" # Fill locally if you do not export AZURE_OPENAI_API_KEY
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.
| `model.minimax_*` | MiniMax `base_url`, `api_key`, shared `minimax_model`, `temperature`, `max_tokens`, and `enable_thinking`; `minimax_model` applies when MiniMax is the target |
| `model.codex_exec_*` | Codex path, sandbox, profile, SDK mode, reasoning, network/search, and approval policy; see compatibility notes below |
| `model.claude_code_exec_*` | Claude path, profile, SDK mode, effort, and thinking-token cap |
| `model.codex_trace_to_optimizer` | When `true` (default) and target is `codex_exec`, inject the agent's codex trace steps into the reflection prompt |
| `model.claude_trace_to_optimizer` | When `true` (default) and target is `claude_code_exec`, inject the agent's claude trace steps into the reflection prompt |
| `model.cursor_exec_path` | Cursor Agent executable path; default `cursor-agent` |
| `model.cursor_exec_sandbox` | Cursor sandbox mode: `enabled` (default) or `disabled`; file-edit rollouts require `enabled` |
| `model.copilot_exec_path` | GitHub Copilot CLI executable path; default `copilot` |
Expand Down
6 changes: 5 additions & 1 deletion scripts/eval_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,11 @@ def _set_role(key: str, value: str) -> None:
_set_role("optimizer_backend", "codex_exec")
_set_role("target_backend", "codex_exec")
elif backend == "claude_code_exec":
_set_role("optimizer_backend", "openai_chat")
# Both roles default to Claude Code so reflection sees the full
# trajectory. A role pinned to a non-default value (e.g. minimax_chat)
# still overrides; an explicit --optimizer_backend openai_chat does
# not, because openai_chat is one of the base-config defaults.
_set_role("optimizer_backend", "claude_code_exec")
_set_role("target_backend", "claude_code_exec")
elif backend == "cursor_exec":
_set_role("optimizer_backend", "openai_chat")
Expand Down
6 changes: 5 additions & 1 deletion scripts/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,11 @@ def _set_role(key: str, value: str) -> None:
_set_role("optimizer_backend", "codex_exec")
_set_role("target_backend", "codex_exec")
elif backend == "claude_code_exec":
_set_role("optimizer_backend", "openai_chat")
# Both roles default to Claude Code so reflection sees the full
# trajectory. A role pinned to a non-default value (e.g. minimax_chat)
# still overrides; an explicit --optimizer_backend openai_chat does
# not, because openai_chat is one of the base-config defaults.
_set_role("optimizer_backend", "claude_code_exec")
_set_role("target_backend", "claude_code_exec")
elif backend == "cursor_exec":
_set_role("optimizer_backend", "openai_chat")
Expand Down
1 change: 1 addition & 0 deletions skillopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"model.copilot_chat_target_model": "copilot_chat_target_model",
"model.copilot_chat_timeout": "copilot_chat_timeout",
"model.codex_trace_to_optimizer": "codex_trace_to_optimizer",
"model.claude_trace_to_optimizer": "claude_trace_to_optimizer",
"model.azure_endpoint": "azure_endpoint",
"model.azure_api_version": "azure_api_version",
"model.azure_api_key": "azure_api_key",
Expand Down
34 changes: 28 additions & 6 deletions skillopt/engine/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,27 @@ def _resolve_train_size(cfg: dict, dataloader) -> int:
_ROLE_BACKEND_DEFAULTS = (None, "", "openai_chat")


def _configure_trace_to_optimizer_gates(target_backend: str, cfg: dict) -> None:
"""Turn on trace-to-optimizer gates for the exec target's trace artifact.

Sets ``REFLACT_CODEX_TRACE_TO_OPTIMIZER`` (codex) and
``REFLACT_CLAUDE_TRACE_TO_OPTIMIZER`` (claude) to ``"1"`` only when the
target actually runs on that exec backend and the matching config knob is
on. ``skillopt.gradient.reflect.fmt_minibatch_trajectories`` reads these
env vars, so a non-exec target never pays the injection.
"""
os.environ["REFLACT_CODEX_TRACE_TO_OPTIMIZER"] = (
"1"
if target_backend == "codex_exec" and cfg.get("codex_trace_to_optimizer", False)
else "0"
)
os.environ["REFLACT_CLAUDE_TRACE_TO_OPTIMIZER"] = (
"1"
if target_backend == "claude_code_exec" and cfg.get("claude_trace_to_optimizer", False)
else "0"
)


def _resolve_role_backends(
backend: str, optimizer_backend: str | None, target_backend: str | None
) -> tuple[str, str]:
Expand All @@ -478,7 +499,12 @@ def _resolve_role_backends(
if target_backend in _ROLE_BACKEND_DEFAULTS:
target_backend = "codex_exec"
elif backend == "claude_code_exec":
optimizer_backend = optimizer_backend or "openai_chat"
# Both roles default to Claude Code so reflection sees the full
# trajectory. A role pinned to a non-default value (e.g. minimax_chat)
# still overrides; an explicit --optimizer_backend openai_chat does not,
# because openai_chat is one of the base-config defaults.
if optimizer_backend in _ROLE_BACKEND_DEFAULTS:
optimizer_backend = "claude_code_exec"
if target_backend in _ROLE_BACKEND_DEFAULTS:
target_backend = "claude_code_exec"
elif backend == "cursor_exec":
Expand Down Expand Up @@ -801,11 +827,7 @@ def _build_eval_env(split: str, env_num: int, seed: int):
minimax_model_cfg = cfg.get("minimax_model")
if minimax_model_cfg and cfg.get("target_backend") == "minimax_chat":
set_target_deployment(str(minimax_model_cfg))
os.environ["REFLACT_CODEX_TRACE_TO_OPTIMIZER"] = (
"1"
if target_backend == "codex_exec" and cfg.get("codex_trace_to_optimizer", False)
else "0"
)
_configure_trace_to_optimizer_gates(target_backend, cfg)
reasoning = cfg.get("reasoning_effort", "") or None
set_reasoning_effort(reasoning)
print(
Expand Down
13 changes: 13 additions & 0 deletions skillopt/gradient/reflect.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,19 @@ def fmt_minibatch_trajectories(
f"{codex_probe_trace_steps}\n"
)

# Claude Code exec backend (issue #233): the SDK's full session trace is
# persisted as claude_trace_steps.txt; surface it so the analyst sees the
# agent's actual tool activity instead of only the collapsed final answer.
# Gated like the codex summary above: only the trainer turns it on, and
# only when the target actually runs on claude_code_exec.
if os.environ.get("REFLACT_CLAUDE_TRACE_TO_OPTIMIZER", "0") == "1":
claude_steps_path = os.path.join(prediction_dir, tid, "claude_trace_steps.txt")
if os.path.exists(claude_steps_path):
with open(claude_steps_path, encoding="utf-8") as f:
claude_steps = f.read().strip()
if claude_steps:
header += f"\n#### Claude Trace Steps\n{claude_steps}\n"

preview = item.get("spreadsheet_preview", "")
if not preview:
preview_path = os.path.join(prediction_dir, tid, "spreadsheet_preview.txt")
Expand Down
44 changes: 43 additions & 1 deletion skillopt/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from skillopt.model import azure_openai as _openai
from skillopt.model import claude_backend as _claude
from skillopt.model import claude_code_backend as _claude_code
from skillopt.model import codex_backend as _codex
from skillopt.model import copilot_backend as _copilot
from skillopt.model import minimax_backend as _minimax
Expand Down Expand Up @@ -55,7 +56,11 @@ def set_backend(name: str | None) -> str:
set_target_backend("codex_exec")
return normalized
if normalized == "claude_code_exec":
set_optimizer_backend("openai_chat")
# Both roles default to Claude Code so reflection sees the full
# trajectory. A role pinned to a non-default value (e.g. minimax_chat)
# still overrides; an explicit --optimizer_backend openai_chat does not,
# because openai_chat is one of the base-config defaults.
set_optimizer_backend("claude_code_exec")
set_target_backend(normalized)
return normalized
if normalized == "cursor_exec":
Expand Down Expand Up @@ -181,6 +186,16 @@ def chat_optimizer(
stage=stage,
timeout=timeout,
)
if get_optimizer_backend() == "claude_code_exec":
return _claude_code.chat_optimizer(
system=system,
user=user,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
timeout=timeout,
reasoning_effort=reasoning_effort,
)
return _openai.chat_optimizer(
system=system,
user=user,
Expand Down Expand Up @@ -346,6 +361,18 @@ def chat_optimizer_messages(
return_message=return_message,
timeout=timeout,
)
if get_optimizer_backend() == "claude_code_exec":
return _claude_code.chat_optimizer_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
timeout=timeout,
reasoning_effort=reasoning_effort,
)
return _openai.chat_optimizer_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
Expand Down Expand Up @@ -509,6 +536,17 @@ def get_token_summary() -> dict:
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
summary[stage]["completion_tokens"] += values["completion_tokens"]
summary[stage]["total_tokens"] += values["total_tokens"]
claude_code_summary = _claude_code.get_token_summary()
for stage, values in claude_code_summary.items():
if stage == "_total":
continue
if stage not in summary:
summary[stage] = values
continue
summary[stage]["calls"] += values["calls"]
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
summary[stage]["completion_tokens"] += values["completion_tokens"]
summary[stage]["total_tokens"] += values["total_tokens"]
qwen_summary = _qwen.get_token_summary()
for stage, values in qwen_summary.items():
if stage == "_total":
Expand Down Expand Up @@ -584,6 +622,7 @@ def get_token_summary() -> dict:
def reset_token_tracker() -> None:
_openai.reset_token_tracker()
_claude.reset_token_tracker()
_claude_code.reset_token_tracker()
_qwen.reset_token_tracker()
_minimax.reset_token_tracker()
_openai_compat.reset_token_tracker()
Expand Down Expand Up @@ -736,6 +775,7 @@ def configure_openai_compatible(
def set_reasoning_effort(effort: str | None) -> None:
_openai.set_reasoning_effort(effort)
_claude.set_reasoning_effort(effort)
_claude_code.set_reasoning_effort(effort)
_qwen.set_reasoning_effort(effort)
_minimax.set_reasoning_effort(effort)
_openai_compat.set_reasoning_effort(effort)
Expand All @@ -745,6 +785,7 @@ def set_reasoning_effort(effort: str | None) -> None:
def set_target_deployment(deployment: str) -> None:
_openai.set_target_deployment(deployment)
_claude.set_target_deployment(deployment)
_claude_code.set_target_deployment(deployment)
_qwen.set_target_deployment(deployment)
_minimax.set_target_deployment(deployment)
_openai_compat.set_target_deployment(deployment)
Expand All @@ -754,6 +795,7 @@ def set_target_deployment(deployment: str) -> None:
def set_optimizer_deployment(deployment: str) -> None:
_openai.set_optimizer_deployment(deployment)
_claude.set_optimizer_deployment(deployment)
_claude_code.set_optimizer_deployment(deployment)
_qwen.set_optimizer_deployment(deployment)
_openai_compat.set_optimizer_deployment(deployment)
_codex.set_optimizer_deployment(deployment)
4 changes: 3 additions & 1 deletion skillopt/model/backend_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,12 @@ def set_optimizer_backend(backend: str) -> None:
"openai_compatible",
"copilot_chat",
"codex_exec",
"claude_code_exec",
}:
raise ValueError(
f"Unsupported optimizer backend: {OPTIMIZER_BACKEND!r}. "
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', 'minimax_chat', "
"'openai_compatible', 'copilot_chat', and 'codex_exec'."
"'openai_compatible', 'copilot_chat', 'codex_exec', and 'claude_code_exec'."
)
os.environ["OPTIMIZER_BACKEND"] = OPTIMIZER_BACKEND

Expand Down Expand Up @@ -176,6 +177,7 @@ def is_optimizer_chat_backend() -> bool:
"openai_compatible",
"copilot_chat",
"codex_exec",
"claude_code_exec",
}


Expand Down
Loading