@coralai/sps-plugin-api 0.10.0 → 0.12.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/dist/index.d.ts CHANGED
@@ -242,13 +242,143 @@ export interface SpsRegistryByFactory<T = unknown> {
242
242
  register(factory: T): () => void;
243
243
  list(): string[];
244
244
  }
245
+ /** 作用域:global 跨项目 / project 单项目 / agent(harness 会话)。 */
246
+ export type MemoryScope = 'global' | 'project' | 'agent';
247
+ export type MemoryCategory = 'convention' | 'decision' | 'gotcha' | 'pattern' | 'reference' | 'note';
248
+ /** 出处:agent 主动写 / capture 自动捕获 / human 人(CLI/Console)。 */
249
+ export type MemorySource = 'agent' | 'capture' | 'human';
250
+ /** 定位一个作用域。`project` scope 必带 project;`agent` scope 必带 agentId。 */
251
+ export interface MemoryScopeRef {
252
+ scope: MemoryScope;
253
+ project?: string;
254
+ agentId?: string;
255
+ }
256
+ export interface MemoryEntry {
257
+ /** 后端内部的裸 id(不带 `<backend>:` 前缀)。 */
258
+ id: string;
259
+ title: string;
260
+ scope: MemoryScope;
261
+ category: MemoryCategory;
262
+ tags: string[];
263
+ /** 显著度 1..5。 */
264
+ salience: number;
265
+ /** 被读回全文的次数 —— "真被用到"的硬信号。后端不支持时恒 0 即可。 */
266
+ uses: number;
267
+ source: MemorySource;
268
+ project?: string;
269
+ created: string;
270
+ updated: string;
271
+ body: string;
272
+ }
273
+ /** 不带 id = 新建;带 id = 更新。 */
274
+ export interface MemorySaveInput {
275
+ id?: string;
276
+ title: string;
277
+ body: string;
278
+ category?: MemoryCategory;
279
+ tags?: string[];
280
+ salience?: number;
281
+ source?: MemorySource;
282
+ project?: string;
283
+ }
284
+ export interface MemoryRecallHit {
285
+ /** **必须带 `<backend>:` 前缀** —— 调用方据此知道回哪个后端读全文。 */
286
+ id: string;
287
+ /** 产出这条结果的后端 id。 */
288
+ source: string;
289
+ title: string;
290
+ snippet: string;
291
+ score: number;
292
+ scope?: string;
293
+ project?: string;
294
+ category?: string;
295
+ tags?: string[];
296
+ }
297
+ export interface MemoryRecallOpts {
298
+ tags?: string[];
299
+ limit?: number;
300
+ }
245
301
  /**
246
- * ⚠️ 同上:这两个是**甲·工厂型**(换实现,不是收一组),
247
- * 今天没有第三方在换它们。声明方法名,形状留 `unknown`。
302
+ * 记忆后端。**必须实现 6 个,可选 4 个**。
303
+ *
304
+ * ⚠️ 可选的四个宿主都有默认实现 —— 不实现不是缺陷:
305
+ * 没有排序反馈机制的后端不该被迫写 `bumpUses`,没有派生产物的不该写 `reindex`。
306
+ *
307
+ * 🔴 读与写**必须一起实现**。只做一半的症状是"存进去的和读出来的不是一回事",
308
+ * 比彻底不工作难查得多。
248
309
  */
249
310
  export interface SpsMemory {
250
- buildInjection(refs: unknown[]): string;
251
- }
311
+ recall(refs: MemoryScopeRef[], query: string, opts?: MemoryRecallOpts): Promise<MemoryRecallHit[]>;
312
+ listEntries(ref: MemoryScopeRef): MemoryEntry[];
313
+ readEntry(ref: MemoryScopeRef, id: string): MemoryEntry | null;
314
+ listAllScopes(): MemoryScopeRef[];
315
+ saveEntry(ref: MemoryScopeRef, input: MemorySaveInput): MemoryEntry;
316
+ deleteEntry(ref: MemoryScopeRef, id: string): boolean;
317
+ /** 后端当前是否可用。返回 false **不是错误** —— 调用方据此静默跳过。 */
318
+ enabled(): boolean;
319
+ bumpUses(ref: MemoryScopeRef, id: string): void;
320
+ /** 重建该作用域的派生产物。批量写入后调一次,不是每条都调。 */
321
+ reindex(ref: MemoryScopeRef): void;
322
+ promoteEntry(from: MemoryScopeRef, id: string, to: MemoryScopeRef, opts: {
323
+ move?: boolean;
324
+ }): MemoryEntry | null;
325
+ /**
326
+ * 宿主写完记忆配置后调一次,让长命进程无需重启即读到新值。
327
+ * 后端不缓存配置 ⇒ 空实现即可。
328
+ */
329
+ reloadConfig(): void;
330
+ /** 后端支持哪些维护操作。默认两个都不支持。 */
331
+ readonly supports: {
332
+ gc?: boolean;
333
+ migrate?: boolean;
334
+ };
335
+ /** 回收一个作用域(硬删 tombstone、去重)。`supports.gc` 为假时不会被调用。 */
336
+ gc(ref: MemoryScopeRef): Promise<MemoryGcStats>;
337
+ /**
338
+ * 迁移该后端自己的历史数据结构。`supports.migrate` 为假时不会被调用。
339
+ *
340
+ * ⚠️ 这是**后端的历史包袱**,不是通用能力 —— 一个新后端不该有可迁的旧结构。
341
+ */
342
+ migrate(target: {
343
+ project?: string;
344
+ all?: boolean;
345
+ }): MemoryMigrateStats;
346
+ /**
347
+ * 要注入 worker 的 Claude Code 插件目录(**绝对路径**)。没有就返回 `[]`。
348
+ *
349
+ * 用于把"自动召回"做进会话:hooks 在 worker 的 claude 进程里跑,
350
+ * 不依赖 agent 主动调工具。
351
+ *
352
+ * ⚠️ 只有 claude 后端会用到 —— 别的 agent(openai/codex)没有对应概念,
353
+ * 它们要等价能力时各自在自己的适配器里解决。
354
+ */
355
+ workerPlugins(): string[];
356
+ }
357
+ export interface MemoryGcStats {
358
+ /** 硬删的 tombstone 数。 */
359
+ purged: number;
360
+ /** 去重软删的重复条数。 */
361
+ deduped: number;
362
+ /** 收敛后剩余条数。 */
363
+ kept: number;
364
+ }
365
+ /** 迁移了多少条 —— 键由后端自定(本地实现给 global/agents/projects)。 */
366
+ export type MemoryMigrateStats = Record<string, number>;
367
+ /** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
368
+ export declare const MEMORY_REDACTED_CODE = "memory-redacted";
369
+ /** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
370
+ export declare function normalizeMemoryBody(s: string): string;
371
+ export interface MemoryDigestOpts {
372
+ /** 标题清单最多列几条(默认 60)。 */
373
+ maxTitles?: number;
374
+ /** 内联几条高 salience 正文(默认 6)。 */
375
+ maxBodies?: number;
376
+ /** 每条正文最多字符(默认 500)。 */
377
+ bodyChars?: number;
378
+ }
379
+ /** entries → 摘要 markdown(标题清单 + top 高 salience 正文)。空集合返回空串。 */
380
+ export declare function buildMemoryDigest(entries: MemoryEntry[], opts?: MemoryDigestOpts): string;
381
+ /** ⚠️ 甲·工厂型(换实现,不是收一组);今天没有第三方在换它。 */
252
382
  export interface SpsNotifier {
253
383
  open(config: unknown): {
254
384
  send(message: string, level?: 'info' | 'success' | 'warning' | 'error'): Promise<void>;
package/dist/index.js CHANGED
@@ -11,6 +11,61 @@
11
11
  * ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
12
12
  * `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
13
13
  */
14
+ // ── 后端可以往 worker 注入一个 Claude Code 插件 ─────────────────────────────
15
+ //
16
+ // 记忆的读路有两半:**pull**(agent 自己调 `memory_recall`)与 **push**(会话自动召回)。
17
+ // 只有 pull 时,工具挂着而 agent 不调 = 等于没记忆 —— 这是实测过的失效模式。
18
+ // push 那一半靠 Claude Code 的 hooks,而 hooks 装在一个 claude 插件目录里。
19
+ //
20
+ // 🔴 宿主**只要一个目录路径**,不解析里面的 hooks.json / .mcp.json ——
21
+ // 那是运行时 substrate 的格式,和"sps 不解析 claude 的 transcript"同源。
22
+ // ⚠️ 目录由 SDK 的 `plugins: [{type:'local', path}]` 加载,**不需要装进
23
+ // `~/.claude/plugins`**(2026-08-27 实测:任意路径可加载,MCP server 正常注册)。
24
+ // 这也意味着关掉 = 不传参数,零残留。
25
+ // ── 跨后端必须一致的算法 ──────────────────────────────────────────────────
26
+ //
27
+ // 🔴 放这里的判据只有一条:**两个后端对它给出不同答案会出错**。
28
+ // "什么算重复"若两个后端不一致,同一条记忆在不同后端行为不同;
29
+ // 摘要格式不一致,人看到的 MEMORY.md 会跟着换后端变形。
30
+ // ⚠️ 别把"本地实现顺手用得上的工具"塞进来 —— 那属于后端自己。
31
+ /** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
32
+ export const MEMORY_REDACTED_CODE = 'memory-redacted';
33
+ /** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
34
+ export function normalizeMemoryBody(s) {
35
+ return s
36
+ .toLowerCase()
37
+ .replace(/[#*`_>\-\s]+/g, ' ')
38
+ .replace(/[^\p{L}\p{N} ]+/gu, '')
39
+ .trim();
40
+ }
41
+ /** entries → 摘要 markdown(标题清单 + top 高 salience 正文)。空集合返回空串。 */
42
+ export function buildMemoryDigest(entries, opts = {}) {
43
+ const maxTitles = opts.maxTitles ?? 60;
44
+ const maxBodies = opts.maxBodies ?? 6;
45
+ const bodyChars = opts.bodyChars ?? 500;
46
+ if (entries.length === 0)
47
+ return '';
48
+ const ranked = entries
49
+ .slice()
50
+ .sort((a, b) => b.salience - a.salience || b.updated.localeCompare(a.updated));
51
+ const lines = ['# 记忆摘要', '', '## 索引'];
52
+ for (const e of ranked.slice(0, maxTitles)) {
53
+ const tags = e.tags.length ? ` #${e.tags.join(' #')}` : '';
54
+ lines.push(`- [${e.id}] (${e.category}, s${e.salience}) ${e.title}${tags}`);
55
+ }
56
+ if (ranked.length > maxTitles) {
57
+ lines.push(`- …还有 ${ranked.length - maxTitles} 条(用 memory_recall 检索)`);
58
+ }
59
+ const bodies = ranked.slice(0, maxBodies);
60
+ if (bodies.length) {
61
+ lines.push('', '## 高显著度');
62
+ for (const e of bodies) {
63
+ const body = e.body.length > bodyChars ? `${e.body.slice(0, bodyChars)}…` : e.body;
64
+ lines.push('', `### ${e.title}`, body);
65
+ }
66
+ }
67
+ return lines.join('\n').trim();
68
+ }
14
69
  // ── 服务名 ───────────────────────────────────────────────────────────────
15
70
  /** 插件 `inject` 里写的名字。 */
16
71
  export const SPS_SERVICES = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralai/sps-plugin-api",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "sps 宿主半区的插件契约:ctx 上那些服务的方法签名。插件装它拿类型。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",