@apollo-music/dsh-sidecar-terms 0.1.4 → 0.1.7
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 +35 -4
- package/dist/coordinator.d.ts +14 -0
- package/dist/coordinator.js +187 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +114 -39
- package/dist/types.d.ts +17 -0
- package/package.json +23 -16
package/README.md
CHANGED
|
@@ -7,18 +7,23 @@
|
|
|
7
7
|
3. 默认对新增命中即时调用 `/resolve`,并只投递尚未出现的原文 `term_delta + rule_delta`;
|
|
8
8
|
4. `tools/post-execute` 在下一次模型生成前 await 工具结果解析,`agent/pre-step` 与 `agent/turn-stopping` 继续作为边界 flush;
|
|
9
9
|
5. request 尚未固定时 inject,step 已开始或已有候选回答时 steer;
|
|
10
|
-
6. steer 修订后不再审计同一 turn 的后续 assistant 输出,避免规则回执自我触发;下一 turn
|
|
10
|
+
6. 同一上下文周期内,steer 修订后不再审计同一 turn 的后续 assistant 输出,避免规则回执自我触发;下一 turn 自动恢复,压缩后按新周期重建;
|
|
11
11
|
7. 最终答案前只 steer 尚未交付的 canonical term / Rule delta。
|
|
12
12
|
8. Catalog、Resolver、鉴权或网络不可用时失败关闭,不生成离线替代规则。
|
|
13
|
+
9. 压缩或裁剪上下文后,以 `session.deriveMessages()` 返回的当前可见消息重新匹配、重建 audit/inject 计数;仅补回仍有命中且提示词已不在上下文里的 term/rule。
|
|
14
|
+
10. 超过 Resolver 单段上限的消息只在术语审计传输层无损分段;超过 100 段文本或 200 个候选词时分批查询,全部成功后合并投递。DSH 交给模型的原消息不变。
|
|
13
15
|
|
|
14
16
|
插件固定只读 `https://apollomusic-poc1.fde0.com/kb/terminology`。`apiUrl` 不再是运行时配置项,线上服务返回 `degraded` 也会按错误处理;插件不包含本地术语表或 P0 fallback。
|
|
15
17
|
|
|
16
18
|
## 安装
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
当前版本为 `0.1.7`,对应 DeepSeek Harness `0.1.7-rc.2`。
|
|
21
|
+
旧版 `0.1.5` 对应 DSH `0.1.1-rc.2`,升级插件时需同步升级宿主;`0.1.6` 是未发布的源码过渡版本。
|
|
22
|
+
|
|
23
|
+
在对应版本的 DeepSeek Harness 项目中安装:
|
|
19
24
|
|
|
20
25
|
```bash
|
|
21
|
-
npm install @apollo-music/dsh-sidecar-terms@0.1.
|
|
26
|
+
npm install @apollo-music/dsh-sidecar-terms@0.1.7
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
## 配置
|
|
@@ -54,6 +59,9 @@ npm ci
|
|
|
54
59
|
npm run build:harness-plugin
|
|
55
60
|
```
|
|
56
61
|
|
|
62
|
+
发布前可在构建后运行 `npm pack ./integrations/deepseek-harness --ignore-scripts`,
|
|
63
|
+
将生成的 `.tgz` 安装到 DSH 测试项目;这不会发布 npm。
|
|
64
|
+
|
|
57
65
|
凭据示例:
|
|
58
66
|
|
|
59
67
|
```bash
|
|
@@ -74,7 +82,30 @@ export APOLLO_TERMINOLOGY_READ_API_KEY='<READ_ONLY_API_KEY>'
|
|
|
74
82
|
|
|
75
83
|
`auditReasoning: true` 只匹配每个 step 已提交的完整 reasoning block;流式 `reasoning-delta` 不参与匹配。thinking 原文只发送给 Resolver 做瞬时匹配,不会回注给模型。若部署边界不允许 thinking 离开 Harness 进程,可设为 `false`。
|
|
76
84
|
|
|
77
|
-
该插件声明依赖 `agents`、`sessions` 与 `tools`;Cordis
|
|
85
|
+
该插件声明依赖 `agents`、`sessions` 与 `tools`;Cordis 会等相关服务存在后再加载。本项目锁定并测试 DSH `0.1.7-rc.2` 的公开 `Agent`、`session/event`、`agent/assistant-stream`、`tools/post-execute`、`agent/pre-step`、`agent/request`、`agent/turn-stopping` 接口。包清单显式声明这一组接口的完整 DSH peer 闭包,避免依赖宿主包管理器自动补齐隐式 peer。
|
|
86
|
+
|
|
87
|
+
`0.1.6` 使用独立的消息来源 `plugin:apollo-terminology-rules`,与上游将旧插件消息迁移到 v4 会话格式后的来源一致。工具结果现在由 `role: tool` 消息承载;若另一插件替换 canonical `value`,本插件等待 DSH 渲染并提交 `tool/result` 后在 pre-step 审计,不匹配已被替换的旧内容。pre-step 增量保留上游 `startsRequestSeries` 等准入字段。
|
|
88
|
+
|
|
89
|
+
## 压缩后的重新计数(0.1.7)
|
|
90
|
+
|
|
91
|
+
这里的“重新计数”指重新建立命中、已投递集合和 audit/inject 指针,不是重新计算模型 token 用量,也不改变 DSH 的压缩策略。
|
|
92
|
+
|
|
93
|
+
- 摘要、未压缩的近期消息和本轮即将准入的用户消息参与重新匹配;已被替换的原始日志不参与。启动/恢复会话也使用同一可见上下文入口。
|
|
94
|
+
- 自身完整保留的 term/rule 提示词继续算已投递;被压掉且仍有命中的提示词在下一次模型请求前补回。被压掉且不再命中的内容不会复活,自身提示词也不反向触发新命中。
|
|
95
|
+
- 新提示词的来源元数据记录 term/rule ID 和正文 SHA-256;正文被裁剪或修改后不沿用旧回执。摘要里提到某条规则不等于完整规则仍在。保留的 0.1.6 原文提示词按已有来源和固定格式读取 ID。
|
|
96
|
+
- 每次替换开启独立 `contextEpoch`,双指针使用该轮上下文内的递增序号;不把重排后的原日志 seq 当作增量水位。压缩前未完成的查询结果作废,队列里过期的本插件增量移除,其他输入不动。
|
|
97
|
+
- 普通压缩在 `agent/pre-step` 重建;上下文溢出后的同一步重试由 `agent/request` 兜底,在请求构建前提交增量,不改写 provider 请求。已生成候选答案时,最终门禁仍只 steer 剩余增量。
|
|
98
|
+
- 重建必须完成后才能继续生成,不受 `queryIntervalMs` 或普通 `flushOnPreStep: false` 的延迟设置影响;查询失败则阻止该次生成。默认开启的最终门禁维持原有语义。
|
|
99
|
+
|
|
100
|
+
实现不依赖具体 compaction 插件,而是监听公开的会话 surface replacement。完整设计、一手上游依据和验证记录见仓库 [`docs/DSH_CONTEXT_RECOUNT.md`](https://github.com/fde0-inc/apollo-terminology-knowledge-base/blob/main/docs/DSH_CONTEXT_RECOUNT.md)。
|
|
101
|
+
|
|
102
|
+
## 验证
|
|
103
|
+
|
|
104
|
+
无凭据的真实 DSH 运行时回归已纳入 `npm test`(模型和 API 均为测试数据,不联网):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx tsx --test tests/harness-runtime.test.ts
|
|
108
|
+
```
|
|
78
109
|
|
|
79
110
|
配置线上只读凭据后验证:
|
|
80
111
|
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type UserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
2
|
import { TerminologyApi } from "./api.js";
|
|
3
3
|
import type { AgentPort, LoggerPort, PluginConfig, ResolveResponse } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Split one audited message for the resolver transport without changing the
|
|
6
|
+
* message that DSH sends to the model. Chunks preserve order and concatenate
|
|
7
|
+
* exactly to the original text; natural or serialized-JSON boundaries are
|
|
8
|
+
* preferred so a multi-word terminology match is not cut unnecessarily.
|
|
9
|
+
*/
|
|
10
|
+
export declare function splitResolverText(text: string): string[];
|
|
4
11
|
interface AuditItem {
|
|
5
12
|
eventSeq: number;
|
|
6
13
|
source: string;
|
|
@@ -14,8 +21,14 @@ export declare class TerminologyCoordinator {
|
|
|
14
21
|
constructor(config: PluginConfig, api: TerminologyApi, logger: LoggerPort);
|
|
15
22
|
attach(agent: AgentPort): void;
|
|
16
23
|
disposeAgent(agent: AgentPort): void;
|
|
24
|
+
/** Retire all asynchronous work belonging to the now-shadowed context. */
|
|
25
|
+
invalidateContext(agent: AgentPort): void;
|
|
26
|
+
/** Start a fresh accounting epoch, crediting only intact visible receipts. */
|
|
27
|
+
resetContext(agent: AgentPort, retained: readonly UserMessage[]): void;
|
|
17
28
|
dispose(): void;
|
|
18
29
|
observe(agent: AgentPort, item: AuditItem): void;
|
|
30
|
+
/** Runtime accounting uses an epoch-local sequence, not reordered log seqs. */
|
|
31
|
+
observeNext(agent: AgentPort, item: Omit<AuditItem, "eventSeq">): void;
|
|
19
32
|
setStepPhase(agent: AgentPort, activeStep: boolean, generationStarted?: boolean): void;
|
|
20
33
|
beginTurn(agent: AgentPort): void;
|
|
21
34
|
shouldAuditAssistant(agent: AgentPort): boolean;
|
|
@@ -26,6 +39,7 @@ export declare class TerminologyCoordinator {
|
|
|
26
39
|
flush(agent: AgentPort, forceSteer?: boolean): Promise<void>;
|
|
27
40
|
flushForPreStep(agent: AgentPort): Promise<UserMessage[]>;
|
|
28
41
|
pointers(agent: AgentPort): {
|
|
42
|
+
contextEpoch: number;
|
|
29
43
|
auditCursor: number;
|
|
30
44
|
injectCursor: number;
|
|
31
45
|
deliveredRuleIds: string[];
|
package/dist/coordinator.js
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
3
|
import { findCatalogMatches } from "./matcher.js";
|
|
3
|
-
|
|
4
|
+
// This also matches the source produced by DSH's v3-to-v4 session migration
|
|
5
|
+
// for our previously published { kind: "plugin", plugin: "apollo-terminology-rules" }.
|
|
6
|
+
const MESSAGE_SOURCE_KIND = "plugin:apollo-terminology-rules";
|
|
7
|
+
const RESOLVER_TEXT_TARGET_CHARS = 90_000;
|
|
8
|
+
const RESOLVER_MAX_SEGMENTS = 100;
|
|
9
|
+
const RESOLVER_MAX_CANDIDATES = 200;
|
|
10
|
+
const NATURAL_TEXT_BOUNDARIES = [
|
|
11
|
+
{ needle: "\n\n", offset: 2 },
|
|
12
|
+
{ needle: "\n", offset: 1 },
|
|
13
|
+
{ needle: "},{\"", offset: 2 },
|
|
14
|
+
{ needle: "\",\"", offset: 2 },
|
|
15
|
+
{ needle: "。", offset: 1 },
|
|
16
|
+
{ needle: "!", offset: 1 },
|
|
17
|
+
{ needle: "?", offset: 1 },
|
|
18
|
+
{ needle: ". ", offset: 2 },
|
|
19
|
+
{ needle: "! ", offset: 2 },
|
|
20
|
+
{ needle: "? ", offset: 2 },
|
|
21
|
+
{ needle: "; ", offset: 2 },
|
|
22
|
+
{ needle: ";", offset: 1 },
|
|
23
|
+
{ needle: " ", offset: 1 },
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Split one audited message for the resolver transport without changing the
|
|
27
|
+
* message that DSH sends to the model. Chunks preserve order and concatenate
|
|
28
|
+
* exactly to the original text; natural or serialized-JSON boundaries are
|
|
29
|
+
* preferred so a multi-word terminology match is not cut unnecessarily.
|
|
30
|
+
*/
|
|
31
|
+
export function splitResolverText(text) {
|
|
32
|
+
if (text.length <= RESOLVER_TEXT_TARGET_CHARS)
|
|
33
|
+
return [text];
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
while (start < text.length) {
|
|
37
|
+
const targetEnd = Math.min(start + RESOLVER_TEXT_TARGET_CHARS, text.length);
|
|
38
|
+
if (targetEnd === text.length) {
|
|
39
|
+
chunks.push(text.slice(start));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
const preferredStart = start + Math.floor(RESOLVER_TEXT_TARGET_CHARS * 0.75);
|
|
43
|
+
let end = 0;
|
|
44
|
+
for (const boundary of NATURAL_TEXT_BOUNDARIES) {
|
|
45
|
+
const index = text.lastIndexOf(boundary.needle, targetEnd - boundary.needle.length);
|
|
46
|
+
const candidate = index + boundary.offset;
|
|
47
|
+
if (index >= preferredStart && candidate > end)
|
|
48
|
+
end = candidate;
|
|
49
|
+
}
|
|
50
|
+
if (end <= start)
|
|
51
|
+
end = targetEnd;
|
|
52
|
+
// JavaScript string limits are measured in UTF-16 code units. Avoid
|
|
53
|
+
// placing a transport boundary between the halves of a surrogate pair.
|
|
54
|
+
const before = text.charCodeAt(end - 1);
|
|
55
|
+
const after = text.charCodeAt(end);
|
|
56
|
+
if (before >= 0xD800 && before <= 0xDBFF
|
|
57
|
+
&& after >= 0xDC00 && after <= 0xDFFF) {
|
|
58
|
+
end -= 1;
|
|
59
|
+
}
|
|
60
|
+
chunks.push(text.slice(start, end));
|
|
61
|
+
start = end;
|
|
62
|
+
}
|
|
63
|
+
return chunks;
|
|
64
|
+
}
|
|
4
65
|
export class TerminologyCoordinator {
|
|
5
66
|
config;
|
|
6
67
|
api;
|
|
@@ -16,13 +77,35 @@ export class TerminologyCoordinator {
|
|
|
16
77
|
this.states.set(agent, createState());
|
|
17
78
|
}
|
|
18
79
|
disposeAgent(agent) {
|
|
80
|
+
this.invalidateContext(agent);
|
|
81
|
+
this.states.delete(agent);
|
|
82
|
+
}
|
|
83
|
+
/** Retire all asynchronous work belonging to the now-shadowed context. */
|
|
84
|
+
invalidateContext(agent) {
|
|
19
85
|
const state = this.states.get(agent);
|
|
20
86
|
if (!state)
|
|
21
87
|
return;
|
|
22
88
|
state.disposed = true;
|
|
23
89
|
if (state.queryTimer)
|
|
24
90
|
clearTimeout(state.queryTimer);
|
|
25
|
-
|
|
91
|
+
}
|
|
92
|
+
/** Start a fresh accounting epoch, crediting only intact visible receipts. */
|
|
93
|
+
resetContext(agent, retained) {
|
|
94
|
+
const epoch = (this.states.get(agent)?.contextEpoch ?? -1) + 1;
|
|
95
|
+
this.invalidateContext(agent);
|
|
96
|
+
const state = createState(epoch);
|
|
97
|
+
for (const message of retained) {
|
|
98
|
+
const receipt = readVisibleReceipt(message);
|
|
99
|
+
if (!receipt)
|
|
100
|
+
continue;
|
|
101
|
+
for (const id of receipt.ruleIds)
|
|
102
|
+
state.deliveredRuleIds.add(id);
|
|
103
|
+
for (const id of receipt.termIds)
|
|
104
|
+
state.deliveredTermIds.add(id);
|
|
105
|
+
if (receipt.metadata)
|
|
106
|
+
state.announcedMetadata = receipt.metadata;
|
|
107
|
+
}
|
|
108
|
+
this.states.set(agent, state);
|
|
26
109
|
}
|
|
27
110
|
dispose() {
|
|
28
111
|
for (const agent of this.states.keys())
|
|
@@ -32,11 +115,17 @@ export class TerminologyCoordinator {
|
|
|
32
115
|
const state = this.requireState(agent);
|
|
33
116
|
if (state.disposed || item.eventSeq <= state.auditCursor)
|
|
34
117
|
return;
|
|
118
|
+
state.nextAuditSeq = Math.max(state.nextAuditSeq, item.eventSeq + 1);
|
|
35
119
|
state.auditQueue.push(item);
|
|
36
120
|
state.processing = state.processing.then(() => this.drainAudit(agent, state)).catch((error) => {
|
|
37
121
|
this.logger.warn("terminology audit deferred: %s", errorMessage(error));
|
|
38
122
|
});
|
|
39
123
|
}
|
|
124
|
+
/** Runtime accounting uses an epoch-local sequence, not reordered log seqs. */
|
|
125
|
+
observeNext(agent, item) {
|
|
126
|
+
const state = this.requireState(agent);
|
|
127
|
+
this.observe(agent, { ...item, eventSeq: state.nextAuditSeq });
|
|
128
|
+
}
|
|
40
129
|
setStepPhase(agent, activeStep, generationStarted = false) {
|
|
41
130
|
const state = this.requireState(agent);
|
|
42
131
|
state.activeStep = activeStep;
|
|
@@ -62,6 +151,8 @@ export class TerminologyCoordinator {
|
|
|
62
151
|
}
|
|
63
152
|
confirmOwnMessage(agent, message) {
|
|
64
153
|
const state = this.requireState(agent);
|
|
154
|
+
if (state.disposed)
|
|
155
|
+
return;
|
|
65
156
|
const delivery = state.deliveries.get(String(message.id));
|
|
66
157
|
if (!delivery)
|
|
67
158
|
return;
|
|
@@ -97,6 +188,7 @@ export class TerminologyCoordinator {
|
|
|
97
188
|
pointers(agent) {
|
|
98
189
|
const state = this.requireState(agent);
|
|
99
190
|
return {
|
|
191
|
+
contextEpoch: state.contextEpoch,
|
|
100
192
|
auditCursor: state.auditCursor,
|
|
101
193
|
injectCursor: state.injectCursor,
|
|
102
194
|
deliveredRuleIds: [...state.deliveredRuleIds].sort(),
|
|
@@ -109,6 +201,8 @@ export class TerminologyCoordinator {
|
|
|
109
201
|
if (state.auditQueue.length === 0 || state.disposed)
|
|
110
202
|
return;
|
|
111
203
|
const catalog = await this.api.getCatalog();
|
|
204
|
+
if (state.disposed)
|
|
205
|
+
return;
|
|
112
206
|
while (state.auditQueue.length) {
|
|
113
207
|
const item = state.auditQueue.shift();
|
|
114
208
|
if (item.eventSeq <= state.auditCursor)
|
|
@@ -165,18 +259,52 @@ export class TerminologyCoordinator {
|
|
|
165
259
|
async queryOnce(agent, state, mode, captured) {
|
|
166
260
|
const segments = [...state.pendingSegments.values()].sort((a, b) => a.event_seq - b.event_seq);
|
|
167
261
|
const throughSeq = segments.at(-1).event_seq;
|
|
262
|
+
const resolverSegments = segments.flatMap(({ event_seq, source, text }) => splitResolverText(text).map((chunk) => ({ event_seq, source, text: chunk })));
|
|
263
|
+
const candidates = [...new Set(segments.flatMap((segment) => segment.candidates ?? []))];
|
|
168
264
|
for (const segment of segments)
|
|
169
265
|
state.pendingSegments.delete(segment.event_seq);
|
|
170
266
|
state.lastQueryAt = Date.now();
|
|
171
267
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
268
|
+
// A whole-context recount can exceed either transport array limit. Hold
|
|
269
|
+
// every batch until all succeed; a failed/stale batch cannot partly
|
|
270
|
+
// advance delivery accounting or leave a half-rebuilt prompt behind.
|
|
271
|
+
const ruleIds = new Set(this.knownRuleIds(state));
|
|
272
|
+
const termIds = new Set(this.knownTermIds(state));
|
|
273
|
+
const rules = new Map();
|
|
274
|
+
const terms = new Map();
|
|
275
|
+
let response;
|
|
276
|
+
const batchCount = Math.max(Math.ceil(resolverSegments.length / RESOLVER_MAX_SEGMENTS), Math.ceil(candidates.length / RESOLVER_MAX_CANDIDATES));
|
|
277
|
+
for (let batch = 0; batch < batchCount; batch += 1) {
|
|
278
|
+
const batchSegments = resolverSegments.slice(batch * RESOLVER_MAX_SEGMENTS, (batch + 1) * RESOLVER_MAX_SEGMENTS);
|
|
279
|
+
const partial = await this.api.resolve({
|
|
280
|
+
session_id: agent.id,
|
|
281
|
+
audit_through_seq: throughSeq,
|
|
282
|
+
// Extra candidate batches still require one non-empty segment.
|
|
283
|
+
segments: batchSegments.length ? batchSegments : resolverSegments.slice(0, 1),
|
|
284
|
+
candidate_terms: candidates.slice(batch * RESOLVER_MAX_CANDIDATES, (batch + 1) * RESOLVER_MAX_CANDIDATES),
|
|
285
|
+
delivered_rule_ids: [...ruleIds].sort(),
|
|
286
|
+
delivered_term_ids: [...termIds].sort(),
|
|
287
|
+
});
|
|
288
|
+
if (state.disposed)
|
|
289
|
+
return;
|
|
290
|
+
for (const rule of partial.rule_delta) {
|
|
291
|
+
rules.set(rule.rule_id, rule);
|
|
292
|
+
ruleIds.add(rule.rule_id);
|
|
293
|
+
}
|
|
294
|
+
for (const term of partial.term_delta ?? []) {
|
|
295
|
+
terms.set(term.term_id, term);
|
|
296
|
+
termIds.add(term.term_id);
|
|
297
|
+
}
|
|
298
|
+
response = {
|
|
299
|
+
...partial,
|
|
300
|
+
hits: [...response?.hits ?? [], ...partial.hits],
|
|
301
|
+
unresolved: [...response?.unresolved ?? [], ...partial.unresolved],
|
|
302
|
+
rule_delta: [...rules.values()],
|
|
303
|
+
term_delta: [...terms.values()],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!response)
|
|
307
|
+
return;
|
|
180
308
|
const checkpoint = {
|
|
181
309
|
throughSeq,
|
|
182
310
|
complete: response.rule_delta.length === 0 && (response.term_delta?.length ?? 0) === 0,
|
|
@@ -193,6 +321,8 @@ export class TerminologyCoordinator {
|
|
|
193
321
|
this.logger.debug("terminology resolved through seq %d: %d hit(s), %d new term(s), %d new rule(s)", throughSeq, response.hits.length, response.term_delta?.length ?? 0, response.rule_delta.length);
|
|
194
322
|
}
|
|
195
323
|
catch (error) {
|
|
324
|
+
if (state.disposed)
|
|
325
|
+
return;
|
|
196
326
|
for (const segment of segments)
|
|
197
327
|
state.pendingSegments.set(segment.event_seq, segment);
|
|
198
328
|
throw error;
|
|
@@ -200,9 +330,20 @@ export class TerminologyCoordinator {
|
|
|
200
330
|
}
|
|
201
331
|
dispatch(agent, state, response, checkpoint, mode) {
|
|
202
332
|
const metadataUpdate = metadataDelta(state.announcedMetadata, response);
|
|
333
|
+
const text = renderDelta(response, metadataUpdate);
|
|
203
334
|
const message = createUserMessage({
|
|
204
|
-
content: [{ type: "text", text
|
|
205
|
-
source: {
|
|
335
|
+
content: [{ type: "text", text }],
|
|
336
|
+
source: {
|
|
337
|
+
kind: MESSAGE_SOURCE_KIND,
|
|
338
|
+
form: "instructions",
|
|
339
|
+
receipt: {
|
|
340
|
+
version: 1,
|
|
341
|
+
ruleIds: response.rule_delta.map((rule) => rule.rule_id),
|
|
342
|
+
termIds: (response.term_delta ?? []).map((term) => term.term_id),
|
|
343
|
+
...metadataSnapshot(response),
|
|
344
|
+
textSha256: textDigest(text),
|
|
345
|
+
},
|
|
346
|
+
},
|
|
206
347
|
});
|
|
207
348
|
const effectiveMode = mode === "auto"
|
|
208
349
|
? (state.activeStep || state.generationStarted ? "steer" : "inject")
|
|
@@ -220,6 +361,8 @@ export class TerminologyCoordinator {
|
|
|
220
361
|
return message;
|
|
221
362
|
}
|
|
222
363
|
promoteUnclaimedInjections(agent, state) {
|
|
364
|
+
if (state.disposed)
|
|
365
|
+
return;
|
|
223
366
|
const pendingIds = new Set(agent.inbox.nextStep.map((message) => String(message.id)));
|
|
224
367
|
for (const delivery of state.deliveries.values()) {
|
|
225
368
|
if (delivery.mode === "steer" || delivery.mode === "capture")
|
|
@@ -264,10 +407,40 @@ export class TerminologyCoordinator {
|
|
|
264
407
|
}
|
|
265
408
|
}
|
|
266
409
|
export function isOwnPluginMessage(message) {
|
|
267
|
-
return message.source.kind ===
|
|
410
|
+
return message.source.kind === MESSAGE_SOURCE_KIND;
|
|
411
|
+
}
|
|
412
|
+
function readVisibleReceipt(message) {
|
|
413
|
+
if (message.source.kind !== MESSAGE_SOURCE_KIND)
|
|
414
|
+
return;
|
|
415
|
+
if (message.content.length !== 1 || message.content[0].type !== "text")
|
|
416
|
+
return;
|
|
417
|
+
const text = message.content[0].text;
|
|
418
|
+
const receipt = message.source.receipt;
|
|
419
|
+
if (receipt !== undefined) {
|
|
420
|
+
if (receipt.version !== 1 || receipt.textSha256 !== textDigest(text))
|
|
421
|
+
return;
|
|
422
|
+
return {
|
|
423
|
+
ruleIds: receipt.ruleIds,
|
|
424
|
+
termIds: receipt.termIds,
|
|
425
|
+
metadata: { kbVersion: receipt.kbVersion, catalogRevision: receipt.catalogRevision },
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// 0.1.6 receipts had only our producer kind and the canonical text framing.
|
|
429
|
+
// Never infer delivered IDs from a summary, user text, or another producer.
|
|
430
|
+
if (!text.startsWith("[阿波罗术语库:原文增量]\n"))
|
|
431
|
+
return;
|
|
432
|
+
return {
|
|
433
|
+
ruleIds: [...text.matchAll(/^- \[P[01]\] (RULE-[A-Z0-9-]+): /gmu)].map((match) => match[1]),
|
|
434
|
+
termIds: [...text.matchAll(/^- \[术语 (TERM-[A-Z0-9-]+) \/ (?:locked|contextual|pending|deprecated)\] /gmu)].map((match) => match[1]),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function textDigest(text) {
|
|
438
|
+
return createHash("sha256").update(text).digest("hex");
|
|
268
439
|
}
|
|
269
|
-
function createState() {
|
|
440
|
+
function createState(contextEpoch = 0) {
|
|
270
441
|
return {
|
|
442
|
+
contextEpoch,
|
|
443
|
+
nextAuditSeq: 0,
|
|
271
444
|
auditCursor: -1,
|
|
272
445
|
injectCursor: -1,
|
|
273
446
|
auditQueue: [],
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const inject: string[];
|
|
|
7
7
|
export type Config = PluginConfig;
|
|
8
8
|
export declare const Config: Schema<Config>;
|
|
9
9
|
export declare function apply(ctx: Context, config: Config): void;
|
|
10
|
-
export declare function messageText(message: Message, auditReasoning?: boolean): string;
|
|
10
|
+
export declare function messageText(message: Message, auditReasoning?: boolean, auditToolArguments?: boolean): string;
|
|
11
11
|
export { APOLLO_TERMINOLOGY_API_URL, TerminologyApi } from "./api.js";
|
|
12
12
|
export { TerminologyCoordinator } from "./coordinator.js";
|
|
13
13
|
export { findCatalogMatches, normalizeMatchKey } from "./matcher.js";
|
package/dist/index.js
CHANGED
|
@@ -23,11 +23,18 @@ export function apply(ctx, config) {
|
|
|
23
23
|
const agentsBySession = new Map();
|
|
24
24
|
const attach = (agent) => {
|
|
25
25
|
coordinator.attach(agent);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
observeEvent(agent, event);
|
|
26
|
+
// Resume from the model-visible projection, never from shadowed raw events.
|
|
27
|
+
agentsBySession.set(agent.session, { agent, needsRecount: true, seenMessages: new Set(), offered: [] });
|
|
29
28
|
};
|
|
30
29
|
const observeEvent = (agent, event) => {
|
|
30
|
+
const binding = agentsBySession.get(agent.session);
|
|
31
|
+
if ("surfaceOp" in event && event.surfaceOp !== "append" && event.type !== "system/message") {
|
|
32
|
+
// Summary compaction and tool pruning both commit through this seam.
|
|
33
|
+
// Invalidate synchronously so a late old-context response cannot steer.
|
|
34
|
+
binding.needsRecount = true;
|
|
35
|
+
coordinator.invalidateContext(agent);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
31
38
|
switch (event.type) {
|
|
32
39
|
case "turn/start":
|
|
33
40
|
coordinator.beginTurn(agent);
|
|
@@ -41,35 +48,28 @@ export function apply(ctx, config) {
|
|
|
41
48
|
case "step/end":
|
|
42
49
|
coordinator.endStep(agent);
|
|
43
50
|
return;
|
|
44
|
-
case "assistant/chunk": {
|
|
45
|
-
const chunk = event.data.chunk;
|
|
46
|
-
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") {
|
|
47
|
-
coordinator.markGenerationStarted(agent);
|
|
48
|
-
}
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
51
|
case "user/message": {
|
|
52
52
|
const message = event.data;
|
|
53
|
+
binding.offered = binding.offered.filter((offered) => offered.id !== message.id);
|
|
53
54
|
if (isOwnPluginMessage(message)) {
|
|
54
55
|
coordinator.confirmOwnMessage(agent, message);
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
|
-
enqueueMessage(agent,
|
|
58
|
+
enqueueMessage(agent, "user", message);
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
case "assistant/message":
|
|
61
62
|
coordinator.markGenerationStarted(agent);
|
|
62
63
|
if (coordinator.shouldAuditAssistant(agent)) {
|
|
63
|
-
enqueueMessage(agent,
|
|
64
|
+
enqueueMessage(agent, "assistant", event.data.message);
|
|
64
65
|
}
|
|
65
66
|
return;
|
|
66
67
|
case "tool/result":
|
|
67
|
-
enqueueMessage(agent,
|
|
68
|
+
enqueueMessage(agent, "tool_result", event.data.message);
|
|
68
69
|
return;
|
|
69
70
|
case "tool/call":
|
|
70
|
-
if (config.auditToolArguments) {
|
|
71
|
-
coordinator.
|
|
72
|
-
eventSeq: event.seq,
|
|
71
|
+
if (config.auditToolArguments && !binding.needsRecount) {
|
|
72
|
+
coordinator.observeNext(agent, {
|
|
73
73
|
source: "tool_call",
|
|
74
74
|
text: `${event.data.name}\n${event.data.arguments}`,
|
|
75
75
|
});
|
|
@@ -79,32 +79,94 @@ export function apply(ctx, config) {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
|
-
const enqueueMessage = (agent,
|
|
83
|
-
const
|
|
82
|
+
const enqueueMessage = (agent, source, message, auditArguments = false) => {
|
|
83
|
+
const binding = agentsBySession.get(agent.session);
|
|
84
|
+
if (binding.needsRecount || binding.seenMessages.has(String(message.id)))
|
|
85
|
+
return;
|
|
86
|
+
binding.seenMessages.add(String(message.id));
|
|
87
|
+
const text = messageText(message, config.auditReasoning, auditArguments);
|
|
84
88
|
if (text)
|
|
85
|
-
coordinator.
|
|
89
|
+
coordinator.observeNext(agent, { source, text });
|
|
90
|
+
};
|
|
91
|
+
const recount = async (agent, offered, signal, capture = true) => {
|
|
92
|
+
const binding = agentsBySession.get(agent.session);
|
|
93
|
+
let messages = offered;
|
|
94
|
+
let additions = [];
|
|
95
|
+
let rebuilt = false;
|
|
96
|
+
try {
|
|
97
|
+
while (binding.needsRecount) {
|
|
98
|
+
signal.throwIfAborted();
|
|
99
|
+
const retained = agent.session.deriveMessages();
|
|
100
|
+
const retainedIds = new Set(retained.map((message) => message.id));
|
|
101
|
+
messages = messages.filter((message) => !isOwnPluginMessage(message) && !retainedIds.has(message.id));
|
|
102
|
+
coordinator.resetContext(agent, retained.filter((message) => message.role === "user" && isOwnPluginMessage(message)));
|
|
103
|
+
binding.seenMessages.clear();
|
|
104
|
+
binding.needsRecount = false;
|
|
105
|
+
// The offered batch has already left the inbox; remove only remaining
|
|
106
|
+
// sidecar messages there. Recompute their relevance from retained input.
|
|
107
|
+
for (const pending of [...agent.inbox.nextStep]) {
|
|
108
|
+
if (isOwnPluginMessage(pending))
|
|
109
|
+
agent.inbox.remove(pending.id);
|
|
110
|
+
}
|
|
111
|
+
for (const message of [...retained, ...messages]) {
|
|
112
|
+
if (message.role === "user" && isOwnPluginMessage(message))
|
|
113
|
+
continue;
|
|
114
|
+
enqueueMessage(agent, `context_${message.role}`, message, config.auditToolArguments);
|
|
115
|
+
}
|
|
116
|
+
if (capture)
|
|
117
|
+
additions = await coordinator.flushForPreStep(agent);
|
|
118
|
+
else
|
|
119
|
+
await coordinator.flush(agent, true);
|
|
120
|
+
signal.throwIfAborted();
|
|
121
|
+
rebuilt = true;
|
|
122
|
+
// Another replacement while querying requires another fresh snapshot.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
binding.needsRecount = true;
|
|
127
|
+
coordinator.invalidateContext(agent);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
return { messages, additions, rebuilt };
|
|
86
131
|
};
|
|
87
132
|
for (const agent of ctx.agents.list())
|
|
88
133
|
attach(agent);
|
|
89
|
-
ctx.on("agent/created", ({ agent }) =>
|
|
134
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
135
|
+
attach(agent);
|
|
136
|
+
return undefined;
|
|
137
|
+
});
|
|
90
138
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
91
139
|
agentsBySession.delete(agent.session);
|
|
92
140
|
coordinator.disposeAgent(agent);
|
|
93
141
|
});
|
|
94
142
|
ctx.on("agent/inbox/discarded", ({ agent, message }) => coordinator.markDiscarded(agent, message));
|
|
143
|
+
ctx.on("agent/assistant-stream", ({ agent, frame }) => {
|
|
144
|
+
if (frame.type !== "chunk")
|
|
145
|
+
return;
|
|
146
|
+
const chunk = frame.chunk;
|
|
147
|
+
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") {
|
|
148
|
+
coordinator.markGenerationStarted(agent);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
95
151
|
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
96
152
|
const decision = await next();
|
|
97
153
|
const agent = exec.agent;
|
|
98
154
|
if (!agent)
|
|
99
155
|
return decision;
|
|
156
|
+
if (agentsBySession.get(agent.session)?.needsRecount)
|
|
157
|
+
return decision;
|
|
158
|
+
// A canonical value replacement is rendered by the registry after this
|
|
159
|
+
// waterfall. Audit its committed tool/result at the next pre-step instead
|
|
160
|
+
// of matching stale content or invoking the tool's renderer a second time.
|
|
161
|
+
if (decision.kind === "accept" && "value" in decision)
|
|
162
|
+
return decision;
|
|
100
163
|
const content = decision.kind === "block"
|
|
101
164
|
? decision.feedback
|
|
102
165
|
: decision.content ?? result.content;
|
|
103
166
|
const text = content.flatMap((block) => blockText(block, config.auditReasoning)).filter(Boolean).join("\n").trim();
|
|
104
167
|
if (!text)
|
|
105
168
|
return decision;
|
|
106
|
-
coordinator.
|
|
107
|
-
eventSeq: Math.max(agent.session.seq, coordinator.pointers(agent).auditCursor + 1),
|
|
169
|
+
coordinator.observeNext(agent, {
|
|
108
170
|
source: "tool_result",
|
|
109
171
|
text,
|
|
110
172
|
});
|
|
@@ -117,30 +179,43 @@ export function apply(ctx, config) {
|
|
|
117
179
|
};
|
|
118
180
|
});
|
|
119
181
|
ctx.on("session/event", (session, event) => {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
observeEvent(agent, event);
|
|
182
|
+
const binding = agentsBySession.get(session);
|
|
183
|
+
if (binding)
|
|
184
|
+
observeEvent(binding.agent, event);
|
|
123
185
|
});
|
|
124
186
|
ctx.on("agent/pre-step", async (payload, next) => {
|
|
125
187
|
const decision = await next();
|
|
126
188
|
if (decision.kind === "reject")
|
|
127
189
|
return decision;
|
|
128
190
|
coordinator.setStepPhase(payload.agent, false, false);
|
|
129
|
-
const
|
|
130
|
-
|
|
191
|
+
const prepared = await recount(payload.agent, decision.messages, payload.signal);
|
|
192
|
+
const binding = agentsBySession.get(payload.agent.session);
|
|
193
|
+
binding.offered = prepared.messages;
|
|
194
|
+
prepared.messages.forEach((message) => {
|
|
131
195
|
if (!isOwnPluginMessage(message))
|
|
132
|
-
enqueueMessage(payload.agent,
|
|
196
|
+
enqueueMessage(payload.agent, "pre_step", message);
|
|
133
197
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return additions
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
const additions = prepared.rebuilt
|
|
199
|
+
? prepared.additions
|
|
200
|
+
: config.flushOnPreStep ? await coordinator.flushForPreStep(payload.agent) : [];
|
|
201
|
+
return { ...decision, messages: [...prepared.messages, ...additions] };
|
|
202
|
+
});
|
|
203
|
+
ctx.on("agent/request", async ({ agent, signal }, next) => {
|
|
204
|
+
const route = await next();
|
|
205
|
+
// Context-overflow recovery retries within the same step, bypassing
|
|
206
|
+
// pre-step. Commit the rebuilt delta before the driver derives that request.
|
|
207
|
+
const binding = agentsBySession.get(agent.session);
|
|
208
|
+
const prepared = await recount(agent, binding.offered, signal);
|
|
209
|
+
for (const message of prepared.additions) {
|
|
210
|
+
signal.throwIfAborted();
|
|
211
|
+
agent.session.append("user/message", message, { surfaceOp: "append" });
|
|
212
|
+
}
|
|
213
|
+
return route;
|
|
140
214
|
});
|
|
141
|
-
ctx.on("agent/turn-stopping", async ({ agent }) => {
|
|
215
|
+
ctx.on("agent/turn-stopping", async ({ agent, signal }) => {
|
|
142
216
|
if (!config.flushOnTurnStopping)
|
|
143
217
|
return;
|
|
218
|
+
await recount(agent, [], signal, false);
|
|
144
219
|
await coordinator.flush(agent, true);
|
|
145
220
|
});
|
|
146
221
|
ctx.effect(() => () => coordinator.dispose());
|
|
@@ -150,8 +225,10 @@ export function apply(ctx, config) {
|
|
|
150
225
|
logger.warn("initial terminology catalog fetch failed; live events will retry: %s", error instanceof Error ? error.message : String(error));
|
|
151
226
|
});
|
|
152
227
|
}
|
|
153
|
-
export function messageText(message, auditReasoning = true) {
|
|
154
|
-
return message.content.flatMap((block) =>
|
|
228
|
+
export function messageText(message, auditReasoning = true, auditToolArguments = false) {
|
|
229
|
+
return message.content.flatMap((block) => block.type === "tool-call" && auditToolArguments
|
|
230
|
+
? [`${block.name}\n${block.arguments}`]
|
|
231
|
+
: blockText(block, auditReasoning)).filter(Boolean).join("\n").trim();
|
|
155
232
|
}
|
|
156
233
|
function blockText(block, auditReasoning) {
|
|
157
234
|
if (block.type === "text")
|
|
@@ -160,8 +237,6 @@ function blockText(block, auditReasoning) {
|
|
|
160
237
|
// Streaming reasoning-delta chunks are lifecycle signals and never enter here.
|
|
161
238
|
if (block.type === "reasoning")
|
|
162
239
|
return auditReasoning ? [block.text] : [];
|
|
163
|
-
if (block.type === "tool-result")
|
|
164
|
-
return block.content.flatMap((item) => blockText(item, auditReasoning));
|
|
165
240
|
return [];
|
|
166
241
|
}
|
|
167
242
|
export { APOLLO_TERMINOLOGY_API_URL, TerminologyApi } from "./api.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import type { UserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
export interface DeliveryReceipt {
|
|
3
|
+
version: 1;
|
|
4
|
+
ruleIds: string[];
|
|
5
|
+
termIds: string[];
|
|
6
|
+
kbVersion: string;
|
|
7
|
+
catalogRevision: number;
|
|
8
|
+
textSha256: string;
|
|
9
|
+
}
|
|
10
|
+
declare module "@deepseek-ai/dsh-llm" {
|
|
11
|
+
interface MessageSourceMap {
|
|
12
|
+
"plugin:apollo-terminology-rules": {
|
|
13
|
+
kind: "plugin:apollo-terminology-rules";
|
|
14
|
+
form: "instructions";
|
|
15
|
+
receipt?: DeliveryReceipt;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
2
19
|
export interface PluginConfig {
|
|
3
20
|
apiKeyEnv: string;
|
|
4
21
|
queryIntervalMs: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollo-music/dsh-sidecar-terms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "DeepSeek Harness sidecar plugin for incremental Apollo terminology and rule injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,20 +40,27 @@
|
|
|
40
40
|
"prepack": "npm run build"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@deepseek-ai/cordis": "
|
|
44
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/
|
|
43
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
44
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-brand": "0.1.7-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.1.7-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-scope": "0.1.7-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-session-persistence": "0.1.7-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-storage": "0.1.7-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-storage-domain": "0.1.7-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-timeout": "0.1.7-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-user-approval": "0.1.7-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-workspace": "0.1.7-rc.2",
|
|
64
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
58
65
|
}
|
|
59
66
|
}
|