@ai-agent-forge/plugin-memory-curation 0.0.0-stage → 0.88.15

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,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # plugins/memory-curation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 第一方默认记忆整理策略插件(设计:`docs/plans/记忆归档与整理设计.md` §7)。
4
+
5
+ 经插件间能力目录提供 `memory.curation-policy@1` 服务能力:memory 插件整理归档会话时以 `propose` 调用本策略,输入会话切片与既有记忆摘要,输出候选记忆(statement + facets)。策略只回答"从会话切片里提炼什么";切片、预算、批次与提交归 memory 插件,策略自身不写 ledger/store。
6
+
7
+ - 文本生成走公共一次性补全面 `api.modelInvocation.complete()`(宿主声明 `model-invocation-v1` 时可用),无工具、无会话状态。
8
+ - 模型选择:`api.selection.get("memory_curation")` 优先;未配置回落主模型(用户裁决:`memory.curationModel` 未配置时用主模型)。
9
+ - 失败语义:宿主未声明该能力、目标不可用或模型失败时返回结构化 `{ error }`,memory 据此暂停批次,不静默返回空结果。
10
+
11
+ 默认策略可禁用、可替换;第三方策略与第一方默认策略走同一契约。
@@ -0,0 +1,19 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.memory-curation",
4
+ "pluginVersion": "0.88.15",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "platform": {
8
+ "os": [
9
+ "windows",
10
+ "macos",
11
+ "linux"
12
+ ],
13
+ "runtime": [
14
+ {
15
+ "name": "node"
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * memory-curation 插件入口:向能力目录提供第一方默认整理策略
3
+ * `memory.curation-policy@1`(manifest `provides` 同步声明同一份,否则宿主
4
+ * 运行时拒绝 provide)。
5
+ *
6
+ * 策略自守卫:宿主未声明 `model-invocation-v1` 或无可用模型目标时 handler
7
+ * 返回结构化失败,manifest 不声明 requiredCapabilities——模型通道是可选探测
8
+ * (search 先例),不是装载前置条件。
9
+ */
10
+ import type { CapabilityAPI } from "@agent-forge/plugin-sdk";
11
+ declare const _default: (api: CapabilityAPI) => {
12
+ dispose: () => void;
13
+ };
14
+ export default _default;
15
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,aAAa,EAA+B,MAAM,yBAAyB,CAAC;;;;AAG1F,wBAOyC","sourcesContent":["/**\n * memory-curation 插件入口:向能力目录提供第一方默认整理策略\n * `memory.curation-policy@1`(manifest `provides` 同步声明同一份,否则宿主\n * 运行时拒绝 provide)。\n *\n * 策略自守卫:宿主未声明 `model-invocation-v1` 或无可用模型目标时 handler\n * 返回结构化失败,manifest 不声明 requiredCapabilities——模型通道是可选探测\n * (search 先例),不是装载前置条件。\n */\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { CURATION_POLICY_DECLARATION, createProposeHandler } from \"./policy.ts\";\n\nexport default ((api: CapabilityAPI) => {\n\tconst registration = api.capabilities.provide(CURATION_POLICY_DECLARATION, createProposeHandler(api));\n\treturn {\n\t\tdispose: () => {\n\t\t\tregistration.dispose();\n\t\t},\n\t};\n}) satisfies CapabilityPluginSyncFactory;\n"]}
package/dist/entry.js ADDED
@@ -0,0 +1,10 @@
1
+ import { CURATION_POLICY_DECLARATION, createProposeHandler } from "./policy.js";
2
+ export default ((api) => {
3
+ const registration = api.capabilities.provide(CURATION_POLICY_DECLARATION, createProposeHandler(api));
4
+ return {
5
+ dispose: () => {
6
+ registration.dispose();
7
+ },
8
+ };
9
+ });
10
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEhF,eAAe,CAAC,CAAC,GAAkB,EAAE,EAAE,CAAC;IACvC,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,2BAA2B,EAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC;IACtG,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC;YACd,YAAY,CAAC,OAAO,EAAE,CAAC;QAAA,CACvB;KACD,CAAC;AAAA,CACF,CAAuC,CAAC","sourcesContent":["/**\n * memory-curation 插件入口:向能力目录提供第一方默认整理策略\n * `memory.curation-policy@1`(manifest `provides` 同步声明同一份,否则宿主\n * 运行时拒绝 provide)。\n *\n * 策略自守卫:宿主未声明 `model-invocation-v1` 或无可用模型目标时 handler\n * 返回结构化失败,manifest 不声明 requiredCapabilities——模型通道是可选探测\n * (search 先例),不是装载前置条件。\n */\nimport type { CapabilityAPI, CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { CURATION_POLICY_DECLARATION, createProposeHandler } from \"./policy.ts\";\n\nexport default ((api: CapabilityAPI) => {\n\tconst registration = api.capabilities.provide(CURATION_POLICY_DECLARATION, createProposeHandler(api));\n\treturn {\n\t\tdispose: () => {\n\t\t\tregistration.dispose();\n\t\t},\n\t};\n}) satisfies CapabilityPluginSyncFactory;\n"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * 记忆整理默认策略(设计:docs/design/记忆归档与整理设计.md §7)。
3
+ *
4
+ * 经插件间能力目录提供 `memory.curation-policy@1` service:memory 插件整理
5
+ * 归档会话时以 `propose` 调用本策略,输入会话切片与既有记忆摘要,输出候选
6
+ * 记忆。策略只回答"从会话切片里提炼什么";切片、预算、批次与提交归 memory
7
+ * 插件——策略不写 ledger/store,不读会话文件,输入只来自调用方。
8
+ *
9
+ * facets key 由模型自由归纳(用户裁决 2026-10-22:具体场景和类型全是 AI 自己
10
+ * 归纳,不做人工预设分类——记忆类型太多,钉死枚举作用有限):提示词只给示例
11
+ * 不给枚举,校验只查形状(非空字符串),不查 key 取值。
12
+ *
13
+ * 失败语义(与调用方约定):返回值带 `error` 字段即结构化失败,memory 暂停
14
+ * 该批次。宿主未声明 `model-invocation-v1`、无可用目标或模型失败时显式失败,
15
+ * 不静默返回空结果,也不做降级提取——提炼质量不可在调用方不知情时改变。
16
+ */
17
+ import type { CapabilityAPI, CapabilityProviderHandler, ModelInvocationTargetV1, SessionEntryView } from "@agent-forge/plugin-sdk";
18
+ /** 能力目录声明(manifest `provides` 必须同步声明同一份)。 */
19
+ export declare const CURATION_POLICY_DECLARATION: {
20
+ readonly id: "memory.curation-policy";
21
+ readonly version: 1;
22
+ readonly kind: "service";
23
+ };
24
+ export interface CurationFacetV1 {
25
+ key: string;
26
+ value: string;
27
+ }
28
+ export interface CurationProposalV1 {
29
+ statement: string;
30
+ facets?: CurationFacetV1[];
31
+ }
32
+ export interface CurationProposeInputV1 {
33
+ op: "propose";
34
+ sessionId: string;
35
+ /** 有界分段:每片 ≤200 条(切片由 memory 插件负责)。 */
36
+ slices: SessionEntryView[][];
37
+ /** 既有记忆 statement 摘要,去重参考。 */
38
+ existingStatements: string[];
39
+ }
40
+ export type CurationProposeResultV1 = {
41
+ proposals: CurationProposalV1[];
42
+ } | {
43
+ error: string;
44
+ };
45
+ /** 投影一个切片为紧凑文本,单片字符预算兜底截断。 */
46
+ export declare function projectSlice(entries: readonly SessionEntryView[]): string;
47
+ /** 模型目标:`memory_curation` 选择优先;未配置回落主模型(用户裁决)。 */
48
+ export declare function resolveCurationTarget(api: CapabilityAPI): {
49
+ target: ModelInvocationTargetV1;
50
+ } | {
51
+ error: string;
52
+ };
53
+ /** 单条 proposal 契约校验:statement 非空 ≤700 字符,facets 非法条目丢弃。 */
54
+ export declare function validateProposal(raw: unknown): CurationProposalV1 | undefined;
55
+ /** 解析模型输出:围栏容忍 → JSON.parse → 契约校验 → 去重 → 截断。 */
56
+ export declare function parseProposeOutput(text: string, existingStatements: readonly string[]): CurationProposeResultV1;
57
+ /**
58
+ * 构造 `propose` handler:注入的 CapabilityAPI 只读地决定目标与调用通道,
59
+ * 输入输出即 §7.2 契约形状。
60
+ */
61
+ export declare function createProposeHandler(api: CapabilityAPI): CapabilityProviderHandler<CurationProposeInputV1, CurationProposeResultV1>;
62
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACX,aAAa,EACb,yBAAyB,EAEzB,uBAAuB,EACvB,gBAAgB,EAChB,MAAM,yBAAyB,CAAC;AAEjC,iFAA6C;AAC7C,eAAO,MAAM,2BAA2B;;;;CAI9B,CAAC;AAEX,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACtC,EAAE,EAAE,SAAS,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,6EAAuC;IACvC,MAAM,EAAE,gBAAgB,EAAE,EAAE,CAAC;IAC7B,sDAA8B;IAC9B,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,uBAAuB,GAAG;IAAE,SAAS,EAAE,kBAAkB,EAAE,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA4F9F,4EAA8B;AAC9B,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAYzE;AAoBD,oGAAkD;AAClD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,uBAAuB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAcjH;AAgBD,mGAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAQ7E;AAED,iGAAiD;AACjD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,SAAS,MAAM,EAAE,GAAG,uBAAuB,CA0B/G;AAQD;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,aAAa,GAChB,yBAAyB,CAAC,sBAAsB,EAAE,uBAAuB,CAAC,CAmC5E","sourcesContent":["/**\n * 记忆整理默认策略(设计:docs/design/记忆归档与整理设计.md §7)。\n *\n * 经插件间能力目录提供 `memory.curation-policy@1` service:memory 插件整理\n * 归档会话时以 `propose` 调用本策略,输入会话切片与既有记忆摘要,输出候选\n * 记忆。策略只回答\"从会话切片里提炼什么\";切片、预算、批次与提交归 memory\n * 插件——策略不写 ledger/store,不读会话文件,输入只来自调用方。\n *\n * facets key 由模型自由归纳(用户裁决 2026-10-22:具体场景和类型全是 AI 自己\n * 归纳,不做人工预设分类——记忆类型太多,钉死枚举作用有限):提示词只给示例\n * 不给枚举,校验只查形状(非空字符串),不查 key 取值。\n *\n * 失败语义(与调用方约定):返回值带 `error` 字段即结构化失败,memory 暂停\n * 该批次。宿主未声明 `model-invocation-v1`、无可用目标或模型失败时显式失败,\n * 不静默返回空结果,也不做降级提取——提炼质量不可在调用方不知情时改变。\n */\n\nimport type {\n\tCapabilityAPI,\n\tCapabilityProviderHandler,\n\tModelInvocationResultV1,\n\tModelInvocationTargetV1,\n\tSessionEntryView,\n} from \"@agent-forge/plugin-sdk\";\n\n/** 能力目录声明(manifest `provides` 必须同步声明同一份)。 */\nexport const CURATION_POLICY_DECLARATION = {\n\tid: \"memory.curation-policy\",\n\tversion: 1,\n\tkind: \"service\",\n} as const;\n\nexport interface CurationFacetV1 {\n\tkey: string;\n\tvalue: string;\n}\n\nexport interface CurationProposalV1 {\n\tstatement: string;\n\tfacets?: CurationFacetV1[];\n}\n\nexport interface CurationProposeInputV1 {\n\top: \"propose\";\n\tsessionId: string;\n\t/** 有界分段:每片 ≤200 条(切片由 memory 插件负责)。 */\n\tslices: SessionEntryView[][];\n\t/** 既有记忆 statement 摘要,去重参考。 */\n\texistingStatements: string[];\n}\n\nexport type CurationProposeResultV1 = { proposals: CurationProposalV1[] } | { error: string };\n\n/** 单条 statement 上限(契约约束,超限条目丢弃)。 */\nconst STATEMENT_LIMIT = 700;\n/** 单条 facets 上限(契约约束;超出部分截断而非丢弃整条)。 */\nconst MAX_FACETS = 8;\n/** 返回 proposals 上限(契约约束,超出截断)。 */\nconst PROPOSAL_LIMIT = 20;\n/** 单条会话条目投影的字符上限。 */\nconst ENTRY_LINE_LIMIT = 500;\n/** 工具调用 arguments 摘要的字符上限。 */\nconst ARGUMENT_SNIPPET_LIMIT = 200;\n/** 单片投影文本的字符上限(兜底;切片条数由调用方保证 ≤200)。 */\nconst SLICE_CHAR_LIMIT = 8000;\n/** 去重参考最多携带的既有 statement 条数。 */\nconst EXISTING_STATEMENT_SAMPLE = 50;\n/** 去重参考单条 statement 的字符上限。 */\nconst EXISTING_STATEMENT_LIMIT = 200;\nconst MAX_TOKENS = 4096;\n\nconst SYSTEM_PROMPT = `你是记忆整理策略,负责从会话切片提炼可长期保留的经验。\n只提炼\"问题→原因→改动→验证\"闭环成立的经验:问题明确、原因有依据、改动或结论具体、验证有结果。没有过程的单条操作记录不提炼。\n规则:\n- 每条经验一句陈述,不超过 300 字。\n- 不引用会话原文长段;不输出密钥、令牌、凭据等敏感内容。\n- 每条经验归纳 0-3 个 facets:key 由你自己归纳——用简短的英文小写标识该条经验最贴切的维度(time、scene、domain、topic 仅是示例,不是限制,也不必只用它们);value 是简短短语。\n- 与\"已有记忆\"重复的经验不要输出。\n- 没有可提炼的经验时输出空数组。\n- 只输出一个 JSON 对象,不输出任何其他文字:\n {\"proposals\":[{\"statement\":\"...\",\"facets\":[{\"key\":\"scene\",\"value\":\"...\"}]}]}`;\n\nfunction truncate(text: string, max: number): string {\n\treturn text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n\n/** 容忍 ```json / ``` 围栏包裹的 JSON 输出。 */\nfunction stripJsonFence(text: string): string {\n\tconst trimmed = text.trim();\n\tconst match = /^```(?:json)?\\s*\\n([\\s\\S]*?)\\n?\\s*```$/.exec(trimmed);\n\tconst inner = match?.[1];\n\treturn inner === undefined ? trimmed : inner.trim();\n}\n\n/** 投影单个 content 块;thinking/image 等非文本块返回空串。 */\nfunction contentBlockText(block: unknown): string {\n\tif (typeof block !== \"object\" || block === null) return \"\";\n\tconst record = block as Record<string, unknown>;\n\tif (record.type === \"text\" && typeof record.text === \"string\") return record.text;\n\t// 工具调用带 arguments 摘要:名称 + 截断后的参数 JSON。\n\tif (record.type === \"toolCall\") {\n\t\tconst name = typeof record.name === \"string\" && record.name !== \"\" ? record.name : \"unknown\";\n\t\tlet argumentsSnippet = \"{}\";\n\t\ttry {\n\t\t\targumentsSnippet = JSON.stringify(record.arguments ?? {});\n\t\t} catch {\n\t\t\targumentsSnippet = \"{}\";\n\t\t}\n\t\treturn `${name}(${truncate(argumentsSnippet, ARGUMENT_SNIPPET_LIMIT)})`;\n\t}\n\treturn \"\";\n}\n\n/** 投影一条 message 条目为 `[type] role: text`;无可投影文本返回 undefined。 */\nfunction projectMessageEntry(entry: SessionEntryView): string | undefined {\n\tconst message = entry.message;\n\tif (message === undefined) return undefined;\n\tconst role = typeof message.role === \"string\" && message.role !== \"\" ? message.role : \"unknown\";\n\tconst content = message.content;\n\tif (typeof content === \"string\") {\n\t\tconst text = content.trim();\n\t\treturn text === \"\" ? undefined : `[message] ${role}: ${truncate(text, ENTRY_LINE_LIMIT)}`;\n\t}\n\tif (!Array.isArray(content)) return undefined;\n\tconst parts: string[] = [];\n\tfor (const block of content) {\n\t\tconst text = contentBlockText(block).trim();\n\t\tif (text !== \"\") parts.push(truncate(text, ENTRY_LINE_LIMIT));\n\t}\n\tif (parts.length === 0) return undefined;\n\treturn `[message] ${role}: ${parts.join(\" | \")}`;\n}\n\n/**\n * 投影一条会话条目。公共视图只携带 message 内容;其余类型(compaction、\n * custom 等)经视图不含 payload,投影为 undefined——既无提炼素材,也不留\n * `[type]` 噪声行(保证\"无文本切片\"判定成立:纯元数据切片不触发模型调用)。\n */\nfunction projectEntry(entry: SessionEntryView): string | undefined {\n\tif (entry.type === \"message\") return projectMessageEntry(entry);\n\treturn undefined;\n}\n\n/** 投影一个切片为紧凑文本,单片字符预算兜底截断。 */\nexport function projectSlice(entries: readonly SessionEntryView[]): string {\n\tconst lines: string[] = [];\n\tlet budget = SLICE_CHAR_LIMIT;\n\tfor (const entry of entries) {\n\t\tconst line = projectEntry(entry);\n\t\tif (line === undefined) continue;\n\t\tconst cost = line.length + 1;\n\t\tif (cost > budget) break;\n\t\tlines.push(line);\n\t\tbudget -= cost;\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction buildUserPrompt(slices: readonly string[], existingStatements: readonly string[]): string {\n\tconst parts: string[] = [];\n\tconst existing = existingStatements\n\t\t.map((statement) => statement.trim())\n\t\t.filter((statement) => statement !== \"\")\n\t\t.slice(0, EXISTING_STATEMENT_SAMPLE);\n\tif (existing.length > 0) {\n\t\tconst lines = existing.map((statement) => `- ${truncate(statement, EXISTING_STATEMENT_LIMIT)}`);\n\t\tparts.push(`已有记忆(不要重复):\\n${lines.join(\"\\n\")}`);\n\t}\n\tfor (const [index, text] of slices.entries()) {\n\t\tif (text === \"\") continue;\n\t\tparts.push(`会话切片 ${index + 1}/${slices.length}:\\n${text}`);\n\t}\n\tparts.push(\"请按系统提示输出 JSON。\");\n\treturn parts.join(\"\\n\\n\");\n}\n\n/** 模型目标:`memory_curation` 选择优先;未配置回落主模型(用户裁决)。 */\nexport function resolveCurationTarget(api: CapabilityAPI): { target: ModelInvocationTargetV1 } | { error: string } {\n\tconst selection = api.selection?.get(\"memory_curation\");\n\tif (selection !== undefined) {\n\t\tconst target = { provider: selection.provider, id: selection.id };\n\t\tif (!selection.available) {\n\t\t\treturn { error: `memory curation target is unavailable: ${selection.error ?? \"unknown reason\"}` };\n\t\t}\n\t\treturn { target };\n\t}\n\tconst mainModel = api.session?.getModel?.();\n\tif (mainModel === undefined) {\n\t\treturn { error: \"no memory_curation target configured and no main model selected\" };\n\t}\n\treturn { target: { provider: mainModel.provider, id: mainModel.id } };\n}\n\nfunction validateFacets(raw: unknown): CurationFacetV1[] | undefined {\n\tif (!Array.isArray(raw)) return undefined;\n\tconst facets: CurationFacetV1[] = [];\n\tfor (const item of raw) {\n\t\tif (typeof item !== \"object\" || item === null) continue;\n\t\tconst record = item as Record<string, unknown>;\n\t\tif (typeof record.key !== \"string\" || record.key.trim() === \"\") continue;\n\t\tif (typeof record.value !== \"string\" || record.value.trim() === \"\") continue;\n\t\tfacets.push({ key: record.key.trim(), value: record.value.trim() });\n\t\tif (facets.length >= MAX_FACETS) break;\n\t}\n\treturn facets.length === 0 ? undefined : facets;\n}\n\n/** 单条 proposal 契约校验:statement 非空 ≤700 字符,facets 非法条目丢弃。 */\nexport function validateProposal(raw: unknown): CurationProposalV1 | undefined {\n\tif (typeof raw !== \"object\" || raw === null) return undefined;\n\tconst record = raw as Record<string, unknown>;\n\tif (typeof record.statement !== \"string\") return undefined;\n\tconst statement = record.statement.trim();\n\tif (statement === \"\" || statement.length > STATEMENT_LIMIT) return undefined;\n\tconst facets = validateFacets(record.facets);\n\treturn facets === undefined ? { statement } : { statement, facets };\n}\n\n/** 解析模型输出:围栏容忍 → JSON.parse → 契约校验 → 去重 → 截断。 */\nexport function parseProposeOutput(text: string, existingStatements: readonly string[]): CurationProposeResultV1 {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripJsonFence(text));\n\t} catch (error) {\n\t\treturn { error: `model output is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };\n\t}\n\tconst rawProposals =\n\t\ttypeof parsed === \"object\" && parsed !== null ? (parsed as { proposals?: unknown }).proposals : undefined;\n\tif (!Array.isArray(rawProposals)) {\n\t\treturn { error: 'model output is invalid: expected {\"proposals\":[...]}' };\n\t}\n\tconst existing = new Set(existingStatements.map((statement) => statement.trim()));\n\tconst seen = new Set<string>();\n\tconst proposals: CurationProposalV1[] = [];\n\tfor (const raw of rawProposals) {\n\t\tconst proposal = validateProposal(raw);\n\t\tif (proposal === undefined) continue;\n\t\t// 确定性去重:与既有记忆或本批内完全相同的 statement 不再输出\n\t\t//(跨会话语义去重仍归 memory 的提交侧,这里只做精确匹配)。\n\t\tif (existing.has(proposal.statement) || seen.has(proposal.statement)) continue;\n\t\tseen.add(proposal.statement);\n\t\tproposals.push(proposal);\n\t\tif (proposals.length >= PROPOSAL_LIMIT) break;\n\t}\n\treturn { proposals };\n}\n\nfunction completionError(result: Exclude<ModelInvocationResultV1, { status: \"completed\" }>): string {\n\treturn result.status === \"cancelled\"\n\t\t? \"model invocation was cancelled\"\n\t\t: `model invocation failed: ${result.error.code}: ${result.error.message}`;\n}\n\n/**\n * 构造 `propose` handler:注入的 CapabilityAPI 只读地决定目标与调用通道,\n * 输入输出即 §7.2 契约形状。\n */\nexport function createProposeHandler(\n\tapi: CapabilityAPI,\n): CapabilityProviderHandler<CurationProposeInputV1, CurationProposeResultV1> {\n\treturn async (input, { signal }) => {\n\t\tif (typeof input !== \"object\" || input === null || input.op !== \"propose\") {\n\t\t\treturn {\n\t\t\t\terror: `unsupported op: ${typeof input === \"object\" && input !== null ? String(input.op) : String(input)}`,\n\t\t\t};\n\t\t}\n\t\tconst invocation = api.modelInvocation;\n\t\tif (invocation === undefined) {\n\t\t\treturn { error: \"model invocation unavailable: the host does not declare model-invocation-v1\" };\n\t\t}\n\t\tconst resolved = resolveCurationTarget(api);\n\t\tif (\"error\" in resolved) return { error: resolved.error };\n\t\tif (!Array.isArray(input.slices)) return { error: \"invalid input: slices must be an array\" };\n\t\tconst slices = input.slices.map((entries) => projectSlice(Array.isArray(entries) ? entries : []));\n\t\tif (slices.every((text) => text === \"\")) return { proposals: [] };\n\t\tconst prompt = buildUserPrompt(slices, Array.isArray(input.existingStatements) ? input.existingStatements : []);\n\t\tlet result: ModelInvocationResultV1;\n\t\ttry {\n\t\t\tresult = await invocation.complete(\n\t\t\t\t{\n\t\t\t\t\tversion: 1,\n\t\t\t\t\ttarget: resolved.target,\n\t\t\t\t\tsystemPrompt: SYSTEM_PROMPT,\n\t\t\t\t\tprompt,\n\t\t\t\t\tmaxTokens: MAX_TOKENS,\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\treturn { error: `model invocation threw: ${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t\tif (result.status !== \"completed\") return { error: completionError(result) };\n\t\treturn parseProposeOutput(result.text, Array.isArray(input.existingStatements) ? input.existingStatements : []);\n\t};\n}\n"]}
package/dist/policy.js ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * 记忆整理默认策略(设计:docs/design/记忆归档与整理设计.md §7)。
3
+ *
4
+ * 经插件间能力目录提供 `memory.curation-policy@1` service:memory 插件整理
5
+ * 归档会话时以 `propose` 调用本策略,输入会话切片与既有记忆摘要,输出候选
6
+ * 记忆。策略只回答"从会话切片里提炼什么";切片、预算、批次与提交归 memory
7
+ * 插件——策略不写 ledger/store,不读会话文件,输入只来自调用方。
8
+ *
9
+ * facets key 由模型自由归纳(用户裁决 2026-10-22:具体场景和类型全是 AI 自己
10
+ * 归纳,不做人工预设分类——记忆类型太多,钉死枚举作用有限):提示词只给示例
11
+ * 不给枚举,校验只查形状(非空字符串),不查 key 取值。
12
+ *
13
+ * 失败语义(与调用方约定):返回值带 `error` 字段即结构化失败,memory 暂停
14
+ * 该批次。宿主未声明 `model-invocation-v1`、无可用目标或模型失败时显式失败,
15
+ * 不静默返回空结果,也不做降级提取——提炼质量不可在调用方不知情时改变。
16
+ */
17
+ /** 能力目录声明(manifest `provides` 必须同步声明同一份)。 */
18
+ export const CURATION_POLICY_DECLARATION = {
19
+ id: "memory.curation-policy",
20
+ version: 1,
21
+ kind: "service",
22
+ };
23
+ /** 单条 statement 上限(契约约束,超限条目丢弃)。 */
24
+ const STATEMENT_LIMIT = 700;
25
+ /** 单条 facets 上限(契约约束;超出部分截断而非丢弃整条)。 */
26
+ const MAX_FACETS = 8;
27
+ /** 返回 proposals 上限(契约约束,超出截断)。 */
28
+ const PROPOSAL_LIMIT = 20;
29
+ /** 单条会话条目投影的字符上限。 */
30
+ const ENTRY_LINE_LIMIT = 500;
31
+ /** 工具调用 arguments 摘要的字符上限。 */
32
+ const ARGUMENT_SNIPPET_LIMIT = 200;
33
+ /** 单片投影文本的字符上限(兜底;切片条数由调用方保证 ≤200)。 */
34
+ const SLICE_CHAR_LIMIT = 8000;
35
+ /** 去重参考最多携带的既有 statement 条数。 */
36
+ const EXISTING_STATEMENT_SAMPLE = 50;
37
+ /** 去重参考单条 statement 的字符上限。 */
38
+ const EXISTING_STATEMENT_LIMIT = 200;
39
+ const MAX_TOKENS = 4096;
40
+ const SYSTEM_PROMPT = `你是记忆整理策略,负责从会话切片提炼可长期保留的经验。
41
+ 只提炼"问题→原因→改动→验证"闭环成立的经验:问题明确、原因有依据、改动或结论具体、验证有结果。没有过程的单条操作记录不提炼。
42
+ 规则:
43
+ - 每条经验一句陈述,不超过 300 字。
44
+ - 不引用会话原文长段;不输出密钥、令牌、凭据等敏感内容。
45
+ - 每条经验归纳 0-3 个 facets:key 由你自己归纳——用简短的英文小写标识该条经验最贴切的维度(time、scene、domain、topic 仅是示例,不是限制,也不必只用它们);value 是简短短语。
46
+ - 与"已有记忆"重复的经验不要输出。
47
+ - 没有可提炼的经验时输出空数组。
48
+ - 只输出一个 JSON 对象,不输出任何其他文字:
49
+ {"proposals":[{"statement":"...","facets":[{"key":"scene","value":"..."}]}]}`;
50
+ function truncate(text, max) {
51
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
52
+ }
53
+ /** 容忍 ```json / ``` 围栏包裹的 JSON 输出。 */
54
+ function stripJsonFence(text) {
55
+ const trimmed = text.trim();
56
+ const match = /^```(?:json)?\s*\n([\s\S]*?)\n?\s*```$/.exec(trimmed);
57
+ const inner = match?.[1];
58
+ return inner === undefined ? trimmed : inner.trim();
59
+ }
60
+ /** 投影单个 content 块;thinking/image 等非文本块返回空串。 */
61
+ function contentBlockText(block) {
62
+ if (typeof block !== "object" || block === null)
63
+ return "";
64
+ const record = block;
65
+ if (record.type === "text" && typeof record.text === "string")
66
+ return record.text;
67
+ // 工具调用带 arguments 摘要:名称 + 截断后的参数 JSON。
68
+ if (record.type === "toolCall") {
69
+ const name = typeof record.name === "string" && record.name !== "" ? record.name : "unknown";
70
+ let argumentsSnippet = "{}";
71
+ try {
72
+ argumentsSnippet = JSON.stringify(record.arguments ?? {});
73
+ }
74
+ catch {
75
+ argumentsSnippet = "{}";
76
+ }
77
+ return `${name}(${truncate(argumentsSnippet, ARGUMENT_SNIPPET_LIMIT)})`;
78
+ }
79
+ return "";
80
+ }
81
+ /** 投影一条 message 条目为 `[type] role: text`;无可投影文本返回 undefined。 */
82
+ function projectMessageEntry(entry) {
83
+ const message = entry.message;
84
+ if (message === undefined)
85
+ return undefined;
86
+ const role = typeof message.role === "string" && message.role !== "" ? message.role : "unknown";
87
+ const content = message.content;
88
+ if (typeof content === "string") {
89
+ const text = content.trim();
90
+ return text === "" ? undefined : `[message] ${role}: ${truncate(text, ENTRY_LINE_LIMIT)}`;
91
+ }
92
+ if (!Array.isArray(content))
93
+ return undefined;
94
+ const parts = [];
95
+ for (const block of content) {
96
+ const text = contentBlockText(block).trim();
97
+ if (text !== "")
98
+ parts.push(truncate(text, ENTRY_LINE_LIMIT));
99
+ }
100
+ if (parts.length === 0)
101
+ return undefined;
102
+ return `[message] ${role}: ${parts.join(" | ")}`;
103
+ }
104
+ /**
105
+ * 投影一条会话条目。公共视图只携带 message 内容;其余类型(compaction、
106
+ * custom 等)经视图不含 payload,投影为 undefined——既无提炼素材,也不留
107
+ * `[type]` 噪声行(保证"无文本切片"判定成立:纯元数据切片不触发模型调用)。
108
+ */
109
+ function projectEntry(entry) {
110
+ if (entry.type === "message")
111
+ return projectMessageEntry(entry);
112
+ return undefined;
113
+ }
114
+ /** 投影一个切片为紧凑文本,单片字符预算兜底截断。 */
115
+ export function projectSlice(entries) {
116
+ const lines = [];
117
+ let budget = SLICE_CHAR_LIMIT;
118
+ for (const entry of entries) {
119
+ const line = projectEntry(entry);
120
+ if (line === undefined)
121
+ continue;
122
+ const cost = line.length + 1;
123
+ if (cost > budget)
124
+ break;
125
+ lines.push(line);
126
+ budget -= cost;
127
+ }
128
+ return lines.join("\n");
129
+ }
130
+ function buildUserPrompt(slices, existingStatements) {
131
+ const parts = [];
132
+ const existing = existingStatements
133
+ .map((statement) => statement.trim())
134
+ .filter((statement) => statement !== "")
135
+ .slice(0, EXISTING_STATEMENT_SAMPLE);
136
+ if (existing.length > 0) {
137
+ const lines = existing.map((statement) => `- ${truncate(statement, EXISTING_STATEMENT_LIMIT)}`);
138
+ parts.push(`已有记忆(不要重复):\n${lines.join("\n")}`);
139
+ }
140
+ for (const [index, text] of slices.entries()) {
141
+ if (text === "")
142
+ continue;
143
+ parts.push(`会话切片 ${index + 1}/${slices.length}:\n${text}`);
144
+ }
145
+ parts.push("请按系统提示输出 JSON。");
146
+ return parts.join("\n\n");
147
+ }
148
+ /** 模型目标:`memory_curation` 选择优先;未配置回落主模型(用户裁决)。 */
149
+ export function resolveCurationTarget(api) {
150
+ const selection = api.selection?.get("memory_curation");
151
+ if (selection !== undefined) {
152
+ const target = { provider: selection.provider, id: selection.id };
153
+ if (!selection.available) {
154
+ return { error: `memory curation target is unavailable: ${selection.error ?? "unknown reason"}` };
155
+ }
156
+ return { target };
157
+ }
158
+ const mainModel = api.session?.getModel?.();
159
+ if (mainModel === undefined) {
160
+ return { error: "no memory_curation target configured and no main model selected" };
161
+ }
162
+ return { target: { provider: mainModel.provider, id: mainModel.id } };
163
+ }
164
+ function validateFacets(raw) {
165
+ if (!Array.isArray(raw))
166
+ return undefined;
167
+ const facets = [];
168
+ for (const item of raw) {
169
+ if (typeof item !== "object" || item === null)
170
+ continue;
171
+ const record = item;
172
+ if (typeof record.key !== "string" || record.key.trim() === "")
173
+ continue;
174
+ if (typeof record.value !== "string" || record.value.trim() === "")
175
+ continue;
176
+ facets.push({ key: record.key.trim(), value: record.value.trim() });
177
+ if (facets.length >= MAX_FACETS)
178
+ break;
179
+ }
180
+ return facets.length === 0 ? undefined : facets;
181
+ }
182
+ /** 单条 proposal 契约校验:statement 非空 ≤700 字符,facets 非法条目丢弃。 */
183
+ export function validateProposal(raw) {
184
+ if (typeof raw !== "object" || raw === null)
185
+ return undefined;
186
+ const record = raw;
187
+ if (typeof record.statement !== "string")
188
+ return undefined;
189
+ const statement = record.statement.trim();
190
+ if (statement === "" || statement.length > STATEMENT_LIMIT)
191
+ return undefined;
192
+ const facets = validateFacets(record.facets);
193
+ return facets === undefined ? { statement } : { statement, facets };
194
+ }
195
+ /** 解析模型输出:围栏容忍 → JSON.parse → 契约校验 → 去重 → 截断。 */
196
+ export function parseProposeOutput(text, existingStatements) {
197
+ let parsed;
198
+ try {
199
+ parsed = JSON.parse(stripJsonFence(text));
200
+ }
201
+ catch (error) {
202
+ return { error: `model output is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
203
+ }
204
+ const rawProposals = typeof parsed === "object" && parsed !== null ? parsed.proposals : undefined;
205
+ if (!Array.isArray(rawProposals)) {
206
+ return { error: 'model output is invalid: expected {"proposals":[...]}' };
207
+ }
208
+ const existing = new Set(existingStatements.map((statement) => statement.trim()));
209
+ const seen = new Set();
210
+ const proposals = [];
211
+ for (const raw of rawProposals) {
212
+ const proposal = validateProposal(raw);
213
+ if (proposal === undefined)
214
+ continue;
215
+ // 确定性去重:与既有记忆或本批内完全相同的 statement 不再输出
216
+ //(跨会话语义去重仍归 memory 的提交侧,这里只做精确匹配)。
217
+ if (existing.has(proposal.statement) || seen.has(proposal.statement))
218
+ continue;
219
+ seen.add(proposal.statement);
220
+ proposals.push(proposal);
221
+ if (proposals.length >= PROPOSAL_LIMIT)
222
+ break;
223
+ }
224
+ return { proposals };
225
+ }
226
+ function completionError(result) {
227
+ return result.status === "cancelled"
228
+ ? "model invocation was cancelled"
229
+ : `model invocation failed: ${result.error.code}: ${result.error.message}`;
230
+ }
231
+ /**
232
+ * 构造 `propose` handler:注入的 CapabilityAPI 只读地决定目标与调用通道,
233
+ * 输入输出即 §7.2 契约形状。
234
+ */
235
+ export function createProposeHandler(api) {
236
+ return async (input, { signal }) => {
237
+ if (typeof input !== "object" || input === null || input.op !== "propose") {
238
+ return {
239
+ error: `unsupported op: ${typeof input === "object" && input !== null ? String(input.op) : String(input)}`,
240
+ };
241
+ }
242
+ const invocation = api.modelInvocation;
243
+ if (invocation === undefined) {
244
+ return { error: "model invocation unavailable: the host does not declare model-invocation-v1" };
245
+ }
246
+ const resolved = resolveCurationTarget(api);
247
+ if ("error" in resolved)
248
+ return { error: resolved.error };
249
+ if (!Array.isArray(input.slices))
250
+ return { error: "invalid input: slices must be an array" };
251
+ const slices = input.slices.map((entries) => projectSlice(Array.isArray(entries) ? entries : []));
252
+ if (slices.every((text) => text === ""))
253
+ return { proposals: [] };
254
+ const prompt = buildUserPrompt(slices, Array.isArray(input.existingStatements) ? input.existingStatements : []);
255
+ let result;
256
+ try {
257
+ result = await invocation.complete({
258
+ version: 1,
259
+ target: resolved.target,
260
+ systemPrompt: SYSTEM_PROMPT,
261
+ prompt,
262
+ maxTokens: MAX_TOKENS,
263
+ }, { signal });
264
+ }
265
+ catch (error) {
266
+ return { error: `model invocation threw: ${error instanceof Error ? error.message : String(error)}` };
267
+ }
268
+ if (result.status !== "completed")
269
+ return { error: completionError(result) };
270
+ return parseProposeOutput(result.text, Array.isArray(input.existingStatements) ? input.existingStatements : []);
271
+ };
272
+ }
273
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 记忆整理默认策略(设计:docs/design/记忆归档与整理设计.md §7)。\n *\n * 经插件间能力目录提供 `memory.curation-policy@1` service:memory 插件整理\n * 归档会话时以 `propose` 调用本策略,输入会话切片与既有记忆摘要,输出候选\n * 记忆。策略只回答\"从会话切片里提炼什么\";切片、预算、批次与提交归 memory\n * 插件——策略不写 ledger/store,不读会话文件,输入只来自调用方。\n *\n * facets key 由模型自由归纳(用户裁决 2026-10-22:具体场景和类型全是 AI 自己\n * 归纳,不做人工预设分类——记忆类型太多,钉死枚举作用有限):提示词只给示例\n * 不给枚举,校验只查形状(非空字符串),不查 key 取值。\n *\n * 失败语义(与调用方约定):返回值带 `error` 字段即结构化失败,memory 暂停\n * 该批次。宿主未声明 `model-invocation-v1`、无可用目标或模型失败时显式失败,\n * 不静默返回空结果,也不做降级提取——提炼质量不可在调用方不知情时改变。\n */\n\nimport type {\n\tCapabilityAPI,\n\tCapabilityProviderHandler,\n\tModelInvocationResultV1,\n\tModelInvocationTargetV1,\n\tSessionEntryView,\n} from \"@agent-forge/plugin-sdk\";\n\n/** 能力目录声明(manifest `provides` 必须同步声明同一份)。 */\nexport const CURATION_POLICY_DECLARATION = {\n\tid: \"memory.curation-policy\",\n\tversion: 1,\n\tkind: \"service\",\n} as const;\n\nexport interface CurationFacetV1 {\n\tkey: string;\n\tvalue: string;\n}\n\nexport interface CurationProposalV1 {\n\tstatement: string;\n\tfacets?: CurationFacetV1[];\n}\n\nexport interface CurationProposeInputV1 {\n\top: \"propose\";\n\tsessionId: string;\n\t/** 有界分段:每片 ≤200 条(切片由 memory 插件负责)。 */\n\tslices: SessionEntryView[][];\n\t/** 既有记忆 statement 摘要,去重参考。 */\n\texistingStatements: string[];\n}\n\nexport type CurationProposeResultV1 = { proposals: CurationProposalV1[] } | { error: string };\n\n/** 单条 statement 上限(契约约束,超限条目丢弃)。 */\nconst STATEMENT_LIMIT = 700;\n/** 单条 facets 上限(契约约束;超出部分截断而非丢弃整条)。 */\nconst MAX_FACETS = 8;\n/** 返回 proposals 上限(契约约束,超出截断)。 */\nconst PROPOSAL_LIMIT = 20;\n/** 单条会话条目投影的字符上限。 */\nconst ENTRY_LINE_LIMIT = 500;\n/** 工具调用 arguments 摘要的字符上限。 */\nconst ARGUMENT_SNIPPET_LIMIT = 200;\n/** 单片投影文本的字符上限(兜底;切片条数由调用方保证 ≤200)。 */\nconst SLICE_CHAR_LIMIT = 8000;\n/** 去重参考最多携带的既有 statement 条数。 */\nconst EXISTING_STATEMENT_SAMPLE = 50;\n/** 去重参考单条 statement 的字符上限。 */\nconst EXISTING_STATEMENT_LIMIT = 200;\nconst MAX_TOKENS = 4096;\n\nconst SYSTEM_PROMPT = `你是记忆整理策略,负责从会话切片提炼可长期保留的经验。\n只提炼\"问题→原因→改动→验证\"闭环成立的经验:问题明确、原因有依据、改动或结论具体、验证有结果。没有过程的单条操作记录不提炼。\n规则:\n- 每条经验一句陈述,不超过 300 字。\n- 不引用会话原文长段;不输出密钥、令牌、凭据等敏感内容。\n- 每条经验归纳 0-3 个 facets:key 由你自己归纳——用简短的英文小写标识该条经验最贴切的维度(time、scene、domain、topic 仅是示例,不是限制,也不必只用它们);value 是简短短语。\n- 与\"已有记忆\"重复的经验不要输出。\n- 没有可提炼的经验时输出空数组。\n- 只输出一个 JSON 对象,不输出任何其他文字:\n {\"proposals\":[{\"statement\":\"...\",\"facets\":[{\"key\":\"scene\",\"value\":\"...\"}]}]}`;\n\nfunction truncate(text: string, max: number): string {\n\treturn text.length <= max ? text : `${text.slice(0, max - 1)}…`;\n}\n\n/** 容忍 ```json / ``` 围栏包裹的 JSON 输出。 */\nfunction stripJsonFence(text: string): string {\n\tconst trimmed = text.trim();\n\tconst match = /^```(?:json)?\\s*\\n([\\s\\S]*?)\\n?\\s*```$/.exec(trimmed);\n\tconst inner = match?.[1];\n\treturn inner === undefined ? trimmed : inner.trim();\n}\n\n/** 投影单个 content 块;thinking/image 等非文本块返回空串。 */\nfunction contentBlockText(block: unknown): string {\n\tif (typeof block !== \"object\" || block === null) return \"\";\n\tconst record = block as Record<string, unknown>;\n\tif (record.type === \"text\" && typeof record.text === \"string\") return record.text;\n\t// 工具调用带 arguments 摘要:名称 + 截断后的参数 JSON。\n\tif (record.type === \"toolCall\") {\n\t\tconst name = typeof record.name === \"string\" && record.name !== \"\" ? record.name : \"unknown\";\n\t\tlet argumentsSnippet = \"{}\";\n\t\ttry {\n\t\t\targumentsSnippet = JSON.stringify(record.arguments ?? {});\n\t\t} catch {\n\t\t\targumentsSnippet = \"{}\";\n\t\t}\n\t\treturn `${name}(${truncate(argumentsSnippet, ARGUMENT_SNIPPET_LIMIT)})`;\n\t}\n\treturn \"\";\n}\n\n/** 投影一条 message 条目为 `[type] role: text`;无可投影文本返回 undefined。 */\nfunction projectMessageEntry(entry: SessionEntryView): string | undefined {\n\tconst message = entry.message;\n\tif (message === undefined) return undefined;\n\tconst role = typeof message.role === \"string\" && message.role !== \"\" ? message.role : \"unknown\";\n\tconst content = message.content;\n\tif (typeof content === \"string\") {\n\t\tconst text = content.trim();\n\t\treturn text === \"\" ? undefined : `[message] ${role}: ${truncate(text, ENTRY_LINE_LIMIT)}`;\n\t}\n\tif (!Array.isArray(content)) return undefined;\n\tconst parts: string[] = [];\n\tfor (const block of content) {\n\t\tconst text = contentBlockText(block).trim();\n\t\tif (text !== \"\") parts.push(truncate(text, ENTRY_LINE_LIMIT));\n\t}\n\tif (parts.length === 0) return undefined;\n\treturn `[message] ${role}: ${parts.join(\" | \")}`;\n}\n\n/**\n * 投影一条会话条目。公共视图只携带 message 内容;其余类型(compaction、\n * custom 等)经视图不含 payload,投影为 undefined——既无提炼素材,也不留\n * `[type]` 噪声行(保证\"无文本切片\"判定成立:纯元数据切片不触发模型调用)。\n */\nfunction projectEntry(entry: SessionEntryView): string | undefined {\n\tif (entry.type === \"message\") return projectMessageEntry(entry);\n\treturn undefined;\n}\n\n/** 投影一个切片为紧凑文本,单片字符预算兜底截断。 */\nexport function projectSlice(entries: readonly SessionEntryView[]): string {\n\tconst lines: string[] = [];\n\tlet budget = SLICE_CHAR_LIMIT;\n\tfor (const entry of entries) {\n\t\tconst line = projectEntry(entry);\n\t\tif (line === undefined) continue;\n\t\tconst cost = line.length + 1;\n\t\tif (cost > budget) break;\n\t\tlines.push(line);\n\t\tbudget -= cost;\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nfunction buildUserPrompt(slices: readonly string[], existingStatements: readonly string[]): string {\n\tconst parts: string[] = [];\n\tconst existing = existingStatements\n\t\t.map((statement) => statement.trim())\n\t\t.filter((statement) => statement !== \"\")\n\t\t.slice(0, EXISTING_STATEMENT_SAMPLE);\n\tif (existing.length > 0) {\n\t\tconst lines = existing.map((statement) => `- ${truncate(statement, EXISTING_STATEMENT_LIMIT)}`);\n\t\tparts.push(`已有记忆(不要重复):\\n${lines.join(\"\\n\")}`);\n\t}\n\tfor (const [index, text] of slices.entries()) {\n\t\tif (text === \"\") continue;\n\t\tparts.push(`会话切片 ${index + 1}/${slices.length}:\\n${text}`);\n\t}\n\tparts.push(\"请按系统提示输出 JSON。\");\n\treturn parts.join(\"\\n\\n\");\n}\n\n/** 模型目标:`memory_curation` 选择优先;未配置回落主模型(用户裁决)。 */\nexport function resolveCurationTarget(api: CapabilityAPI): { target: ModelInvocationTargetV1 } | { error: string } {\n\tconst selection = api.selection?.get(\"memory_curation\");\n\tif (selection !== undefined) {\n\t\tconst target = { provider: selection.provider, id: selection.id };\n\t\tif (!selection.available) {\n\t\t\treturn { error: `memory curation target is unavailable: ${selection.error ?? \"unknown reason\"}` };\n\t\t}\n\t\treturn { target };\n\t}\n\tconst mainModel = api.session?.getModel?.();\n\tif (mainModel === undefined) {\n\t\treturn { error: \"no memory_curation target configured and no main model selected\" };\n\t}\n\treturn { target: { provider: mainModel.provider, id: mainModel.id } };\n}\n\nfunction validateFacets(raw: unknown): CurationFacetV1[] | undefined {\n\tif (!Array.isArray(raw)) return undefined;\n\tconst facets: CurationFacetV1[] = [];\n\tfor (const item of raw) {\n\t\tif (typeof item !== \"object\" || item === null) continue;\n\t\tconst record = item as Record<string, unknown>;\n\t\tif (typeof record.key !== \"string\" || record.key.trim() === \"\") continue;\n\t\tif (typeof record.value !== \"string\" || record.value.trim() === \"\") continue;\n\t\tfacets.push({ key: record.key.trim(), value: record.value.trim() });\n\t\tif (facets.length >= MAX_FACETS) break;\n\t}\n\treturn facets.length === 0 ? undefined : facets;\n}\n\n/** 单条 proposal 契约校验:statement 非空 ≤700 字符,facets 非法条目丢弃。 */\nexport function validateProposal(raw: unknown): CurationProposalV1 | undefined {\n\tif (typeof raw !== \"object\" || raw === null) return undefined;\n\tconst record = raw as Record<string, unknown>;\n\tif (typeof record.statement !== \"string\") return undefined;\n\tconst statement = record.statement.trim();\n\tif (statement === \"\" || statement.length > STATEMENT_LIMIT) return undefined;\n\tconst facets = validateFacets(record.facets);\n\treturn facets === undefined ? { statement } : { statement, facets };\n}\n\n/** 解析模型输出:围栏容忍 → JSON.parse → 契约校验 → 去重 → 截断。 */\nexport function parseProposeOutput(text: string, existingStatements: readonly string[]): CurationProposeResultV1 {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(stripJsonFence(text));\n\t} catch (error) {\n\t\treturn { error: `model output is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };\n\t}\n\tconst rawProposals =\n\t\ttypeof parsed === \"object\" && parsed !== null ? (parsed as { proposals?: unknown }).proposals : undefined;\n\tif (!Array.isArray(rawProposals)) {\n\t\treturn { error: 'model output is invalid: expected {\"proposals\":[...]}' };\n\t}\n\tconst existing = new Set(existingStatements.map((statement) => statement.trim()));\n\tconst seen = new Set<string>();\n\tconst proposals: CurationProposalV1[] = [];\n\tfor (const raw of rawProposals) {\n\t\tconst proposal = validateProposal(raw);\n\t\tif (proposal === undefined) continue;\n\t\t// 确定性去重:与既有记忆或本批内完全相同的 statement 不再输出\n\t\t//(跨会话语义去重仍归 memory 的提交侧,这里只做精确匹配)。\n\t\tif (existing.has(proposal.statement) || seen.has(proposal.statement)) continue;\n\t\tseen.add(proposal.statement);\n\t\tproposals.push(proposal);\n\t\tif (proposals.length >= PROPOSAL_LIMIT) break;\n\t}\n\treturn { proposals };\n}\n\nfunction completionError(result: Exclude<ModelInvocationResultV1, { status: \"completed\" }>): string {\n\treturn result.status === \"cancelled\"\n\t\t? \"model invocation was cancelled\"\n\t\t: `model invocation failed: ${result.error.code}: ${result.error.message}`;\n}\n\n/**\n * 构造 `propose` handler:注入的 CapabilityAPI 只读地决定目标与调用通道,\n * 输入输出即 §7.2 契约形状。\n */\nexport function createProposeHandler(\n\tapi: CapabilityAPI,\n): CapabilityProviderHandler<CurationProposeInputV1, CurationProposeResultV1> {\n\treturn async (input, { signal }) => {\n\t\tif (typeof input !== \"object\" || input === null || input.op !== \"propose\") {\n\t\t\treturn {\n\t\t\t\terror: `unsupported op: ${typeof input === \"object\" && input !== null ? String(input.op) : String(input)}`,\n\t\t\t};\n\t\t}\n\t\tconst invocation = api.modelInvocation;\n\t\tif (invocation === undefined) {\n\t\t\treturn { error: \"model invocation unavailable: the host does not declare model-invocation-v1\" };\n\t\t}\n\t\tconst resolved = resolveCurationTarget(api);\n\t\tif (\"error\" in resolved) return { error: resolved.error };\n\t\tif (!Array.isArray(input.slices)) return { error: \"invalid input: slices must be an array\" };\n\t\tconst slices = input.slices.map((entries) => projectSlice(Array.isArray(entries) ? entries : []));\n\t\tif (slices.every((text) => text === \"\")) return { proposals: [] };\n\t\tconst prompt = buildUserPrompt(slices, Array.isArray(input.existingStatements) ? input.existingStatements : []);\n\t\tlet result: ModelInvocationResultV1;\n\t\ttry {\n\t\t\tresult = await invocation.complete(\n\t\t\t\t{\n\t\t\t\t\tversion: 1,\n\t\t\t\t\ttarget: resolved.target,\n\t\t\t\t\tsystemPrompt: SYSTEM_PROMPT,\n\t\t\t\t\tprompt,\n\t\t\t\t\tmaxTokens: MAX_TOKENS,\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t} catch (error) {\n\t\t\treturn { error: `model invocation threw: ${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t\tif (result.status !== \"completed\") return { error: completionError(result) };\n\t\treturn parseProposeOutput(result.text, Array.isArray(input.existingStatements) ? input.existingStatements : []);\n\t};\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
- "name": "@ai-agent-forge/plugin-memory-curation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@ai-agent-forge/plugin-memory-curation",
3
+ "version": "0.88.15",
4
+ "description": "First-party default memory curation policy: proposes experience statements from archived session slices via the memory.curation-policy@1 capability",
5
+ "type": "module",
6
+ "main": "./dist/entry.js",
7
+ "types": "./dist/entry.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/entry.d.ts",
11
+ "import": "./dist/entry.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "agent-forge.json",
17
+ "plugin.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "clean": "shx rm -rf dist",
22
+ "build": "tsgo -p tsconfig.build.json",
23
+ "test": "vitest --run",
24
+ "prepublishOnly": "npm run clean && npm run build",
25
+ "test:coverage": "vitest --run --coverage"
26
+ },
27
+ "keywords": [
28
+ "agent-forge",
29
+ "plugin",
30
+ "memory",
31
+ "curation"
32
+ ],
33
+ "author": "Agent Forge contributors",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/agent-forge/agent-forge.git",
38
+ "directory": "plugins/memory-curation"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/agent-forge/agent-forge/issues"
42
+ },
43
+ "homepage": "https://github.com/agent-forge/agent-forge#readme",
44
+ "engines": {
45
+ "node": ">=22.19.0"
46
+ },
47
+ "dependencies": {},
48
+ "devDependencies": {
49
+ "shx": "0.3.4",
50
+ "vitest": "4.1.11"
51
+ }
52
+ }
package/plugin.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "agent-forge.plugin.memory-curation",
3
+ "version": "0.88.15",
4
+ "apiVersion": "1-draft",
5
+ "entry": "./dist/entry.js",
6
+ "hostVersion": ">=0.84.4",
7
+ "description": "记忆整理默认策略: 从归档会话切片提炼经验候选, 经 memory.curation-policy@1 服务能力提供给 memory 插件; 模型通道不可用时结构化失败, 整理批次由 memory 暂停",
8
+ "provides": [
9
+ {
10
+ "id": "memory.curation-policy",
11
+ "version": 1,
12
+ "kind": "service"
13
+ }
14
+ ]
15
+ }