@coralai/sps-plugin-api 0.9.0 → 0.11.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,18 +242,162 @@ 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
+ export interface MemoryGcStats {
348
+ /** 硬删的 tombstone 数。 */
349
+ purged: number;
350
+ /** 去重软删的重复条数。 */
351
+ deduped: number;
352
+ /** 收敛后剩余条数。 */
353
+ kept: number;
354
+ }
355
+ /** 迁移了多少条 —— 键由后端自定(本地实现给 global/agents/projects)。 */
356
+ export type MemoryMigrateStats = Record<string, number>;
357
+ /** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
358
+ export declare const MEMORY_REDACTED_CODE = "memory-redacted";
359
+ /** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
360
+ export declare function normalizeMemoryBody(s: string): string;
361
+ export interface MemoryDigestOpts {
362
+ /** 标题清单最多列几条(默认 60)。 */
363
+ maxTitles?: number;
364
+ /** 内联几条高 salience 正文(默认 6)。 */
365
+ maxBodies?: number;
366
+ /** 每条正文最多字符(默认 500)。 */
367
+ bodyChars?: number;
368
+ }
369
+ /** entries → 摘要 markdown(标题清单 + top 高 salience 正文)。空集合返回空串。 */
370
+ export declare function buildMemoryDigest(entries: MemoryEntry[], opts?: MemoryDigestOpts): string;
371
+ /** ⚠️ 甲·工厂型(换实现,不是收一组);今天没有第三方在换它。 */
252
372
  export interface SpsNotifier {
253
373
  open(config: unknown): {
254
374
  send(message: string, level?: 'info' | 'success' | 'warning' | 'error'): Promise<void>;
255
375
  };
256
376
  }
377
+ export interface SpsStorageObject {
378
+ key: string;
379
+ /** **内容判据**(单段上传时等于 md5)。不看时间。 */
380
+ etag: string;
381
+ size: number;
382
+ }
383
+ /**
384
+ * 对象存储(读 / 列 / 写)。桶是共享面 —— 多节点下 studio 和工作区不在同一台机器上。
385
+ *
386
+ * 🔴 **它只搬字节,不做账。** id / 索引 / 缩略图 / 分类 / 配额 / 计费都在平台侧。
387
+ *
388
+ * ⚠️ **权限按前缀分,而且很紧**(单写方规则)。
389
+ * 读不了的前缀会**抛**,不会回空 —— "库是空的"和"我没权限"混成一个,
390
+ * 人会去补素材而不是去补策略。
391
+ * ⚠️ 桶没配时三个方法都抛 ⇒ **先用 `configured()` 问一句**,别拿异常当分支。
392
+ */
393
+ export interface SpsStorage {
394
+ configured(): boolean;
395
+ /** 不存在 ⇒ `null`;连不上/没权限 ⇒ **抛**。 */
396
+ get(key: string): Promise<Buffer | null>;
397
+ list(prefix: string): Promise<SpsStorageObject[]>;
398
+ /** ⚠️ `contentType` 自己决定 —— 桶是直连的,缺省会让预览变成一次下载。 */
399
+ put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
400
+ }
257
401
  /** 与 `SpsAsyncPoll` 同形 —— async 能力的 `poll` 可以**直接透传**。 */
258
402
  export type SpsJobStatus = SpsAsyncPoll;
259
403
  /**
@@ -479,6 +623,7 @@ export declare const SPS_SERVICES: {
479
623
  readonly projects: "sps.projects";
480
624
  readonly cards: "sps.cards";
481
625
  readonly jobs: "sps.jobs";
626
+ readonly storage: "sps.storage";
482
627
  readonly subprocess: "sps.subprocess";
483
628
  readonly attachments: "sps.attachments";
484
629
  readonly media: "sps.media";
@@ -501,6 +646,7 @@ export interface SpsPluginContext {
501
646
  'sps.projects': SpsProjects;
502
647
  'sps.cards': SpsCards;
503
648
  'sps.jobs': SpsJobs;
649
+ 'sps.storage': SpsStorage;
504
650
  'sps.capabilities': SpsCapabilities;
505
651
  'sps.media': SpsMedia;
506
652
  'sps.im': SpsRegistryByKey;
package/dist/index.js CHANGED
@@ -11,6 +11,50 @@
11
11
  * ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
12
12
  * `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
13
13
  */
14
+ // ── 跨后端必须一致的算法 ──────────────────────────────────────────────────
15
+ //
16
+ // 🔴 放这里的判据只有一条:**两个后端对它给出不同答案会出错**。
17
+ // "什么算重复"若两个后端不一致,同一条记忆在不同后端行为不同;
18
+ // 摘要格式不一致,人看到的 MEMORY.md 会跟着换后端变形。
19
+ // ⚠️ 别把"本地实现顺手用得上的工具"塞进来 —— 那属于后端自己。
20
+ /** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
21
+ export const MEMORY_REDACTED_CODE = 'memory-redacted';
22
+ /** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
23
+ export function normalizeMemoryBody(s) {
24
+ return s
25
+ .toLowerCase()
26
+ .replace(/[#*`_>\-\s]+/g, ' ')
27
+ .replace(/[^\p{L}\p{N} ]+/gu, '')
28
+ .trim();
29
+ }
30
+ /** entries → 摘要 markdown(标题清单 + top 高 salience 正文)。空集合返回空串。 */
31
+ export function buildMemoryDigest(entries, opts = {}) {
32
+ const maxTitles = opts.maxTitles ?? 60;
33
+ const maxBodies = opts.maxBodies ?? 6;
34
+ const bodyChars = opts.bodyChars ?? 500;
35
+ if (entries.length === 0)
36
+ return '';
37
+ const ranked = entries
38
+ .slice()
39
+ .sort((a, b) => b.salience - a.salience || b.updated.localeCompare(a.updated));
40
+ const lines = ['# 记忆摘要', '', '## 索引'];
41
+ for (const e of ranked.slice(0, maxTitles)) {
42
+ const tags = e.tags.length ? ` #${e.tags.join(' #')}` : '';
43
+ lines.push(`- [${e.id}] (${e.category}, s${e.salience}) ${e.title}${tags}`);
44
+ }
45
+ if (ranked.length > maxTitles) {
46
+ lines.push(`- …还有 ${ranked.length - maxTitles} 条(用 memory_recall 检索)`);
47
+ }
48
+ const bodies = ranked.slice(0, maxBodies);
49
+ if (bodies.length) {
50
+ lines.push('', '## 高显著度');
51
+ for (const e of bodies) {
52
+ const body = e.body.length > bodyChars ? `${e.body.slice(0, bodyChars)}…` : e.body;
53
+ lines.push('', `### ${e.title}`, body);
54
+ }
55
+ }
56
+ return lines.join('\n').trim();
57
+ }
14
58
  // ── 服务名 ───────────────────────────────────────────────────────────────
15
59
  /** 插件 `inject` 里写的名字。 */
16
60
  export const SPS_SERVICES = {
@@ -24,6 +68,7 @@ export const SPS_SERVICES = {
24
68
  projects: 'sps.projects',
25
69
  cards: 'sps.cards',
26
70
  jobs: 'sps.jobs',
71
+ storage: 'sps.storage',
27
72
  subprocess: 'sps.subprocess',
28
73
  attachments: 'sps.attachments',
29
74
  media: 'sps.media',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralai/sps-plugin-api",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "sps 宿主半区的插件契约:ctx 上那些服务的方法签名。插件装它拿类型。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",