@morlay/dsh-context-assembler 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,181 +1,36 @@
1
1
  # @morlay/dsh-context-assembler
2
2
 
3
- 提示词注入能力组:**一个包四个能力**——主出口是组装插件(所以装配面只有一行),各能力另有子出口可单独装;
4
- 能力名就是子出口名。装配行是 `@morlay/dsh-context-assembler`(组装)或 `@morlay/dsh-context-assembler/<capability>`(单个),
5
- 见 [`src/rows.ts` 的 `contextChannel()`](./src/rows.ts)。引用展开原先也在这个包里,
6
- 现在独立成 [`@morlay/dsh-reference`](../dsh-reference/README.md)(它只挂 `agent/pre-step`、不依赖通道)。
3
+ 提示词通道:**装配结果上的文本转换**(`replace` / `suppress`)与**降级 section 的按步送达**(本步没留在系统提示词
4
+ 里的 section 改以 reminder 紧随用户消息送达,压缩后的重试再补投一次)。**不给扩展点**:没有注册面,也不接管任何
5
+ "面"——工作区指令与技能目录用官方行(`@deepseek-ai/dsh-agent-instructions` / `@deepseek-ai/dsh-tool-skill`)自己的
6
+ 注入方式。按会话的工具收口与三个注入开关(`instructions` / 技能目录 / 动态快照)**不在本包**:收口的输入是模式定义、
7
+ 唯一消费者也是模式,所以它住在 [`@morlay/dsh-session-mode`](../../profile/dsh-session-mode/README.md)。
7
8
 
8
- | 出口 | 行 id / 插件 name | 做什么 |
9
- | ---------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
10
- | `.` | `context-assembler`(插件 name `context-assembler-tree`) | **组装出口**:按 config 决定装哪些能力、各带什么参数(缺省即四套能力) |
11
- | `./assembler` | `context-assembler` | 注入通道:唯一渲染者与唯一覆盖判定处,发布 `ctx.contextAssembler` |
12
- | `./agent-instructions` | `context-agent-instructions` | 工作区指令链(`$DSH_HOME/AGENTS.md` + 项目根到 cwd 逐级) |
13
- | `./skill-catalog` | `context-skill-catalog` | skill 目录规则块 + 模型侧 `skill` 工具 |
14
- | `./scope` | `context-assembler-scope` | 按会话收口:工具白名单、instruction 总开关、动态快照开关(模式把定义推给它) |
9
+ ## 用法
15
10
 
16
- 规则、id 表与分层的 home 在 [上下文注入规则](./.agents/designs/20260921-上下文注入规则.md);术语见
17
- [本包 CONTEXT](./.agents/CONTEXT.md)。
18
-
19
- ## 装配
20
-
21
- `dsh.profile.bundles` 列出本包即装**一次**:`cordis.patch.yml` 是一行组装出口(显式给三项能力:通道本体、
22
- `agent-instructions`、`skill-catalog`;config 的 `capabilities` 缺省是全部四项)。**不做隔离**——工具说明
23
- ([`@morlay/dsh-agent-toolkit`](../../profile/dsh-agent-toolkit/README.md) 的 `guidance`)这类消费者住在别的包里,
24
- 隔离会把它们挡在组外(行停在 waiting,不报错);理由见
25
- [ADR 通道作为全局服务装配不隔离](./.agents/adrs/20260923-通道作为全局服务装配不隔离.md)。`scope` **不在这份 patch 里**
26
- ——它是模式那一层的收口行,由 [`@morlay/dsh-session-mode`](../../profile/dsh-session-mode/README.md) 的 patch 与模式定义
27
- 一起装(行 id 与 name 归本包的 `scopeRow()`,单一 home)。
28
-
29
- 装一次不等于每次都注入:这两条面各自按**会话**判归谁——`agent-instructions` 在预设自带那一行时**让位**,
30
- `skill-catalog` 反过来把同名工具注册进 agent 自己那一层**抢面**(见下面两节)。所以 `capabilities` 不在装配期
31
- 按模式裁。
32
-
33
- 工具说明(汉化 / 精简 / 用法分组)也不在这里:它归
34
- [`@morlay/dsh-agent-toolkit`](../../profile/dsh-agent-toolkit/README.md),同样在 profile 平面装一次。
35
-
36
- ## 装配形态
37
-
38
- 本包是 bundle:`cordis.patch.yml` 由 [`src/rows.ts`](./src/rows.ts) 从 [`src/rows.ts`](./src/rows.ts) 渲染,
39
- `rows` 出口导出同一份清单。`dsh.profile.bundles` 列出本包即装一次,**服务全局共享、不隔离**;模式的收口是
40
- 另一行(`scopeRow()`:工具白名单 / instruction / 动态快照),别的包的行(工具说明、skill 目录)直接 `inject`
41
- 同一份通道。
42
-
43
- ## 为什么合成一个包
44
-
45
- 这 4 个能力**总是一起装配**(通道在装配平面装一份、不隔离:注入方住在别的包里,隔离会把它们挡在组外),
46
- 注入方**全都只用通道的类型与服务面**。包边界在这里只是演进留下的:合成一个包之后,加一个能力 = 加一个子出口,装配面不动
47
- (组装出口按 config 装它)。
48
-
49
- 工具说明(汉化精简 + 用法分组)也不住在这里:它管的是"工具怎么被讲清楚",与工具的清单同源,
50
- 现在归 [`@morlay/dsh-agent-toolkit`](../../profile/dsh-agent-toolkit/README.md)(它也 `inject` 通道,装的是同一份
51
- 全局通道)。本包只做上下文重排。
52
-
53
- 引用展开不住在这里:它不依赖 `ctx.contextAssembler`(`inject` 只有 `skills`),也没有「读文件 + 字节预算」
54
- 以外与组装出口共享的东西,所以独立成包、由 [`@morlay/better-session`](../../bundles/better-session/cordis.patch.yml)
55
- 装配(见 [ADR-引用展开拆成独立包](../../../.agents/adrs/20260923-引用展开拆成独立包并按profile装配.md))。
56
-
57
- **每个出口仍是独立的 cordis 插件**(各自的 `apply` 与 `inject`)——这一点是硬要求:合成单入口会让 `inject`
58
- 变成并集(`agents, contextAssembler, skills, systemPrompt, tools`),任何一个可选搭档缺席都拖垮整包。
59
-
60
- ## assembler
61
-
62
- 注入的唯一通道。system prompt 里只留 `keep`(部署 persona + 覆盖规则),其余 section 按处置分流:丢弃
63
- (平台说明)、改写(两段中文文案)、或降级成规则块;pre-step 时把要送的内容逐条以 `<system-reminder>`
64
- user 消息注入(每条按文本幂等,只有变化的那条重发)。
65
-
66
- | 声明 | 到达方式 |
67
- | ------------------------------------- | -------------------------------------------------------------------------------- |
68
- | `registerSkill` + `on-demand` | 注册成模型可用 skill:目录常驻一行摘要,正文由 `skill` 工具加载 |
69
- | `registerSkill` + `auto` | 正文随 reminder 常驻;skill 标 `modelInvocable: false`(用户仍可 `skill:` 引用) |
70
- | `registerRule({ id, text, source? })` | 规则块,幂等键是 `id`;`source` 声明对外身份 |
71
- | `replaceSection` / `suppressSection` | 装配结果上的文本改写 / 丢弃 |
72
- | `setInstructions` / `restrictTools` | 按会话登记模式的两处收口(不要 instruction / 工具白名单) |
73
- | `visibleTools(agent, registered)` | 合成该会话的工具可见性:注册表可见 × 已登记的白名单(目录与组正文共用) |
74
- | `hiddenSkills(visible)` | 声明了 `requires` 而入口工具一个都不可见的 skill 名 |
75
-
76
- 配置(`keep` / `suppress` / `replace`)的默认值在 [`src/assembler/defaults.ts`](./src/assembler/defaults.ts)。
77
- **全局一份、不隔离**:模式差异由 `context-assembler-scope` 登记(`setInstructions` / `restrictTools`)收口,跨包的
78
- 消费者(工具说明、技能目录)直接 `inject` 同一份通道(见
79
- [ADR 通道作为全局服务装配不隔离](./.agents/adrs/20260923-通道作为全局服务装配不隔离.md))。
80
-
81
- ## agent-instructions
82
-
83
- `$DSH_HOME/AGENTS.md` 加项目根到 cwd 的逐级 `AGENTS.md` / `AGENTS.local.md`,**一条文件一条 id**
84
- (`agent-instructions:<根标识>:<文件>`,根标识是根目录的 8 位摘要——同进程两个项目根的同名文件因此不会互相
85
- 顶掉),文件变化时只重发变了的那一份。取代上游 `@deepseek-ai/dsh-agent-instructions`(host 那行由上游
86
- web-app bundle 自己设在装配层;我们那份由本包的组装出口在 profile 平面装)——**在该会话的 preset 行清单没有
87
- 上游那一行时**。
88
-
89
- - **preset 自带那一行时让位**:官方 `standard` / `ptc` / `cordis` 的 preset 行清单里有
90
- `@deepseek-ai/dsh-agent-instructions`(`minimal` 没有),装上的会话由 preset 侧注入,这一条一条都不注入;
91
- `chat` 挂的 `minimal` 会话仍由我们提供。判据与理由见
92
- [ADR-工作区指令让位skill面由通道抢面](./.agents/adrs/20260929-工作区指令让位skill面由通道抢面.md)。
93
- - **不跟踪 `read`/`write`/`edit`**:上游靠 touch 上浮触发刷新;本部署的 `AGENTS.md` 几乎不变,按
94
- `mtime:size` 对账足够。
95
- - **超预算可见**:单文件超过 `maxBytes` 时截断并留一行提示,不静默丢内容。
96
- - **与上游的 baseline 认领对齐**:`source` 带 `kind: "agent-instructions"` + `baseline: true` +
97
- `baselineIdentity`(身份与上游 `workspaceBaselineIdentity` 逐字相等)。上游的认领判据只看本步 claimed 的
98
- 消息与会话 surface,所以它救不了首步两侧同时注入(那由让位管),但管得住另一件事:**我们注入过基线的历史
99
- 会话在 preset 会话里恢复时**,上游认得出那条、不再补一份自己的模板("Use them as guidance…")。
100
- - 配置:`instructionFileCandidates` / `localInstructionFileCandidates`(覆盖成只读 `AGENTS*`,不含 CLAUDE
101
- 系列)、`maxBytes`(65536)、`dshHome`。
102
-
103
- ## skill-catalog
104
-
105
- `skill-catalog` 规则块(`<available_skills>`,一行 `名字: 摘要`,只列模型可调用的 skill)加模型侧 `skill`
106
- 工具。取代上游 `@deepseek-ai/dsh-tool-skill`(`skill-filesystem` 保留——它提供 skill 发现)。目录变更走同 id
107
- 覆盖;`auto` 的 skill 不进目录(正文已随提示送达)。
108
-
109
- **这一面归通道**(工作区指令那一面相反,是让位):`skill` 工具按**会话**注册进 agent 自己那一层,最近的一层
110
- 赢,遮蔽 preset 那份;上游的目录发布判据是"它自己注册的那个工具是本会话可见的那个",被遮蔽之后它不发目录,
111
- 目录由我们发布。要点:
112
-
113
- - 目录那条注入消息的 **kind 是我们自己的**(`context-assembler` + `form: "catalog"` + `entries`)。上游把任何
114
- `kind: "skill-catalog"` 且条目可读的消息当成它自己的账本:kind 用它的,它会在首步删掉我们这条、在有可见
115
- 目录之后补一条"没有可用 skill"把目录顶掉(它文档里的 visibility-loss 语义)。`form` 与 `entries` 照旧,
116
- 客户端那张条目表不受影响;注入行的标签变成 `context-assembler`。
117
- - 按会话注册随 agent 收回;这一行重挂(HMR / 设置面)时不再重复注册同一份定义。
118
- - `chat`(挂 `minimal`,没有上游那一行):工具与目录同样是我们提供的,但它的模式定义不收这两样
119
- (`instructions: false`、白名单里没有 `skill`)——白名单把它挡在模型目录外,执行层 guard 拒绝调用。
120
-
121
- 取舍与上游判据见 [ADR-工作区指令让位skill面由通道抢面](./.agents/adrs/20260929-工作区指令让位skill面由通道抢面.md)。
122
-
123
- ## 工具说明(不在这个包里)
124
-
125
- 工具的汉化精简与用法分组归 [`@morlay/dsh-agent-toolkit`](../../profile/dsh-agent-toolkit/README.md) 的 `guidance` 出口:
126
- 族索引(加一个工具改一族)、组 skill、短描述投影、丢弃清单都在那边。它 `inject` 通道,装的是同一份全局通道
127
- ——本包只提供它们要用的通道,不碰工具怎么讲。
128
-
129
- ## scope
130
-
131
- 按会话收口的出口(行 id `context-assembler-scope`):工具白名单(装配期投影 + 执行层 guard + 与工具同源的
132
- `tool:<工具名>` 说明 section,三侧同判据)、`instructions`(关掉一切 instruction 类送达:常驻组正文、规则块
133
- 与降级 section;按需 skill 与 `@` 引用材料不受它管)、`runtimeContext`
134
- (关掉沙箱 / 审批那两条动态快照,按 scope 抑制)。
135
-
136
- **定义由消费方推给它**,它自己不认识"模式":`ctx.sessionToolScope.apply(agent, mode)` 是唯一入口,落在该
137
- agent 的 `ctx` 上(抑制器、guard 都在那里),同一个 agent 再 apply 一次就是换一份——旧的 effect 树收回、按新
138
- 的装一遍。它解决的是"模式只能决定加什么、管不了 host 平面":`dsh.profile.bundles` 打开的包(工具行、
139
- 实验性的 Agent Teams)在 host 平面插行,对所有会话一视同仁;模式要表达"我只有这几个工具、一条提示词都不要"
140
- 时,唯一与来源无关的做法是在会话语义上收口。
141
-
142
- 白名单同时登记给通道(`restrictTools`):技能目录与用法组正文按 `visibleTools` 收口,所以被收窄的工具既不在
143
- 目录、也不会被讲——注册表里"装着"不等于这个会话"能用"。
144
-
145
- ## 装配
146
-
147
- **profile 平面**:`dsh.profile.bundles` 列出本包即装一次,`cordis.patch.yml` 就是一行组装出口:
11
+ 装配面只有一行:包根(`@morlay/dsh-context-assembler`)就是通道本体,config 是 `keep` / `suppress` / `replace` 的
12
+ 缺省。行清单的真源是 [`src/rows.ts`](./src/rows.ts)(`contextChannel()`,经 `./rows` 出口供装配层引用),本部署把它
13
+ 渲染进 [`@morlay/session-mode-profile`](../../bundles/session-mode-profile/cordis.patch.yml) 的 patch:
148
14
 
149
15
  ```yaml
150
16
  - insert:
151
17
  - id: context-assembler
152
18
  name: "@morlay/dsh-context-assembler"
153
- config:
154
- capabilities: [assembler, agent-instructions, skill-catalog]
155
- ```
156
-
157
- **模式平面**:收口那一行(不带 config——模式定义在 `session-mode` 行的 `config.modes` 里)由
158
- [`@morlay/dsh-session-mode`](../../profile/dsh-session-mode/README.md) 的 patch 装,通道由 profile 那份共享:
159
-
160
- ```yaml
161
- - id: context-assembler-scope
162
- name: "@morlay/dsh-context-assembler/scope"
163
19
  ```
164
20
 
165
- 几点硬要求:
21
+ | 出口 | 行 id / 插件 name | 做什么 |
22
+ | -------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `.` | `context-assembler` | 通道本体:装配结果上的文本转换 + 降级 section 的按步送达,发布 `ctx.contextAssembler`;config 是 `keep` / `suppress` / `replace` |
24
+ | `./rows` | —(不是插件,只出那一行) | `contextChannel()`,装配层据此渲染 patch |
166
25
 
167
- - **每个能力仍是独立的 cordis 插件**(组装出口只是 `ctx.plugin()` 装它们),各自带自己的 `inject`;合成单入口会让
168
- `inject` 变并集,一个可选搭档缺席就拖垮整包。
169
- - **通道不做隔离**:消费者跨包(`@morlay/dsh-agent-toolkit` 的工具说明、本包的技能目录),隔离会把它们挡在组外
170
- (行停在 waiting);模式差异改由 `context-assembler-scope` 登记给通道,见
171
- [ADR 通道作为全局服务装配不隔离](./.agents/adrs/20260923-通道作为全局服务装配不隔离.md)。真装配的判据是
172
- `pnpm exec tsx packages/desktop/dsh-desktop-host/tool/verify-session-mode.mts`(装配层可见一份、收口行装上、
173
- 官方 agent preset 不在)。
174
- - **子出口仍可单独装配**(`@morlay/dsh-context-assembler/assembler` 等):同一些插件,只是默认走组装出口。
26
+ **通道全局一份、不隔离**:消费者([`@morlay/dsh-tool-guidance` 的包根](../../profile/dsh-tool-guidance/README.md),
27
+ 以及把 `instructions` 开关拨过来的 `dsh-session-mode` 收口)住在别的包里,隔离会让它们解析不到服务(行停在 waiting,
28
+ 不报错)。模式差异不在通道上表达:通道只认按 agent 推来的那一个开关(`setInstructions`),收口与模式定义都在
29
+ `@morlay/dsh-session-mode` 里。
175
30
 
176
- ## 维护注意
31
+ 工具说明(汉化精简 + 用法分组)不在这个包里:它归 [`@morlay/dsh-tool-guidance`](../../profile/dsh-tool-guidance/README.md)
32
+ (包根即运行时),对通道的依赖只剩 `suppressSection` 一处;引用展开也是独立包
33
+ [`@morlay/dsh-reference`](../dsh-reference/README.md)(它不依赖通道服务)。
177
34
 
178
- - 测试落点在 `src/__tests__/<capability>/`(根 vitest 的 include 是 `packages/**/src/__tests__/**`,所以不能
179
- 按能力就近放 `src/<capability>/__tests__/`)。
180
- - 读上游文件的覆盖性测试用 `process.cwd()`(vitest 从仓库根跑),不要按文件深度算相对路径。
181
- - 新增一个能力:加 `src/<capability>/`,再把它登记进 `src/index.ts` 的 `CAPABILITIES`(子出口与 tsdown 入口一起加);装配面不动。
35
+ 规则与 id 见[设计 上下文注入规则](./.agents/designs/20260921-上下文注入规则.md);本包留什么、三份默认清单的归属见
36
+ [设计 通道只做转换](./.agents/designs/20260929-通道只做转换.md)。
package/dist/index.d.mts CHANGED
@@ -1,25 +1,45 @@
1
- import { t as index_d_exports } from "./agent-instructions.mjs";
2
- import { r as index_d_exports$1 } from "./index-zhmD45A9.mjs";
3
- import { t as index_d_exports$2 } from "./scope.mjs";
4
- import { t as index_d_exports$3 } from "./skill-catalog.mjs";
5
1
  import z from "@deepseek-ai/schemastery";
6
- import { Context } from "@deepseek-ai/cordis";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
3
+ import { Agent } from "@deepseek-ai/dsh-agent";
4
+ //#region src/defaults.d.ts
5
+ declare const DEFAULT_KEEP: readonly string[];
6
+ declare const DEFAULT_SUPPRESS: readonly string[];
7
+ declare const DEFAULT_REPLACE: Readonly<Record<string, string>>;
8
+ //#endregion
9
+ //#region src/channel.d.ts
10
+ declare class ContextAssembler extends Service {
11
+ private readonly replacements;
12
+ private readonly suppressed;
13
+ private readonly states;
14
+ private readonly withoutInstructions;
15
+ constructor(ctx: Context);
16
+ replaceSection(name: string, text: string): () => void;
17
+ suppressSection(name: string): () => void;
18
+ isSuppressed(name: string): boolean;
19
+ replacement(name: string): string | undefined;
20
+ setInstructions(agent: Agent, on: boolean): void;
21
+ collect(agent: Agent): Map<string, string>;
22
+ private sectionsOf;
23
+ sync(agent: Agent, input: {
24
+ readonly sections: ReadonlyMap<string, string>;
25
+ }): void;
26
+ private stateOf;
27
+ }
28
+ declare module "@deepseek-ai/cordis" {
29
+ interface Context {
30
+ contextAssembler: ContextAssembler;
31
+ }
32
+ }
33
+ //#endregion
7
34
  //#region src/index.d.ts
8
- declare const name = "context-assembler-tree";
9
- declare const CAPABILITIES: {
10
- readonly assembler: typeof index_d_exports$1;
11
- readonly "agent-instructions": typeof index_d_exports;
12
- readonly "skill-catalog": typeof index_d_exports$3;
13
- readonly scope: typeof index_d_exports$2;
14
- };
15
- type Capability = keyof typeof CAPABILITIES;
35
+ declare const name = "context-assembler";
36
+ declare const inject: string[];
16
37
  interface Config {
17
- /** 装哪些能力(名字即子出口名);缺省全部——标准模式要完整的一套。未知名字在装载时报错。 */
18
- capabilities?: string[];
19
- /** 各能力自己的 config,按能力名给(例如 `{ scope: { allowTools: [...] } }`)。 */
20
- options?: Record<string, Record<string, unknown>>;
38
+ keep?: string[];
39
+ suppress?: string[];
40
+ replace?: Record<string, string>;
21
41
  }
22
42
  declare const Config: z<Config>;
23
43
  declare function apply(ctx: Context, config: Config): void;
24
44
  //#endregion
25
- export { Capability, Config, apply, name };
45
+ export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, apply, inject, name };
package/dist/index.mjs CHANGED
@@ -1,29 +1,194 @@
1
- import { n as agent_instructions_exports } from "./agent-instructions-CIgUVIeo.mjs";
2
- import { r as assembler_exports } from "./assembler-CDMIBAi8.mjs";
3
- import { t as scope_exports } from "./scope.mjs";
4
- import { t as skill_catalog_exports } from "./skill-catalog.mjs";
5
1
  import z from "@deepseek-ai/schemastery";
6
- //#region src/index.ts
7
- const name = "context-assembler-tree";
8
- const CAPABILITIES = {
9
- assembler: assembler_exports,
10
- "agent-instructions": agent_instructions_exports,
11
- "skill-catalog": skill_catalog_exports,
12
- scope: scope_exports
2
+ import { Service } from "@deepseek-ai/cordis";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
5
+ //#region src/reminder.ts
6
+ const REMINDER_OPEN = "<system-reminder";
7
+ const REMINDER_CLOSE = "</system-reminder>";
8
+ const RULES_SECTION = "assembler:rules";
9
+ function promptEntryIdOf(source) {
10
+ const id = source.id;
11
+ return typeof id === "string" ? id : void 0;
12
+ }
13
+ function escapeFrameBody(body) {
14
+ return body.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
15
+ }
16
+ function escapeAttr(value) {
17
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
18
+ }
19
+ function renderReminder(id, body) {
20
+ return `${REMINDER_OPEN} id="${escapeAttr(id)}">\n${escapeFrameBody(body)}\n${REMINDER_CLOSE}`;
21
+ }
22
+ function latestReminderText(agent, id) {
23
+ for (const seq of agent.session.surface.nodes.toReversed()) {
24
+ const event = agent.session.eventAt(seq);
25
+ if (event?.type !== "user/message") continue;
26
+ if (promptEntryIdOf(event.data.source) !== id) continue;
27
+ const [block] = event.data.content;
28
+ return event.data.content.length === 1 && block?.type === "text" ? block.text : "";
29
+ }
30
+ }
31
+ function reminderMessage(key, text) {
32
+ return createUserMessage({
33
+ content: [{
34
+ type: "text",
35
+ text
36
+ }],
37
+ source: {
38
+ kind: "context-assembler",
39
+ form: "instructions",
40
+ id: key
41
+ }
42
+ });
43
+ }
44
+ //#endregion
45
+ //#region src/channel.ts
46
+ var ContextAssembler = class extends Service {
47
+ replacements = /* @__PURE__ */ new Map();
48
+ suppressed = /* @__PURE__ */ new Set();
49
+ states = /* @__PURE__ */ new WeakMap();
50
+ withoutInstructions = /* @__PURE__ */ new WeakSet();
51
+ constructor(ctx) {
52
+ super(ctx, "contextAssembler");
53
+ }
54
+ replaceSection(name, text) {
55
+ this.replacements.set(name, text);
56
+ return () => {
57
+ if (this.replacements.get(name) === text) this.replacements.delete(name);
58
+ };
59
+ }
60
+ suppressSection(name) {
61
+ this.suppressed.add(name);
62
+ return () => {
63
+ this.suppressed.delete(name);
64
+ };
65
+ }
66
+ isSuppressed(name) {
67
+ return this.suppressed.has(name);
68
+ }
69
+ replacement(name) {
70
+ return this.replacements.get(name);
71
+ }
72
+ setInstructions(agent, on) {
73
+ if (on) this.withoutInstructions.delete(agent);
74
+ else this.withoutInstructions.add(agent);
75
+ }
76
+ collect(agent) {
77
+ const entries = /* @__PURE__ */ new Map();
78
+ if (this.withoutInstructions.has(agent)) return entries;
79
+ for (const [id, text] of this.sectionsOf(agent)) entries.set(id, renderReminder(id, text));
80
+ return entries;
81
+ }
82
+ sectionsOf(agent) {
83
+ return this.states.get(agent)?.sections ?? /* @__PURE__ */ new Map();
84
+ }
85
+ sync(agent, input) {
86
+ this.stateOf(agent).sections = input.sections;
87
+ }
88
+ stateOf(agent) {
89
+ const existing = this.states.get(agent);
90
+ if (existing !== void 0) return existing;
91
+ const state = { sections: /* @__PURE__ */ new Map() };
92
+ this.states.set(agent, state);
93
+ return state;
94
+ }
95
+ };
96
+ //#endregion
97
+ //#region src/defaults.ts
98
+ const DEFAULT_KEEP = [
99
+ PERSONA_PREFIX_SECTION,
100
+ PERSONA_SUFFIX_SECTION,
101
+ RULES_SECTION
102
+ ];
103
+ const DEFAULT_SUPPRESS = [
104
+ "harness:identity",
105
+ "harness:source",
106
+ "app:web-surface",
107
+ "tool:lsp",
108
+ "tool:pty",
109
+ "tool:session-query",
110
+ "tool:cordis",
111
+ "tool:ralph"
112
+ ];
113
+ const DEFAULT_REPLACE = {
114
+ "context:file-reference": "以 @ 开头的词是用户显式引用的工作区路径,相对于工作区根。结尾带斜杠表示目录:需要时列目录内容。其余是文件:需要内容时用 read 工具读取,不要在读取前声称自己已经看过。用 @\"...\" 括住含空格的路径。",
115
+ "ui:deliverable-file-references": "成功创建或修改文件后,在最终回复里点明主要产物。除命令、配置表达式与代码块之外,每处提到已存在的文件都要链到它的完整路径(相对工作目录或绝对路径);已知行号时在目标后加 `#L24` 或 `#L24-L30`。链接文本用文件名或清晰别名,只加够区分的父目录,不要把完整路径放进标签;默认只用文件名,需要精确位置时在标签后接 `:24` 或 `:24–30`(不带 `#` 或 `L`)。"
13
116
  };
14
- const CAPABILITY_NAMES = Object.keys(CAPABILITIES);
117
+ //#endregion
118
+ //#region src/rules.ts
119
+ const RULES_TEXT = ["system-reminder 与 skill_content 块是本系统提示词的一部分,持续有效:同一条内容的最新一条取代更早的(带 id 的块以 id 为准,其余按来源),不同来源互不影响。", "skill_content 是要遵循的技能说明(随请求送来的与你后来加载的一样),file_content 是本步引用的文件内容。"].join("");
120
+ //#endregion
121
+ //#region src/index.ts
122
+ const name = "context-assembler";
123
+ const RULES_ORDER = 1;
124
+ const inject = ["systemPrompt"];
15
125
  const Config = z.object({
16
- capabilities: z.array(z.string()).default([...CAPABILITY_NAMES]),
17
- options: z.any().default({})
126
+ keep: z.array(z.string()).default([...DEFAULT_KEEP]),
127
+ suppress: z.array(z.string()).default([...DEFAULT_SUPPRESS]),
128
+ replace: z.dict(z.string()).default({ ...DEFAULT_REPLACE })
18
129
  });
19
130
  function apply(ctx, config) {
20
- const wanted = config.capabilities ?? CAPABILITY_NAMES;
21
- const options = config.options ?? {};
22
- for (const name of wanted) {
23
- const capability = CAPABILITIES[name];
24
- if (capability === void 0) throw new Error(`context: unknown capability "${name}"(可选:${CAPABILITY_NAMES.join(" / ")})`);
25
- ctx.plugin(capability, options[name] ?? {});
26
- }
131
+ const channel = new ContextAssembler(ctx);
132
+ const keep = new Set(config.keep ?? DEFAULT_KEEP);
133
+ for (const name of config.suppress ?? DEFAULT_SUPPRESS) channel.suppressSection(name);
134
+ for (const [name, text] of Object.entries(config.replace ?? DEFAULT_REPLACE)) channel.replaceSection(name, text);
135
+ ctx.systemPrompt.section({
136
+ name: RULES_SECTION,
137
+ order: RULES_ORDER,
138
+ text: RULES_TEXT
139
+ });
140
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
141
+ const result = await next();
142
+ const agent = context.agent;
143
+ const demoted = /* @__PURE__ */ new Map();
144
+ const sections = [];
145
+ let changed = false;
146
+ for (const section of result.sections) {
147
+ if (channel.isSuppressed(section.name)) {
148
+ changed = true;
149
+ continue;
150
+ }
151
+ const text = channel.replacement(section.name) ?? section.text;
152
+ if (text !== section.text) changed = true;
153
+ if (keep.has(section.name) || text.length === 0) {
154
+ sections.push(text === section.text ? section : {
155
+ ...section,
156
+ text
157
+ });
158
+ continue;
159
+ }
160
+ changed = true;
161
+ demoted.set(sectionId(section.name), text);
162
+ }
163
+ if (agent !== void 0) channel.sync(agent, { sections: demoted });
164
+ return changed ? {
165
+ ...result,
166
+ sections
167
+ } : result;
168
+ });
169
+ ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
170
+ const decision = await next();
171
+ if (decision.kind === "reject" || decision.messages.length === 0) return decision;
172
+ const pending = pendingEntries(agent, channel);
173
+ if (pending.length === 0) return decision;
174
+ const claimedEnd = decision.messages.findLastIndex((message) => messages.includes(message));
175
+ return {
176
+ ...decision,
177
+ messages: decision.messages.toSpliced(claimedEnd + 1, 0, ...pending.map(([key, text]) => reminderMessage(key, text)))
178
+ };
179
+ });
180
+ ctx.on("agent/request-error", async ({ agent, signal }, next) => {
181
+ const action = await next();
182
+ if (action?.kind !== "retry" || signal.aborted) return action;
183
+ for (const [key, text] of pendingEntries(agent, channel)) agent.session.append("user/message", reminderMessage(key, text), { surfaceOp: "append" });
184
+ return action;
185
+ }, { prepend: true });
186
+ }
187
+ function sectionId(name) {
188
+ return `section:${name}`;
189
+ }
190
+ function pendingEntries(agent, channel) {
191
+ return [...channel.collect(agent)].filter(([key, text]) => latestReminderText(agent, key) !== text).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
27
192
  }
28
193
  //#endregion
29
- export { Config, apply, name };
194
+ export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, apply, inject, name };
package/dist/rows.d.mts CHANGED
@@ -1,48 +1,10 @@
1
1
  //#region src/rows.d.ts
2
- /**
3
- * 本包作为 **bundle** 的行清单:`dsh.profile.bundles` 列出本包时由它渲染成 `cordis.patch.yml`。
4
- *
5
- * 通道装在 host 平面,整份部署共享一套注入。**不做隔离**——工具说明
6
- * (`@morlay/dsh-agent-toolkit/guidance`,在别的包里)与 skill 目录这类消费者都在别的行上
7
- * `inject` 它,隔离会把它们挡在组外(行停在 waiting,不报错)。
8
- *
9
- * 本包自己的 bundle 只装通道(`PATCH_ROWS`)。模式的收口是另一行(`scopeRow()`),它由
10
- * [`@morlay/dsh-session-mode`](../../../profile/dsh-session-mode/README.md) 的 patch 与模式定义一起装——
11
- * 那一行的 config 就是模式清单。
12
- */
13
- /** 组装出口(包根)就是那个"按 config 装能力"的插件。 */
14
2
  declare const CONTEXT_PACKAGE = "@morlay/dsh-context-assembler";
15
- /** 一行 plugin entry(本模块只描述形状,类型由消费方自己那份行类型决定)。 */
16
3
  interface ContextRow {
17
4
  readonly id: string;
18
5
  readonly name: string;
19
6
  readonly config?: Readonly<Record<string, unknown>>;
20
7
  }
21
- /**
22
- * 通道那一行:包的主出口是**组装插件**——按 config 决定装哪些能力、各带什么参数,所以装配面
23
- * 只有这一行,组成与各能力的 config 都归包内(缺省即完整的一套;`chat` 用它裁剪)。
24
- */
25
- declare function contextChannel(config?: Readonly<Record<string, unknown>>): ContextRow;
26
- /**
27
- * 模式收口那一行:`scope` 出口按 `ctx.sessionModes` 的模式定义收口(工具白名单、instruction 总开关、
28
- * 动态快照开关)。
29
- *
30
- * 行 id 用全名 `context-assembler-scope`:它的搭档是同一份装配里的 `context-assembler`(通道),两个 id 放在
31
- * 一起才看得出这一行属于哪个包、做什么——`context-scope` 那种短名会被误当成 `ctx.contextScope` 之类的服务。
32
- *
33
- * 行本身**不带 config**:装哪些模式、每个模式收什么,都由 `session-mode` 行的 `config.modes` 决定,
34
- * 这一行只把它落到会话上(`inject` `sessionModes`,由
35
- * [`@morlay/dsh-session-mode`](../../../profile/dsh-session-mode/README.md) 的 patch 插一行)。
36
- */
37
- declare function scopeRow(): ContextRow;
38
- /**
39
- * bundle patch 的行清单:只有通道一行(模式收口那一行归 `session-mode` 的 patch 装)。
40
- *
41
- * 形态是 `insert`:这些行由本包提供给 host 平面。写成 `- id: x` 那种"改已有行"的形态时,装配期找不到
42
- * 目标行(只 warn 后跳过),通道根本没装上,引用它的行停在 waiting。
43
- */
44
- declare const PATCH_ROWS: readonly {
45
- readonly insert: readonly ContextRow[];
46
- }[];
8
+ declare function contextChannel(): ContextRow;
47
9
  //#endregion
48
- export { CONTEXT_PACKAGE, ContextRow, PATCH_ROWS, contextChannel, scopeRow };
10
+ export { CONTEXT_PACKAGE, ContextRow, contextChannel };
package/dist/rows.mjs CHANGED
@@ -1,55 +1,10 @@
1
1
  //#region src/rows.ts
2
- /**
3
- * 本包作为 **bundle** 的行清单:`dsh.profile.bundles` 列出本包时由它渲染成 `cordis.patch.yml`。
4
- *
5
- * 通道装在 host 平面,整份部署共享一套注入。**不做隔离**——工具说明
6
- * (`@morlay/dsh-agent-toolkit/guidance`,在别的包里)与 skill 目录这类消费者都在别的行上
7
- * `inject` 它,隔离会把它们挡在组外(行停在 waiting,不报错)。
8
- *
9
- * 本包自己的 bundle 只装通道(`PATCH_ROWS`)。模式的收口是另一行(`scopeRow()`),它由
10
- * [`@morlay/dsh-session-mode`](../../../profile/dsh-session-mode/README.md) 的 patch 与模式定义一起装——
11
- * 那一行的 config 就是模式清单。
12
- */
13
- /** 组装出口(包根)就是那个"按 config 装能力"的插件。 */
14
2
  const CONTEXT_PACKAGE = "@morlay/dsh-context-assembler";
15
- /**
16
- * 通道那一行:包的主出口是**组装插件**——按 config 决定装哪些能力、各带什么参数,所以装配面
17
- * 只有这一行,组成与各能力的 config 都归包内(缺省即完整的一套;`chat` 用它裁剪)。
18
- */
19
- function contextChannel(config) {
3
+ function contextChannel() {
20
4
  return {
21
5
  id: "context-assembler",
22
- name: CONTEXT_PACKAGE,
23
- ...config === void 0 ? {} : { config }
6
+ name: CONTEXT_PACKAGE
24
7
  };
25
8
  }
26
- /**
27
- * 模式收口那一行:`scope` 出口按 `ctx.sessionModes` 的模式定义收口(工具白名单、instruction 总开关、
28
- * 动态快照开关)。
29
- *
30
- * 行 id 用全名 `context-assembler-scope`:它的搭档是同一份装配里的 `context-assembler`(通道),两个 id 放在
31
- * 一起才看得出这一行属于哪个包、做什么——`context-scope` 那种短名会被误当成 `ctx.contextScope` 之类的服务。
32
- *
33
- * 行本身**不带 config**:装哪些模式、每个模式收什么,都由 `session-mode` 行的 `config.modes` 决定,
34
- * 这一行只把它落到会话上(`inject` `sessionModes`,由
35
- * [`@morlay/dsh-session-mode`](../../../profile/dsh-session-mode/README.md) 的 patch 插一行)。
36
- */
37
- function scopeRow() {
38
- return {
39
- id: "context-assembler-scope",
40
- name: "@morlay/dsh-context-assembler/scope"
41
- };
42
- }
43
- /**
44
- * bundle patch 的行清单:只有通道一行(模式收口那一行归 `session-mode` 的 patch 装)。
45
- *
46
- * 形态是 `insert`:这些行由本包提供给 host 平面。写成 `- id: x` 那种"改已有行"的形态时,装配期找不到
47
- * 目标行(只 warn 后跳过),通道根本没装上,引用它的行停在 waiting。
48
- */
49
- const PATCH_ROWS = [{ insert: [contextChannel({ capabilities: [
50
- "assembler",
51
- "agent-instructions",
52
- "skill-catalog"
53
- ] })] }];
54
9
  //#endregion
55
- export { CONTEXT_PACKAGE, PATCH_ROWS, contextChannel, scopeRow };
10
+ export { CONTEXT_PACKAGE, contextChannel };
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Context Assembler",
4
- "description": "Prompt-injection capability set: the single injection channel plus workspace instructions, skill catalog and the scope gate that narrows tools per session mode."
4
+ "description": "The prompt channel: text conversion on the assembly result (replace / suppress) plus per-step delivery of demoted sections. The per-session scope gate (tool allow / deny lists, injection switches) lives in @morlay/dsh-session-mode."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "注入能力组",
4
- "description": "提示词注入能力组:唯一注入通道,以及工作区指令、skill 目录与模式收口。"
3
+ "title": "上下文装配通道",
4
+ "description": "提示词通道:装配结果上的文本转换(replace / suppress)与降级 section 的按步送达;模式收口(工具白名单与黑名单、注入开关)归 @morlay/dsh-session-mode。"
5
5
  }
6
6
  }