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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
6 changes: 4 additions & 2 deletions codex-deepseek-worker/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions codex-deepseek-worker/evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
]
}
7 changes: 7 additions & 0 deletions codex-deepseek-worker/references/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down
19 changes: 16 additions & 3 deletions codex-deepseek-worker/scripts/deepseek_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand Down
15 changes: 12 additions & 3 deletions scripts/test_worker_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down