@nightpainters/dsh-ling 1.6.1 → 1.7.0
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/CHANGELOG.md +207 -41
- package/DESIGN.md +121 -29
- package/PHILOSOPHY.md +184 -0
- package/README.md +112 -21
- package/cordis.patch.yml +32 -2
- package/lib/client.js +2520 -55
- package/lib/host/agent-tools.js +286 -0
- package/lib/host/api.js +612 -61
- package/lib/host/archive.js +3 -3
- package/lib/host/autotree.js +2 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/config.js +38 -0
- package/lib/host/deep-index-skill.js +9 -0
- package/lib/host/deepsummary.js +75 -2
- package/lib/host/dsweb-summary.js +6 -3
- package/lib/host/guard.js +53 -0
- package/lib/host/inject.js +12 -2
- package/lib/host/l1.js +59 -10
- package/lib/host/library-checker.js +552 -0
- package/lib/host/library-import-engine.js +211 -0
- package/lib/host/library-import.js +911 -0
- package/lib/host/library-runtime.js +199 -0
- package/lib/host/library-sqlite.js +344 -0
- package/lib/host/library-store.js +1714 -0
- package/lib/host/memory.js +89 -30
- package/lib/host/persona.js +13 -3
- package/lib/host/rawlog.js +10 -5
- package/lib/host/recall-search.js +192 -17
- package/lib/host/snapshot.js +2 -1
- package/lib/host/surgery.js +7 -2
- package/lib/host/tools.js +506 -31
- package/lib/host/util.js +1 -1
- package/lib/index.js +120 -81
- package/package.json +11 -5
- package/tools/deep-import-docs.mjs +1210 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// 器灵 · agent 作用域工具的**存量补绑**(1.7 热重载缺口修复 · 隔离副本)
|
|
2
|
+
//
|
|
3
|
+
// ── 症状(真机 + 隔离探针都复现过)────────────────────────────────────────────────
|
|
4
|
+
// agent 作用域的那一份工具只在 `agent/created` / `agent/session-start` 事件里注册。
|
|
5
|
+
// 插件热重载时,已经活着的 agent **不会再收到 created 事件** ⇒ 新插件的 scope 里一件工具都没有;
|
|
6
|
+
// 而全局那份又被 agent preset 的 `restrict({allow:[…]})` 掩码挡在可见面之外
|
|
7
|
+
// (规则实读:`dsh-tools/lib/index.js:2959-2977`,`layers.every(layer => layer.admits(name))`
|
|
8
|
+
// 过滤继承层,`own` 那一份再 append)⇒ 表现就是"器灵的工具从清单里消失"。
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ 不能靠"卸载时别撤销旧注册"来绕过:旧注册的闭包持有**已关闭**的 LibraryRuntime,
|
|
11
|
+
// 留着它等于把工具接到一具尸体上(搜索会回 `reason:'closed'`)。正确做法是**重绑新的**。
|
|
12
|
+
//
|
|
13
|
+
// ── 本模块的做法(用已核的真实宿主接口)──────────────────────────────────────────
|
|
14
|
+
// 1. **先挂监听,再枚举存量**:`install()` 挂事件 → `bindExisting()` 走
|
|
15
|
+
// `ctx.agents.list()`(实读 `dsh-agent/lib/index.js:612-614`:`return [...this.store.values()]
|
|
16
|
+
// .map((entry) => entry.agent);`,每次都回**新数组**)补绑。中间新创建的 agent 不会漏。
|
|
17
|
+
// 2. **身份 = agent/context 对象**(`agent.ctx`,见 `dsh-agent` 类型声明 L148-149:
|
|
18
|
+
// "Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject
|
|
19
|
+
// registration afterward."),**不是**会话 id + 3 秒时间窗 —— 同一个 sid 的两个不同 agent
|
|
20
|
+
// 必须各绑各的;同一次创建的两个事件名必须只绑一次。
|
|
21
|
+
// 3. **注册只走 registerLingTools**(注入进来的 `register`):内部的 `guard()`(含
|
|
22
|
+
// lossless、参数校验)**不许绕过**;本模块不直接碰 `ctx.tools.register`。
|
|
23
|
+
// 4. **部分失败不静默**:`registerLingTools` 对单个 spec 的注册失败只 `console.debug` 并少一个
|
|
24
|
+
// disposer(实读 `tools.js:1628-1639`:`return { ok: disposers.length > 0, disposers }`),
|
|
25
|
+
// 所以这里额外拿**全局注册的工具数**当基准,数量不足 ⇒ 明确记 `partial`,不算全成功。
|
|
26
|
+
// 5. **延迟/缺席的服务**:`agents` 服务尚未挂载时走既有 `ctx.inject(['agents'], cb)`
|
|
27
|
+
// (与 entry 里 tools 的接续方式同一套路),不造枚举 API、不起"全库轮询"定时器。
|
|
28
|
+
// 6. **卸载即清自己**:`dispose()` 撤销本实例注册的全部工具并清空绑定表;
|
|
29
|
+
// 之后 `stopping` 锁死,任何事件/inject 回调都不再回绑。
|
|
30
|
+
|
|
31
|
+
import { pick } from './util.js';
|
|
32
|
+
|
|
33
|
+
/** 两个跨 cohort 的事件名(与 lifecycle.js 同一套口径,不另发明)。 */
|
|
34
|
+
export const AGENT_TOOL_EVENTS = ['agent/created', 'agent/session-start'];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 一个值"能不能在上面注册工具"= 它是不是一个可用的 ctx。
|
|
38
|
+
* ⚠️ 判据必须比"有 on + 有 get"宽:宿主/测试里的 agent ctx 最小形态可能**只有** `tools.register`
|
|
39
|
+
* (实读主线 `tests/library-runtime.test.mjs:151` 的桩就是 `{ tools: { register } }`)——
|
|
40
|
+
* 拿 on/get 当门槛会把这种合法 ctx 判成"没有 ctx",于是工具不绑、断言当场红。
|
|
41
|
+
*/
|
|
42
|
+
const canRegister = (v) => !!v && typeof v === 'object'
|
|
43
|
+
&& ((!!v.tools && typeof v.tools.register === 'function') || typeof v.get === 'function');
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 取"该绑在谁身上"的身份对象。
|
|
47
|
+
*
|
|
48
|
+
* 顺序(有讲究):
|
|
49
|
+
* ① 形状像 Agent(`{ session, ctx }`,Agent 自己**不可注册**)⇒ 用它的 `ctx`
|
|
50
|
+
* (实读 `dsh-agent` 类型声明 L148-149:agent-scoped context)。
|
|
51
|
+
* ② 它自己就可注册 ⇒ 用它自己(裸 `payload.agentCtx`,以及原生 Context:
|
|
52
|
+
* **不要**先去读 `.ctx`,原生 Context 上的 `.ctx` 可能指回别处,会把不同 agent 判成同一个身份)。
|
|
53
|
+
* ③ 兜底再用 `.ctx`。
|
|
54
|
+
*/
|
|
55
|
+
export function agentIdentity(target) {
|
|
56
|
+
if (!target || typeof target !== 'object') return null;
|
|
57
|
+
const nested = target.ctx;
|
|
58
|
+
if (nested && typeof nested === 'object' && nested !== target && canRegister(nested) && !canRegister(target)) return nested;
|
|
59
|
+
if (canRegister(target)) return target;
|
|
60
|
+
if (nested && typeof nested === 'object' && canRegister(nested)) return nested;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 仅用于日志/留痕的会话 id(失败信息要能指认是谁);不参与去重。 */
|
|
65
|
+
function sessionIdOf(target) {
|
|
66
|
+
try {
|
|
67
|
+
const sid = pick(
|
|
68
|
+
() => target?.session?.id,
|
|
69
|
+
() => target?.agent?.session?.id,
|
|
70
|
+
() => target?.id,
|
|
71
|
+
);
|
|
72
|
+
return sid === undefined ? '' : String(sid);
|
|
73
|
+
} catch { return ''; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 建一个补绑器。
|
|
78
|
+
* @param {object} options
|
|
79
|
+
* @param {object} options.ctx 插件 ctx(挂在它上面的 `agents` 服务/事件/inject)
|
|
80
|
+
* @param {Function} options.register registerLingTools(注入,便于测试;不许换成裸注册)
|
|
81
|
+
* @param {object} [options.deps] 传给 register 的依赖(含**本次装载**的 library 实例)
|
|
82
|
+
* @param {Function} [options.isDisposed] 入口是否已进入卸载
|
|
83
|
+
* @param {number|Function} [options.expectedToolCount] 全局注册到的工具数(0 = 不校验数量)
|
|
84
|
+
* @param {Function} [options.onNote] (level, message, data) => void,留痕用(写入入口的 kv)
|
|
85
|
+
*/
|
|
86
|
+
export function createAgentToolBinder({
|
|
87
|
+
ctx,
|
|
88
|
+
register,
|
|
89
|
+
deps = {},
|
|
90
|
+
isDisposed = () => false,
|
|
91
|
+
expectedToolCount = 0,
|
|
92
|
+
onNote = () => {},
|
|
93
|
+
} = {}) {
|
|
94
|
+
/** 身份对象 → 绑定记录。用 Map(不是 WeakMap):卸载时要能逐一撤销并清空。 */
|
|
95
|
+
const bound = new Map();
|
|
96
|
+
const failures = [];
|
|
97
|
+
const ownDisposers = [];
|
|
98
|
+
let installed = false;
|
|
99
|
+
let stopping = false;
|
|
100
|
+
let listed = 0;
|
|
101
|
+
let already = 0;
|
|
102
|
+
let refusedAfterDispose = 0;
|
|
103
|
+
let serviceAvailable = false;
|
|
104
|
+
let injectHooked = false;
|
|
105
|
+
|
|
106
|
+
const expected = () => {
|
|
107
|
+
const n = typeof expectedToolCount === 'function' ? expectedToolCount() : expectedToolCount;
|
|
108
|
+
const value = Number(n);
|
|
109
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
110
|
+
};
|
|
111
|
+
const note = (level, message, data) => {
|
|
112
|
+
try { onNote(level, message, data); } catch { /* 留痕失败不许影响主流程 */ }
|
|
113
|
+
if (level === 'warn' || level === 'error') console.warn('[dsh-ling] %s', message);
|
|
114
|
+
};
|
|
115
|
+
const fail = (reason, target, extra) => {
|
|
116
|
+
const entry = { reason, sid: sessionIdOf(target), ...(extra || {}) };
|
|
117
|
+
failures.push(entry);
|
|
118
|
+
if (failures.length > 50) failures.shift();
|
|
119
|
+
return entry;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** 绑一个 agent(或一个裸 agent-ctx)。已绑过的身份直接跳过 —— 这就是去重。 */
|
|
123
|
+
function bindOne(target, source) {
|
|
124
|
+
if (stopping || isDisposed()) {
|
|
125
|
+
refusedAfterDispose += 1;
|
|
126
|
+
return { ok: false, reason: 'disposed' };
|
|
127
|
+
}
|
|
128
|
+
const identity = agentIdentity(target);
|
|
129
|
+
if (!identity) {
|
|
130
|
+
const entry = fail('no-agent-context', target, { source });
|
|
131
|
+
note('warn', `agent 作用域工具补绑失败(${source}):payload 里没有可用的 agent ctx`, entry);
|
|
132
|
+
return { ok: false, reason: entry.reason };
|
|
133
|
+
}
|
|
134
|
+
if (bound.has(identity)) {
|
|
135
|
+
already += 1;
|
|
136
|
+
return { ok: true, reason: 'already' };
|
|
137
|
+
}
|
|
138
|
+
let result = null;
|
|
139
|
+
try {
|
|
140
|
+
result = register(identity, deps); // ← 唯一注册入口(内部的 guard 不许绕过)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const entry = fail('register-threw', target, { source, detail: String(error?.message ?? error).slice(0, 200) });
|
|
143
|
+
note('warn', `agent 作用域工具注册抛错(${source}): ${String(error?.message ?? error).slice(0, 200)}`, entry);
|
|
144
|
+
return { ok: false, reason: entry.reason };
|
|
145
|
+
}
|
|
146
|
+
const disposers = Array.isArray(result?.disposers) ? result.disposers.filter((d) => typeof d === 'function') : [];
|
|
147
|
+
if (!result || result.ok !== true) {
|
|
148
|
+
const entry = fail(String(result?.reason || 'not-ok'), target, { source, disposers: disposers.length });
|
|
149
|
+
note('warn', `agent 作用域工具未注册(${source} / ${entry.reason})`, entry);
|
|
150
|
+
return { ok: false, reason: entry.reason };
|
|
151
|
+
}
|
|
152
|
+
const want = expected();
|
|
153
|
+
const partial = want > 0 && disposers.length < want;
|
|
154
|
+
bound.set(identity, { disposers, sid: sessionIdOf(target), source, partial, got: disposers.length, want });
|
|
155
|
+
if (partial) {
|
|
156
|
+
// 「部分失败不静默」:少了几件工具就必须说清,不能当全成功(否则又是"工具悄悄没了")。
|
|
157
|
+
const entry = fail('partial', target, { source, got: disposers.length, want });
|
|
158
|
+
note('warn', `agent 作用域工具只注册到 ${disposers.length}/${want} 件(${source})`, entry);
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, reason: partial ? 'partial' : 'bound', partial, got: disposers.length };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveRegistry(c = ctx) {
|
|
164
|
+
try {
|
|
165
|
+
const registry = pick(() => c?.agents, () => (typeof c?.get === 'function' ? c.get('agents') : undefined));
|
|
166
|
+
return registry && typeof registry.list === 'function' ? registry : null;
|
|
167
|
+
} catch { return null; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 第 2 步:枚举存量 agent 补绑(`list()` 每次都回新数组,不受调用方改动影响)。 */
|
|
171
|
+
function bindExisting(c = ctx) {
|
|
172
|
+
if (stopping || isDisposed()) {
|
|
173
|
+
refusedAfterDispose += 1;
|
|
174
|
+
return { ok: false, reason: 'disposed' };
|
|
175
|
+
}
|
|
176
|
+
const registry = resolveRegistry(c);
|
|
177
|
+
if (!registry) {
|
|
178
|
+
serviceAvailable = false;
|
|
179
|
+
return { ok: false, reason: 'no-agents-service' };
|
|
180
|
+
}
|
|
181
|
+
serviceAvailable = true;
|
|
182
|
+
let raw = null;
|
|
183
|
+
try { raw = registry.list(); }
|
|
184
|
+
catch (error) {
|
|
185
|
+
const entry = fail('list-failed', null, { detail: String(error?.message ?? error).slice(0, 200) });
|
|
186
|
+
note('warn', `agents.list() 抛错: ${entry.detail}`, entry);
|
|
187
|
+
return { ok: false, reason: entry.reason };
|
|
188
|
+
}
|
|
189
|
+
const agents = Array.isArray(raw) ? raw : [];
|
|
190
|
+
listed = agents.length;
|
|
191
|
+
let boundNow = 0;
|
|
192
|
+
for (const agent of agents) {
|
|
193
|
+
const r = bindOne(agent, 'enumerate');
|
|
194
|
+
if (r.reason === 'bound' || r.reason === 'partial') boundNow += 1;
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, listed: agents.length, boundNow, status: status() };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** `agents` 服务迟到(或 provider 重载):走既有 ctx.inject 接续;不造枚举 API、不起轮询。 */
|
|
200
|
+
function hookLateService() {
|
|
201
|
+
if (injectHooked || stopping || isDisposed()) return false;
|
|
202
|
+
injectHooked = true;
|
|
203
|
+
try {
|
|
204
|
+
const disposer = ctx?.inject?.(['agents'], (child) => {
|
|
205
|
+
if (stopping || isDisposed()) { refusedAfterDispose += 1; return; }
|
|
206
|
+
bindExisting(child || ctx);
|
|
207
|
+
});
|
|
208
|
+
if (typeof disposer === 'function') ownDisposers.push(disposer);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const entry = fail('inject-failed', null, { detail: String(error?.message ?? error).slice(0, 200) });
|
|
212
|
+
note('warn', `ctx.inject(['agents']) 失败: ${entry.detail}`, entry);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 第 1 步:挂事件监听(**必须在枚举之前**)。 */
|
|
218
|
+
function install() {
|
|
219
|
+
if (installed || stopping) return status();
|
|
220
|
+
installed = true;
|
|
221
|
+
for (const name of AGENT_TOOL_EVENTS) {
|
|
222
|
+
try {
|
|
223
|
+
const disposer = ctx?.on?.(name, (payload) => {
|
|
224
|
+
const target = pick(
|
|
225
|
+
() => payload?.agent,
|
|
226
|
+
() => payload?.session?.agent,
|
|
227
|
+
() => payload?.agentCtx,
|
|
228
|
+
);
|
|
229
|
+
if (!target) {
|
|
230
|
+
const entry = fail('no-agent-in-payload', payload, { source: name });
|
|
231
|
+
note('warn', `事件 ${name} 的载荷里没有 agent,补绑跳过(不静默)`, entry);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
bindOne(target, name);
|
|
235
|
+
});
|
|
236
|
+
if (typeof disposer === 'function') ownDisposers.push(disposer);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const entry = fail('listener-failed', null, { source: name, detail: String(error?.message ?? error).slice(0, 200) });
|
|
239
|
+
note('warn', `挂 ${name} 监听失败: ${entry.detail}`, entry);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return status();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function status() {
|
|
246
|
+
return {
|
|
247
|
+
installed,
|
|
248
|
+
stopping,
|
|
249
|
+
serviceAvailable,
|
|
250
|
+
injectHooked,
|
|
251
|
+
listed,
|
|
252
|
+
bound: bound.size,
|
|
253
|
+
already,
|
|
254
|
+
refusedAfterDispose,
|
|
255
|
+
partial: [...bound.values()].filter((b) => b.partial).length,
|
|
256
|
+
failures: failures.map((f) => ({ ...f })),
|
|
257
|
+
expectedTools: expected(),
|
|
258
|
+
events: [...AGENT_TOOL_EVENTS],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 卸载:撤销本实例的全部注册、清空绑定表,并锁死后续回绑。 */
|
|
263
|
+
function dispose() {
|
|
264
|
+
stopping = true;
|
|
265
|
+
const pending = [];
|
|
266
|
+
for (const [identity, record] of bound) {
|
|
267
|
+
for (const d of record.disposers) {
|
|
268
|
+
try { pending.push(d()); } catch (error) { pending.push(Promise.reject(error)); }
|
|
269
|
+
}
|
|
270
|
+
bound.delete(identity);
|
|
271
|
+
}
|
|
272
|
+
for (const d of ownDisposers.splice(0).reverse()) {
|
|
273
|
+
try { pending.push(d()); } catch (error) { pending.push(Promise.reject(error)); }
|
|
274
|
+
}
|
|
275
|
+
if (!pending.length) return { ok: true, released: 0 };
|
|
276
|
+
return Promise.allSettled(pending).then((results) => {
|
|
277
|
+
const failed = results.filter((r) => r.status === 'rejected');
|
|
278
|
+
if (failed.length) {
|
|
279
|
+
note('warn', `agent 作用域工具卸载时 ${failed.length} 个 disposer 失败`, { failed: failed.length });
|
|
280
|
+
}
|
|
281
|
+
return { ok: failed.length === 0, released: results.length, failed: failed.length };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { install, bindExisting, hookLateService, status, dispose, bindOne };
|
|
286
|
+
}
|