feat(agents): configurable LLM model + effort per-agent and per-project (ORCH-41) (#36)
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit was merged in pull request #36.
This commit is contained in:
@@ -15,6 +15,82 @@ from ..plane_sync import notify_stage_change as plane_notify_stage, add_comment
|
||||
|
||||
logger = logging.getLogger("orchestrator.launcher")
|
||||
|
||||
# ORCH-41: valid --effort values accepted by the Claude CLI. An effort that is
|
||||
# not in this set is treated as misconfiguration: logged and dropped (no flag),
|
||||
# never passed through to the CLI.
|
||||
VALID_EFFORTS = frozenset({"low", "medium", "high", "xhigh", "max"})
|
||||
|
||||
|
||||
def _resolve_agent_attr(agent, project_id, project_map_attr, env_attr_prefix,
|
||||
default_attr):
|
||||
"""ORCH-41 shared resolver with priority:
|
||||
1. ProjectConfig.<project_map_attr>[agent] (per-project override)
|
||||
2. settings.<env_attr_prefix><agent> (per-agent env, if non-empty)
|
||||
3. settings.<default_attr> (global default)
|
||||
4. "" (no flag -> CLI default)
|
||||
|
||||
project_id is the Plane project uuid. It is resolved to a ProjectConfig via
|
||||
the registry; an unknown / empty id simply skips level 1. A missing per-agent
|
||||
settings attribute (e.g. unknown agent name) skips level 2.
|
||||
"""
|
||||
# Level 1: per-project override.
|
||||
if project_id:
|
||||
from ..projects import get_project_by_plane_id
|
||||
proj = get_project_by_plane_id(project_id)
|
||||
if proj is not None:
|
||||
override = getattr(proj, project_map_attr, {}).get(agent)
|
||||
if override:
|
||||
return override
|
||||
|
||||
# Level 2: per-agent env (settings.<prefix><agent>), if defined & non-empty.
|
||||
per_agent = getattr(settings, f"{env_attr_prefix}{agent}", "")
|
||||
if per_agent:
|
||||
return per_agent
|
||||
|
||||
# Level 3: global default.
|
||||
default = getattr(settings, default_attr, "")
|
||||
if default:
|
||||
return default
|
||||
|
||||
# Level 4: nothing -> CLI default.
|
||||
return ""
|
||||
|
||||
|
||||
def resolve_agent_model(agent: str, project_id: str = None) -> str:
|
||||
"""ORCH-41: resolve the LLM model for an agent (optionally per-project).
|
||||
|
||||
Returns "" when no model is configured at any level -> caller omits --model
|
||||
and the CLI default applies. See _resolve_agent_attr for the priority order.
|
||||
"""
|
||||
return _resolve_agent_attr(
|
||||
agent, project_id,
|
||||
project_map_attr="agent_models",
|
||||
env_attr_prefix="agent_model_",
|
||||
default_attr="agent_model_default",
|
||||
)
|
||||
|
||||
|
||||
def resolve_agent_effort(agent: str, project_id: str = None) -> str:
|
||||
"""ORCH-41: resolve the --effort level for an agent (optionally per-project).
|
||||
|
||||
Same priority as resolve_agent_model. The resolved value is validated against
|
||||
VALID_EFFORTS; an invalid value is logged and dropped (returns "") so a typo
|
||||
in env/projects_json can never pass a bad flag to the CLI.
|
||||
"""
|
||||
value = _resolve_agent_attr(
|
||||
agent, project_id,
|
||||
project_map_attr="agent_efforts",
|
||||
env_attr_prefix="agent_effort_",
|
||||
default_attr="agent_effort_default",
|
||||
)
|
||||
if value and value not in VALID_EFFORTS:
|
||||
logger.warning(
|
||||
f"Invalid effort '{value}' for agent '{agent}' "
|
||||
f"(allowed: {sorted(VALID_EFFORTS)}); omitting --effort"
|
||||
)
|
||||
return ""
|
||||
return value
|
||||
|
||||
|
||||
def prune_run_logs(runs_dir, keep_days=30, keep_max=500, active_paths=None):
|
||||
"""L-2: best-effort rotation of per-run logs (<runs_dir>/*.log).
|
||||
@@ -85,7 +161,6 @@ class AgentLauncher:
|
||||
"system_prompt": ".openclaw/agents/architect.md",
|
||||
"task_file": ".task-arch.md",
|
||||
"allowed_tools": "Read,Write,Edit,Bash",
|
||||
"model": "opus",
|
||||
},
|
||||
"developer": {
|
||||
"system_prompt": ".openclaw/agents/developer.md",
|
||||
@@ -96,7 +171,6 @@ class AgentLauncher:
|
||||
"system_prompt": ".openclaw/agents/reviewer.md",
|
||||
"task_file": ".task-review.md",
|
||||
"allowed_tools": "Read,Write,Edit,Bash",
|
||||
"model": "opus",
|
||||
},
|
||||
"tester": {
|
||||
"system_prompt": ".openclaw/agents/tester.md",
|
||||
@@ -171,6 +245,12 @@ class AgentLauncher:
|
||||
_br_row = get_db().execute("SELECT branch FROM tasks WHERE id=?", (task_id,)).fetchone() if task_id else None
|
||||
agent_branch = _br_row[0] if _br_row else "main"
|
||||
|
||||
# ORCH-41: resolve the Plane project uuid for this repo so per-project
|
||||
# model/effort overrides apply. Unknown repo -> None (env/default only).
|
||||
from ..projects import get_project_by_repo
|
||||
_proj = get_project_by_repo(repo)
|
||||
project_id = _proj.plane_project_id if _proj else None
|
||||
|
||||
# Ensure the per-branch worktree exists and is on the right branch.
|
||||
work_path = ensure_worktree(repo, agent_branch)
|
||||
|
||||
@@ -204,8 +284,14 @@ class AgentLauncher:
|
||||
system_prompt = config["system_prompt"]
|
||||
allowed_tools = config["allowed_tools"]
|
||||
|
||||
model = config.get("model", "")
|
||||
# ORCH-41: model + effort + optional fallback are resolved from config
|
||||
# (project-override > per-agent env > default), not hardcoded in AGENT_CONFIGS.
|
||||
model = resolve_agent_model(agent, project_id)
|
||||
effort = resolve_agent_effort(agent, project_id)
|
||||
model_flag = f"--model {model} " if model else ""
|
||||
effort_flag = f"--effort {effort} " if effort else ""
|
||||
fb = settings.agent_fallback_model
|
||||
fb_flag = f"--fallback-model {fb} " if fb else ""
|
||||
|
||||
# No git fetch/checkout here: ensure_worktree() already put the worktree on
|
||||
# the right branch. The agent simply runs inside its isolated work_path.
|
||||
@@ -218,7 +304,7 @@ class AgentLauncher:
|
||||
f'cd {work_path} && '
|
||||
f'{self.CLAUDE_BIN} --print '
|
||||
f'--output-format json '
|
||||
f'{model_flag}'
|
||||
f'{model_flag}{effort_flag}{fb_flag}'
|
||||
f'"$(cat {task_file})" '
|
||||
f'--system-prompt "$(cat {system_prompt})" '
|
||||
f'--allowedTools {allowed_tools}'
|
||||
|
||||
Reference in New Issue
Block a user