diff --git a/agentd/core.py b/agentd/core.py index 1c1164e..27a3fa8 100644 --- a/agentd/core.py +++ b/agentd/core.py @@ -491,6 +491,8 @@ class Agentd: r["model"] = label[6:] elif label.startswith("effort:"): r["effort"] = label[7:] + elif label.startswith("tier:"): + r["tier"] = label[5:] elif label.startswith("cap:"): caps.append(label[4:]) elif parse_budget(label) is not None: @@ -503,6 +505,10 @@ class Agentd: ecfg = self.cfg.executors[r["executor"]] r["model"] = r["model"] or (ecfg.get("models") or [None])[0] r["effort"] = r["effort"] or ecfg.get("default_effort") + if r["tier"] is None: # per-model default unless a label set one; `tier:none` = no tier argument + r["tier"] = (ecfg.get("tier_defaults") or {}).get(r["model"]) + if r["tier"] in ("none", ""): + r["tier"] = None return r def protocol(self, pcfg): diff --git a/config/agentd.example.yaml b/config/agentd.example.yaml index dac154c..2afbd06 100644 --- a/config/agentd.example.yaml +++ b/config/agentd.example.yaml @@ -45,6 +45,7 @@ executors: run: "codex exec --json -C '{repo}' -s workspace-write -m {model} -c 'model_reasoning_effort=\"{effort}\"' {tier} -o '{out}' -" resume: "codex exec resume {thread} --json -m {model} -c 'model_reasoning_effort=\"{effort}\"' {tier} -o '{out}' -" tier_arg: "-c 'service_tier=\"{tier}\"'" + tier_defaults: {gpt-6.1-sol: fast} # tier when no label sets one (`tier:`, `tier:none` = no tier) thread_from: events.jsonl:thread.started.thread_id last_message: '{out}' models: [gpt-6.1-sol, gpt-6-astra] diff --git a/config/agentd.yaml b/config/agentd.yaml index 2e7aa15..4ca7f1c 100644 --- a/config/agentd.yaml +++ b/config/agentd.yaml @@ -44,6 +44,7 @@ executors: # `exec resume` takes no -C/-s: the runner already cd's into the repo, the sandbox goes in as a -c override resume: "codex exec resume {thread} --json -m {model} -c 'model_reasoning_effort=\"{effort}\"' -c 'sandbox_mode=\"workspace-write\"' {tier} -o '{out}' -" tier_arg: "-c 'service_tier=\"{tier}\"'" + tier_defaults: {gpt-6.1-sol: fast} # tier when no label sets one (`tier:`, `tier:none` = no tier) thread_from: events.jsonl:thread.started.thread_id last_message: '{out}' models: [gpt-6.1-sol, gpt-6-astra] diff --git a/config/schema.json b/config/schema.json index ba0250f..5409259 100644 --- a/config/schema.json +++ b/config/schema.json @@ -76,6 +76,7 @@ "default_effort": {"type": "string"}, "permission_mode": {"type": "string"}, "tier_arg": {"type": "string"}, + "tier_defaults": {"type": "object", "additionalProperties": {"type": "string"}}, "script": {"type": "string"} } }, diff --git a/tests/fakes.py b/tests/fakes.py index 3e3a8c1..57b1a1e 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -9,7 +9,8 @@ from agentd.store import Store STATES = {"Backlog": "s-bl", "Todo": "s-todo", "In Progress": "s-run", "Waiting": "s-wait", "Review": "s-rev", "Done": "s-done"} MEMBERS = {"window10": "u-w10", "oregon": "u-or", "kappiee": "u-k"} -LABELS = {"model:astra": "l-astra", "needs-game": "l-game", "exec:claude": "l-claude", "budget:30m": "l-b30", "cap:gpu": "l-gpu"} +LABELS = {"model:astra": "l-astra", "needs-game": "l-game", "exec:claude": "l-claude", "budget:30m": "l-b30", "cap:gpu": "l-gpu", + "tier:none": "l-tnone", "tier:flex": "l-tflex"} def config_data(): diff --git a/tests/test_core.py b/tests/test_core.py index afd285d..e1d3873 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -362,5 +362,32 @@ class EnvFailureTest(unittest.TestCase): h.close() + +class TierDefaultTest(unittest.TestCase): + """executors..tier_defaults: tier per model unless a label (`tier:`, `tier:none`) sets one.""" + def run_with(self, labels): + data = config_data() + data["executors"]["codex"]["tier_defaults"] = {"sol": "fast"} + h = Harness(data) + try: + iid = h.plane.add_issue(1, labels=labels) + h.tick() + return h.execs[("win10", "codex")].started[0].tier, h.plane.texts(iid)[-1] + finally: + h.close() + + def test_default_model_gets_its_tier(self): + tier, text = self.run_with(()) + self.assertEqual(tier, "fast") + self.assertIn("codex / sol / medium / fast @ win10", text) + + def test_other_model_has_no_default(self): + self.assertIsNone(self.run_with(("model:astra",))[0]) + + def test_labels_override(self): + self.assertEqual(self.run_with(("tier:flex",))[0], "flex") + self.assertIsNone(self.run_with(("tier:none",))[0]) + + if __name__ == "__main__": unittest.main()