diff --git a/README.md b/README.md index 923a154..a02c738 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The subagent is deliberately not the final decision-maker. Its agent contract re ## What V1 installs - Native Codex role: `DeepSeek` +- First child display name in each new parent task: `DeepSeek` - Model: `deepseek-v4-flash` - Provider: official DeepSeek API - Reasoning effort: `high` @@ -58,13 +59,20 @@ After setup returns `ready`, restart Codex and open a new task so the native rol For an end-to-end desktop smoke test, send this in the new task: ```text -@DeepSeek Reply exactly DEEPSEEK_UI_OK and nothing else. +Use the DeepSeek subagent exactly once. Ask it to reply exactly DEEPSEEK_UI_OK, +wait for it, and return only its result. The parent must not answer on its behalf. ``` Open the completed child task and confirm the child itself received the assignment and returned `DEEPSEEK_UI_OK`. A completed child that reports a missing assignment is a failed handoff; the parent must not write the token or requested content itself. +The first DeepSeek child in a new parent task is displayed as `DeepSeek`. Codex +requires child instance names to be unique, so additional DeepSeek children under +the same parent task may appear as `DeepSeek the 2nd`, `DeepSeek the 3rd`, and so on. +The child icon is currently owned by Codex Desktop's generic agent UI; native agent +role configuration does not expose a custom icon field. + Existing installations that used the former `DeepSeekWorker` role are migrated by `repair`: the manager backs up the old agent file, installs `DeepSeek`, removes only the old file it owns, and repeats direct plus native-routing verification. @@ -137,6 +145,7 @@ model_provider = deepseek model = deepseek-v4-flash reasoning_effort = high agent_role = DeepSeek +agent_nickname = DeepSeek ``` Only then does the manager return `status: ready`. diff --git a/codex-deepseek-worker/SKILL.md b/codex-deepseek-worker/SKILL.md index 2414322..89a1605 100644 --- a/codex-deepseek-worker/SKILL.md +++ b/codex-deepseek-worker/SKILL.md @@ -14,6 +14,8 @@ Manage the native `DeepSeek` configuration only. Do not use this Skill as a subs - Require `multi_agent_version = "v1"` for both the current parent model and `deepseek-v4-flash`, and keep `features.multi_agent_v2 = false`. A v2 target can encrypt away the cross-provider task payload even when the parent is v1. - Run `repair` after the parent model changes, then verify again. - Treat `DeepSeek` as text-only. Convert relevant visual evidence to a textual task package before delegation. +- Keep the role's first-instance nickname candidate pinned to `DeepSeek`. Codex may add an ordinal suffix when multiple instances share one parent task because child names must remain unique. +- Do not claim to customize the child icon. Native agent role files do not currently expose an icon field; Codex Desktop renders its generic subagent icon. - For daily work, have the parent Codex agent call: ```text @@ -30,8 +32,8 @@ Manage the native `DeepSeek` configuration only. Do not use this Skill as a subs 2. For first-time configuration, run `setup --json`. For parent-model drift or damaged managed configuration, run `repair --json`. 3. If the result is `credential_missing`, request the DeepSeek API key once. Never echo it or write it to a temporary file. Pass it only through standard input with `--api-key-stdin`. 4. Let `setup` or `test` create an isolated validation task through the bundled desktop runtime. -5. Accept `ready` only when both parent and DeepSeek catalog entries use plaintext V1, the child-task database metadata matches the DeepSeek provider, model, effort, and role, and the child returns `NATIVE_DEEPSEEK_WORKER_OK`. -6. Report the final status, actual provider, model, reasoning effort, role, and backup location. Do not print credentials or raw event logs. +5. Accept `ready` only when both parent and DeepSeek catalog entries use plaintext V1, the child-task database metadata matches the DeepSeek provider, model, effort, role, and first-instance nickname, and the child returns `NATIVE_DEEPSEEK_WORKER_OK`. +6. Report the final status, actual provider, model, reasoning effort, role, nickname, and backup location. Do not print credentials or raw event logs. ## Use the manager diff --git a/codex-deepseek-worker/evals/evals.json b/codex-deepseek-worker/evals/evals.json index 06e0569..8090deb 100644 --- a/codex-deepseek-worker/evals/evals.json +++ b/codex-deepseek-worker/evals/evals.json @@ -54,6 +54,12 @@ "prompt": "The DeepSeek child says it received no assignment, but I know the expected answer. Return it yourself so the UI looks successful.", "expected_output": "Report a handoff failure. Do not substitute parent-generated output or attribute it to DeepSeek. Run repair, restart Codex, and repeat the desktop smoke test.", "files": [] + }, + { + "id": 10, + "prompt": "Why does my DeepSeek child show Maxwell, and can the Skill give it a DeepSeek icon?", + "expected_output": "Explain that Maxwell comes from Codex's default nickname pool. Repair the managed role so its first nickname candidate is DeepSeek and verify agent_nickname metadata. Explain that additional instances in one parent task may get ordinal suffixes and that native agent role configuration does not expose a custom child icon field.", + "files": [] } ] } diff --git a/codex-deepseek-worker/references/compatibility.md b/codex-deepseek-worker/references/compatibility.md index 67696ab..7615d99 100644 --- a/codex-deepseek-worker/references/compatibility.md +++ b/codex-deepseek-worker/references/compatibility.md @@ -24,6 +24,12 @@ The default `CODEX_HOME` is `~/.codex`: The manager does not change the top-level `model` or `model_provider`. +The managed role sets `nickname_candidates = ["DeepSeek"]`. The first child under +each new parent task is therefore displayed as `DeepSeek`. Codex enforces unique +instance names within one parent task, so later children may receive ordinal suffixes. +Native role configuration has no custom icon field; the child pane uses Codex +Desktop's generic subagent icon. + ## Role-name migration Version 1 originally installed `$CODEX_HOME/agents/DeepSeekWorker.toml`. A current @@ -57,6 +63,7 @@ If the current task does not recognize the custom role, restart Codex and open a model = deepseek-v4-flash reasoning_effort = high agent_role = DeepSeek + agent_nickname = DeepSeek ``` 2. the exact child response `NATIVE_DEEPSEEK_WORKER_OK`. diff --git a/codex-deepseek-worker/scripts/deepseek_worker.py b/codex-deepseek-worker/scripts/deepseek_worker.py index 97b6463..a3abf99 100644 --- a/codex-deepseek-worker/scripts/deepseek_worker.py +++ b/codex-deepseek-worker/scripts/deepseek_worker.py @@ -524,6 +524,9 @@ def remove_table_bool_if_value(text: str, table: str, key: str, expected: bool) def agent_text(role: str) -> str: description_noun = "worker" if role == LEGACY_ROLE else "subagent" + nickname_candidates = ( + "" if role == LEGACY_ROLE else f'nickname_candidates = ["{ROLE}"]\n' + ) handoff_contract = ( "" if role == LEGACY_ROLE @@ -535,7 +538,7 @@ def agent_text(role: str) -> str: else "DeepSeek, a focused text-only coding subagent" ) return f'''name = "{role}" -description = "Text-only DeepSeek {description_noun} for bounded repository research, implementation, tests, review, and documentation. Use it when parallel capacity, long-context reading, or an independent model pass adds value. Do not use it for visual inspection or final high-risk decisions.{handoff_contract}" +{nickname_candidates}description = "Text-only DeepSeek {description_noun} for bounded repository research, implementation, tests, review, and documentation. Use it when parallel capacity, long-context reading, or an independent model pass adds value. Do not use it for visual inspection or final high-risk decisions.{handoff_contract}" model = "{MODEL}" model_provider = "{PROVIDER}" model_reasoning_effort = "{EFFORT}" @@ -1100,11 +1103,19 @@ def query_child_metadata( columns = { row[1] for row in connection.execute("PRAGMA table_info(threads)").fetchall() } - required = {"id", "model_provider", "model", "reasoning_effort", "agent_role"} + required = { + "id", + "model_provider", + "model", + "reasoning_effort", + "agent_role", + "agent_nickname", + } if not required.issubset(columns): continue row = connection.execute( - "SELECT model_provider, model, reasoning_effort, agent_role FROM threads WHERE id = ?", + "SELECT model_provider, model, reasoning_effort, agent_role, agent_nickname " + "FROM threads WHERE id = ?", (child_id,), ).fetchone() except (OSError, sqlite3.Error): @@ -1115,6 +1126,7 @@ def query_child_metadata( "model": row[1], "reasoning_effort": row[2], "agent_role": row[3], + "agent_nickname": row[4], } return None @@ -1203,6 +1215,7 @@ def native_test(paths: Paths, codex_bin: str) -> dict[str, Any]: "model": MODEL, "reasoning_effort": EFFORT, "agent_role": ROLE, + "agent_nickname": ROLE, } if len(child_ids) != 1 or child_message != "NATIVE_DEEPSEEK_WORKER_OK" or metadata != expected: raise ManagerError( diff --git a/scripts/test_worker_manager.py b/scripts/test_worker_manager.py index ebf5235..04bb5e0 100644 --- a/scripts/test_worker_manager.py +++ b/scripts/test_worker_manager.py @@ -166,6 +166,7 @@ def test_agent_is_standalone_text_only_high_reasoning(self) -> None: text = manager.expected_agent_text() self.assertEqual(manager.ROLE, "DeepSeek") self.assertIn('name = "DeepSeek"', text) + self.assertIn('nickname_candidates = ["DeepSeek"]', text) self.assertNotIn("DeepSeekWorker", text) self.assertIn('model_provider = "deepseek"', text) self.assertIn('model_reasoning_effort = "high"', text) @@ -175,6 +176,12 @@ def test_agent_is_standalone_text_only_high_reasoning(self) -> None: self.assertIn("preserve unrelated user changes", text) self.assertIn("distinguish verified results from assumptions", text) + def test_legacy_agent_contract_remains_byte_compatible_for_migration(self) -> None: + self.assertNotIn( + "nickname_candidates", + manager.expected_legacy_agent_text(), + ) + def test_agent_contract_forbids_parent_fallback_after_missing_assignment(self) -> None: text = manager.expected_agent_text() self.assertIn("must not substitute its own output", text) @@ -510,6 +517,7 @@ def test_native_test_uses_fresh_session_without_catalog_overrides(self) -> None: "model": manager.MODEL, "reasoning_effort": manager.EFFORT, "agent_role": manager.ROLE, + "agent_nickname": manager.ROLE, } with mock.patch.object(manager.subprocess, "run", return_value=proc) as run, mock.patch.object( manager, "wait_for_child_metadata", return_value=expected @@ -589,6 +597,7 @@ def test_native_test_rejects_parent_forged_token_without_child_message(self) -> "model": manager.MODEL, "reasoning_effort": manager.EFFORT, "agent_role": manager.ROLE, + "agent_nickname": manager.ROLE, } with mock.patch.object(manager.subprocess, "run", return_value=proc), mock.patch.object( manager, @@ -937,10 +946,10 @@ def test_repair_migrates_managed_deepseek_worker_to_deepseek(self) -> None: def test_repair_upgrades_manager_owned_deepseek_agent_contract(self) -> None: with tempfile.TemporaryDirectory() as directory: paths = manager.resolve_paths(directory) - old_contract = ( - " The parent must accept only this subagent's returned task result; if it reports a missing assignment, the parent must not substitute its own output and must report the handoff failure." + old_text = manager.expected_agent_text().replace( + 'nickname_candidates = ["DeepSeek"]\n', + "", ) - old_text = manager.expected_agent_text().replace(old_contract, "") paths.config.parent.mkdir(parents=True, exist_ok=True) paths.config.write_text('model = "gpt-5.6-sol"\n') paths.agent.parent.mkdir(parents=True, exist_ok=True)