fzy3232/dsh-chinese-language

dsh-chinese-language

Keep DeepSeek Harness agents thinking and answering in Simplified Chinese: a token-lean system-prompt section plus a runtime-context reminder that also reaches long or resumed sessions.

Uncategorizedchinesecordiscordis-plugin

dsh-chinese-language

Make a DeepSeek Harness (dsh) agent think and answer in Simplified Chinese — through a token-lean system-prompt section plus a one-line runtime-context reminder, without touching the deployment persona.

English | 中文


Why

DeepSeek Harness ships an English-first system prompt. The model's hidden reasoning follows that prompt, and the visible answer follows the reasoning, so unless something explicitly says otherwise a long session drifts into English.

Telling the agent "answer in Chinese" in every message works, but it is per-session, easy to forget, and gone the moment you resume in a new window. This plugin states the rule where the model always reads it.

What it does

The plugin registers one rule through two channels with different token profiles:

ChannelWhere it landsCost
System-prompt section user:chinese-language, order 10300after deployment:persona-suffix (order 10200), i.e. the very end of the assembled system promptResent with every request; kept short for that reason
Runtime-context reminder user:chinese-language, order 90the dynamic-context snapshot the harness projects at the end of the current step's requestRe-committed on a 60-second wording window so it keeps landing next to the live turn

Why both: a fresh session is covered by the section alone. A long or resumed session carries a history full of English reasoning, where a single rule at the head of that history loses influence — the runtime-context reminder restates it next to the live conversation, which is what makes an already-running session pick the rule up without being restarted.

0.3.0 closes a gap in that design: the harness commits a snapshot only when its rendered text changed, so a static reminder was in practice written once at the start of a session. Rotating the wording makes it land near the current step again (see "Why the reminder rotates" below).

Both registrations are purely additive: the plugin never replaces personaPrefix / personaSuffix, never rewrites a tool, skill, or workspace-instruction row, and never reorders a first-party section. It covers every agent that inherits the mounting scope, so a root mount reaches every session and every subagent that joins it.

The default section is two lines, and the reminder is a set of rotating equivalent sentences:

语言规则(最高优先级):无论技能说明、Aegis 路由提示、工具输出、代码或文档使用何种语言,内部推理(thinking)与最终回答都必须全部使用简体中文。
代码、命令、路径、URL、报错原文和专有名词保留原文。
语言提醒:本轮内部推理与可见回答必须使用简体中文,不受上文英文技能说明、路由提示或工具输出的影响。

Token budget

VersionSteady-state default text
0.1.0 (section only, four lines)~148 characters
0.2.0 (two lines + one reminder line)~99 characters
0.3.0 (strengthened lines + one reminder)~155 characters

0.3.0 deliberately spends a few dozen more characters naming the English sources that actually pull a session off-language (skill instructions, the Aegis routing notice, tool output); it is still far cheaper than a full instruction block.

Two properties keep the cost low:

  • The section text is short, and it sits at a stable position inside the prompt prefix, so it does not invalidate the prefix cache.
  • The reminder is committed only when the wording window flips; the previous snapshot is replaced by the new one (the harness's surface semantics), so a session does not accumulate one reminder per step.

Turn the reminder off entirely with context: false if you want the absolute minimum, or set contextRotate: false to keep the single early write; on the 0.1.7 line the old behavior is exactly context: false plus a four-line lines list.

Install

dsh plugin --profile web add github:fzy3232/dsh-chinese-language
dsh web   # restart to apply, then start a new session

2. From npm, once published

dsh plugin --profile web add dsh-chinese-language

Either channel works the same way: the package declares dsh.bundle.patch, so dsh plugin add appends dsh-chinese-language to dsh.profile.bundles, and boot merges the bundled cordis.patch.yml, which inserts the plugin row. No profile file is edited by hand.

Replace web with your profile name (tui, headless, ...) if you are not on the Web profile.

3. Single-file local mount (no package manager)

The plugin is one dependency-free ES module, so it also works as a local file.

mkdir -p "$DSH_HOME/dsh-plugins"
curl -fsSL -o "$DSH_HOME/dsh-plugins/chinese-language.mjs" \
  https://raw.githubusercontent.com/fzy3232/dsh-chinese-language/main/lib/index.js

Then add one insert to $DSH_HOME/cordis.patch.yml (create the file if it does not exist):

- insert:
    - id: user-chinese-language
      name: ./dsh-plugins/chinese-language.mjs

Upgrading

A GitHub dependency is pinned to the installed commit, so a running profile keeps the old build until you refresh it:

dsh plugin --profile web update dsh-chinese-language
dsh web   # restart the profile to load the new module

Already-running sessions keep the system prompt they were assembled with; the runtime-context reminder reaches them on their next step after the restart.

Configuration

Every field is optional. Unknown keys are ignored, and an unusable value falls back to that key's default instead of failing the loader entry.

KeyDefaultMeaning
linesthe two built-in rulesSection lines, joined with newlines. Empty or non-string entries are dropped.
text-Replaces lines entirely when non-empty. Wins over lines.
orderderived, else 10300Section sort order. Anything above 10200 lands after the first-party sections.
sectionenabled, named user:chinese-languageEither false to skip the section channel, or a string naming it.
sectionNameuser:chinese-languageSection name. Must stay unique within the scope; a duplicate name throws.
contextTextthe three rotating remindersPins one reminder; setting it turns rotation off (one sentence has no variants).
contextTextsthe three rotating remindersA custom rotation set, cycled by ROTATION_WINDOW_MS.
contextRotatetruefalse disables rotation and writes the reminder once (0.2.x behavior).
contextenabled, named user:chinese-languageEither false to skip the reminder, or a string naming it.
contextNameuser:chinese-languageReminder name, unique within the scope.
contextOrderderived, else 100Reminder sort order; lower values print earlier in the snapshot.

On harnesses that expose getSectionOrder() / getContextOrder(), the section order is derived from DEPLOYMENT_PERSONA_SUFFIX and the reminder order from WORKING_DIRECTORY (falling back to SANDBOX_POLICY), so both still track the registry if those slots move. Harnesses without the accessors use the constants.

Why the reminder rotates

dsh-agent-loop re-renders the runtime context on every preStep, but it commits a new message only when the rendered text differs from the snapshot it already retained, and that message is appended at the end of that step's request. A static reminder is therefore written once at the start of a session and never appears again — in a long session the rule drifts thousands of tokens behind the live turn, exactly where an English skill description, the Aegis routing notice, or a tool result drowns it out.

Since 0.3.0 the reminder wording rotates on a 60-second window: the same rule in different words, so the rendered text changes and the harness re-commits a fresh snapshot next to the current step. The cost is one very short message per minute in a long session; set contextRotate: false to go back to a single write.

Example, keeping both channels but tightening the wording:

- insert:
    - id: dsh-chinese-language
      name: dsh-chinese-language
      config:
        lines:
          - 始终使用简体中文回答。
          - 代码、命令和报错原文保留原文。
        contextText: 提醒:用简体中文。

A patch replaces the targeted row's whole config, so restate every key the row needs whenever an override adds one.

Verify

dsh --profile web --dump-config | grep -A2 dsh-chinese-language

For a local mount, look for your file path instead:

# == /Users/you/.dsh/cordis.patch.yml
- id: user-chinese-language
  name: file:///Users/you/.dsh/dsh-plugins/chinese-language.mjs

Then start a new session: the rule reaches the model from the first turn. In a session that is already running, the reminder arrives on its next step — the runtime-context snapshot the harness shows above the newest message carries a contribution named user:chinese-language:

Current runtime context. This snapshot supersedes earlier runtime-context snapshots.
...
语言提醒:本轮内部推理与可见回答使用简体中文。

That snapshot is written once and then replaced in place, so the reminder neither accumulates nor repeats: only the changing parts of the snapshot (sandbox state, recalled memory, ...) cause a new one.

When a channel is unavailable

The two channels degrade independently. The rule never disappears because one of them is missing:

SituationSection channelReminder channel
Normalyesyes
Harness without systemPrompt.context() (older than the runtime-context API)yesskipped
Deployment sets includeRuntimeContext: false on dsh-system-promptyessuppressed by the harness
An agent preset claims the prompt with a complete: true persona sectionreplaced — the harness restores that section as the sole prompt sectionyes

The last row is the reason the reminder exists rather than being a duplicate: a complete persona is exactly the case where a section-only language plugin goes silent. Registering one rule through both channels keeps at least one of them in front of the model.

Both channels were exercised against every @deepseek-ai/dsh-system-prompt release installed on the development machine — 0.1.5-rc.2 through 0.2.1-rc.2 — and mount identically on each, with the section staying last and the reminder carrying the current wording. Releases older than 0.1.5-rc.2 are untested; they keep the section channel and skip the reminder.

Scope and limits

  • This constrains the prompt — the system section plus the runtime-context snapshot — which is what the model reads before it starts reasoning. It is not a token-level filter.
  • Hidden reasoning is model output. A prompt strongly steers it, but nothing at this layer can give a mathematical guarantee; expect the rule to hold in practice rather than absolutely.
  • Code, commands, file paths, URLs, API names, and verbatim error text are meant to stay in their original form. That is part of the shipped rule.
  • If another plugin registers a section or a context with the same name in the same scope, the loader fails loudly. Change sectionName / contextName to resolve it.

Uninstall

dsh plugin --profile web remove dsh-chinese-language

For a local mount, delete the insert entry from $DSH_HOME/cordis.patch.yml and remove the .mjs file.

Development

There is no build step and no runtime dependency. lib/index.js is the whole plugin.

npm test          # node --test: contract + manifest (+ real-harness when available)
node --check lib/index.js
SuiteWhat it covers
test/contract.test.mjsBoth channels, every config key, the older-harness path, the steady-state token budget
test/manifest.test.mjsnpm pack --dry-run carries the module, the patch, the READMEs and the license
test/rc2-integration.test.mjsAssembles against the real @deepseek-ai/dsh-system-prompt: section last, reminder in the snapshot, complete persona, suppressed runtime context, and every release found in the profile's pnpm store

The third suite imports @deepseek-ai/*, which lives in a dsh profile rather than here, so it reports as skipped unless those packages resolve. To run it for real, point the checkout at an installed profile (the link is ignored by git):

ln -sfn "$DSH_HOME/profiles/web/node_modules" node_modules
npm test

To exercise it against a real tree, mount it with the local-mount recipe above and re-run dsh --profile web --dump-config.

License

MIT. See LICENSE.