dsh-continual-evolve 0.15.3 → 0.15.5
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 +13 -9
- package/README.zh.md +13 -9
- package/client/client.js +181 -16
- package/lib/card-routes.d.ts +2 -0
- package/lib/card-routes.js +66 -1
- package/lib/extraction.d.ts +5 -2
- package/lib/extraction.js +84 -7
- package/lib/index.d.ts +4 -2
- package/lib/index.js +7 -2
- package/lib/memory-snapshot.d.ts +52 -0
- package/lib/memory-snapshot.js +100 -4
- package/lib/skill-rules.d.ts +73 -0
- package/lib/skill-rules.js +158 -0
- package/lib/skill-tools.d.ts +9 -0
- package/lib/skill-tools.js +198 -0
- package/lib/store.d.ts +73 -5
- package/lib/store.js +504 -6
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-facing skill tools: `skill_write` (the explicit write path
|
|
3
|
+
* through the store's gates, plus the manual move to the global scope) and
|
|
4
|
+
* `skill_read` (bodies and search across the project partition and the
|
|
5
|
+
* global scope, project shadowing global on name collisions).
|
|
6
|
+
*
|
|
7
|
+
* Skills are created in the project only — global rows arise solely from an
|
|
8
|
+
* explicit `move-to-global`. There is no promotion mechanism: no counters,
|
|
9
|
+
* no confidence, no automation. Definitions are hand-built to the shape
|
|
10
|
+
* `ctx.tools.register` consumes (standard JSON Schema with a root-level
|
|
11
|
+
* `required` array, docs/FAQ.md #2). Host API shapes are declared locally;
|
|
12
|
+
* the plugin imports nothing from DSH at runtime.
|
|
13
|
+
*/
|
|
14
|
+
import { cwdOf } from "./memory-section.js";
|
|
15
|
+
function textTool(name, description, parameters, execute) {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
description,
|
|
19
|
+
parameters,
|
|
20
|
+
output: {
|
|
21
|
+
schema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: { text: { type: "string", description: "结果说明" } },
|
|
24
|
+
required: ["text"],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
28
|
+
},
|
|
29
|
+
execute: async (args, exec) => ({ text: await execute(args, exec) }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** The calling agent's session id (provenance for the ledger). */
|
|
33
|
+
function sessionIdOf(exec) {
|
|
34
|
+
const id = exec?.agent?.id;
|
|
35
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
36
|
+
}
|
|
37
|
+
const SKILL_WRITE_PARAMETERS = {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
action: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: ["create", "update", "delete", "move-to-global"],
|
|
43
|
+
description: "create 在本项目新建技能;update 更新已有技能(只给要改的字段);delete 删除技能;move-to-global 把本项目技能手动搬到全局(项目侧归档,没有自动晋升)。",
|
|
44
|
+
},
|
|
45
|
+
scope: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: ["project", "global"],
|
|
48
|
+
description: "update/delete 的目标分区(默认 project;全局行只可显式指定 global 更新或删除)。",
|
|
49
|
+
},
|
|
50
|
+
id: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "短横线小写标识。update/delete/move-to-global 必填;create 可省略(从 title 推导,中文标题会得到哈希后缀)。",
|
|
53
|
+
},
|
|
54
|
+
title: { type: "string", description: "短标题(create 必填)。" },
|
|
55
|
+
description: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "一句话触发钩子——决定未来会话会不会想起这个技能(create 必填,会进 SKILL.md 的 description)。",
|
|
58
|
+
},
|
|
59
|
+
body: { type: "string", description: "技能正文(create 必填;update 时传新正文整体替换)。" },
|
|
60
|
+
},
|
|
61
|
+
required: ["action"],
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
};
|
|
64
|
+
const SKILL_READ_PARAMETERS = {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
id: { type: "string", description: "按 id 直取一条技能的完整内容(同名时项目遮蔽全局)。" },
|
|
68
|
+
query: { type: "string", description: "关键词检索(不传 id 时生效;不传 query 则列出本项目与全局技能)。" },
|
|
69
|
+
limit: { type: "number", description: "检索/列出的最大条数,默认 3,上限 10。" },
|
|
70
|
+
},
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Register the skill tools and return a disposer that removes them (the
|
|
75
|
+
* host may return one; call only the functions among whatever comes back).
|
|
76
|
+
*/
|
|
77
|
+
export function registerSkillTools(tools, store) {
|
|
78
|
+
const disposers = [];
|
|
79
|
+
disposers.push(tools.register(textTool("skill_write", "写入可复用的技能(create 新建 / update 更新 / delete 删除 / move-to-global 手动搬到全局)。用户明确要求沉淀技能时使用;技能只在本项目创建,全局只接受手动搬家,由插件代码校验后落库并物化为 SKILL.md。", SKILL_WRITE_PARAMETERS, async (args, exec) => {
|
|
80
|
+
const cwd = cwdOf(exec.agent);
|
|
81
|
+
if (cwd === undefined) {
|
|
82
|
+
return "错误:当前会话没有可用的工作区路径,无法写入技能。";
|
|
83
|
+
}
|
|
84
|
+
const action = args.action;
|
|
85
|
+
const { outcomes, warnings } = store.applySkillProposals({ workspaceId: cwd, trigger: "explicit", sessionId: sessionIdOf(exec) }, [
|
|
86
|
+
{
|
|
87
|
+
action,
|
|
88
|
+
scope: typeof args.scope === "string" ? args.scope : undefined,
|
|
89
|
+
id: typeof args.id === "string" ? args.id : undefined,
|
|
90
|
+
title: typeof args.title === "string" ? args.title : undefined,
|
|
91
|
+
description: typeof args.description === "string" ? args.description : undefined,
|
|
92
|
+
body: typeof args.body === "string" ? args.body : undefined,
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
const outcome = outcomes[0];
|
|
96
|
+
if (outcome === undefined || !outcome.ok) {
|
|
97
|
+
return `写入被门禁拒绝,技能未落盘:${outcome?.reason ?? "未知原因"}`;
|
|
98
|
+
}
|
|
99
|
+
const warningText = warnings.length > 0 ? `(文件投影告警:${warnings.map((w) => w.warning).join(";")})` : "";
|
|
100
|
+
if (outcome.action === "delete") {
|
|
101
|
+
return `已删除技能 \`${outcome.id}\`。旧内容保留在技能账本中,可由用户恢复。${warningText}`;
|
|
102
|
+
}
|
|
103
|
+
if (outcome.action === "move-to-global") {
|
|
104
|
+
return `已把技能 \`${outcome.id}\` 手动搬到全局(本项目侧已归档)。重启后所有工作区可用。${warningText}`;
|
|
105
|
+
}
|
|
106
|
+
return `已${outcome.action === "create" ? "写入" : "更新"}技能 \`${outcome.id}\`(本项目,重启后本工作区可用)。${warningText}`;
|
|
107
|
+
})));
|
|
108
|
+
disposers.push(tools.register(textTool("skill_read", "读取技能:按 id 取一条的完整内容,或按关键词检索本项目与全局的技能(同名时项目遮蔽全局,只返回项目那条)。", SKILL_READ_PARAMETERS, async (args, exec) => {
|
|
109
|
+
const cwd = cwdOf(exec.agent);
|
|
110
|
+
if (cwd === undefined) {
|
|
111
|
+
return "错误:当前会话没有可用的工作区路径,无法读取技能。";
|
|
112
|
+
}
|
|
113
|
+
const limit = clampLimit(args.limit);
|
|
114
|
+
if (typeof args.id === "string" && args.id.length > 0) {
|
|
115
|
+
const found = readWithScope(store, cwd, args.id);
|
|
116
|
+
return found === undefined ? `没有找到 id 为 "${args.id}" 的技能。` : formatSkill(found.record, found.scope);
|
|
117
|
+
}
|
|
118
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
119
|
+
const merged = query.length > 0 ? searchWithScope(store, cwd, query, limit) : listWithScope(store, cwd, limit);
|
|
120
|
+
if (merged.length === 0) {
|
|
121
|
+
return query.length > 0 ? `没有匹配 "${query}" 的技能。` : "本项目和全局都还没有技能。";
|
|
122
|
+
}
|
|
123
|
+
return merged.map((entry) => formatSkill(entry.record, entry.scope)).join("\n\n---\n\n");
|
|
124
|
+
})));
|
|
125
|
+
return () => {
|
|
126
|
+
for (const disposer of disposers) {
|
|
127
|
+
if (typeof disposer === "function") {
|
|
128
|
+
disposer();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Project-first read: a project row shadows the global row of the same id. */
|
|
134
|
+
function readWithScope(store, workspaceId, id) {
|
|
135
|
+
const project = store.getSkill(workspaceId, id);
|
|
136
|
+
if (project !== undefined && project.status !== "archived") {
|
|
137
|
+
return { record: project, scope: "project" };
|
|
138
|
+
}
|
|
139
|
+
const global = store.getSkill("", id);
|
|
140
|
+
if (global !== undefined && global.status !== "archived") {
|
|
141
|
+
return { record: global, scope: "global" };
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/** Scope-merged search: project wins on id collisions, global fills the rest. */
|
|
146
|
+
function searchWithScope(store, workspaceId, query, limit) {
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
const merged = [];
|
|
149
|
+
for (const record of store.searchSkills(workspaceId, query, limit)) {
|
|
150
|
+
if (seen.has(record.id)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
seen.add(record.id);
|
|
154
|
+
merged.push({ record, scope: "project" });
|
|
155
|
+
}
|
|
156
|
+
if (merged.length < limit) {
|
|
157
|
+
for (const record of store.searchSkills("", query, limit)) {
|
|
158
|
+
if (seen.has(record.id)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
seen.add(record.id);
|
|
162
|
+
merged.push({ record, scope: "global" });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return merged.slice(0, limit);
|
|
166
|
+
}
|
|
167
|
+
/** Scope-merged listing: project rows first, then global rows. */
|
|
168
|
+
function listWithScope(store, workspaceId, limit) {
|
|
169
|
+
const merged = store
|
|
170
|
+
.listSkills(workspaceId)
|
|
171
|
+
.slice(0, limit)
|
|
172
|
+
.map((record) => ({ record, scope: "project" }));
|
|
173
|
+
if (merged.length < limit) {
|
|
174
|
+
const seen = new Set(merged.map((entry) => entry.record.id));
|
|
175
|
+
for (const record of store.listSkills("")) {
|
|
176
|
+
if (merged.length >= limit) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (seen.has(record.id)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
seen.add(record.id);
|
|
183
|
+
merged.push({ record, scope: "global" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return merged;
|
|
187
|
+
}
|
|
188
|
+
function clampLimit(value) {
|
|
189
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
190
|
+
return 3;
|
|
191
|
+
}
|
|
192
|
+
return Math.min(10, Math.max(1, Math.floor(value)));
|
|
193
|
+
}
|
|
194
|
+
/** Full skill text: header line with scope, then the body. */
|
|
195
|
+
function formatSkill(record, scope) {
|
|
196
|
+
return [`# ${record.title}`, `id: ${record.id} | 范围: ${scope === "global" ? "全局" : "本项目"} | 状态: ${record.status} | 更新: ${record.updatedAt}`, `钩子: ${record.description}`, "", record.body].join("\n");
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=skill-tools.js.map
|
package/lib/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MemoryProposal } from "./memory-rules.js";
|
|
2
|
+
import { type SkillProposal } from "./skill-rules.js";
|
|
2
3
|
import { type ProposalContext, type ProposalOutcome } from "./store-apply.js";
|
|
3
4
|
export type { MemoryProposal, ProposalContext, ProposalOutcome };
|
|
4
5
|
/** Thrown when the runtime lacks `node:sqlite` — the degrade signal. */
|
|
@@ -12,6 +13,36 @@ export declare const MD_IMPORT_STATE_KEY = "md-imported";
|
|
|
12
13
|
export declare const GLOBAL_WORKSPACE = "";
|
|
13
14
|
/** Preview size cap for the card's content endpoint (ZCode's viewer uses 5 MiB). */
|
|
14
15
|
export declare const MEMORY_FILE_PREVIEW_LIMIT: number;
|
|
16
|
+
/** One stored skill record (snake_case columns camelCased). Skills carry no type. */
|
|
17
|
+
export interface SkillRecord {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
body: string;
|
|
23
|
+
status: "active" | "quarantined" | "archived";
|
|
24
|
+
/** Origin session id, `null` for imports. */
|
|
25
|
+
sourceSession: string | null;
|
|
26
|
+
/** Trajectory seq range the skill came from ("820-831"), `null` when unknown. */
|
|
27
|
+
sourceSeqs: string | null;
|
|
28
|
+
/** The run that wrote it; `'explicit'` for tool writes. */
|
|
29
|
+
sourceRun: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
}
|
|
33
|
+
/** What a single skill proposal produced. */
|
|
34
|
+
export interface SkillProposalOutcome {
|
|
35
|
+
id: string;
|
|
36
|
+
action: SkillProposal["action"];
|
|
37
|
+
ok: boolean;
|
|
38
|
+
/** Blocking reason when `ok` is false. */
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A non-fatal file-projection problem the caller should surface. */
|
|
42
|
+
export interface SkillFileWarning {
|
|
43
|
+
id: string;
|
|
44
|
+
warning: string;
|
|
45
|
+
}
|
|
15
46
|
/** One stored memory record (snake_case columns camelCased). */
|
|
16
47
|
export interface MemoryRecord {
|
|
17
48
|
workspaceId: string;
|
|
@@ -67,6 +98,20 @@ export interface PatrolResult {
|
|
|
67
98
|
id: string;
|
|
68
99
|
reason: string;
|
|
69
100
|
}[];
|
|
101
|
+
/** Orphan skill FTS rows (no matching skill) removed. */
|
|
102
|
+
skillOrphanFtsRows: number;
|
|
103
|
+
/** Skills quarantined by the secret screen, with the redacted reasons. */
|
|
104
|
+
skillQuarantined: {
|
|
105
|
+
workspaceId: string;
|
|
106
|
+
id: string;
|
|
107
|
+
reason: string;
|
|
108
|
+
}[];
|
|
109
|
+
/** Managed SKILL.md files rewritten or created to match the store. */
|
|
110
|
+
skillFilesReconciled: number;
|
|
111
|
+
/** Orphan managed SKILL.md files (no active store row) removed. */
|
|
112
|
+
skillOrphanFilesRemoved: number;
|
|
113
|
+
/** Managed file operations that failed (retried by the next patrol). */
|
|
114
|
+
skillFileErrors: number;
|
|
70
115
|
}
|
|
71
116
|
/** The store surface every consumer (tool, extraction, card, injection) uses. */
|
|
72
117
|
export interface MemoryStore {
|
|
@@ -93,11 +138,11 @@ export interface MemoryStore {
|
|
|
93
138
|
listWorkspaces(): WorkspaceSummary[];
|
|
94
139
|
/**
|
|
95
140
|
* The partition with the most recent session activity: the newest ledger
|
|
96
|
-
* instant against the newest memory write, whichever is later.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* fallback for a partition whose ledger rows were pruned.
|
|
100
|
-
* when the store holds neither.
|
|
141
|
+
* instant against the newest memory or skill write, whichever is later.
|
|
142
|
+
* The ledgers carry a row for every run — including skips and explicit
|
|
143
|
+
* writes — so they track "a session worked here", while the memory write
|
|
144
|
+
* is the durable fallback for a partition whose ledger rows were pruned.
|
|
145
|
+
* Undefined only when the store holds neither.
|
|
101
146
|
*/
|
|
102
147
|
mostRecentlyActiveWorkspace(): string | undefined;
|
|
103
148
|
/** Hygiene pass: orphan FTS cleanup + secret quarantine. */
|
|
@@ -105,6 +150,29 @@ export interface MemoryStore {
|
|
|
105
150
|
/** Ledger rows older than the cutoff, removed (the one unbounded table). */
|
|
106
151
|
pruneLedger(beforeIso: string): number;
|
|
107
152
|
close(): void;
|
|
153
|
+
/** One active skill row from a partition, undefined when absent. */
|
|
154
|
+
getSkill(workspaceId: string, id: string): SkillRecord | undefined;
|
|
155
|
+
/** Active skills of one partition, newest first. */
|
|
156
|
+
listSkills(workspaceId: string): SkillRecord[];
|
|
157
|
+
/** Every non-archived skill of one partition, newest first. */
|
|
158
|
+
listAllSkills(workspaceId: string): SkillRecord[];
|
|
159
|
+
/**
|
|
160
|
+
* Keyword search over skills, workspace-scoped: same AND-combined
|
|
161
|
+
* trigram/LIKE discipline as memory search.
|
|
162
|
+
*/
|
|
163
|
+
searchSkills(workspaceId: string, query: string, limit?: number): SkillRecord[];
|
|
164
|
+
/**
|
|
165
|
+
* The sole skill write gate: validate all proposals, apply atomically,
|
|
166
|
+
* ledger with before/after snapshots, then sync the materialized files.
|
|
167
|
+
* File sync never fails the batch — the database is the source of truth
|
|
168
|
+
* and patrol retries whatever the filesystem refused.
|
|
169
|
+
*/
|
|
170
|
+
applySkillProposals(ctx: ProposalContext, proposals: readonly SkillProposal[]): {
|
|
171
|
+
outcomes: SkillProposalOutcome[];
|
|
172
|
+
warnings: SkillFileWarning[];
|
|
173
|
+
};
|
|
174
|
+
/** Partitions holding active skills (the card's skill fence). */
|
|
175
|
+
listSkillWorkspaces(): WorkspaceSummary[];
|
|
108
176
|
}
|
|
109
177
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
110
178
|
export declare function defaultStorePath(): string;
|