Kaynağa Gözat

Merge latest master prompt placement through V3 integration

Preserve system/message persistence while adopting named persona prefix/suffix and environment facts after reusable instructions. Resolve the Web assertion against the logged system message, not retired header.system. Keep the existing protected V3 integration PR as the root publication path; no rules are bypassed.
Tianyi Cui 5 gün önce
ebeveyn
işleme
57885bd73d
100 değiştirilmiş dosya ile 507 ekleme ve 262 silme
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
  7. 6 0
      .agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.i18n.yaml
  8. 37 0
      .agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.md
  9. 37 0
      .agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml
  11. 2 2
      .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
  12. 2 2
      .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md
  13. 1 1
      apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml
  14. 1 1
      apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts
  15. 1 1
      apps/cli/tests/profiles/headless/tests/compaction.e2e.ts
  16. 1 1
      apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts
  17. 4 4
      apps/cli/tests/profiles/headless/tests/harness.ts
  18. 2 2
      apps/cli/tests/profiles/headless/tests/ptc.e2e.ts
  19. 2 2
      apps/cli/tests/profiles/headless/tests/resume.e2e.ts
  20. 1 1
      apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts
  21. 1 1
      apps/cli/tests/web-agent-presets.e2e.ts
  22. 8 3
      apps/web/tests/replay-round-trip.e2e.ts
  23. 2 2
      docs/config-catalog.i18n.yaml
  24. 19 9
      docs/config-catalog.md
  25. 19 9
      docs/config-catalog.zh.md
  26. 2 2
      docs/subsystems/core.i18n.yaml
  27. 1 1
      docs/subsystems/core.md
  28. 1 1
      docs/subsystems/core.zh.md
  29. 2 2
      docs/subsystems/subagent.i18n.yaml
  30. 1 1
      docs/subsystems/subagent.md
  31. 1 1
      docs/subsystems/subagent.zh.md
  32. 2 2
      docs/subsystems/system-prompt.i18n.yaml
  33. 2 0
      docs/subsystems/system-prompt.md
  34. 2 0
      docs/subsystems/system-prompt.zh.md
  35. 1 1
      packages/acp/acp/tests/harness.ts
  36. 1 1
      packages/api/session-controller/tests/session-fork.host.spec.ts
  37. 1 1
      packages/api/session-controller/tests/session-models.host.spec.ts
  38. 2 2
      packages/boot/app-boot/README.i18n.yaml
  39. 1 1
      packages/boot/app-boot/README.md
  40. 1 1
      packages/boot/app-boot/README.zh.md
  41. 4 3
      packages/boot/app-boot/src/index.ts
  42. 9 4
      packages/boot/app-boot/tests/app-boot.spec.ts
  43. 2 2
      packages/bundle/acp-app/README.i18n.yaml
  44. 1 1
      packages/bundle/acp-app/README.md
  45. 1 1
      packages/bundle/acp-app/README.zh.md
  46. 3 2
      packages/bundle/acp-app/cordis.patch.yml
  47. 1 1
      packages/bundle/base/cordis.patch.yml
  48. 2 2
      packages/bundle/headless/README.i18n.yaml
  49. 1 1
      packages/bundle/headless/README.md
  50. 1 1
      packages/bundle/headless/README.zh.md
  51. 3 2
      packages/bundle/headless/cordis.patch.yml
  52. 2 2
      packages/bundle/sdk-app/README.i18n.yaml
  53. 1 1
      packages/bundle/sdk-app/README.md
  54. 1 1
      packages/bundle/sdk-app/README.zh.md
  55. 3 2
      packages/bundle/sdk-app/cordis.patch.yml
  56. 1 1
      packages/bundle/sdk-minimal/cordis.patch.yml
  57. 1 1
      packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
  58. 2 2
      packages/bundle/web-app/README.i18n.yaml
  59. 3 3
      packages/bundle/web-app/README.md
  60. 3 3
      packages/bundle/web-app/README.zh.md
  61. 3 2
      packages/bundle/web-app/cordis.patch.yml
  62. 4 4
      packages/bundle/web-app/tests/web-app.spec.ts
  63. 1 1
      packages/client/ui-deliverables/tests/prompt.client.spec.ts
  64. 1 1
      packages/context/agent-instructions/tests/agent-instructions.e2e.ts
  65. 1 1
      packages/context/file-reference-local/tests/service.spec.ts
  66. 2 2
      packages/core/agent-loop/tests/loop.spec.ts
  67. 1 1
      packages/core/agent-loop/tests/request-cache.e2e.ts
  68. 3 3
      packages/core/agent-loop/tests/request-reconstruction.spec.ts
  69. 7 7
      packages/core/agent-loop/tests/scope-lifecycle.spec.ts
  70. 4 4
      packages/core/agent-loop/tests/tool-calls.spec.ts
  71. 1 1
      packages/core/agent-loop/tests/tool-order.spec.ts
  72. 2 2
      packages/core/system-prompt/README.i18n.yaml
  73. 10 8
      packages/core/system-prompt/README.md
  74. 10 8
      packages/core/system-prompt/README.zh.md
  75. 29 13
      packages/core/system-prompt/src/index.ts
  76. 5 5
      packages/core/system-prompt/tests/scoped.spec.ts
  77. 68 16
      packages/core/system-prompt/tests/system-prompt.spec.ts
  78. 1 1
      packages/core/system-prompt/tests/tool-order.spec.ts
  79. 1 1
      packages/fs/tool-fs/tests/harness.ts
  80. 2 2
      packages/fs/tool-fs/tests/tools.spec.ts
  81. 1 1
      packages/plan/plan-mode/tests/projection.spec.ts
  82. 3 2
      packages/preset/agent-presets/presets/cordis/agent.cordis.yml
  83. 1 1
      packages/preset/agent-presets/presets/minimal/agent.cordis.yml
  84. 3 2
      packages/preset/agent-presets/presets/ptc/agent.cordis.yml
  85. 3 2
      packages/preset/agent-presets/presets/standard/agent.cordis.yml
  86. 2 2
      packages/preset/agent-presets/tests/composition-inventory.spec.ts
  87. 1 1
      packages/preset/agent-presets/tests/invariant.spec.ts
  88. 3 3
      packages/preset/agent-presets/tests/mount.spec.ts
  89. 1 1
      packages/preset/agent-presets/tests/remote.spec.ts
  90. 1 1
      packages/preset/agent-presets/tests/settings.spec.ts
  91. 2 2
      packages/preset/persona/README.i18n.yaml
  92. 13 12
      packages/preset/persona/README.md
  93. 12 11
      packages/preset/persona/README.zh.md
  94. 22 11
      packages/preset/persona/src/index.ts
  95. 52 15
      packages/preset/persona/tests/persona.spec.ts
  96. 4 3
      packages/shell/tool-bash/tests/tools.spec.ts
  97. 1 1
      packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/child.patch.yml
  98. 1 1
      packages/subagent/subagent-spawn-in-process/tests/harness.ts
  99. 2 2
      packages/subagent/subagent/src/child-agent.ts
  100. 1 1
      packages/subagent/subagent/src/types.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
-2026-07-05-prompt-variables-and-tool-guidance-ownership.md: f4364453c5ddded2fc0cb1d733732059feee6632
-2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: d8ee8ef906d02d5a96ae7db3fb705685618223ee
+2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 35bb7c6fabc85ae6f93bdbb67e13910eea627ca3
+2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 53d69cf45c02f6534334561b626d2c2ae6087c05

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md

@@ -32,7 +32,7 @@ Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, prov
 
 ### Persona as the order-0 section
 
-`dsh-system-prompt` owns `harness:identity` at first-party order `-1000` and the configured `deployment:persona` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and the routed request header therefore records the exact prompt later replayed by `ctx.tokenMeter` for compaction pressure. An agent-scoped `deployment:persona` shadows the global default and lets subagent providers install a persona before publication. The [`dsh-system-prompt` README](../../../../packages/core/system-prompt/README.md) owns the sparse named placements for identity, policy, tool guidance, generated protocol, and final-output obligations.
+`dsh-system-prompt` owns `harness:identity` at first-party order `-1000` and the configured `deployment:persona-prefix` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and the routed request header therefore records the exact prompt later replayed by `ctx.tokenMeter` for compaction pressure. An agent-scoped `deployment:persona-prefix` shadows the global default and lets subagent providers install a persona before publication. The [`dsh-system-prompt` README](../../../../packages/core/system-prompt/README.md) owns the sparse named placements for identity, policy, tool guidance, generated protocol, and final-output obligations.
 
 ### Tool guidance ownership
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md

@@ -32,7 +32,7 @@ Status: implemented
 
 ### Persona 作为 order-0 section
 
-`dsh-system-prompt` 拥有 first-party order 为 `-1000` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的提示词,稍后由 `ctx.tokenMeter` 为压缩(compaction)压力回放。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。[`dsh-system-prompt` README](../../../../packages/core/system-prompt/README.zh.md)规定身份、策略、工具指导、生成协议和最终输出义务的稀疏具名位置。
+`dsh-system-prompt` 拥有 first-party order 为 `-1000` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona-prefix`,因此两者在循环被替换时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的提示词,稍后由 `ctx.tokenMeter` 为压缩(compaction)压力回放。agent 作用域的 `deployment:persona-prefix` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。[`dsh-system-prompt` README](../../../../packages/core/system-prompt/README.zh.md)规定身份、策略、工具指导、生成协议和最终输出义务的稀疏具名位置。
 
 ### 工具指导归属
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
-2026-07-08-agent-scope-contexts.md: eb3f6f247bac1a1d81aa2644132c7b9cc04d602c
-2026-07-08-agent-scope-contexts.zh.md: a0f4ffb0ef80dd2fc1ee61c9ab3f4730c28c687e
+2026-07-08-agent-scope-contexts.md: 6a1fd4aed49cb8edef061c8fb6f0edcd0a09c30f
+2026-07-08-agent-scope-contexts.zh.md: 8408c4afff6075c129c6a96c47393c9c812b04b7

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md

@@ -66,7 +66,7 @@ const handle = await ctx.agents.create({
   agentOptions: { model: 'model-name' },
   setup(agentCtx) {
     agentCtx.systemPrompt.section({
-      name: 'deployment:persona',
+      name: 'deployment:persona-prefix',
       order: 0,
       text: 'Review code, but do not modify files.',
     })

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md

@@ -66,7 +66,7 @@ const handle = await ctx.agents.create({
   agentOptions: { model: 'model-name' },
   setup(agentCtx) {
     agentCtx.systemPrompt.section({
-      name: 'deployment:persona',
+      name: 'deployment:persona-prefix',
       order: 0,
       text: 'Review code, but do not modify files.',
     })

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.md
+2026-09-06-environment-prompt-suffix.md: 3438df0fec7b3296084c94b7db68d2800c6d6c98
+2026-09-06-environment-prompt-suffix.zh.md: 102d1196639ac7d7a754a24fc5a93bc93c1d05ba

+ 37 - 0
.agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.md

@@ -0,0 +1,37 @@
+# Agent Note: Environment facts follow reusable prompt instructions
+
+Status: implemented
+
+English | [中文](2026-09-06-environment-prompt-suffix.zh.md)
+
+## Problem
+
+The local Web URL, Harness checkout path, and session cwd differ across users and machines. Placing those facts before reusable tool instructions makes otherwise identical prompts diverge near their beginning, limiting the prefix available for same-model cache reuse. The model-name introduction identifies the agent and can remain early.
+
+## Decision
+
+The [system-prompt registry](../../../../packages/core/system-prompt/README.md) keeps the fixed Harness identity first and `DEPLOYMENT_PERSONA_PREFIX` at `0`. First-party reusable instructions through `STRUCTURED_OUTPUT` precede the environment suffix: `HARNESS_SOURCE` at `10000`, `WEB_SURFACE` at `10100`, and `DEPLOYMENT_PERSONA_SUFFIX` at `10200`.
+
+Global system-prompt config accepts `personaPrefix` and `personaSuffix`, both defaulting to empty. The [scoped persona row](../../../../packages/preset/persona/README.md) requires `prefix` and accepts `suffix`, defaulting to empty. They register `deployment:persona-prefix` and `deployment:persona-suffix` through the exported `PERSONA_PREFIX_SECTION` and `PERSONA_SUFFIX_SECTION` names. An omitted or empty scoped `suffix` shadows the global suffix away. The shipped Web, headless, SDK, and ACP bundles and standard, PTC, and Cordis presets keep the model introduction in the prefix and place only `Your working directory is {{cwd}}.` in the suffix. These names specify placement, not a classification of the text; no persona parsing or OS field is added.
+
+The [prompt-variables and tool-guidance ownership note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) retains its identity-first persona placement, single-owner rule, strict interpolation, and tool-guidance responsibilities.
+
+## Alternatives considered
+
+**Move the entire persona late.** That moves the model-name introduction away from the beginning without helping same-model reuse. Separating cwd preserves the introduction and reusable instructions together.
+
+**Move only the source path and Web URL.** Leaving cwd inside the early persona still breaks the reusable prefix across workspaces.
+
+**Infer environment fragments from persona text.** Parsing deployment-authored prose makes placement depend on wording. Explicit templates give shipped compositions and custom deployments direct control.
+
+**Move these facts into runtime-context messages.** That changes their message role and persistence placement rather than only separating system sections.
+
+## Consequences
+
+Byte-identical prefixes require the same model introduction, persona prefix, tools, configuration, and preceding section text. Arbitrary extension orders and assembly listeners remain authoritative; this is a first-party placement policy, not a universal stable-prefix guarantee. Provider cache sharing and hit-rate improvements are not measured or promised.
+
+Environment and Web/source guidance follow structured-output instructions. A `complete: true` persona uses only the rendered prefix and ignores the suffix, suppressing every other system section without disabling tool schemas or runtime context. Source and Web facts retain their distinction between the Harness checkout, session workspace, and current working directory.
+
+## Testing
+
+[Registry tests](../../../../packages/core/system-prompt/tests/system-prompt.spec.ts) compare reusable prefixes with the same model and changed checkout paths, URLs, and cwd values; they also cover strict interpolation and complete overrides. [Loop tests](../../../../packages/core/agent-loop/tests/loop.spec.ts) pin early model identity and session-cwd interpolation. [Persona tests](../../../../packages/preset/persona/tests/persona.spec.ts) cover scoped suffix replacement, empty shadowing, and complete personas. [Recorded prompt snapshots](../../../../docs/testing.md) cover emitted prompts in native-tool and generated-SDK compositions; they do not measure provider cache hits.

+ 37 - 0
.agents/notes/implemented/bug-fix/2026-09-06-environment-prompt-suffix.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: 环境事实位于可复用提示词指令之后
+
+Status: implemented
+
+[English](2026-09-06-environment-prompt-suffix.md) | 中文
+
+## 问题
+
+本地 Web URL、Harness checkout 路径和会话 cwd 因用户与机器而异。将这些事实放在可复用工具指令之前,会使其余内容相同的提示词在开头附近就出现差异,限制可供同模型缓存复用的前缀。模型名称介绍标识 agent(智能体),可以保留在靠前的位置。
+
+## 决策
+
+[系统提示词注册表](../../../../packages/core/system-prompt/README.zh.md)将固定 Harness 身份保留在最前,并将 `DEPLOYMENT_PERSONA_PREFIX` 保留在 `0`。截至 `STRUCTURED_OUTPUT` 的第一方可复用指令位于环境后缀之前:`HARNESS_SOURCE` 位于 `10000`,`WEB_SURFACE` 位于 `10100`,`DEPLOYMENT_PERSONA_SUFFIX` 位于 `10200`。
+
+全局 system-prompt 配置接受 `personaPrefix` 与 `personaSuffix`,两者均默认为空。[带作用域的 persona 行](../../../../packages/preset/persona/README.zh.md)要求提供 `prefix`,并接受默认为空的 `suffix`。它们通过导出的 `PERSONA_PREFIX_SECTION` 与 `PERSONA_SUFFIX_SECTION` 名称注册 `deployment:persona-prefix` 与 `deployment:persona-suffix`。省略或为空的作用域 `suffix` 会遮蔽掉全局后缀。交付的 Web、headless、SDK、ACP bundle 以及 standard、PTC、Cordis preset 将模型介绍保留在前缀中,仅将 `Your working directory is {{cwd}}.` 放入后缀。这些名称指定位置,而不对文本分类;不添加 persona 解析或 OS 字段。
+
+[提示词变量与工具指导归属记录](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)仍保留 identity-first 的 persona 位置、单一归属规则、严格插值和工具指导职责。
+
+## 曾考虑的替代方案
+
+**将整个 persona 后移。** 这会将模型名称介绍移离开头,却无助于同模型复用。分离 cwd 可以将介绍与可复用指令一起保留。
+
+**仅移动源码路径与 Web URL。** 若 cwd 仍位于靠前的 persona 内,不同工作区之间的可复用前缀仍会被打断。
+
+**从 persona 文本推断环境片段。** 解析部署方撰写的行文会使位置依赖措辞。显式模板让交付组合与自定义部署直接控制位置。
+
+**将这些事实移到 runtime-context 消息。** 这会改变其消息角色和持久化位置,而不只是分离系统段落。
+
+## 后果
+
+字节相同的前缀要求模型介绍、persona 前缀、工具、配置和前置段落文本一致。任意扩展顺序与组装监听器仍决定最终结果;这是一项第一方位置策略,而非通用稳定前缀保证。不测量或承诺提供方共享缓存及命中率提升。
+
+环境与 Web/源码指导位于结构化输出指令之后。`complete: true` persona 仅使用渲染后的前缀并忽略后缀,抑制其他所有系统段落,但不禁用工具 schema 或 runtime context。源码与 Web 事实保留 Harness checkout、会话工作区和当前工作目录之间的区分。
+
+## 测试
+
+[注册表测试](../../../../packages/core/system-prompt/tests/system-prompt.spec.ts)在模型相同、checkout 路径、URL 和 cwd 值变化时比较可复用前缀;同时覆盖严格插值与完整覆盖。[循环测试](../../../../packages/core/agent-loop/tests/loop.spec.ts)固定靠前的模型身份和会话 cwd 插值。[Persona 测试](../../../../packages/preset/persona/tests/persona.spec.ts)覆盖作用域后缀替换、空值遮蔽与完整 persona。[录制的提示词快照](../../../../docs/testing.zh.md)覆盖原生工具与生成 SDK 组合发出的提示词;它们不测量提供方缓存命中。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
-2026-07-12-subagent-persona-tool-filter-and-depth.md: 7ba9768df3679da6b07728cf64237c47d4c73b2f
-2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: d3a8240542d896a27e82b1be1b491c241003e27e
+2026-07-12-subagent-persona-tool-filter-and-depth.md: 511e340c81b811ebcaaea48946c377c99c53da54
+2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: 8a213f33a9b70a9bdec6f23b5bec4db5d94f110f

+ 2 - 2
.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md

@@ -18,7 +18,7 @@ The controls answer different questions:
 
 | Control | Question | Result |
 |---|---|---|
-| `persona` | What role instructions replace the deployment persona for this child? | A child-local prompt section shadows `deployment:persona` |
+| `persona` | What role instructions replace the deployment persona for this child? | A child-local prompt section shadows `deployment:persona-prefix` |
 | `toolFilter` | Which deployment-global tools enter this child's visible tool view? | A scoped restriction filters globals before child-local tools are added |
 | `maxDepth` | How deep may this delegation tree grow? | A start whose child depth exceeds the absolute cap is rejected |
 
@@ -26,7 +26,7 @@ The controls answer different questions:
 
 ### Persona is a scoped shadow
 
-The persona control changes one child without changing deployment-wide prompt assembly. During unpublished setup, an in-process provider registers a child-scoped section named `deployment:persona`; ordinary most-specific-wins resolution replaces the global section only in that child's assemblies.
+The persona control changes one child without changing deployment-wide prompt assembly. During unpublished setup, an in-process provider registers a child-scoped section named `deployment:persona-prefix`; ordinary most-specific-wins resolution replaces the global section only in that child's assemblies.
 
 The value has the same strict template semantics as the deployment persona. Omitting it inherits the deployment section through the global layer; an explicit empty string shadows the global persona with an empty section. Parent and sibling personas never enter the child's flat scope.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md

@@ -18,7 +18,7 @@ subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `ma
 
 | 控制 | 问题 | 结果 |
 |---|---|---|
-| `persona` | 什么角色指令替换该子 agent 的部署人设? | 一个子 agent 局部的提示词段落遮蔽 `deployment:persona` |
+| `persona` | 什么角色指令替换该子 agent 的部署人设? | 一个子 agent 局部的提示词段落遮蔽 `deployment:persona-prefix` |
 | `toolFilter` | 部署全局工具中哪些进入该子 agent 的可见工具视图? | 一个有作用域的限制在添加子 agent 局部工具之前过滤全局工具 |
 | `maxDepth` | 这棵委派树最深可以长到多少层? | 子 agent 深度超过绝对上限时,启动请求被拒绝 |
 
@@ -26,7 +26,7 @@ subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `ma
 
 ### 人设是有作用域的遮蔽
 
-人设控制改变一个子 agent 的行为,而不改变部署级的提示词组装。在未发布的设置阶段,进程内提供方在子 agent 作用域中注册一个名为 `deployment:persona` 的段落;普通的最具体者优先解析规则仅在该子 agent 的组装中替换全局段落。
+人设控制改变一个子 agent 的行为,而不改变部署级的提示词组装。在未发布的设置阶段,进程内提供方在子 agent 作用域中注册一个名为 `deployment:persona-prefix` 的段落;普通的最具体者优先解析规则仅在该子 agent 的组装中替换全局段落。
 
 其值与部署人设具有相同的严格模板语义。省略时通过全局层继承部署段落;显式空字符串则以空段落遮蔽全局人设。父级和兄弟级的人设永远不会进入子 agent 的扁平作用域。
 

+ 1 - 1
apps/cli/tests/profiles/acp/tests/fixtures/image-offload.cordis.yml

@@ -35,7 +35,7 @@
 - id: system-prompt
   name: '@deepseek-ai/dsh-system-prompt'
   config:
-    persona: |
+    personaPrefix: |
       You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
 
       Keep answers brief and factual.

+ 1 - 1
apps/cli/tests/profiles/headless/tests/coding-task.e2e.ts

@@ -54,7 +54,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('coding task: fix a failing test
     const before = spawnSync('node', ['add.test.js'], { cwd: workdir })
     expect(before.status).not.toBe(0)
 
-    ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT })
+    ctx = await codingHarness(workdir, { personaPrefix: SYSTEM_PROMPT })
     const agent = await ctx.agentLoop.create(SessionId('e2e-task'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
 
     agent.followup(createUserMessage({

+ 1 - 1
apps/cli/tests/profiles/headless/tests/compaction.e2e.ts

@@ -33,7 +33,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
 
     // Reasoning tokens require a larger generation cap than the retained checkpoint.
     ctx = await codingHarness(workdir, {
-      persona: SYSTEM_PROMPT,
+      personaPrefix: SYSTEM_PROMPT,
       modelContextWindow: 2000,
       compact: {
         thresholdRatio: 0.5,

+ 1 - 1
apps/cli/tests/profiles/headless/tests/full-loop.e2e.ts

@@ -28,7 +28,7 @@ afterEach(async () => {
 describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bash tool', () => {
   it('runs a bash command on request and reports its output', async () => {
     workdir = await mkdtemp(join(tmpdir(), 'dsh-full-loop-e2e-'))
-    ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT })
+    ctx = await codingHarness(workdir, { personaPrefix: SYSTEM_PROMPT })
     const agent = await ctx.agentLoop.create(SessionId('e2e-loop'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
 
     agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }], source: { kind: 'user' } }))

+ 4 - 4
apps/cli/tests/profiles/headless/tests/harness.ts

@@ -38,10 +38,10 @@ export const TODO_SYSTEM_PROMPT = 'You are a coding agent. For multi-step work,
 /** Options for {@link codingHarness}. */
 export interface CodingHarnessOptions {
   /**
-   * Deployment persona for the tree (the system-prompt plugin's `persona`
-   * config — per-context, not per-agent). Omitted ⇒ no persona section.
+   * Deployment persona prefix for the tree (the system-prompt plugin's `personaPrefix`
+   * config — per-context, not per-agent). Omitted ⇒ no persona prefix section.
    */
-  persona?: string
+  personaPrefix?: string
   /** Durable JSONL persistence root (the resume suite needs it; others stay file-free). */
   persistenceRoot?: string
   /**
@@ -58,7 +58,7 @@ export async function codingHarness(workdir: string, options: CodingHarnessOptio
   const ctx = new Context()
   await ctx.plugin(SessionProjectionRegistry)
   await mountAgentLoopTestDependencies(ctx, {
-    systemPrompt: { persona: options.persona ?? '' },
+    systemPrompt: { personaPrefix: options.personaPrefix ?? '' },
   })
   await ctx.plugin(AgentLoop, { agents: [] })
   await ctx.plugin(LlmDeepSeek, options.modelContextWindow === undefined ? {} : {

+ 2 - 2
apps/cli/tests/profiles/headless/tests/ptc.e2e.ts

@@ -55,7 +55,7 @@ async function ptcModeHarness(cwd: string): Promise<Context> {
   await harness.plugin(LlmRuntime)
   await harness.plugin(SessionStore)
   await harness.plugin(SessionProjectionRegistry)
-  await harness.plugin(SystemPrompt, { persona: PERSONA })
+  await harness.plugin(SystemPrompt, { personaPrefix: PERSONA })
   await harness.plugin(ToolRuntime, { mode: 'ptc' })
   await harness.plugin(AgentRegistry)
   await harness.plugin(AgentLoop, { agents: [] })
@@ -73,7 +73,7 @@ async function workspacePtcModeHarness(): Promise<Context> {
   await harness.plugin(LlmRuntime)
   await harness.plugin(SessionStore)
   await harness.plugin(SessionProjectionRegistry)
-  await harness.plugin(SystemPrompt, { persona: PERSONA })
+  await harness.plugin(SystemPrompt, { personaPrefix: PERSONA })
   await harness.plugin(ToolRuntime, { mode: 'ptc' })
   await harness.plugin(AgentRegistry)
   await harness.plugin(LocalFileSystem, { cwd: '/' })

+ 2 - 2
apps/cli/tests/profiles/headless/tests/resume.e2e.ts

@@ -37,7 +37,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
     // Run 1: a fresh agent on a KNOWN session id learns a secret, then we
     // dispose the whole context (simulating process exit) so only the JSONL
     // log on disk survives.
-    ctx = await codingHarness(process.cwd(), { persona: SYSTEM_PROMPT, persistenceRoot: root })
+    ctx = await codingHarness(process.cwd(), { personaPrefix: SYSTEM_PROMPT, persistenceRoot: root })
     const first = (await ctx.agents.create({
       sessionId: SESSION_ID,
       agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
@@ -50,7 +50,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
     // Run 2: a brand-new context over the SAME root resumes the persisted
     // session. The loaded event log seeds the live session, so the model sees
     // run 1's exchange as conversation history.
-    ctx = await codingHarness(process.cwd(), { persona: SYSTEM_PROMPT, persistenceRoot: root })
+    ctx = await codingHarness(process.cwd(), { personaPrefix: SYSTEM_PROMPT, persistenceRoot: root })
     const resumed = (await ctx.agents.resume({
       resumeSessionId: SESSION_ID,
       agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },

+ 1 - 1
apps/cli/tests/profiles/headless/tests/todo-write.e2e.ts

@@ -26,7 +26,7 @@ afterEach(async () => {
 describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a plan', () => {
   it('appends a todo/write event with the model-produced task list', async () => {
     workdir = await mkdtemp(join(tmpdir(), 'dsh-todo-write-e2e-'))
-    ctx = await codingHarness(workdir, { persona: TODO_SYSTEM_PROMPT })
+    ctx = await codingHarness(workdir, { personaPrefix: TODO_SYSTEM_PROMPT })
     const agent = await ctx.agentLoop.create(SessionId('e2e-todo'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
 
     agent.followup(createUserMessage({

+ 1 - 1
apps/cli/tests/web-agent-presets.e2e.ts

@@ -292,7 +292,7 @@ describe('the shipped Web composition', () => {
     try {
       const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
       expect(assembly.sections).toEqual([
-        { name: 'deployment:persona', text: MINIMAL_PROMPT },
+        { name: 'deployment:persona-prefix', text: MINIMAL_PROMPT },
       ])
       expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'str_replace_editor'])
       expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)

+ 8 - 3
apps/web/tests/replay-round-trip.e2e.ts

@@ -107,17 +107,22 @@ describe('web e2e: fresh round trip through the real assembly', () => {
     }
   }, 200_000)
 
-  it('records the Web surface, source checkout, and session cwd in the system prompt', async () => {
+  it('ends the system prompt with the source checkout, Web surface, and session cwd', async () => {
     if (settledSessionId === undefined) throw new Error('the drive turn did not publish a session id')
     const agent = scaffold.ctx.agents.get(settledSessionId)
     if (agent === undefined) throw new Error(`the settled Web agent ${settledSessionId} is no longer live`)
     const system = systemPromptText(agent.session)
     if (system === undefined) throw new Error('the settled Web request has no system prompt')
-    const prefix = system.split('\n\n').slice(0, 4).join('\n\n')
+    const paragraphs = system.split('\n\n')
+    expect(paragraphs.slice(0, 2)).toEqual([
+      'You are an AI agent powered by DeepSeek Harness.',
+      'You are a coding agent powered by the deepseek-v4-flash model.',
+    ])
+    const suffix = paragraphs.slice(-3).join('\n\n')
       .split(REPO_ROOT).join('{{sourceRoot}}')
       .split(join(scaffold.workspaceCwd, 'workspace')).join('{{cwd}}')
       .split(scaffold.baseUrl).join('{{webUrl}}')
-    await compareOrRefreshGolden(WEB_CONTEXT_EXPECTED, prefix, MODE)
+    await compareOrRefreshGolden(WEB_CONTEXT_EXPECTED, suffix, MODE)
   })
 
   it('exposes the assembled Web URL to the real bash tool', async () => {

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 79779117f1e5484ab5ffa84ec425b711bd81007f
-config-catalog.zh.md: eccaf6cda9f87c2049c2615bdabba8476aa134a3
+config-catalog.md: 41924a3aa6e8f1d194353fa7750cba6a14e8675b
+config-catalog.zh.md: a3e35cee2b7681a06d30d414fe6d9772e456da26

+ 19 - 9
docs/config-catalog.md

@@ -1576,12 +1576,17 @@ Requires: `systemPrompt`
 /** Plugin config: the persona text this composition contributes. */
 export interface Config {
   /**
-   * Persona prose rendered as the `deployment:persona` section. A template:
+   * Persona prose rendered as the `deployment:persona-prefix` section. A template:
    * complete `{{…}}` groups interpolate strictly against registered prompt
    * variables. Empty text drops the section at render, matching the registry.
    */
-  text: string
-  /** Make this persona the complete system prompt, suppressing every other section. */
+  prefix: string
+  /**
+   * Persona suffix template rendered after first-party guidance. Omitted or empty
+   * text shadows the deployment suffix away; interpolation is strict.
+   */
+  suffix?: string
+  /** Make the prefix the complete system prompt, suppressing the suffix and every other section. */
   complete?: boolean
   /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */
   includeRuntimeContext?: boolean
@@ -2510,17 +2515,22 @@ Source: [`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subproce
 ## `@deepseek-ai/dsh-system-prompt`
 
 ```ts config-catalog
-/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.persona} for its contract). */
+/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
 export interface Config {
   /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
   includeHarnessIdentity?: boolean
   /** Include dynamic runtime-context snapshots in model history (default true). */
   includeRuntimeContext?: boolean
   /**
-   * Deployment-wide order-0 persona template. A scoped section named
-   * `deployment:persona` shadows it; `{{variable}}` references are strict.
+   * Deployment-wide persona prefix template before first-party guidance. A scoped section named
+   * `deployment:persona-prefix` shadows it; `{{variable}}` references are strict.
    */
-  persona?: string
+  personaPrefix?: string
+  /**
+   * Persona suffix template after first-party guidance. A scoped `deployment:persona-suffix`
+   * section shadows it; `{{variable}}` references are strict. Defaults to empty.
+   */
+  personaSuffix?: string
   /**
    * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
    * Invalid fields fail at load and unknown names fail at assembly; known names
@@ -2530,7 +2540,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/core/system-prompt/src/index.ts:237`](../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:242`](../packages/core/system-prompt/src/index.ts)
 
 <a id="deepseek-aidsh-terminal-bash"></a>
 
@@ -2944,7 +2954,7 @@ export interface Config {
    */
   agentOptions?: AgentOptions
   /**
-   * Per-child persona that shadows `deployment:persona`. Requires the
+   * Per-child persona that shadows `deployment:persona-prefix`. Requires the
    * provider's `persona` capability; omission preserves the deployment persona.
    */
   persona?: string

+ 19 - 9
docs/config-catalog.zh.md

@@ -1578,12 +1578,17 @@ export interface PresetSpec {
 /** Plugin config: the persona text this composition contributes. */
 export interface Config {
   /**
-   * Persona prose rendered as the `deployment:persona` section. A template:
+   * Persona prose rendered as the `deployment:persona-prefix` section. A template:
    * complete `{{…}}` groups interpolate strictly against registered prompt
    * variables. Empty text drops the section at render, matching the registry.
    */
-  text: string
-  /** Make this persona the complete system prompt, suppressing every other section. */
+  prefix: string
+  /**
+   * Persona suffix template rendered after first-party guidance. Omitted or empty
+   * text shadows the deployment suffix away; interpolation is strict.
+   */
+  suffix?: string
+  /** Make the prefix the complete system prompt, suppressing the suffix and every other section. */
   complete?: boolean
   /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */
   includeRuntimeContext?: boolean
@@ -2512,17 +2517,22 @@ export interface Config {
 ## `@deepseek-ai/dsh-system-prompt`
 
 ```ts config-catalog
-/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.persona} for its contract). */
+/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
 export interface Config {
   /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
   includeHarnessIdentity?: boolean
   /** Include dynamic runtime-context snapshots in model history (default true). */
   includeRuntimeContext?: boolean
   /**
-   * Deployment-wide order-0 persona template. A scoped section named
-   * `deployment:persona` shadows it; `{{variable}}` references are strict.
+   * Deployment-wide persona prefix template before first-party guidance. A scoped section named
+   * `deployment:persona-prefix` shadows it; `{{variable}}` references are strict.
    */
-  persona?: string
+  personaPrefix?: string
+  /**
+   * Persona suffix template after first-party guidance. A scoped `deployment:persona-suffix`
+   * section shadows it; `{{variable}}` references are strict. Defaults to empty.
+   */
+  personaSuffix?: string
   /**
    * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
    * Invalid fields fail at load and unknown names fail at assembly; known names
@@ -2532,7 +2542,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/core/system-prompt/src/index.ts:237`](../packages/core/system-prompt/src/index.ts)
+来源:[`packages/core/system-prompt/src/index.ts:242`](../packages/core/system-prompt/src/index.ts)
 
 <a id="deepseek-aidsh-terminal-bash"></a>
 
@@ -2946,7 +2956,7 @@ export interface Config {
    */
   agentOptions?: AgentOptions
   /**
-   * Per-child persona that shadows `deployment:persona`. Requires the
+   * Per-child persona that shadows `deployment:persona-prefix`. Requires the
    * provider's `persona` capability; omission preserves the deployment persona.
    */
   persona?: string

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: cc39cf701786b763a66374ff1ec6a2f46f5e770c
-core.zh.md: 165575c08dc3fe58c552f098bd1feacd43588dbf
+core.md: b6413f4ba4da5093b65f84e1fe822c67a6d90379
+core.zh.md: 2bec691f880322c7c6965ab8797492873c241334

+ 1 - 1
docs/subsystems/core.md

@@ -206,7 +206,7 @@ interface AgentOptions {
 }
 ```
 
-Dispatch requires `provider` and `model` after `agent/request`. An explicit `reasoningEffort` seeds the first request on that route; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona` prompt section may shadow the global default persona.
+Dispatch requires `provider` and `model` after `agent/request`. An explicit `reasoningEffort` seeds the first request on that route; exact-model resolution validates it, while omission allows the adapter default to materialize. When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. An agent-scoped `deployment:persona-prefix` prompt section may shadow the global default persona.
 
 The inbox is the delivery vocabulary — two ordered pending-message lists the agent owns as a durable projection:
 

+ 1 - 1
docs/subsystems/core.zh.md

@@ -210,7 +210,7 @@ interface AgentOptions {
 }
 ```
 
-在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。显式 `reasoningEffort` 会为该路由的首次请求提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona` 提示词段落可以遮蔽全局默认 persona。
+在 `agent/request` 之后,分发要求 `provider` 与 `model` 都存在。显式 `reasoningEffort` 会为该路由的首次请求提供初始值;确切模型解析会校验该值,省略时则允许填入适配器默认值。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。agent 作用域的 `deployment:persona-prefix` 提示词段落可以遮蔽全局默认 persona。
 
 inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待处理消息列表:
 

+ 2 - 2
docs/subsystems/subagent.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
-subagent.md: fdd09a53c7d53f90b22a86d72b892d06f02de83d
-subagent.zh.md: 6ac92003aea1805a98b24869927070a26d5daf25
+subagent.md: 616300f92ffa827f14c4780a7648f60f52df56ad
+subagent.zh.md: 059dc4af988ad5ce66bed64827b72449f5a781ca

+ 1 - 1
docs/subsystems/subagent.md

@@ -96,7 +96,7 @@ interface SubagentStartRequest {
   /**
    * Optional per-child persona. Requires {@link SubagentCapabilities.persona};
    * rejected at start otherwise. In-process backends register it as a scoped
-   * `deployment:persona` section on the child, SHADOWING the deployment's
+   * `deployment:persona-prefix` section on the child, SHADOWING the deployment's
    * persona for this child alone — same template semantics as the deployment
    * persona (strict `{{…}}` interpolation against the registered variables).
    */

+ 1 - 1
docs/subsystems/subagent.zh.md

@@ -96,7 +96,7 @@ interface SubagentStartRequest {
   /**
    * Optional per-child persona. Requires {@link SubagentCapabilities.persona};
    * rejected at start otherwise. In-process backends register it as a scoped
-   * `deployment:persona` section on the child, SHADOWING the deployment's
+   * `deployment:persona-prefix` section on the child, SHADOWING the deployment's
    * persona for this child alone — same template semantics as the deployment
    * persona (strict `{{…}}` interpolation against the registered variables).
    */

+ 2 - 2
docs/subsystems/system-prompt.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/system-prompt.md
-system-prompt.md: e5149d24899ad796ceabb66345f3e75f3aaddda3
-system-prompt.zh.md: e84e42b92cbcf21630ad48ebcf78a43f8b214d3c
+system-prompt.md: 55b48d02b7341b9b48ce6c4eececff1a005799f0
+system-prompt.zh.md: a6fdfb08ec00d66bf727ae75e14e6a4a02d2b21f

+ 2 - 0
docs/subsystems/system-prompt.md

@@ -39,6 +39,8 @@ interface ToolProviderResult {
 
 ## Prompt sections
 
+The exported `PERSONA_PREFIX_SECTION` (`deployment:persona-prefix`) and `PERSONA_SUFFIX_SECTION` (`deployment:persona-suffix`) name the slots shared by global configuration and scoped contributions. Their `PromptSectionOrderName` entries are `DEPLOYMENT_PERSONA_PREFIX` and `DEPLOYMENT_PERSONA_SUFFIX`; the [package README](../../packages/core/system-prompt/README.md#configure-the-prompt) owns their placement and template configuration.
+
 `PromptSection` is a readonly same-process registration contract. Its text may be static or resolved from the current assembly context. Sections sort by ascending order and then code-unit name; repository contributors resolve the service-owned named allocation through `getSectionOrder()`. Runtime-context contributors resolve their independent allocation through `getContextOrder()`. One effective `complete` section becomes the sole prompt section after cooperative assembly. agent-loop renders the assembled sections with `renderPrompt` and commits the text as the `system/message` surface node 0 — appended on the first step, replaced in place when the rendered text changes — so the prompt reaches the model as the leading message of derived history rather than as a request field ([decision](../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md)).
 
 ```ts type-equiv

+ 2 - 0
docs/subsystems/system-prompt.zh.md

@@ -39,6 +39,8 @@ interface ToolProviderResult {
 
 ## 提示词段落
 
+导出的 `PERSONA_PREFIX_SECTION`(`deployment:persona-prefix`)与 `PERSONA_SUFFIX_SECTION`(`deployment:persona-suffix`)为全局配置和带作用域贡献所共享的段落命名。它们对应的 `PromptSectionOrderName` 项为 `DEPLOYMENT_PERSONA_PREFIX` 与 `DEPLOYMENT_PERSONA_SUFFIX`;[包 README](../../packages/core/system-prompt/README.zh.md#configure-the-prompt)规定其位置与模板配置。
+
 `PromptSection` 是一份只读的同进程注册约定。其文本可以是静态的,也可以从当前组装上下文动态解析。各段先按 order 升序排列,再按名称的代码单元顺序排列;仓库贡献方通过 `getSectionOrder()` 解析服务持有的具名分配。Runtime-context 贡献方通过 `getContextOrder()` 解析独立分配。协作式组装完成后,一个有效的 `complete` 段会成为唯一的提示词段落。agent loop(智能体循环)用 `renderPrompt` 渲染组装后的各段,并把文本作为 `system/message` surface 第 0 号节点提交——首个步骤追加,渲染文本变化时原地替换——因此提示词作为派生历史的开头消息而不是请求字段到达模型([决策](../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md))。
 
 ```ts type-equiv

+ 1 - 1
packages/acp/acp/tests/harness.ts

@@ -231,7 +231,7 @@ export async function makeBridgeHarness(options: {
   const ctx = new Context()
   const ownsPersistenceRoot = options.persistenceRoot === undefined
   const persistenceRoot = options.persistenceRoot ?? await mkdtemp(join(tmpdir(), 'dsh-acp-test-'))
-  await mountAgentLoopTestDependencies(ctx, { systemPrompt: { persona: options.persona ?? '' } })
+  await mountAgentLoopTestDependencies(ctx, { systemPrompt: { personaPrefix: options.persona ?? '' } })
   // The agent loop and the composed approval/permission services declare
   // sessionProjections a required injection: mount the registry (and with it
   // the loop's turnBoundary unit) before the loop activates.

+ 1 - 1
packages/api/session-controller/tests/session-fork.host.spec.ts

@@ -23,7 +23,7 @@ function request<P>(payload: P): P {
 async function composed(workspaces: readonly Workspace[] = []): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(AgentRegistry)
   installSessionReadTestServices(ctx)
   ctx.provide('workspaceRegistry', { list: () => workspaces } as never)

+ 1 - 1
packages/api/session-controller/tests/session-models.host.spec.ts

@@ -95,7 +95,7 @@ async function harness(logged?: {
 }> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(AgentRegistry)
   ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
-README.md: 3cc8247eb366f278acc8bb86b30da75fe3ab3ee4
-README.zh.md: 1ea2ce4a90c5aa57edf9761ae2b85b04405e103c
+README.md: e6afceaebf7fc94f5a6d708639de128cb9005d72
+README.zh.md: a98be1fd5d0624f971af5d0ce5a81fdc0cafab54

+ 1 - 1
packages/boot/app-boot/README.md

@@ -128,7 +128,7 @@ Indirectly, through the loaded plugin tree, which alone contributes model contex
 
 #### KV Cache effect
 
-Boot itself invalidates nothing in the request prefix. A consumer that calls `addHarnessSourceSection` places one short line near the system prompt's head, before per-request content, so it does not invalidate the cache across turns; any other request-prefix change is owned by the named consumer.
+Boot itself changes no request prefix. `addHarnessSourceSection` places its source path after first-party reusable instructions, so different checkouts leave those preceding bytes unchanged when tools and configuration match. Provider cache reuse is not guaranteed.
 
 ## Known Limitations and Deferred Work
 

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -128,7 +128,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
 
 #### KV Cache 影响
 
-启动本身不会使请求前缀中的任何内容失效。消费方调用 `addHarnessSourceSection` 时,会在系统提示词靠前位置、逐请求内容之前添加一行短文本,因此不会使跨轮次缓存失效;请求前缀的其他任何变化均由相应的具名消费方负责
+启动本身不改变请求前缀。`addHarnessSourceSection` 将源码路径放在第一方可复用指令之后,因此工具与配置一致时,不同 checkout 不会改变前置字节。不保证提供方复用缓存
 
 ## 已知限制与延期工作
 

+ 4 - 3
packages/boot/app-boot/src/index.ts

@@ -840,9 +840,10 @@ export const HARNESS_SOURCE_SECTION = 'harness:source'
  * explicitly distinguishing it from the task workspace and current working
  * directory. The self-referential `dsh-tool-cordis` toolset reads and edits this
  * checkout. Call once on the settled boot context ({@link boot}); the section
- * uses the shared first-party placement just after the harness identity opener
- * and before the deployment persona. A booted tree with no `systemPrompt` service has no prompt to
- * augment, so this is then a no-op that returns `undefined`. The section is
+ * uses the shared first-party placement after reusable instructions
+ * and before the Web surface and persona suffix. A booted tree with no
+ * `systemPrompt` service has no prompt to augment, so this is then a no-op
+ * that returns `undefined`. The section is
  * registered against the `systemPrompt` service's fiber, so a dev HMR reload of
  * that plugin drops it until the next boot.
  * @param ctx - the settled boot context whose global system prompt to augment.

+ 9 - 4
packages/boot/app-boot/tests/app-boot.spec.ts

@@ -894,10 +894,13 @@ describe('addHarnessSourceSection', () => {
   const SOURCE_ROOT = `${sep}opt${sep}harness-src`
   const EXPECTED = `The DeepSeek Harness implementation checkout is at ${SOURCE_ROOT}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`
 
-  it('distinguishes the source path from the current workdir between identity and persona', async () => {
+  it('distinguishes the source path from the current workdir after reusable instructions', async () => {
     const ctx = new Context()
     try {
-      await ctx.plugin(SystemPrompt, { persona: 'You are a coding agent.' })
+      await ctx.plugin(SystemPrompt, { personaPrefix: 'You are a coding agent.' })
+      ctx.systemPrompt.section({
+        name: 'tools:sdk', order: ctx.systemPrompt.getSectionOrder('TOOLS_SDK'), text: 'Reusable tool SDK.',
+      })
       const dispose = addHarnessSourceSection(ctx, SOURCE_ROOT)
       expect(dispose).toBeTypeOf('function')
       const systemPrompt = ctx.get('systemPrompt')!
@@ -910,8 +913,10 @@ describe('addHarnessSourceSection', () => {
       const personaAt = rendered.indexOf('You are a coding agent.')
       expect(identityAt).toBeGreaterThanOrEqual(0)
       expect(personaAt).toBeGreaterThanOrEqual(0)
-      expect(identityAt).toBeLessThan(sourceAt)
-      expect(sourceAt).toBeLessThan(personaAt)
+      const sdkAt = rendered.indexOf('Reusable tool SDK.')
+      expect(personaAt).toBeGreaterThan(identityAt)
+      expect(sdkAt).toBeGreaterThan(personaAt)
+      expect(sdkAt).toBeLessThan(sourceAt)
     } finally {
       await ctx.fiber.dispose()
     }

+ 2 - 2
packages/bundle/acp-app/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/acp-app/README.md
-README.md: d27beb0882b1ea1da894ff89ebaa62011df548bc
-README.zh.md: ac60cd408db2df746d989f27433841d1eaa13386
+README.md: 25e51402af8f522a65ea0e9f0a402f29e6d73bc2
+README.zh.md: 4ecf61c43c55c62fdcaebe307b3331a6c60fc15d

+ 1 - 1
packages/bundle/acp-app/README.md

@@ -44,7 +44,7 @@ The complete supported method matrix, MCP trust model, update mapping, and stop
 
 #### What the model sees
 
-The profile supplies `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.` before the base tool and context contributions. The ACP row's route and each `session/new` cwd resolve the placeholders.
+The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance and `Your working directory is {{cwd}}.` in a separate persona suffix. The ACP row's route and each `session/new` cwd resolve the placeholders.
 
 #### Token effect
 

+ 1 - 1
packages/bundle/acp-app/README.zh.md

@@ -44,7 +44,7 @@ ACP v1 SDK 客户端先初始化 `dsh --profile acp`,再用绝对 `cwd` 与可
 
 #### 模型看到什么
 
-在 base 的工具和上下文贡献之前,profile 提供 `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.`。ACP 配置项的路由与每个 `session/new` 的 cwd 会解析其中的占位符。
+profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`,并在独立的 persona 后缀中提供 `Your working directory is {{cwd}}.`。ACP 配置项的路由与每个 `session/new` 的 cwd 会解析其中的占位符。
 
 #### Token 影响
 

+ 3 - 2
packages/bundle/acp-app/cordis.patch.yml

@@ -2,8 +2,9 @@
 
 - id: system-prompt
   config:
-    persona: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    personaSuffix: Your working directory is {{cwd}}.
+    personaPrefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 - id: session-title-llm
   disabled: true

+ 1 - 1
packages/bundle/base/cordis.patch.yml

@@ -465,7 +465,7 @@
     - id: system-prompt
       name: '@deepseek-ai/dsh-system-prompt'
       config:
-        persona: ''
+        personaPrefix: ''
 
     # Agents created at startup. The base stays empty; raw overlays may create
     # agents, while Web creates sessions on client request.

+ 2 - 2
packages/bundle/headless/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/headless/README.md
-README.md: 644a96ebb19c9ccecbfb3a08fdf4182dc668f0e5
-README.zh.md: e1b954f876b7f90167e4dab313b88f66b1d20512
+README.md: 98a9cb2294d8b05a40287c990b3ff53f755747cf
+README.zh.md: 0f4721d856357a85964a13433c3bbc701f8fb299

+ 1 - 1
packages/bundle/headless/README.md

@@ -65,7 +65,7 @@ The runner awaits the complete application (`ctx.get('loader')?.await()`) so the
 
 ### Patch surface over base
 
-The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts PTC mode's worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config.
+The patch rides over `dsh-base`: it inherits the projection cache, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, inserts PTC mode's worker as a core execution capability, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task from lazy config.
 
 ### Exit mapping
 

+ 1 - 1
packages/bundle/headless/README.zh.md

@@ -65,7 +65,7 @@ runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组
 
 ### 叠加在 base 之上的 patch 表层
 
-patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-prompt` 行上设置编码 persona,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR 行,把 PTC mode 的 worker 作为核心执行能力插入,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。
+patch 叠加在 `dsh-base` 之上:继承投影缓存,在基础 `system-prompt` 行上设置编码 persona 前缀与独立的 cwd 后缀,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR 行,把 PTC mode 的 worker 作为核心执行能力插入,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务。
 
 ### 退出映射
 

+ 3 - 2
packages/bundle/headless/cordis.patch.yml

@@ -6,8 +6,9 @@
 
 - id: system-prompt
   config:
-    persona: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    personaSuffix: Your working directory is {{cwd}}.
+    personaPrefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 - id: tools
   config:

+ 2 - 2
packages/bundle/sdk-app/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/sdk-app/README.md
-README.md: 68ea9670ea3feee699bf1cce501472e265006882
-README.zh.md: c342e3d90508a8d147dd38800c3d314b619c6e36
+README.md: 538e695e017b823b93ca8696f9e3202f33274ce4
+README.zh.md: 50dfda2b4d5ccc8b7e2c2d67eb920a8e180d5d32

+ 1 - 1
packages/bundle/sdk-app/README.md

@@ -42,7 +42,7 @@ The SDK uses the base `read`, `write`, and `edit` defaults. To add `str_replace_
 
 #### What the model sees
 
-The profile supplies `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.` before the base tool and context contributions. The exact SDK initialization route and session cwd resolve the placeholders. Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`.
+The profile supplies `You are a coding agent powered by the {{model}} model.` before first-party guidance and `Your working directory is {{cwd}}.` in a separate persona suffix. The exact SDK initialization route and session cwd resolve the placeholders. Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`.
 
 #### Token effect
 

+ 1 - 1
packages/bundle/sdk-app/README.zh.md

@@ -42,7 +42,7 @@ SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_rep
 
 #### 模型看到什么
 
-profile 会在 base 工具与上下文贡献之前提供 `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.`。确切的 SDK 初始化路由与会话 cwd 会解析其中的占位符。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。
+profile 在第一方指导之前提供 `You are a coding agent powered by the {{model}} model.`,并在独立的 persona 后缀中提供 `Your working directory is {{cwd}}.`。确切的 SDK 初始化路由与会话 cwd 会解析其中的占位符。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。
 
 #### Token 影响
 

+ 3 - 2
packages/bundle/sdk-app/cordis.patch.yml

@@ -2,8 +2,9 @@
 
 - id: system-prompt
   config:
-    persona: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    personaSuffix: Your working directory is {{cwd}}.
+    personaPrefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 - id: session-title-llm
   disabled: true

+ 1 - 1
packages/bundle/sdk-minimal/cordis.patch.yml

@@ -93,7 +93,7 @@
       config:
         includeHarnessIdentity: false
         includeRuntimeContext: false
-        persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
+        personaPrefix: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
 
     - id: tools
       name: '@deepseek-ai/dsh-tools'

+ 1 - 1
packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts

@@ -73,7 +73,7 @@ describe('dsh-sdk-minimal bundle', () => {
     expect(rows.find(row => row.id === 'system-prompt')?.config).toEqual({
       includeHarnessIdentity: false,
       includeRuntimeContext: false,
-      persona: { __jsExpr: "process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'" },
+      personaPrefix: { __jsExpr: "process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'" },
     })
     expect(rows.find(row => row.id === 'agent-loop')?.config).toEqual({ agents: [] })
     expect(rows.find(row => row.id === 'terminal-bash')).toMatchObject({

+ 2 - 2
packages/bundle/web-app/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
-README.md: c352ec937ecfa51f36eae1970067a62aed51b643
-README.zh.md: cc3b5e5660cd7fdb38f6d9084669491b1df27ee7
+README.md: 0f71be178c25c0e6687a6e51ff777a9d6ac76a5a
+README.zh.md: ea7747c0b814dc36d222d0d7445732159f589d7b

+ 3 - 3
packages/bundle/web-app/README.md

@@ -73,7 +73,7 @@ The bundle is one patch plus one runtime glue plugin. The storage stack and proj
 
 ### Patch semantics
 
-A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona, the `DSH_TOOLS_MODE` PTC mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch.
+A patch replaces the targeted row's whole `config`, so each web row restates every key it owns: the persona prefix and suffix templates, the `DSH_TOOLS_MODE` PTC mode opt-in, and the `session-query-sqlite` values on the base rows, then `insert` adds the web host rows, transport, and browser roster. The per-agent tool rows the base mounts process-wide are disabled here and the preset roster takes over; the reasoning for each host-plane versus preset-plane decision is inline in the patch.
 
 ### Readiness
 
@@ -124,7 +124,7 @@ Read these pages when you want to go deeper into the shared core, the browser re
 
 #### What the model sees
 
-When `surfaceContext` is true, the `harness:source` section identifies the on-disk Harness implementation without claiming it is the working directory, and the `app:web-surface` global section (first-party order −800) orients the model to the GUI: the canonical local URL, the "this page" referent, the update contract (the reload receiver is always on; no-refresh reloads additionally need the `pnpm run dev:web` watcher), and the instruction not to start replacement servers. `DSH_WEB_URL` additionally appears in the managed bash environment with its description, resolved per invocation from the live server. When it is false, neither section nor the variable is registered.
+When `surfaceContext` is true, the `harness:source` section identifies the on-disk Harness implementation without claiming it is the working directory, and the `app:web-surface` global section (first-party order 10100, after reusable instructions) orients the model to the GUI: the canonical local URL, the "this page" referent, the update contract (the reload receiver is always on; no-refresh reloads additionally need the `pnpm run dev:web` watcher), and the instruction not to start replacement servers. `DSH_WEB_URL` additionally appears in the managed bash environment with its description, resolved per invocation from the live server. When it is false, neither section nor the variable is registered.
 
 #### Token effect
 
@@ -132,7 +132,7 @@ One source line and one prompt paragraph per session plus two managed-environmen
 
 #### KV Cache effect
 
-The prompt section sits near the system prompt's head and is stable for the life of the process (the port is a boot fact), so it does not invalidate the cache across turns.
+Source and Web sections follow first-party reusable instructions. Different checkout paths or local ports leave that preceding prefix unchanged when tools and configuration match; provider cache reuse is not guaranteed.
 
 ## Known Limitations and Deferred Work
 

+ 3 - 3
packages/bundle/web-app/README.zh.md

@@ -73,7 +73,7 @@ dsh --profile web --no-open --port 8080
 
 ### patch 语义
 
-patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona、`DSH_TOOLS_MODE` PTC mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。
+patch 会替换目标行的整个 `config`,因此每个 Web 行都重述自己拥有的每个键:基础行上的 persona 前缀与后缀模板、`DSH_TOOLS_MODE` PTC mode 开关与 `session-query-sqlite` 值,随后 `insert` 添加 Web 宿主行、传输层与浏览器名录。base 以进程级挂载的按 agent 工具行在这里被禁用,由 preset 名录接管;每项宿主层与 preset 层归属决策的理由以行内注释写在 patch 里。
 
 ### 就绪宣告
 
@@ -124,7 +124,7 @@ URL 行与浏览器交接都是就绪信号:监督方一观察到该行就发
 
 #### 模型看到什么
 
-当 `surfaceContext` 为 true 时,`harness:source` 段落标明磁盘上的 Harness 实现,但不会声称它就是工作目录;全局段落 `app:web-surface`(first-party 顺序 −800)则向模型说明 GUI:规范的本地 URL、「this page」指代什么、更新约定(重载接收端始终开启;无刷新重载还需要 `pnpm run dev:web` watcher),以及不要启动替代服务器的指令。`DSH_WEB_URL` 还会连同描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。当它为 false 时,这两个段落和该变量都不会注册。
+当 `surfaceContext` 为 true 时,`harness:source` 段落标明磁盘上的 Harness 实现,但不会声称它就是工作目录;全局段落 `app:web-surface`(first-party 顺序 10100,位于可复用指令之后)则向模型说明 GUI:规范的本地 URL、「this page」指代什么、更新约定(重载接收端始终开启;无刷新重载还需要 `pnpm run dev:web` watcher),以及不要启动替代服务器的指令。`DSH_WEB_URL` 还会连同描述出现在受管 bash 环境中,每次调用时从运行中的服务器解析。当它为 false 时,这两个段落和该变量都不会注册。
 
 #### Token 影响
 
@@ -132,7 +132,7 @@ URL 行与浏览器交接都是就绪信号:监督方一观察到该行就发
 
 #### KV Cache 影响
 
-该提示词段落位于系统提示词靠前位置,且在进程整个生命周期内稳定(端口是启动期事实),因此不会使跨轮次缓存失效
+源码与 Web 段落位于第一方可复用指令之后。工具与配置一致时,不同 checkout 路径或本地端口不会改变前置前缀;不保证提供方复用缓存
 
 ## 已知限制与延期工作
 

+ 3 - 2
packages/bundle/web-app/cordis.patch.yml

@@ -15,8 +15,9 @@
 
 - id: system-prompt
   config:
-    persona: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    personaSuffix: Your working directory is {{cwd}}.
+    personaPrefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 # Full-text session search is opt-in (the base row's `openAt: never`). This
 # restatement keeps the Web values on one ephemeral in-memory index; a

+ 4 - 4
packages/bundle/web-app/tests/web-app.spec.ts

@@ -134,7 +134,7 @@ describe('web-app runtime glue', () => {
     const openBrowser = vi.fn(async (url: string) => { lifecycle.push(`open:${url}`) })
     internals.openBrowser = openBrowser
     apply(ctx, new Config({ openBrowser: true, printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     // Settle the injected registrations.
     await new Promise(resolve => setTimeout(resolve, 0))
 
@@ -172,7 +172,7 @@ describe('web-app runtime glue', () => {
     const openBrowser = vi.fn(async () => {})
     internals.openBrowser = openBrowser
     apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await new Promise(resolve => setTimeout(resolve, 0))
     expect(log).not.toHaveBeenCalled()
     expect(openBrowser).not.toHaveBeenCalled()
@@ -195,7 +195,7 @@ describe('web-app runtime glue', () => {
       },
     } as never)
     apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: false, trustedHosts: [] }))
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await new Promise(resolve => setTimeout(resolve, 0))
     const assembly = await ctx.systemPrompt.assemble()
     expect(assembly.sections.some(entry => entry.name === 'app:web-surface')).toBe(false)
@@ -323,7 +323,7 @@ describe('web-app runtime glue', () => {
     ctx.provide('webServer', server)
     provideConnection(ctx)
     apply(ctx, new Config({ openBrowser: false, printUrl: false, surfaceContext: true, trustedHosts: [] }))
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await new Promise(resolve => setTimeout(resolve, 0))
     await expect(ctx.systemPrompt.assemble()).rejects.toThrow('webServer service missing')
     await ctx.fiber.dispose()

+ 1 - 1
packages/client/ui-deliverables/tests/prompt.client.spec.ts

@@ -15,7 +15,7 @@ afterEach(async () => {
 describe('ui-deliverables node plugin', () => {
   it('registers final-response file-reference guidance only while mounted', async () => {
     ctx = new Context()
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     const mounted = ctx.plugin({ apply, inject })
     await mounted.await()
 

+ 1 - 1
packages/context/agent-instructions/tests/agent-instructions.e2e.ts

@@ -41,7 +41,7 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> {
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona: 'Answer the user exactly and concisely.' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: 'Answer the user exactly and concisely.' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(LocalFileSystem, { cwd: '/' })

+ 1 - 1
packages/context/file-reference-local/tests/service.spec.ts

@@ -21,7 +21,7 @@ afterEach(async () => {
 async function harness(): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRegistry)
   await ctx.plugin(AgentRegistry)
   return ctx

+ 2 - 2
packages/core/agent-loop/tests/loop.spec.ts

@@ -20,7 +20,7 @@ async function harness(adapter: MockAdapter, persona = '') {
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona })
+  await ctx.plugin(SystemPrompt, { personaPrefix: persona })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentLoop, { agents: [] })
@@ -564,7 +564,7 @@ describe('agent loop', () => {
 
     expect(adapter.requests).toHaveLength(0) // the request was never sent
     expect(errors.map(error => error.message)).toEqual([
-      'prompt variable "{{cwd}}" has no value for this assembly (section "deployment:persona")',
+      'prompt variable "{{cwd}}" has no value for this assembly (section "deployment:persona-prefix")',
     ])
     const turnEnd = agent.session.snapshotEvents().find(e => e.type === 'turn/end')
     expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('error')

+ 1 - 1
packages/core/agent-loop/tests/request-cache.e2e.ts

@@ -43,7 +43,7 @@ async function loopHarness(): Promise<Context> {
   await created.plugin(LlmRuntime)
   await created.plugin(SessionStore)
   await created.plugin(SessionProjectionRegistry)
-  await created.plugin(SystemPrompt, { persona: SYSTEM })
+  await created.plugin(SystemPrompt, { personaPrefix: SYSTEM })
   await created.plugin(ToolRuntime)
   await created.plugin(AgentRegistry)
   await created.plugin(AgentLoop, { agents: [] })

+ 3 - 3
packages/core/agent-loop/tests/request-reconstruction.spec.ts

@@ -30,7 +30,7 @@ async function harnessRoutes(
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona })
+  await ctx.plugin(SystemPrompt, { personaPrefix: persona })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentLoop, { agents: [] })
@@ -343,7 +343,7 @@ describe('request stability across the loop', () => {
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(SystemPrompt, { persona: 'stable base' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(AgentLoop, { agents: [] })
@@ -462,7 +462,7 @@ describe('request stability across the loop', () => {
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(SystemPrompt, { persona: 'stable base' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(AgentLoop, { agents: [] })

+ 7 - 7
packages/core/agent-loop/tests/scope-lifecycle.spec.ts

@@ -19,7 +19,7 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: 'You are the deployment.' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
@@ -174,25 +174,25 @@ describe('agent scope lifecycle', () => {
     const ctx = await harness()
     const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { provider: 'mock', model: 'mock' } })
     const { agent } = handle
-    agent.ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You run tests.' })
+    agent.ctx.systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: 'You run tests.' })
     agent.ctx.tools.register(defineContentToolFixture({
       name: 'mine', description: 'scoped', parameters: {},
       execute: () => Promise.resolve(text('ran')),
     }))
 
     const scopedAssembly = await ctx.systemPrompt.assemble(assembleContextFor(agent))
-    expect(scopedAssembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You run tests.')
+    expect(scopedAssembly.sections.find(s => s.name === 'deployment:persona-prefix')?.text).toBe('You run tests.')
     expect(scopedAssembly.tools.map(t => t.name)).toContain('mine')
     // Other assemblies are untouched.
     const globalAssembly = await ctx.systemPrompt.assemble()
-    expect(globalAssembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You are the deployment.')
+    expect(globalAssembly.sections.find(s => s.name === 'deployment:persona-prefix')?.text).toBe('You are the deployment.')
     expect(globalAssembly.tools.map(t => t.name)).not.toContain('mine')
 
     await handle.dispose()
     // The scoped world unwound with the agent: nothing leaked into the registries.
     expect(ctx.tools.get('mine', agent)).toBeUndefined()
     const after = await ctx.systemPrompt.assemble(assembleContextFor(agent))
-    expect(after.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You are the deployment.')
+    expect(after.sections.find(s => s.name === 'deployment:persona-prefix')?.text).toBe('You are the deployment.')
   })
 
   it('agent.ctx listeners hear only their own agent (scoped dispatch end to end)', async () => {
@@ -223,7 +223,7 @@ describe('agent scope lifecycle', () => {
       order.push('session-start')
       // The scoped section is already registered by the time session-start fires.
       void ctx.systemPrompt.assemble(assembleContextFor(agent)).then((assembly) => {
-        order.push(`persona:${assembly.sections.find(s => s.name === 'deployment:persona')?.text}`)
+        order.push(`persona:${assembly.sections.find(s => s.name === 'deployment:persona-prefix')?.text}`)
       })
     })
 
@@ -233,7 +233,7 @@ describe('agent scope lifecycle', () => {
       setup: async (agentCtx) => {
         order.push('setup')
         await Promise.resolve()
-        agentCtx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You are the child.' })
+        agentCtx.systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: 'You are the child.' })
       },
     })
     await new Promise(resolve => setTimeout(resolve, 0))

+ 4 - 4
packages/core/agent-loop/tests/tool-calls.spec.ts

@@ -22,7 +22,7 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentLoop, {
@@ -283,7 +283,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
 
@@ -352,7 +352,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(AgentLoop, { agents: [], maxParallelToolCalls: 1 })
@@ -711,7 +711,7 @@ describe('PTC mode native-tool denial through the agent loop', () => {
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await ctx.plugin(ToolRuntime, { mode: 'ptc' })
     // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakeCodeRuntime is an internal test helper with an opaque type shape
     await ctx.plugin(FakeCodeRuntime as any)

+ 1 - 1
packages/core/agent-loop/tests/tool-order.spec.ts

@@ -25,7 +25,7 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
+  await ctx.plugin(SystemPrompt, { personaPrefix: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentLoop, { agents: [] })

+ 2 - 2
packages/core/system-prompt/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md
-README.md: 9e4f3b22f4278e0c4584708adef61b11aa0a1f28
-README.zh.md: 62d427b45591b51115d8a606bb7d7d30651c93e1
+README.md: ea981867d5d0125080536d23cfe88d73fd6cea28
+README.zh.md: 18d32f802b6304e5da431304fb09d1a7c81ae314

+ 10 - 8
packages/core/system-prompt/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-system-prompt` assembles the system prompt and tool schemas the model receives before each step. Plugins contribute ordered prompt sections, dynamic runtime context, tool-schema providers, and named variables; the loop calls `assemble()` once per step, renders the result into the complete model prompt, and commits that text as the `system/message` surface node 0 owned by its `SystemPromptProjection` — appended on the first step, replaced in place when the rendered text changes ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md)). The package provides the fixed harness identity and the global deployment persona, while an agent-scoped contribution shadows the global default for one agent. Config controls the harness identity opener, dynamic runtime context, the deployment persona, and an explicit model-facing tool order. Choose it when you need to add a prompt section, a prompt variable, or a tool-schema source — it is the assembly point all model-facing prose flows through.
+`dsh-system-prompt` assembles the system prompt and tool schemas the model receives before each step. Plugins contribute ordered prompt sections, dynamic runtime context, tool-schema providers, and named variables; the loop calls `assemble()` once per step, renders the result into the complete model prompt, and commits that text as the `system/message` surface node 0 owned by its `SystemPromptProjection` — appended on the first step, replaced in place when the rendered text changes ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md)). The package provides the fixed harness identity and the global deployment persona prefix and suffix, while an agent-scoped contribution shadows the global default for one agent. Config controls the harness identity opener, dynamic runtime context, the deployment persona prefix and suffix, and an explicit model-facing tool order. Choose it when you need to add a prompt section, a prompt variable, or a tool-schema source — it is the assembly point all model-facing prose flows through.
 
 ## Table of Contents
 
@@ -27,16 +27,17 @@ English | [中文](README.zh.md)
 
 Mount `dsh-system-prompt` wherever agents run: it provides `ctx.systemPrompt`, the registry every prompt contribution lands in. Contributions are scoped — registering through `agent.ctx` affects that agent alone and shadows a same-named global.
 
+<a id="configure-the-prompt"></a>
 ### Configure the prompt
 
-The config owns the fixed opener, runtime context, deployment persona, and tool order; everything else comes from registered contributions.
+The config owns the fixed opener, runtime context, deployment persona prefix and suffix, and tool order; everything else comes from registered contributions.
 
 ```yaml
 - name: '@deepseek-ai/dsh-system-prompt'
   config:
     includeHarnessIdentity: true
     includeRuntimeContext: true
-    persona: 'You are the deployment assistant.'
+    personaPrefix: 'You are the deployment assistant.'
     toolOrder: ['<unlisted-tools>']
 ```
 
@@ -44,7 +45,8 @@ The config owns the fixed opener, runtime context, deployment persona, and tool
 |---|---|---|
 | `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` first-party opener at order −1000. Set false only when a compatibility deployment owns the complete system prompt. |
 | `includeRuntimeContext` | `true` | Include ordered dynamic runtime context in assembly |
-| `persona` | `''` | The global deployment-persona prompt fragment, rendered at order `0` |
+| `personaPrefix` | `''` | Global persona prefix template at order `0`, before first-party guidance |
+| `personaSuffix` | `''` | Global `deployment:persona-suffix` template at order `10200`, after first-party guidance |
 | `toolOrder` | — | Explicit model-facing tool order with one `'<unlisted-tools>'` rest entry |
 
 The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-system-prompt) is the exhaustive source for every accepted field. A `toolOrder` list without exactly one rest entry or with duplicates fails at load; a listed name with no registered tool rejects every `assemble()`.
@@ -130,7 +132,7 @@ The package-level contract is enough for most consumers; read these when you nee
 
 #### What the model sees
 
-By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. The rendered prompt reaches the model as the leading system-role message of derived history (surface node 0), never as a separate request field. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes them all.
+First-party sections render the harness identity, deployment persona prefix (including the model-name introduction), reusable instructions (including the generated tools SDK and structured-output guidance), then the environment-bearing suffix: harness source (`10000`), Web surface (`10100`), and deployment persona suffix (`10200`). External section orders and assembly listeners remain authoritative. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete — that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. The rendered prompt reaches the model as the leading system-role message of derived history (surface node 0); neither the loop request nor `request/header` carries a separate `system` field. Ordered dynamic contexts are separate from sections and become sourced user-role snapshots only when present; `includeRuntimeContext: false` or a scoped suppressor removes them all.
 
 ##### Harness identity
 
@@ -140,11 +142,11 @@ You are an AI agent powered by DeepSeek Harness.
 
 #### Token effect
 
-Identity is a fixed per-request cost when enabled. Persona and plugin text are repeated per request and scale with their rendered content.
+Identity is a fixed per-request cost when enabled. Persona prefixes, suffixes, and plugin text are repeated per request and scale with their rendered content.
 
 #### KV Cache effect
 
-Prefix-stable while identity, persona, variables, section text, and order render identically: an unchanged rendering leaves surface node 0 untouched. Any change replaces node 0 with a new `system/message`, so the request differs from its first token and reuse is lost for the whole prefix.
+An unchanged rendering leaves surface node 0 untouched; a changed rendering replaces that node in place with a new `system/message`. With the same model, persona prefix, tools, and preceding instructions, different source paths, local Web URLs, or persona suffix values leave the reusable first-party prefix unchanged. Persona prefix changes can alter the early prefix. Any change may invalidate reuse from the first changed token; provider cache sharing and measured hit rates are not guaranteed.
 
 ### Tool schemas
 
@@ -167,7 +169,7 @@ Prefix-stable while the visible schema set, rendering, and order are unchanged.
 
 These limits define when prompt assembly needs special care. They are current package constraints, not a task backlog.
 
-- **Deployment-authored prompt text is config/composition only** — this plugin owns the global persona default, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API.
+- **Deployment-authored prompt text is config/composition only** — this plugin owns the global persona prefix and suffix defaults, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API.
 - **No escape syntax for literal `{{…}}` braces** — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one.
 - **`toolOrder` misconfiguration surfaces at prompt assembly (the first turn), not at boot** — only shape violations throw at config load.
 

+ 10 - 8
packages/core/system-prompt/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-system-prompt` 组装模型在每个步骤之前收到的系统提示词与工具 schema。插件贡献有序提示词段、动态 runtime 上下文、工具 schema 提供方与具名变量;循环每个步骤调用一次 `assemble()`,把结果渲染为完整模型提示词,并把该文本作为由其 `SystemPromptProjection` 拥有的 `system/message` surface 第 0 号节点提交——首个步骤追加,渲染文本变化时原地替换([决策](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md))。该包提供固定 harness 身份与全局部署 persona,而 agent 作用域的贡献会为单个 agent 遮蔽全局默认值。配置控制 harness 身份开场白、动态 runtime 上下文、部署 persona 与显式的面向模型工具顺序。需要添加提示词段、提示词变量或工具 schema 来源时请选择本包——它是所有面向模型文案流经的组装点。
+`dsh-system-prompt` 组装模型在每个步骤之前收到的系统提示词与工具 schema。插件贡献有序提示词段、动态 runtime 上下文、工具 schema 提供方与具名变量;循环每个步骤调用一次 `assemble()`,把结果渲染为完整模型提示词,并把该文本作为由其 `SystemPromptProjection` 拥有的 `system/message` surface 第 0 号节点提交——首个步骤追加,渲染文本变化时原地替换([决策](../../../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md))。该包提供固定 harness 身份、全局部署 persona 前缀与后缀,而 agent 作用域的贡献会为单个 agent 遮蔽全局默认值。配置控制 harness 身份开场白、动态 runtime 上下文、部署 persona 前缀后缀,以及显式的面向模型工具顺序。需要添加提示词段、提示词变量或工具 schema 来源时请选择本包——它是所有面向模型文案流经的组装点。
 
 ## 目录
 
@@ -27,16 +27,17 @@ kind: "package-reference"
 
 在任何运行 agent 的地方挂载 `dsh-system-prompt`:它提供 `ctx.systemPrompt`,即每个提示词贡献所落入的注册表。贡献带作用域——通过 `agent.ctx` 注册只影响该 agent,并遮蔽同名全局项。
 
+<a id="configure-the-prompt"></a>
 ### 配置提示词
 
-配置拥有固定开场白、runtime 上下文、部署 persona 与工具顺序;其余一切来自已注册的贡献。
+配置拥有固定开场白、runtime 上下文、部署 persona 前缀与后缀与工具顺序;其余一切来自已注册的贡献。
 
 ```yaml
 - name: '@deepseek-ai/dsh-system-prompt'
   config:
     includeHarnessIdentity: true
     includeRuntimeContext: true
-    persona: 'You are the deployment assistant.'
+    personaPrefix: 'You are the deployment assistant.'
     toolOrder: ['<unlisted-tools>']
 ```
 
@@ -44,7 +45,8 @@ kind: "package-reference"
 |---|---|---|
 | `includeHarnessIdentity` | `true` | 是否包含顺序为 −1000 的 first-party 固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容性部署拥有完整系统提示词时设为 false。 |
 | `includeRuntimeContext` | `true` | 是否在组装中包含有序动态 runtime 上下文 |
-| `persona` | `''` | 全局部署 persona 提示词片段,渲染在顺序 `0` |
+| `personaPrefix` | `''` | 全局 persona 前缀模板,位于第一方指导之前的顺序 `0` |
+| `personaSuffix` | `''` | 全局 `deployment:persona-suffix` 模板,位于第一方指导之后的顺序 `10200` |
 | `toolOrder` | — | 显式面向模型工具顺序,含一个 `'<unlisted-tools>'` 其余项标记 |
 
 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-system-prompt)是每个受支持字段的穷尽式真源。没有恰好一个其余项或存在重复项的 `toolOrder` 列表会在加载时失败;已列名称没有对应已注册工具会使每次 `assemble()` 被拒绝。
@@ -130,7 +132,7 @@ ctx.systemPrompt.variable('cwd', ({ agent }) => agent?.session.header.cwd)
 
 #### 模型看到什么
 
-默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文、工具与变量保持不变。渲染后的提示词作为派生历史开头的 system 角色消息(surface 第 0 号节点)到达模型,从不作为单独的请求字段。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除全部这类上下文。
+第一方段落依次渲染 harness 身份、部署 persona 前缀(含模型名称介绍)、可复用指令(包括生成的工具 SDK 和结构化输出指导),最后是携带环境信息的后缀:harness 源码(`10000`)、Web 表层(`10100`)和部署 persona 后缀(`10200`)。外部段落的顺序与组装监听器仍决定其最终结果。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段与变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete——此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文、工具与变量保持不变。渲染后的提示词作为派生历史开头的 system 角色消息(surface 第 0 号节点)到达模型;循环请求与 `request/header` 均不含单独的 `system` 字段。有序动态上下文与段分离,只在存在时才会成为带来源的 user 角色快照;`includeRuntimeContext: false` 或带作用域的抑制器会移除全部这类上下文。
 
 ##### harness 身份
 
@@ -140,11 +142,11 @@ You are an AI agent powered by DeepSeek Harness.
 
 #### Token 影响
 
-启用时,身份是每次请求的固定成本。Persona 与插件文本在每次请求中重复,成本随渲染内容增长。
+启用时,身份是每次请求的固定成本。Persona 前缀、后缀与插件文本在每次请求中重复,成本随渲染内容增长。
 
 #### KV Cache 影响
 
-只要身份、persona、变量、段文本与顺序的渲染完全相同,前缀就保持稳定:渲染未变时 surface 第 0 号节点保持不动。任何变更都会用新的 `system/message` 替换第 0 号节点,因此请求从第一个 token 起就不同,整个前缀的复用都会丢失
+渲染未变时 surface 第 0 号节点保持不动;渲染变化时,循环用新的 `system/message` 原地替换该节点。模型、persona 前缀、工具与前置指令一致时,不同源码路径、本地 Web URL 或 persona 后缀值不会改变可复用的第一方前缀。Persona 前缀变化可能改变靠前的前缀。任何变更都可能从第一个变化的 token 起使复用失效;不保证提供方共享缓存或实际命中率
 
 ### 工具 schema
 
@@ -167,7 +169,7 @@ schema token 在每次请求中重复。限制工具会为该 agent 移除其全
 
 这些限制说明提示词组装何时需要特别留意。它们是当前包约束,不是任务积压。
 
-- **部署方编写的提示词文本只来自配置/组合**:此插件拥有全局 persona 默认值;创建方插件可以注册 agent 作用域的遮蔽项;其他段来自拥有相应事实的插件。不存在终端用户提示词编辑 API。
+- **部署方编写的提示词文本只来自配置/组合**:此插件拥有全局 persona 前缀与后缀默认值;创建方插件可以注册 agent 作用域的遮蔽项;其他段来自拥有相应事实的插件。不存在终端用户提示词编辑 API。
 - **没有表示字面量 `{{…}}` 花括号的转义语法**:每个完整组都会按已注册变量插值;只有实际提示词需要转义时才会实现。
 - **`toolOrder` 配置错误在提示词组装(首轮)时出现,而不是启动时**:只有形状违规会在配置加载时抛出。
 

+ 29 - 13
packages/core/system-prompt/src/index.ts

@@ -120,9 +120,7 @@ export interface PromptAssembly {
 
 const SECTION_ORDERS = {
   HARNESS_IDENTITY: -1000,
-  HARNESS_SOURCE: -900,
-  WEB_SURFACE: -800,
-  DEPLOYMENT_PERSONA: 0,
+  DEPLOYMENT_PERSONA_PREFIX: 0,
   PLAN_POLICY: 500,
   TEAM_POLICY: 600,
   PTC_ONLY: 800,
@@ -149,6 +147,10 @@ const SECTION_ORDERS = {
   TOOLS_SDK: 5000,
   DELIVERABLE_FILE_REFERENCES: 9000,
   STRUCTURED_OUTPUT: 9900,
+  // Local paths and endpoints follow reusable instructions.
+  HARNESS_SOURCE: 10000,
+  WEB_SURFACE: 10100,
+  DEPLOYMENT_PERSONA_SUFFIX: 10200,
 } as const
 
 /** Name of a centrally allocated prompt-section position. */
@@ -164,12 +166,15 @@ const CONTEXT_ORDERS = {
 export type PromptContextOrderName = keyof typeof CONTEXT_ORDERS
 
 /**
- * The deployment persona's section name. Exported because a
+ * The deployment persona prefix's section name. Exported because a
  * composition can replace this slot — an agent preset shadows the
  * deployment's persona with its own — and both sides naming the same section
  * is what makes the replacement work rather than duplicate.
  */
-export const PERSONA_SECTION = 'deployment:persona'
+export const PERSONA_PREFIX_SECTION = 'deployment:persona-prefix'
+
+/** Deployment persona suffix section name shared by global and scoped contributions. */
+export const PERSONA_SUFFIX_SECTION = 'deployment:persona-suffix'
 
 /** Valid variable names: how they are written between the braces. */
 const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/
@@ -233,17 +238,22 @@ function compareToolNames(a: ToolSchema, b: ToolSchema): number {
   return compareNames(a.name, b.name)
 }
 
-/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.persona} for its contract). */
+/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
 export interface Config {
   /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
   includeHarnessIdentity?: boolean
   /** Include dynamic runtime-context snapshots in model history (default true). */
   includeRuntimeContext?: boolean
   /**
-   * Deployment-wide order-0 persona template. A scoped section named
-   * `deployment:persona` shadows it; `{{variable}}` references are strict.
+   * Deployment-wide persona prefix template before first-party guidance. A scoped section named
+   * `deployment:persona-prefix` shadows it; `{{variable}}` references are strict.
+   */
+  personaPrefix?: string
+  /**
+   * Persona suffix template after first-party guidance. A scoped `deployment:persona-suffix`
+   * section shadows it; `{{variable}}` references are strict. Defaults to empty.
    */
-  persona?: string
+  personaSuffix?: string
   /**
    * Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
    * Invalid fields fail at load and unknown names fail at assembly; known names
@@ -390,7 +400,8 @@ export class SystemPrompt extends Service {
   static Config: z<Config> = z.object({
     includeHarnessIdentity: z.boolean().default(true),
     includeRuntimeContext: z.boolean().default(true),
-    persona: z.string().default(''),
+    personaPrefix: z.string().default(''),
+    personaSuffix: z.string().default(''),
     // Preserve omission because an explicit empty order lacks the rest marker.
     toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
   })
@@ -413,10 +424,15 @@ export class SystemPrompt extends Service {
       })
     }
     this.section({
-      name: PERSONA_SECTION,
-      order: this.getSectionOrder('DEPLOYMENT_PERSONA'),
+      name: PERSONA_PREFIX_SECTION,
+      order: this.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),
       // The fallback narrows the optional input type; the schema already defaults it.
-      text: config.persona ?? '',
+      text: config.personaPrefix ?? '',
+    })
+    this.section({
+      name: PERSONA_SUFFIX_SECTION,
+      order: this.getSectionOrder('DEPLOYMENT_PERSONA_SUFFIX'),
+      text: config.personaSuffix ?? '',
     })
     if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext()
   }

+ 5 - 5
packages/core/system-prompt/tests/scoped.spec.ts

@@ -30,10 +30,10 @@ function scopeKeyOf(scope: Scope): ScopeKey {
 }
 
 describe('scoped sections', () => {
-  it('a scoped persona shadows deployment:persona for that scope only (either order)', async () => {
-    const ctx = await mount({ persona: 'You are the deployment.' })
+  it('a scoped persona shadows deployment:persona-prefix for that scope only (either order)', async () => {
+    const ctx = await mount({ personaPrefix: 'You are the deployment.' })
     const scope = await mintScope(ctx, 'child')
-    scope.ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You run tests.' })
+    scope.ctx.systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: 'You run tests.' })
 
     const scoped = renderPrompt(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))
     const global = renderPrompt(await ctx.systemPrompt.assemble())
@@ -82,7 +82,7 @@ describe('scoped sections', () => {
 
 describe('scoped variables', () => {
   it('a scoped variable shadows its global name-twin for that scope', async () => {
-    const ctx = await mount({ persona: 'Mode: {{mode}}.' })
+    const ctx = await mount({ personaPrefix: 'Mode: {{mode}}.' })
     const scope = await mintScope(ctx, 'child')
     ctx.systemPrompt.variable('mode', () => 'normal')
     scope.ctx.systemPrompt.variable('mode', () => 'strict')
@@ -103,7 +103,7 @@ describe('scoped variables', () => {
   })
 
   it('defers a scoped variable that replaces the last provider in its generation', async () => {
-    const ctx = await mount({ persona: 'Mode: {{mode}}.' })
+    const ctx = await mount({ personaPrefix: 'Mode: {{mode}}.' })
     const scope = await mintScope(ctx, 'child')
     const key = scopeKeyOf(scope)
     const calls: string[] = []

+ 68 - 16
packages/core/system-prompt/tests/system-prompt.spec.ts

@@ -7,20 +7,21 @@ import type { PromptContextOrderName, PromptSectionOrderName } from '@deepseek-a
 
 /**
  * Every assembly carries the plugin's own built-ins — `harness:identity`
- * and `deployment:persona` (from config). Tests about
+ * and `deployment:persona-prefix` / `deployment:persona-suffix` (from config). Tests about
  * registry MECHANICS strip them with {@link contributed} to stay focused on
  * their own sections; the built-ins' behavior is pinned by its own describe.
  */
-const BUILT_IN = ['harness:identity', 'deployment:persona']
+const BUILT_IN = ['harness:identity', 'deployment:persona-prefix', 'deployment:persona-suffix']
 const IDENTITY = 'You are an AI agent powered by DeepSeek Harness.'
 const SECTION_ORDER_NAMES = [
-  'HARNESS_IDENTITY', 'HARNESS_SOURCE', 'WEB_SURFACE', 'DEPLOYMENT_PERSONA',
+  'HARNESS_IDENTITY', 'DEPLOYMENT_PERSONA_PREFIX',
   'PLAN_POLICY', 'TEAM_POLICY', 'PTC_ONLY', 'FILE_REFERENCE', 'TOOL_BASH',
   'TOOL_PWSH', 'TOOL_READ', 'TOOL_WRITE', 'TOOL_EDIT', 'TOOL_GLOB',
   'TOOL_GREP', 'TOOL_JOBS', 'TOOL_PTY', 'TOOL_WEB_SEARCH', 'TOOL_WEB_FETCH',
   'TOOL_LSP', 'TOOL_SESSION_QUERY', 'TOOL_GOAL', 'TOOL_CORDIS', 'TOOL_WORKFLOW',
   'TOOL_RALPH', 'TOOL_SUBAGENT', 'TOOL_REPORT', 'TOOLS_SDK',
   'DELIVERABLE_FILE_REFERENCES', 'STRUCTURED_OUTPUT',
+  'HARNESS_SOURCE', 'WEB_SURFACE', 'DEPLOYMENT_PERSONA_SUFFIX',
 ] as const satisfies readonly PromptSectionOrderName[]
 const CONTEXT_ORDER_NAMES = [
   'SANDBOX_POLICY', 'APPROVAL_POLICY', 'SUBAGENT_DELEGATION',
@@ -40,6 +41,39 @@ describe('SystemPrompt', () => {
     expect(sorted.slice(1).every((order, index) => order - sorted[index]! >= 10)).toBe(true)
   })
 
+  it('keeps reusable instructions identical across local environments', async () => {
+    const ctx = new Context()
+    try {
+      await ctx.plugin(SystemPrompt, { personaPrefix: 'Model {{model}}.', personaSuffix: 'In {{cwd}} on {{platform}}.' })
+      let environment = { model: 'model-a', cwd: '/alice/project', platform: 'darwin', source: '/alice/dsh', url: 'http://127.0.0.1:3080' }
+      for (const key of ['model', 'cwd', 'platform'] as const) {
+        ctx.systemPrompt.variable(key, () => environment[key])
+      }
+      const reusable = SECTION_ORDER_NAMES.filter(name =>
+        !['HARNESS_IDENTITY', 'DEPLOYMENT_PERSONA_PREFIX', 'HARNESS_SOURCE', 'WEB_SURFACE', 'DEPLOYMENT_PERSONA_SUFFIX'].includes(name))
+      for (const name of [...reusable].reverse()) {
+        ctx.systemPrompt.section({ name, order: ctx.systemPrompt.getSectionOrder(name), text: name })
+      }
+      ctx.systemPrompt.section({
+        name: 'source', order: ctx.systemPrompt.getSectionOrder('HARNESS_SOURCE'), text: () => environment.source,
+      })
+      ctx.systemPrompt.section({
+        name: 'web', order: ctx.systemPrompt.getSectionOrder('WEB_SURFACE'), text: () => environment.url,
+      })
+      const first = renderPrompt(await ctx.systemPrompt.assemble())
+      environment = { model: 'model-a', cwd: 'C:/bob/project', platform: 'win32', source: 'C:/bob/dsh', url: 'http://127.0.0.1:4080' }
+      const second = renderPrompt(await ctx.systemPrompt.assemble())
+      const prefix = [IDENTITY, 'Model model-a.', ...reusable].join('\n\n') + '\n\n'
+      expect(first).toBe(prefix + '/alice/dsh\n\nhttp://127.0.0.1:3080\n\nIn /alice/project on darwin.')
+      expect(second).toBe(prefix + 'C:/bob/dsh\n\nhttp://127.0.0.1:4080\n\nIn C:/bob/project on win32.')
+      environment.model = 'model-b'
+      expect(renderPrompt(await ctx.systemPrompt.assemble()))
+        .toBe(second.replace('Model model-a.', 'Model model-b.'))
+    } finally {
+      await ctx.fiber.dispose()
+    }
+  })
+
   it('keeps repository context placements unique and integral', async () => {
     const ctx = new Context()
     await ctx.plugin(SystemPrompt, {})
@@ -49,19 +83,37 @@ describe('SystemPrompt', () => {
   })
 
   describe('built-in sections', () => {
+    it('renders the environment after guidance and reports its strict interpolation errors', async () => {
+      const ctx = new Context()
+      try {
+        await ctx.plugin(SystemPrompt, { personaPrefix: 'Model {{model}}.', personaSuffix: 'Workspace {{cwd}}.' })
+        ctx.systemPrompt.variable('model', () => 'm')
+        ctx.systemPrompt.section({ name: 'guidance', order: 100, text: 'Use tools.' })
+        const unresolved = await ctx.systemPrompt.assemble()
+        expect(() => renderPrompt(unresolved))
+          .toThrow('unknown prompt variable "{{cwd}}" in section "deployment:persona-suffix"')
+        ctx.systemPrompt.variable('cwd', () => '/work')
+        expect(renderPrompt(await ctx.systemPrompt.assemble()))
+          .toBe(`${IDENTITY}\n\nModel m.\n\nUse tools.\n\nWorkspace /work.`)
+      } finally {
+        await ctx.fiber.dispose()
+      }
+    })
+
     it('registers the harness identity and the configured deployment persona', async () => {
       const ctx = new Context()
-      await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness.' })
+      await ctx.plugin(SystemPrompt, { personaPrefix: 'You are DeepSeek Harness.' })
 
       const assembly = await ctx.systemPrompt.assemble()
       expect(assembly.sections.map(s => s.name)).toEqual([
         'harness:identity',
-        'deployment:persona',
+        'deployment:persona-prefix',
+        'deployment:persona-suffix',
       ])
       expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness.`)
       // The names are reserved by the plugin — one owner per section.
-      expect(() => ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'imposter' }))
-        .toThrow('prompt section "deployment:persona" is already registered')
+      expect(() => ctx.systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: 'imposter' }))
+        .toThrow('prompt section "deployment:persona-prefix" is already registered')
     })
 
     it('renders no persona section for a persona-less deployment (empty default)', async () => {
@@ -74,11 +126,11 @@ describe('SystemPrompt', () => {
       const ctx = new Context()
       await ctx.plugin(SystemPrompt, {
         includeHarnessIdentity: false,
-        persona: 'You are a helpful software engineer assistant.',
+        personaPrefix: 'You are a helpful software engineer assistant.',
       })
 
       const assembly = await ctx.systemPrompt.assemble()
-      expect(assembly.sections.map(section => section.name)).toEqual(['deployment:persona'])
+      expect(assembly.sections.map(section => section.name)).toEqual(['deployment:persona-prefix', 'deployment:persona-suffix'])
       expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.')
     })
 
@@ -112,7 +164,7 @@ describe('SystemPrompt', () => {
 
   it('assembles sections in order with context-resolved text and collected tools', async () => {
     const ctx = new Context()
-    await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness.' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: 'You are DeepSeek Harness.' })
 
     ctx.systemPrompt.section({ name: 'cwd', order: 20, text: () => 'cwd: /tmp' })
     ctx.systemPrompt.section({ name: 'rules', order: 10, text: 'Be precise.' })
@@ -121,8 +173,8 @@ describe('SystemPrompt', () => {
     ctx.systemPrompt.tools(() => ({ schemas: [{ name: 'echo', description: 'echo back', parameters: {} }] }))
 
     const assembly = await ctx.systemPrompt.assemble()
-    expect(assembly.sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona', 'rules', 'cwd'])
-    expect(assembly.sections.map(s => s.text)).toEqual([IDENTITY, 'You are DeepSeek Harness.', 'Be precise.', 'cwd: /tmp'])
+    expect(assembly.sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona-prefix', 'rules', 'cwd', 'deployment:persona-suffix'])
+    expect(assembly.sections.map(s => s.text)).toEqual([IDENTITY, 'You are DeepSeek Harness.', 'Be precise.', 'cwd: /tmp', ''])
     expect(assembly.contexts).toEqual([
       { name: 'earlier', text: 'context 1' },
       { name: 'later', text: 'context 2' },
@@ -307,8 +359,8 @@ describe('SystemPrompt', () => {
 
     const passed: AssembleContext = {}
     const assembly = await ctx.systemPrompt.assemble(passed)
-    expect(seen).toEqual([['harness:identity', 'deployment:persona', 'base', 'from-a']])
-    expect(assembly.sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona', 'base', 'from-a'])
+    expect(seen).toEqual([['harness:identity', 'deployment:persona-prefix', 'base', 'deployment:persona-suffix', 'from-a']])
+    expect(assembly.sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona-prefix', 'base', 'deployment:persona-suffix', 'from-a'])
     expect(contexts[0]).toBe(passed) // the caller's context reaches listeners
   })
 
@@ -368,7 +420,7 @@ describe('SystemPrompt', () => {
     firstParameters.properties['leak'] = { type: 'string' }
 
     const second = await ctx.systemPrompt.assemble()
-    expect(second.sections.map(section => section.name)).toEqual(['harness:identity', 'deployment:persona', 'base'])
+    expect(second.sections.map(section => section.name)).toEqual(['harness:identity', 'deployment:persona-prefix', 'base', 'deployment:persona-suffix'])
     expect(second.sections[0]!.text).toBe(IDENTITY)
     expect(second.contexts).toEqual([])
     expect(second.tools).toEqual([{ name: 't', description: 'tool', parameters: { type: 'object', properties: {} } }])
@@ -520,7 +572,7 @@ describe('SystemPrompt', () => {
 
     it('interpolates {{name}} references in section text at render — the persona included', async () => {
       const ctx = new Context()
-      await ctx.plugin(SystemPrompt, { persona: 'You run on {{model}} in {{cwd}}.' })
+      await ctx.plugin(SystemPrompt, { personaPrefix: 'You run on {{model}} in {{cwd}}.' })
       ctx.systemPrompt.variable('model', () => 'deepseek-v4')
       ctx.systemPrompt.variable('cwd', () => '/work')
 

+ 1 - 1
packages/core/system-prompt/tests/tool-order.spec.ts

@@ -7,7 +7,7 @@ function tool(name: string, description = name): ToolSchema {
   return { name, description, parameters: { type: 'object', properties: {} } }
 }
 
-async function mount(config: { persona?: string; toolOrder?: string[] } = {}): Promise<Context> {
+async function mount(config: { personaPrefix?: string; toolOrder?: string[] } = {}): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(SystemPrompt, config)
   return ctx

+ 1 - 1
packages/fs/tool-fs/tests/harness.ts

@@ -16,7 +16,7 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
 export async function fsHarness(fsCwd: string, persona = ''): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(SessionProjectionRegistry)
-  await mountAgentLoopTestDependencies(ctx, { systemPrompt: { persona } })
+  await mountAgentLoopTestDependencies(ctx, { systemPrompt: { personaPrefix: persona } })
   await ctx.plugin(AgentLoop, { agents: [] })
   await ctx.plugin(LlmDeepSeek)
   await ctx.plugin(LocalFileSystem, { cwd: fsCwd })

+ 2 - 2
packages/fs/tool-fs/tests/tools.spec.ts

@@ -196,11 +196,11 @@ describe('registration', () => {
     // withdraw both, not just the schemas.
     expect(ctx.tools.schemas()).toHaveLength(3)
     const sectionNames = (a: { sections: { name: string }[] }) => a.sections.map(s => s.name).sort()
-    expect(sectionNames(await ctx.systemPrompt.assemble())).toEqual(['deployment:persona', 'harness:identity', 'tool:edit', 'tool:read', 'tool:write'])
+    expect(sectionNames(await ctx.systemPrompt.assemble())).toEqual(['deployment:persona-prefix', 'deployment:persona-suffix', 'harness:identity', 'tool:edit', 'tool:read', 'tool:write'])
     await fiber.dispose()
     expect(ctx.tools.schemas()).toHaveLength(0)
     // Only the system-prompt plugin's own built-in sections remain.
-    expect(sectionNames(await ctx.systemPrompt.assemble())).toEqual(['deployment:persona', 'harness:identity'])
+    expect(sectionNames(await ctx.systemPrompt.assemble())).toEqual(['deployment:persona-prefix', 'deployment:persona-suffix', 'harness:identity'])
   })
 })
 

+ 1 - 1
packages/plan/plan-mode/tests/projection.spec.ts

@@ -22,7 +22,7 @@ interface Bench {
 async function harness(withPlanMode: boolean): Promise<Bench> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(UserQuestionService)
   await ctx.plugin(AgentRegistry)

+ 3 - 2
packages/preset/agent-presets/presets/cordis/agent.cordis.yml

@@ -17,8 +17,9 @@
 - id: persona
   name: '@deepseek-ai/dsh-persona'
   config:
-    text: |-
-      You are a coding agent powered by the {{model}} model, running on the DeepSeek Harness. Your working directory is {{cwd}}.
+    suffix: Your working directory is {{cwd}}.
+    prefix: |-
+      You are a coding agent powered by the {{model}} model, running on the DeepSeek Harness.
 
       You can read and modify the harness you run on. Its composition is Cordis: every capability is a plugin row in a `cordis.yml`, and an agent preset is one such file mounted for a single session.
 

+ 1 - 1
packages/preset/agent-presets/presets/minimal/agent.cordis.yml

@@ -9,7 +9,7 @@
 - id: persona
   name: '@deepseek-ai/dsh-persona'
   config:
-    text: You are a helpful software engineer assistant.
+    prefix: You are a helpful software engineer assistant.
     complete: true
     includeRuntimeContext: false
 

+ 3 - 2
packages/preset/agent-presets/presets/ptc/agent.cordis.yml

@@ -31,8 +31,9 @@
 - id: persona
   name: '@deepseek-ai/dsh-persona'
   config:
-    text: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    suffix: Your working directory is {{cwd}}.
+    prefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 - id: agent-instructions
   name: '@deepseek-ai/dsh-agent-instructions'

+ 3 - 2
packages/preset/agent-presets/presets/standard/agent.cordis.yml

@@ -24,8 +24,9 @@
 - id: persona
   name: '@deepseek-ai/dsh-persona'
   config:
-    text: >-
-      You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+    suffix: Your working directory is {{cwd}}.
+    prefix: >-
+      You are a coding agent powered by the {{model}} model.
 
 - id: agent-instructions
   name: '@deepseek-ai/dsh-agent-instructions'

+ 2 - 2
packages/preset/agent-presets/tests/composition-inventory.spec.ts

@@ -55,7 +55,7 @@ async function harness(roster: Config): Promise<Context> {
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(SessionProjectionRegistry)
@@ -377,7 +377,7 @@ describe('AgentPresets.compositionInventory', () => {
     ctx.loader.builtins['agent-presets'] = AgentPresets
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
-    await ctx.plugin(SystemPrompt, { persona: '' })
+    await ctx.plugin(SystemPrompt, { personaPrefix: '' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(SessionProjectionRegistry)

+ 1 - 1
packages/preset/agent-presets/tests/invariant.spec.ts

@@ -28,7 +28,7 @@ async function harness(roster: Partial<Config> = {}): Promise<Context> {
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(SessionProjectionRegistry)

+ 3 - 3
packages/preset/agent-presets/tests/mount.spec.ts

@@ -51,7 +51,7 @@ async function harness(roster: Config = { default: 'standard', roots: ROOTS, inc
   ctx.loader.builtins.group = Group
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(SessionProjectionRegistry)
@@ -459,7 +459,7 @@ describe('the preset file is an input, never a persistence target', () => {
     scoped.loader.builtins.group = Group
     await scoped.plugin(LlmRuntime)
     await scoped.plugin(SessionStore)
-    await scoped.plugin(SystemPrompt, { persona: '' })
+    await scoped.plugin(SystemPrompt, { personaPrefix: '' })
     await scoped.plugin(ToolRuntime)
     await scoped.plugin(AgentRegistry)
     await scoped.plugin(SessionProjectionRegistry)
@@ -648,7 +648,7 @@ describe('replacing a composition', () => {
     scoped.loader.builtins.group = Group
     await scoped.plugin(LlmRuntime)
     await scoped.plugin(SessionStore)
-    await scoped.plugin(SystemPrompt, { persona: '' })
+    await scoped.plugin(SystemPrompt, { personaPrefix: '' })
     await scoped.plugin(ToolRuntime)
     await scoped.plugin(AgentRegistry)
     await scoped.plugin(SessionProjectionRegistry)

+ 1 - 1
packages/preset/agent-presets/tests/remote.spec.ts

@@ -79,7 +79,7 @@ async function harness(
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(SessionProjectionRegistry)

+ 1 - 1
packages/preset/agent-presets/tests/settings.spec.ts

@@ -51,7 +51,7 @@ async function harness(
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(SystemPrompt, { persona: '' })
+  await ctx.plugin(SystemPrompt, { personaPrefix: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentLoop, { agents: [] })

+ 2 - 2
packages/preset/persona/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/preset/persona/README.md
-README.md: 753e40ebcb1848635ba4497d686b6ac8a56c31e3
-README.zh.md: 141784dc6d4214aae83298713327a1896445f41b
+README.md: 11f9fdc1d3968fdb9ba0792936a050aca04d4e8b
+README.zh.md: bbf702dc752f84b120a9540fed7e793f5bb692ce

+ 13 - 12
packages/preset/persona/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-persona` gives one agent its own persona: a preset mounts this composable row to register the `deployment:persona` system-prompt section, shadowing the deployment-wide persona for that session. It can also make that persona the session's complete system prompt, suppressing every other section, and can turn off dynamic runtime-context snapshots for the session. Mount it inside a preset composition — mounting it globally collides with the prompt registry's own persona registration and fails loud. Without this row, a preset could change an agent's tools but never its identity.
+`dsh-persona` gives one agent its own persona: a preset mounts this composable row to register persona prefix and suffix sections, shadowing the deployment-wide defaults for that session. It can also make the prefix the session's complete system prompt, suppressing every other section, and can turn off dynamic runtime-context snapshots for the session. Mount it inside a preset composition — mounting it globally collides with the prompt registry's own persona registration and fails loud. Without this row, a preset could change an agent's tools but never its identity.
 
 ## Table of Contents
 
@@ -25,27 +25,28 @@ English | [中文](README.zh.md)
 <a id="use-this-package"></a>
 ## Use this package
 
-Mount this row inside a preset composition to give that preset's sessions their own persona. The row needs an agent scope: mounted outside one it collides with the prompt registry's own `deployment:persona` registration and fails loud — the deployment persona already has an owner, and the whole point of this row is to shadow it for one agent.
+Mount this row inside a preset composition to give that preset's sessions their own persona. The row needs an agent scope: mounted outside one it collides with the prompt registry's own `deployment:persona-prefix` registration and fails loud — the deployment persona already has an owner, and the whole point of this row is to shadow it for one agent.
 
 ### Configuration
 
 ```yaml
 - name: '@deepseek-ai/dsh-persona'
   config:
-    text: You are a terse systems engineer who answers in short commands.
+    prefix: You are a terse systems engineer who answers in short commands.
 ```
 
 | Field | Default | Meaning |
 |---|---|---|
-| `text` | required | Persona prose rendered as the `deployment:persona` section |
-| `complete` | `false` | Restore this persona after assembly as the only system-prompt section |
+| `prefix` | required | Persona prose rendered as the `deployment:persona-prefix` section |
+| `suffix` | `''` | Template for `deployment:persona-suffix`; omitted or empty text shadows the global suffix away |
+| `complete` | `false` | Use only the rendered prefix as the system prompt; ignore the suffix |
 | `includeRuntimeContext` | `true` | Include dynamic runtime-context snapshots for this agent scope; false suppresses every context contribution without disabling its owning services |
 
 The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-persona) is the exhaustive source for every accepted field and its JSDoc.
 
 ### Persona behavior
 
-The persona `text` is a template: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Empty text still occupies the slot — it shadows the deployment persona away entirely, then disappears at render. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, but the prompt registry restores this exact persona as the sole section; no identity, tool guidance, or listener can append prompt text. With `includeRuntimeContext: false`, context providers are not evaluated for this scope and contexts added by assembly listeners are discarded.
+The persona `prefix` and `suffix` are templates: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Each empty template still shadows its deployment-wide section, then disappears at render. Omitted `suffix` defaults to empty; it does not inherit the global suffix. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, but the prompt registry restores this exact prefix as the sole section; no identity, suffix, tool guidance, or listener can append prompt text. With `includeRuntimeContext: false`, context providers are not evaluated for this scope and contexts added by assembly listeners are discarded.
 
 ### When to use it
 
@@ -61,18 +62,18 @@ Use this row when a preset must change an agent's identity and not only its tool
 
 ### How the row registers
 
-`apply` registers one prompt section through `ctx.systemPrompt.section({ name: PERSONA_SECTION, order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA'), text, complete? })` inside the mounting context's scope, so the section lands at order 0 — immediately after the harness identity opener — and only for agents joined to the preset. The shared section name makes a preset persona shadow the deployment's instead of landing beside it, while the service-owned order lookup keeps repository contributors on the central allocation. `includeRuntimeContext: false` calls `ctx.systemPrompt.suppressRuntimeContext()`.
+The row registers scoped persona prefix and suffix sections using the registry's shared names and named orders. Each shadows its deployment default instead of appearing beside it; the registry owns ordering, interpolation, and complete-prompt enforcement. `includeRuntimeContext: false` calls `ctx.systemPrompt.suppressRuntimeContext()`.
 
 ### Why the row is scope-only
 
-`dsh-system-prompt` owns the global persona as its own config and registers `deployment:persona` unconditionally, so a process has exactly one. This row collides with that registration outside an agent scope, by design: the row exists because a preset cannot mount the prompt registry itself.
+`dsh-system-prompt` owns the global persona as its own config and registers `deployment:persona-prefix` unconditionally, so a process has exactly one. This row collides with that registration outside an agent scope, by design: the row exists because a preset cannot mount the prompt registry itself.
 
 ### Source map
 
 | File | Role |
 |---|---|
 | [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, persona section registration, runtime-context suppression |
-| — | No runtime invariant companion is published; this row owns no event stream or mutable runtime data — it registers one prompt section and the prompt registry owns identity, complete-prompt enforcement, shadowing, and disposal. |
+| — | No runtime invariant companion is published; this row owns no event stream or mutable runtime data — it registers prompt sections and the prompt registry owns identity, complete-prompt enforcement, shadowing, and disposal. |
 
 </details>
 
@@ -96,15 +97,15 @@ Read these pages when the package-level contract is not enough; they move from t
 
 #### What the model sees
 
-The `deployment:persona` section at order 0, immediately after the harness identity opener, carrying exactly this row's configured `text` with prompt variables resolved. For an agent whose preset mounts this row, it replaces whatever persona the deployment configured. In complete mode, the model sees only this rendered section as its system prompt. Runtime context remains enabled by default; when disabled, a fresh agent receives no runtime-context snapshot from sandbox policy, approval policy, delegation, or another system-prompt context provider.
+The `deployment:persona-prefix` section at order `0` carries this row's `prefix`; `deployment:persona-suffix` at order `10200` carries its `suffix`, after first-party guidance. Both replace their deployment defaults and resolve prompt variables. In complete mode, the model sees only the rendered prefix section as its system prompt. Runtime context remains enabled by default; when disabled, a fresh agent receives no runtime-context snapshot from sandbox policy, approval policy, delegation, or another system-prompt context provider.
 
 #### Token effect
 
-Fixed for a given preset: the persona's own tokens on every request that agent makes, and none for any other agent. Empty text contributes nothing. Complete mode removes every other system-prompt token for that agent.
+Fixed for a given preset: the persona prefix and suffix tokens on every request that agent makes, and none for any other agent. Empty text contributes nothing. Complete mode removes every other system-prompt token for that agent.
 
 #### KV Cache effect
 
-Prefix-stable for the life of an agent — the row mounts once, before the agent is published and therefore before its first request, and its text never changes while the agent runs. Two agents on different presets establish different prefixes from this section onward; neither can invalidate the other's reuse.
+Prefix-stable while the rendered template variables and text are unchanged. Suffix changes leave preceding instructions unchanged when the model, prefix, and tools match. Prefix changes affect the early prefix; provider cache sharing is not guaranteed.
 
 ## Known Limitations and Deferred Work
 

+ 12 - 11
packages/preset/persona/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-persona` 让单个 agent(智能体)拥有自己的人设:preset 挂载这一可组装的行来注册 `deployment:persona` 系统提示词段落,为该会话遮蔽部署级人设。它还可以把人设变成该会话的完整系统提示词、抑制所有其他段落,并可为该会话关闭动态 runtime-context 快照。请把它挂在 preset 组装内部——全局挂载会与提示词注册表自身的人设注册相撞并明确报错。没有这一行,preset 能改变 agent 的工具,却永远改不了它的身份。
+`dsh-persona` 让单个 agent(智能体)拥有自己的人设:preset 挂载这一可组装的行来注册人设前缀与后缀段落,为该会话遮蔽部署级默认值。它还可以把前缀变成该会话的完整系统提示词、抑制所有其他段落,并可为该会话关闭动态 runtime-context 快照。请把它挂在 preset 组装内部——全局挂载会与提示词注册表自身的人设注册相撞并明确报错。没有这一行,preset 能改变 agent 的工具,却永远改不了它的身份。
 
 ## 目录
 
@@ -25,27 +25,28 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在 preset 组装内部挂载本行,让该 preset 的会话拥有自己的人设。本行需要 agent scope:在 scope 之外挂载会与提示词注册表自身的 `deployment:persona` 注册相撞并明确报错——部署级人设已经有归属,而本行存在的意义正是为某一个 agent 遮蔽它。
+在 preset 组装内部挂载本行,让该 preset 的会话拥有自己的人设。本行需要 agent scope:在 scope 之外挂载会与提示词注册表自身的 `deployment:persona-prefix` 注册相撞并明确报错——部署级人设已经有归属,而本行存在的意义正是为某一个 agent 遮蔽它。
 
 ### 配置
 
 ```yaml
 - name: '@deepseek-ai/dsh-persona'
   config:
-    text: You are a terse systems engineer who answers in short commands.
+    prefix: You are a terse systems engineer who answers in short commands.
 ```
 
 | 字段 | 默认值 | 含义 |
 |---|---|---|
-| `text` | 必填 | 作为 `deployment:persona` 段落渲染的人设文本 |
-| `complete` | `false` | 组装后将此人设恢复为唯一的系统提示词段落 |
+| `prefix` | 必填 | 作为 `deployment:persona-prefix` 段落渲染的人设文本 |
+| `suffix` | `''` | `deployment:persona-suffix` 模板;省略或空文本会遮蔽掉全局后缀 |
+| `complete` | `false` | 仅将渲染后的前缀用作系统提示词;忽略后缀 |
 | `includeRuntimeContext` | `true` | 是否为此 agent 作用域包含动态 runtime-context 快照;false 会抑制所有上下文贡献,但不禁用拥有它们的服务 |
 
 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-persona)是每个受支持字段及其 JSDoc 的穷尽式真源。
 
 ### 人设行为
 
-人设 `text` 是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。空文本同样占据该槽位——它会把部署级人设整个遮蔽掉,然后在渲染时消失。启用 `complete: true` 时,组装仍会解析上下文、工具、变量与协作式监听器,但提示词注册表会把这确切人设恢复为唯一段落;身份、工具引导或监听器都无法追加提示词文本。启用 `includeRuntimeContext: false` 时,此作用域的上下文提供方不会被求值,组装监听器添加的上下文也会被丢弃。
+人设 `prefix` 与 `suffix` 都是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。每个空模板仍会遮蔽对应的部署级段落,然后在渲染时消失。省略 `suffix` 时默认为空,不继承全局后缀。启用 `complete: true` 时,组装仍会解析上下文、工具、变量与协作式监听器,但提示词注册表会把这确切前缀恢复为唯一段落;身份、后缀、工具引导或监听器都无法追加提示词文本。启用 `includeRuntimeContext: false` 时,此作用域的上下文提供方不会被求值,组装监听器添加的上下文也会被丢弃。
 
 ### 何时使用
 
@@ -61,11 +62,11 @@ kind: "package-reference"
 
 ### 本行如何注册
 
-`apply` 在挂载上下文的 scope 内通过 `ctx.systemPrompt.section({ name: PERSONA_SECTION, order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA'), text, complete? })` 注册一个提示词段落,因此该段落落在 order 0——紧随 harness 身份开场白之后——且只对加入该 preset 的 agent 生效。共享段落名让 preset 人设遮蔽部署人设,而不是落在它旁边;服务持有的 order 查询则让仓库自带贡献方服从集中分配。`includeRuntimeContext: false` 会调用 `ctx.systemPrompt.suppressRuntimeContext()`。
+本行使用注册表共享的名称与具名顺序来注册带作用域的人设前缀与后缀段落。两者分别遮蔽对应的部署默认值,而不是出现在其旁边;排序、插值与完整提示词执行归注册表所有。`includeRuntimeContext: false` 会调用 `ctx.systemPrompt.suppressRuntimeContext()`。
 
 ### 本行为何仅限 scope 内使用
 
-`dsh-system-prompt` 以自身配置持有全局人设并无条件注册 `deployment:persona`,因此一个进程只有一份。本行在 agent scope 之外与该项注册相撞,这是刻意的:本行的存在是因为 preset 无法自行挂载提示词注册表。
+`dsh-system-prompt` 以自身配置持有全局人设并无条件注册 `deployment:persona-prefix`,因此一个进程只有一份。本行在 agent scope 之外与该项注册相撞,这是刻意的:本行的存在是因为 preset 无法自行挂载提示词注册表。
 
 ### 源码地图
 
@@ -96,15 +97,15 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-位于 order 0 的 `deployment:persona` 段落,紧随 harness 身份开场白之后,携带本行配置的 `text`,其中的提示词变量已解析。对于其 preset 挂载了本行的 agent,它会替换部署所配置的任何人设。在完整模式下,模型只会看到这个渲染后的段落作为系统提示词。Runtime context 默认保持启用;禁用后,新建 agent 不会收到来自沙箱策略、批准策略、委派或其他 system-prompt 上下文提供方的 runtime-context 快照。
+位于 order `0` 的 `deployment:persona-prefix` 段落携带本行的 `prefix`;位于 order `10200` 的 `deployment:persona-suffix` 在第一方指导之后携带其 `suffix`。两者分别替换对应的部署默认值,并解析提示词变量。在完整模式下,模型只会看到渲染后的前缀段落作为系统提示词。Runtime context 默认保持启用;禁用后,新建 agent 不会收到来自沙箱策略、批准策略、委派或其他 system-prompt 上下文提供方的 runtime-context 快照。
 
 #### Token 影响
 
-对给定 preset 而言是固定的:该 agent 的每次请求都携带人设自身的 token,其他 agent 一个都不带。空文本不贡献任何 token。完整模式会移除该 agent 的其他所有系统提示词 token。
+对给定 preset 而言是固定的:该 agent 的每次请求都携带人设前缀与后缀的 token,其他 agent 一个都不带。空文本不贡献任何 token。完整模式会移除该 agent 的其他所有系统提示词 token。
 
 #### KV Cache 影响
 
-在一个 agent 的整个生命周期内保持前缀稳定——本行只挂载一次,发生在 agent 发布之前、因而也在它的首个请求之前,且在 agent 运行期间文本不再改变。两个使用不同 preset 的 agent 从该段落起建立各自不同的前缀,谁都无法让对方失去缓存复用
+渲染后的模板变量与文本不变时,前缀保持稳定。模型、前缀与工具一致时,后缀变化不改变前置指令。前缀变化会影响靠前的前缀;不保证提供方共享缓存
 
 ## 已知限制与延期工作
 

+ 22 - 11
packages/preset/persona/src/index.ts

@@ -16,9 +16,9 @@
 import type { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import type {} from '@deepseek-ai/dsh-system-prompt'
-import { PERSONA_SECTION } from '@deepseek-ai/dsh-system-prompt'
+import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from '@deepseek-ai/dsh-system-prompt'
 
-export { PERSONA_SECTION }
+export { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION }
 
 /** Cordis plugin name. */
 export const name = 'persona'
@@ -29,12 +29,17 @@ export const inject = ['systemPrompt']
 /** Plugin config: the persona text this composition contributes. */
 export interface Config {
   /**
-   * Persona prose rendered as the `deployment:persona` section. A template:
+   * Persona prose rendered as the `deployment:persona-prefix` section. A template:
    * complete `{{…}}` groups interpolate strictly against registered prompt
    * variables. Empty text drops the section at render, matching the registry.
    */
-  text: string
-  /** Make this persona the complete system prompt, suppressing every other section. */
+  prefix: string
+  /**
+   * Persona suffix template rendered after first-party guidance. Omitted or empty
+   * text shadows the deployment suffix away; interpolation is strict.
+   */
+  suffix?: string
+  /** Make the prefix the complete system prompt, suppressing the suffix and every other section. */
   complete?: boolean
   /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */
   includeRuntimeContext?: boolean
@@ -42,23 +47,29 @@ export interface Config {
 
 /** Runtime schema for the persona row. */
 export const Config: z<Config> = z.object({
-  text: z.string().required(),
+  prefix: z.string().required(),
+  suffix: z.string().default(''),
   complete: z.boolean().default(false),
   includeRuntimeContext: z.boolean().default(true),
 })
 
 /**
- * Register the persona section for the mounting context's scope.
+ * Register the persona prefix and suffix sections for the mounting context's scope.
  * @param ctx - an agent scope context; an unscoped context collides with the
  * prompt registry's own persona registration and rejects.
- * @param config - the persona text and complete-prompt policy.
+ * @param config - the prefix, suffix, and complete-prompt policy.
  */
 export function apply(ctx: Context, config: Config): void {
   ctx.effect(() => ctx.systemPrompt.section({
-    name: PERSONA_SECTION,
-    order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA'),
-    text: config.text,
+    name: PERSONA_PREFIX_SECTION,
+    order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),
+    text: config.prefix,
     ...(config.complete ? { complete: true } : {}),
   }), 'persona.section()')
+  ctx.effect(() => ctx.systemPrompt.section({
+    name: PERSONA_SUFFIX_SECTION,
+    order: ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA_SUFFIX'),
+    text: config.suffix ?? '',
+  }), 'persona.suffix()')
   if (!(config.includeRuntimeContext ?? true)) ctx.systemPrompt.suppressRuntimeContext()
 }

+ 52 - 15
packages/preset/persona/tests/persona.spec.ts

@@ -3,26 +3,63 @@ import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
 import { createScope, type ScopeKey } from '@deepseek-ai/dsh-scope'
 import { describe, expect, it } from 'vitest'
 import * as Persona from '@deepseek-ai/dsh-persona'
-import { PERSONA_SECTION } from '@deepseek-ai/dsh-persona'
+import { PERSONA_SUFFIX_SECTION, PERSONA_PREFIX_SECTION } from '@deepseek-ai/dsh-persona'
 
 async function harness(deploymentPersona: string): Promise<Context> {
   const ctx = new Context()
-  await ctx.plugin(SystemPrompt, { persona: deploymentPersona })
+  await ctx.plugin(SystemPrompt, { personaPrefix: deploymentPersona })
   return ctx
 }
 
 /** The rendered text of the persona slot as one scope sees it. */
 async function personaText(ctx: Context, scope?: ScopeKey): Promise<string | undefined> {
   const assembly = await ctx.systemPrompt.assemble(scope === undefined ? {} : { scope })
-  return assembly.sections.find(section => section.name === PERSONA_SECTION)?.text
+  return assembly.sections.find(section => section.name === PERSONA_PREFIX_SECTION)?.text
 }
 
 describe('the persona row', () => {
+  it('shadows and interpolates the environment per scope, restoring both defaults on disposal', async () => {
+    const ctx = new Context()
+    try {
+      await ctx.plugin(SystemPrompt, { personaPrefix: 'Deployment.', personaSuffix: 'Global workspace.' })
+      ctx.systemPrompt.variable('cwd', () => '/local')
+      ctx.systemPrompt.section({ name: 'guidance', order: 100, text: 'Use tools.' })
+      const key: ScopeKey = { agent: 'environment' }
+      const scope = createScope(ctx, key)
+      const fiber = await scope.ctx.plugin(Persona, { prefix: 'Preset.', suffix: 'Workspace {{cwd}}.' })
+      const assembly = await ctx.systemPrompt.assemble({ scope: key })
+      expect(assembly.sections.find(section => section.name === PERSONA_SUFFIX_SECTION)?.text).toBe('Workspace {{cwd}}.')
+      expect(renderPrompt(assembly)).toBe('You are an AI agent powered by DeepSeek Harness.\n\nPreset.\n\nUse tools.\n\nWorkspace /local.')
+      expect(renderPrompt(await ctx.systemPrompt.assemble())).toContain('Global workspace.')
+      await fiber.dispose()
+      const restored = renderPrompt(await ctx.systemPrompt.assemble({ scope: key }))
+      expect(restored).toContain('Deployment.')
+      expect(restored).toContain('Global workspace.')
+      expect(restored).not.toContain('Workspace /local.')
+    } finally {
+      await ctx.fiber.dispose()
+    }
+  })
+
+  it.each([{}, { suffix: '' }])('shadows the default environment with an omitted or empty value: %j', async (environment) => {
+    const ctx = new Context()
+    try {
+      await ctx.plugin(SystemPrompt, { personaSuffix: 'Global workspace.' })
+      const key: ScopeKey = { agent: 'empty-environment' }
+      const scope = createScope(ctx, key)
+      await scope.ctx.plugin(Persona, { prefix: 'Preset.', ...environment })
+      expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: key }))).not.toContain('Global workspace.')
+      expect((await ctx.systemPrompt.assemble({ scope: key })).sections.find(section => section.name === PERSONA_SUFFIX_SECTION)?.text).toBe('')
+    } finally {
+      await ctx.fiber.dispose()
+    }
+  })
+
   it('rejects an unscoped mount, which would collide with the registry default', async () => {
     const ctx = await harness('deployment identity')
 
-    await expect(ctx.plugin(Persona, { text: 'composition identity' }))
-      .rejects.toThrow(/"deployment:persona" is already registered/)
+    await expect(ctx.plugin(Persona, { prefix: 'composition identity' }))
+      .rejects.toThrow(/"deployment:persona-prefix" is already registered/)
   })
 
   it('shadows the deployment default for one scope only', async () => {
@@ -30,7 +67,7 @@ describe('the persona row', () => {
     const key: ScopeKey = { agent: 'a1' }
     const scope = createScope(ctx, key)
 
-    await scope.ctx.plugin(Persona, { text: 'preset identity' })
+    await scope.ctx.plugin(Persona, { prefix: 'preset identity' })
 
     expect(await personaText(ctx, key)).toBe('preset identity')
     expect(await personaText(ctx)).toBe('deployment identity')
@@ -41,8 +78,8 @@ describe('the persona row', () => {
     const first: ScopeKey = { agent: 'a1' }
     const second: ScopeKey = { agent: 'a2' }
 
-    await createScope(ctx, first).ctx.plugin(Persona, { text: 'first identity' })
-    await createScope(ctx, second).ctx.plugin(Persona, { text: 'second identity' })
+    await createScope(ctx, first).ctx.plugin(Persona, { prefix: 'first identity' })
+    await createScope(ctx, second).ctx.plugin(Persona, { prefix: 'second identity' })
 
     expect(await personaText(ctx, first)).toBe('first identity')
     expect(await personaText(ctx, second)).toBe('second identity')
@@ -52,7 +89,7 @@ describe('the persona row', () => {
     const ctx = await harness('deployment identity')
     const key: ScopeKey = { agent: 'a1' }
 
-    await createScope(ctx, key).ctx.plugin(Persona, { text: '' })
+    await createScope(ctx, key).ctx.plugin(Persona, { prefix: '' })
 
     // The slot is still occupied, so the deployment persona is gone for this
     // agent; an empty section is dropped when the prompt renders.
@@ -64,7 +101,7 @@ describe('the persona row', () => {
     const ctx = await harness('deployment identity')
     const key: ScopeKey = { agent: 'a1' }
     const scope = createScope(ctx, key)
-    const fiber = await scope.ctx.plugin(Persona, { text: 'preset identity' })
+    const fiber = await scope.ctx.plugin(Persona, { prefix: 'preset identity' })
     expect(await personaText(ctx, key)).toBe('preset identity')
 
     await fiber.dispose()
@@ -77,7 +114,7 @@ describe('the persona row', () => {
     const key: ScopeKey = { agent: 'a1' }
     ctx.systemPrompt.variable('model', () => 'deepseek-v4-pro')
 
-    await createScope(ctx, key).ctx.plugin(Persona, { text: 'You run on {{model}}.' })
+    await createScope(ctx, key).ctx.plugin(Persona, { prefix: 'You run on {{model}}.' })
 
     // `assemble()` keeps section text uninterpolated; `renderPrompt()` is the
     // stage that resolves `{{…}}` against the assembly's variables.
@@ -92,14 +129,14 @@ describe('the persona row', () => {
     const scope = createScope(ctx, key)
     ctx.systemPrompt.section({ name: 'global:extra', order: 100, text: 'global guidance' })
 
-    await scope.ctx.plugin(Persona, { text: 'Only this.', complete: true })
+    await scope.ctx.plugin(Persona, { prefix: 'Only this.', suffix: '{{unknown}}', complete: true })
     scope.ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
       assembly.sections.push({ name: 'late:extra', text: 'late guidance' })
       return next()
     }, { prepend: true })
 
     const assembly = await ctx.systemPrompt.assemble({ scope: key })
-    expect(assembly.sections).toEqual([{ name: PERSONA_SECTION, text: 'Only this.' }])
+    expect(assembly.sections).toEqual([{ name: PERSONA_PREFIX_SECTION, text: 'Only this.' }])
     expect(renderPrompt(assembly)).toBe('Only this.')
   })
 
@@ -110,7 +147,7 @@ describe('the persona row', () => {
     ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
 
     const fiber = await scope.ctx.plugin(Persona, {
-      text: 'Only this.',
+      prefix: 'Only this.',
       includeRuntimeContext: false,
     })
     const suppressed = await ctx.systemPrompt.assemble({ scope: key })
@@ -132,7 +169,7 @@ describe('the persona row', () => {
     ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
 
     await ctx.plugin(Object.assign((inner: Context) => {
-      Persona.apply(createScope(inner, key).ctx, { text: 'Scoped identity.' })
+      Persona.apply(createScope(inner, key).ctx, { prefix: 'Scoped identity.' })
     }, { inject: ['systemPrompt'] }))
 
     expect((await ctx.systemPrompt.assemble({ scope: key })).contexts).toEqual([

+ 4 - 3
packages/shell/tool-bash/tests/tools.spec.ts

@@ -400,10 +400,11 @@ describe('bash tool', () => {
     const section = assembly.sections.find(s => s.name === 'tool:bash')
     expect(assembly.sections.map(s => s.name)).toEqual([
       'harness:identity',
-      'deployment:persona',
+      'deployment:persona-prefix',
       'test:before-bash',
       'tool:bash',
       'test:after-bash',
+      'deployment:persona-suffix',
     ])
     expect(section?.text).toContain('[exit code: N]')
   })
@@ -417,11 +418,11 @@ describe('bash tool', () => {
     await ctx.plugin(BashEnvPlugin)
     const fiber = await ctx.plugin(ToolBash)
     expect(ctx.tools.schemas()).toHaveLength(1)
-    expect((await ctx.systemPrompt.assemble()).sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona', 'tool:bash'])
+    expect((await ctx.systemPrompt.assemble()).sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona-prefix', 'tool:bash', 'deployment:persona-suffix'])
     await fiber.dispose()
     expect(ctx.tools.schemas()).toHaveLength(0)
     // Only the system-prompt plugin's own built-in sections remain.
-    expect((await ctx.systemPrompt.assemble()).sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona'])
+    expect((await ctx.systemPrompt.assemble()).sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona-prefix', 'deployment:persona-suffix'])
   })
 
   it('tools depend on the executor: no registration without ctx.shell', async () => {

+ 1 - 1
packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/child.patch.yml

@@ -7,7 +7,7 @@
 - id: system-prompt
   name: '@deepseek-ai/dsh-system-prompt'
   config:
-    persona: 'Echo where you run.'
+    personaPrefix: 'Echo where you run.'
 
 - id: agent-instructions
   name: '@deepseek-ai/dsh-agent-instructions'

+ 1 - 1
packages/subagent/subagent-spawn-in-process/tests/harness.ts

@@ -27,7 +27,7 @@ export async function spawnHarness(workdir: string): Promise<Context> {
   // own description.
   await ctx.plugin(SessionProjectionRegistry)
   await mountAgentLoopTestDependencies(ctx, {
-    systemPrompt: { persona: 'You are a coding agent. Report only when the requested work is done.' },
+    systemPrompt: { personaPrefix: 'You are a coding agent. Report only when the requested work is done.' },
   })
   await ctx.plugin(AgentLoop, { agents: [] })
   await ctx.plugin(LlmDeepSeek)

+ 2 - 2
packages/subagent/subagent/src/child-agent.ts

@@ -209,8 +209,8 @@ export function applyChildComposition(
   })
   if (composition.persona !== undefined) {
     childCtx.systemPrompt.section({
-      name: 'deployment:persona',
-      order: childCtx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA'),
+      name: 'deployment:persona-prefix',
+      order: childCtx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),
       text: composition.persona,
     })
   }

+ 1 - 1
packages/subagent/subagent/src/types.ts

@@ -149,7 +149,7 @@ export interface SubagentStartRequest {
   /**
    * Optional per-child persona. Requires {@link SubagentCapabilities.persona};
    * rejected at start otherwise. In-process backends register it as a scoped
-   * `deployment:persona` section on the child, SHADOWING the deployment's
+   * `deployment:persona-prefix` section on the child, SHADOWING the deployment's
    * persona for this child alone — same template semantics as the deployment
    * persona (strict `{{…}}` interpolation against the registered variables).
    */

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor