@roarpeng/graphflow 1.9.13 → 1.9.14
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/AGENTS.md +2 -0
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +2 -0
- package/README.md +49 -8
- package/README.zh.md +129 -0
- package/cordis.patch.yml +30 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +1 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +6 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/graph/snapshot-view.d.ts +4 -0
- package/dist/graph/snapshot-view.d.ts.map +1 -1
- package/dist/graph/snapshot-view.js +36 -2
- package/dist/graph/snapshot-view.js.map +1 -1
- package/dist/integrations/agent-mcp-installer.d.ts.map +1 -1
- package/dist/integrations/agent-mcp-installer.js +8 -0
- package/dist/integrations/agent-mcp-installer.js.map +1 -1
- package/dist/integrations/agent-profiles/index.d.ts +2 -0
- package/dist/integrations/agent-profiles/index.d.ts.map +1 -1
- package/dist/integrations/agent-profiles/index.js +2 -0
- package/dist/integrations/agent-profiles/index.js.map +1 -1
- package/dist/integrations/agent-profiles/profiles/dsh.d.ts +2 -0
- package/dist/integrations/agent-profiles/profiles/dsh.d.ts.map +1 -0
- package/dist/integrations/agent-profiles/profiles/dsh.js +16 -0
- package/dist/integrations/agent-profiles/profiles/dsh.js.map +1 -0
- package/dist/integrations/agent-profiles/skills/dsh.d.ts +2 -0
- package/dist/integrations/agent-profiles/skills/dsh.d.ts.map +1 -0
- package/dist/integrations/agent-profiles/skills/dsh.js +13 -0
- package/dist/integrations/agent-profiles/skills/dsh.js.map +1 -0
- package/dist/integrations/dsh-harness-installer.d.ts +49 -0
- package/dist/integrations/dsh-harness-installer.d.ts.map +1 -0
- package/dist/integrations/dsh-harness-installer.js +193 -0
- package/dist/integrations/dsh-harness-installer.js.map +1 -0
- package/dist/integrations/skill-installer.d.ts.map +1 -1
- package/dist/integrations/skill-installer.js +6 -0
- package/dist/integrations/skill-installer.js.map +1 -1
- package/dist/learning/dialogue-thread.d.ts +88 -0
- package/dist/learning/dialogue-thread.d.ts.map +1 -0
- package/dist/learning/dialogue-thread.js +419 -0
- package/dist/learning/dialogue-thread.js.map +1 -0
- package/dist/learning/workbench-topic.d.ts +116 -0
- package/dist/learning/workbench-topic.d.ts.map +1 -0
- package/dist/learning/workbench-topic.js +642 -0
- package/dist/learning/workbench-topic.js.map +1 -0
- package/dist/surfaces/cli/index.js +123 -5
- package/dist/surfaces/cli/index.js.map +1 -1
- package/dist/surfaces/cli/init.d.ts +3 -0
- package/dist/surfaces/cli/init.d.ts.map +1 -1
- package/dist/surfaces/cli/init.js +36 -3
- package/dist/surfaces/cli/init.js.map +1 -1
- package/dist/surfaces/cli/output.d.ts.map +1 -1
- package/dist/surfaces/cli/output.js +3 -0
- package/dist/surfaces/cli/output.js.map +1 -1
- package/dist/surfaces/cli/runtime/dialogue.d.ts +23 -0
- package/dist/surfaces/cli/runtime/dialogue.d.ts.map +1 -0
- package/dist/surfaces/cli/runtime/dialogue.js +50 -0
- package/dist/surfaces/cli/runtime/dialogue.js.map +1 -0
- package/dist/surfaces/cli/runtime/graph.d.ts +11 -2
- package/dist/surfaces/cli/runtime/graph.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime/graph.js +225 -5
- package/dist/surfaces/cli/runtime/graph.js.map +1 -1
- package/dist/surfaces/cli/runtime/routing.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime/routing.js +44 -3
- package/dist/surfaces/cli/runtime/routing.js.map +1 -1
- package/dist/surfaces/cli/runtime/types.d.ts +48 -0
- package/dist/surfaces/cli/runtime/types.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime.d.ts +2 -1
- package/dist/surfaces/cli/runtime.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime.js +7 -2
- package/dist/surfaces/cli/runtime.js.map +1 -1
- package/dist/surfaces/cursor-rules/graphflow.mdc +10 -3
- package/dist/surfaces/mcp/tool-definitions.d.ts.map +1 -1
- package/dist/surfaces/mcp/tool-definitions.js +23 -3
- package/dist/surfaces/mcp/tool-definitions.js.map +1 -1
- package/dist/surfaces/mcp/tool-handlers.d.ts.map +1 -1
- package/dist/surfaces/mcp/tool-handlers.js +27 -3
- package/dist/surfaces/mcp/tool-handlers.js.map +1 -1
- package/dist/surfaces/trae-skill/graphflow/SKILL.md +50 -5
- package/package.json +11 -2
- package/plugin.json +27 -4
- package/skills/graphflow/SKILL.md +50 -5
- package/src/surfaces/cursor-rules/graphflow.mdc +10 -3
- package/src/surfaces/trae-skill/graphflow/SKILL.md +50 -5
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WORKBENCH_TOPIC_KIND = exports.WORKBENCH_ROOT_KIND = exports.WORKBENCH_TOPIC_PREFIX = exports.WORKBENCH_ROOT_PREFIX = void 0;
|
|
4
|
+
exports.workbenchRootIdFor = workbenchRootIdFor;
|
|
5
|
+
exports.workbenchTopicIdFor = workbenchTopicIdFor;
|
|
6
|
+
exports.isWorkbenchTopicNode = isWorkbenchTopicNode;
|
|
7
|
+
exports.isWorkbenchRootNode = isWorkbenchRootNode;
|
|
8
|
+
exports.parseWorkbenchTopic = parseWorkbenchTopic;
|
|
9
|
+
exports.buildWorkbenchOutlines = buildWorkbenchOutlines;
|
|
10
|
+
exports.formatWorkbenchOutlineLines = formatWorkbenchOutlineLines;
|
|
11
|
+
exports.loadWorkbenchOutlines = loadWorkbenchOutlines;
|
|
12
|
+
exports.seedWorkbenchFromPlan = seedWorkbenchFromPlan;
|
|
13
|
+
exports.activateTopic = activateTopic;
|
|
14
|
+
exports.forkIsolatedTopic = forkIsolatedTopic;
|
|
15
|
+
exports.appendTopicMessage = appendTopicMessage;
|
|
16
|
+
exports.topicPendingReply = topicPendingReply;
|
|
17
|
+
exports.loadActiveTopic = loadActiveTopic;
|
|
18
|
+
exports.loadWorkbenchContext = loadWorkbenchContext;
|
|
19
|
+
exports.formatWorkbenchPrompt = formatWorkbenchPrompt;
|
|
20
|
+
exports.scoreOverlap = scoreOverlap;
|
|
21
|
+
const hash_1 = require("../utils/hash");
|
|
22
|
+
/**
|
|
23
|
+
* Workbench topic containers: the canvas node is a *function/topic*, not a turn.
|
|
24
|
+
*
|
|
25
|
+
* Complex task → seed a root + one topic per plan DAG step (mainline).
|
|
26
|
+
* Click a topic → that container becomes active; further Q&A appends as messages.
|
|
27
|
+
* Drift → an isolated (non-mainline) topic is forked so the mainline stays clean.
|
|
28
|
+
* Click a mainline topic → active pointer jumps back; next context is Goal + path + local messages.
|
|
29
|
+
*/
|
|
30
|
+
exports.WORKBENCH_ROOT_PREFIX = "workbench:";
|
|
31
|
+
exports.WORKBENCH_TOPIC_PREFIX = "topic:";
|
|
32
|
+
exports.WORKBENCH_ROOT_KIND = "workbench-root";
|
|
33
|
+
exports.WORKBENCH_TOPIC_KIND = "workbench-topic";
|
|
34
|
+
const MAX_TITLE = 48;
|
|
35
|
+
const MAX_DESCRIPTION = 400;
|
|
36
|
+
const MAX_MESSAGE = 4_000;
|
|
37
|
+
const MAX_MESSAGES_PER_TOPIC = 40;
|
|
38
|
+
const DRIFT_OVERLAP_THRESHOLD = 0.18;
|
|
39
|
+
function workbenchRootIdFor(task, workspaceRoot) {
|
|
40
|
+
return `${exports.WORKBENCH_ROOT_PREFIX}${(0, hash_1.hashText)(`${normalizeTask(task)}|${(workspaceRoot ?? "").trim().toLowerCase()}`)}`;
|
|
41
|
+
}
|
|
42
|
+
function workbenchTopicIdFor(rootId, planStepId) {
|
|
43
|
+
const rootHash = rootId.startsWith(exports.WORKBENCH_ROOT_PREFIX)
|
|
44
|
+
? rootId.slice(exports.WORKBENCH_ROOT_PREFIX.length)
|
|
45
|
+
: (0, hash_1.hashText)(rootId);
|
|
46
|
+
return `${exports.WORKBENCH_TOPIC_PREFIX}${rootHash}:${planStepId}`;
|
|
47
|
+
}
|
|
48
|
+
function isWorkbenchTopicNode(node) {
|
|
49
|
+
return node.metadata?.kind === exports.WORKBENCH_TOPIC_KIND || node.id.startsWith(exports.WORKBENCH_TOPIC_PREFIX);
|
|
50
|
+
}
|
|
51
|
+
function isWorkbenchRootNode(node) {
|
|
52
|
+
return node.metadata?.kind === exports.WORKBENCH_ROOT_KIND || node.id.startsWith(exports.WORKBENCH_ROOT_PREFIX);
|
|
53
|
+
}
|
|
54
|
+
function parseWorkbenchTopic(node) {
|
|
55
|
+
if (!isWorkbenchTopicNode(node))
|
|
56
|
+
return undefined;
|
|
57
|
+
return deserializeTopic(node);
|
|
58
|
+
}
|
|
59
|
+
function buildWorkbenchOutlines(nodes, edges) {
|
|
60
|
+
const roots = nodes
|
|
61
|
+
.map((node) => (isWorkbenchRootNode(node) ? deserializeRoot(node) : undefined))
|
|
62
|
+
.filter((root) => Boolean(root))
|
|
63
|
+
.sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
|
|
64
|
+
const topics = nodes
|
|
65
|
+
.map((node) => parseWorkbenchTopic(node))
|
|
66
|
+
.filter((topic) => Boolean(topic));
|
|
67
|
+
if (roots.length === 0 && topics.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const parentBySide = new Map();
|
|
71
|
+
for (const edge of edges) {
|
|
72
|
+
if (edge.relation !== "co_occurs")
|
|
73
|
+
continue;
|
|
74
|
+
const from = topics.find((topic) => topic.id === edge.from);
|
|
75
|
+
if (from?.isolated) {
|
|
76
|
+
parentBySide.set(from.id, edge.to);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return roots.map((root) => {
|
|
80
|
+
const owned = topics.filter((topic) => topic.rootId === root.id);
|
|
81
|
+
const mainline = orderMainline(owned.filter((topic) => topic.mainline && !topic.isolated), edges);
|
|
82
|
+
const sides = owned.filter((topic) => topic.isolated);
|
|
83
|
+
const attached = new Set();
|
|
84
|
+
const nodesForRoot = mainline.map((topic) => {
|
|
85
|
+
const children = sides
|
|
86
|
+
.filter((side) => parentBySide.get(side.id) === topic.id)
|
|
87
|
+
.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id))
|
|
88
|
+
.map((side) => {
|
|
89
|
+
attached.add(side.id);
|
|
90
|
+
return toOutlineNode(side, root.activeTopicId, []);
|
|
91
|
+
});
|
|
92
|
+
return toOutlineNode(topic, root.activeTopicId, children);
|
|
93
|
+
});
|
|
94
|
+
for (const side of sides) {
|
|
95
|
+
if (attached.has(side.id))
|
|
96
|
+
continue;
|
|
97
|
+
nodesForRoot.push(toOutlineNode(side, root.activeTopicId, []));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
rootId: root.id,
|
|
101
|
+
task: root.task,
|
|
102
|
+
activeTopicId: root.activeTopicId,
|
|
103
|
+
nodes: nodesForRoot,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function formatWorkbenchOutlineLines(outlines) {
|
|
108
|
+
if (outlines.length === 0) {
|
|
109
|
+
return ["Workbench: (empty) — call graphflow_plan to seed the function DAG."];
|
|
110
|
+
}
|
|
111
|
+
const lines = [];
|
|
112
|
+
const walk = (node, depth) => {
|
|
113
|
+
lines.push(formatOutlineLine(node, depth));
|
|
114
|
+
for (const child of node.children) {
|
|
115
|
+
walk(child, depth + 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
for (const outline of outlines) {
|
|
119
|
+
lines.push(`Workbench: ${clip(outline.task, 80)}`);
|
|
120
|
+
for (const node of outline.nodes) {
|
|
121
|
+
walk(node, 0);
|
|
122
|
+
}
|
|
123
|
+
lines.push(`Resume: graphflow_context({ topicId: "${outline.activeTopicId}" })`);
|
|
124
|
+
}
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
async function loadWorkbenchOutlines(client) {
|
|
128
|
+
const snapshot = client.readSnapshot?.();
|
|
129
|
+
if (snapshot) {
|
|
130
|
+
return buildWorkbenchOutlines(snapshot.nodes, snapshot.edges);
|
|
131
|
+
}
|
|
132
|
+
return buildWorkbenchOutlines(await collectWorkbenchNodes(client), []);
|
|
133
|
+
}
|
|
134
|
+
function formatOutlineLine(node, depth) {
|
|
135
|
+
const indent = " ".repeat(depth);
|
|
136
|
+
const mark = node.kind === "side" ? "旁支" : "主线";
|
|
137
|
+
const active = node.active ? " [active]" : "";
|
|
138
|
+
const pending = node.pendingReply ? " pending" : "";
|
|
139
|
+
return `${indent}${mark}${active}${pending}: ${node.title} ${node.id}`;
|
|
140
|
+
}
|
|
141
|
+
function toOutlineNode(topic, activeTopicId, children) {
|
|
142
|
+
const lastUser = [...topic.messages].reverse().find((message) => message.role === "user");
|
|
143
|
+
return {
|
|
144
|
+
id: topic.id,
|
|
145
|
+
title: topic.title,
|
|
146
|
+
kind: topic.isolated ? "side" : "mainline",
|
|
147
|
+
active: topic.id === activeTopicId,
|
|
148
|
+
messageCount: topic.messages.length,
|
|
149
|
+
pendingReply: topic.messages[topic.messages.length - 1]?.role === "user",
|
|
150
|
+
...(lastUser ? { lastUserPreview: clip(lastUser.content, 72) } : {}),
|
|
151
|
+
children,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function orderMainline(topics, edges) {
|
|
155
|
+
if (topics.length <= 1)
|
|
156
|
+
return [...topics];
|
|
157
|
+
const ids = new Set(topics.map((topic) => topic.id));
|
|
158
|
+
const remaining = new Map();
|
|
159
|
+
const blockedBy = new Map();
|
|
160
|
+
for (const topic of topics) {
|
|
161
|
+
remaining.set(topic.id, 0);
|
|
162
|
+
blockedBy.set(topic.id, []);
|
|
163
|
+
}
|
|
164
|
+
for (const edge of edges) {
|
|
165
|
+
if (edge.relation !== "depends_on" || !ids.has(edge.from) || !ids.has(edge.to))
|
|
166
|
+
continue;
|
|
167
|
+
remaining.set(edge.from, (remaining.get(edge.from) ?? 0) + 1);
|
|
168
|
+
blockedBy.get(edge.to)?.push(edge.from);
|
|
169
|
+
}
|
|
170
|
+
const ready = topics
|
|
171
|
+
.filter((topic) => (remaining.get(topic.id) ?? 0) === 0)
|
|
172
|
+
.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
|
|
173
|
+
const ordered = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
while (ready.length > 0) {
|
|
176
|
+
const next = ready.shift();
|
|
177
|
+
if (seen.has(next.id))
|
|
178
|
+
continue;
|
|
179
|
+
seen.add(next.id);
|
|
180
|
+
ordered.push(next);
|
|
181
|
+
for (const id of blockedBy.get(next.id) ?? []) {
|
|
182
|
+
const left = (remaining.get(id) ?? 1) - 1;
|
|
183
|
+
remaining.set(id, left);
|
|
184
|
+
if (left === 0) {
|
|
185
|
+
const topic = topics.find((item) => item.id === id);
|
|
186
|
+
if (topic)
|
|
187
|
+
ready.push(topic);
|
|
188
|
+
ready.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const topic of topics) {
|
|
193
|
+
if (!seen.has(topic.id))
|
|
194
|
+
ordered.push(topic);
|
|
195
|
+
}
|
|
196
|
+
return ordered;
|
|
197
|
+
}
|
|
198
|
+
async function seedWorkbenchFromPlan(client, input) {
|
|
199
|
+
const now = input.now ?? Date.now();
|
|
200
|
+
const steps = input.steps.filter((step) => step.id.trim() && step.description.trim());
|
|
201
|
+
const rootId = workbenchRootIdFor(input.task, input.workspaceRoot);
|
|
202
|
+
const existingRoot = await loadRoot(client, rootId);
|
|
203
|
+
const topics = [];
|
|
204
|
+
const idByStep = new Map();
|
|
205
|
+
if (steps.length === 0) {
|
|
206
|
+
const fallbackId = workbenchTopicIdFor(rootId, "root");
|
|
207
|
+
const topic = (await loadTopic(client, fallbackId)) ?? {
|
|
208
|
+
id: fallbackId,
|
|
209
|
+
rootId,
|
|
210
|
+
title: clip(input.task, MAX_TITLE),
|
|
211
|
+
description: clip(input.task, MAX_DESCRIPTION),
|
|
212
|
+
mainline: true,
|
|
213
|
+
isolated: false,
|
|
214
|
+
planStepId: "root",
|
|
215
|
+
messages: [],
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
};
|
|
219
|
+
topics.push(topic);
|
|
220
|
+
idByStep.set("root", topic.id);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
for (const step of steps) {
|
|
224
|
+
const id = workbenchTopicIdFor(rootId, step.id);
|
|
225
|
+
const prior = await loadTopic(client, id);
|
|
226
|
+
const topic = prior
|
|
227
|
+
? {
|
|
228
|
+
...prior,
|
|
229
|
+
title: prior.title || titleFromDescription(step.description),
|
|
230
|
+
description: prior.description || clip(step.description, MAX_DESCRIPTION),
|
|
231
|
+
mainline: true,
|
|
232
|
+
isolated: false,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
}
|
|
235
|
+
: {
|
|
236
|
+
id,
|
|
237
|
+
rootId,
|
|
238
|
+
title: titleFromDescription(step.description),
|
|
239
|
+
description: clip(step.description, MAX_DESCRIPTION),
|
|
240
|
+
mainline: true,
|
|
241
|
+
isolated: false,
|
|
242
|
+
planStepId: step.id,
|
|
243
|
+
messages: [],
|
|
244
|
+
createdAt: now,
|
|
245
|
+
updatedAt: now,
|
|
246
|
+
};
|
|
247
|
+
topics.push(topic);
|
|
248
|
+
idByStep.set(step.id, id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const activeTopicId = existingRoot?.activeTopicId && topics.some((topic) => topic.id === existingRoot.activeTopicId)
|
|
252
|
+
? existingRoot.activeTopicId
|
|
253
|
+
: topics[0].id;
|
|
254
|
+
const root = {
|
|
255
|
+
id: rootId,
|
|
256
|
+
task: input.task.trim(),
|
|
257
|
+
activeTopicId,
|
|
258
|
+
createdAt: existingRoot?.createdAt ?? now,
|
|
259
|
+
updatedAt: now,
|
|
260
|
+
};
|
|
261
|
+
await persistRoot(client, root);
|
|
262
|
+
for (const topic of topics) {
|
|
263
|
+
await persistTopic(client, topic);
|
|
264
|
+
await upsertUniqueEdges(client, [{ from: topic.id, to: root.id, relation: "part_of" }]);
|
|
265
|
+
}
|
|
266
|
+
for (const step of steps) {
|
|
267
|
+
const fromId = idByStep.get(step.id);
|
|
268
|
+
if (!fromId)
|
|
269
|
+
continue;
|
|
270
|
+
for (const dep of step.dependencies) {
|
|
271
|
+
const toId = idByStep.get(dep);
|
|
272
|
+
if (!toId)
|
|
273
|
+
continue;
|
|
274
|
+
await upsertUniqueEdges(client, [{ from: fromId, to: toId, relation: "depends_on" }]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { root, topics };
|
|
278
|
+
}
|
|
279
|
+
async function activateTopic(client, topicId, now = Date.now()) {
|
|
280
|
+
const topic = await loadTopic(client, topicId);
|
|
281
|
+
if (!topic)
|
|
282
|
+
return undefined;
|
|
283
|
+
const root = await loadRoot(client, topic.rootId);
|
|
284
|
+
if (!root)
|
|
285
|
+
return topic;
|
|
286
|
+
await persistRoot(client, { ...root, activeTopicId: topic.id, updatedAt: now });
|
|
287
|
+
return topic;
|
|
288
|
+
}
|
|
289
|
+
async function forkIsolatedTopic(client, input) {
|
|
290
|
+
const parent = await loadTopic(client, input.fromTopicId);
|
|
291
|
+
if (!parent)
|
|
292
|
+
return undefined;
|
|
293
|
+
const now = input.now ?? Date.now();
|
|
294
|
+
const forkKey = `fork-${(0, hash_1.hashText)(`${parent.id}|${input.title}|${now}`)}`;
|
|
295
|
+
const topic = {
|
|
296
|
+
id: workbenchTopicIdFor(parent.rootId, forkKey),
|
|
297
|
+
rootId: parent.rootId,
|
|
298
|
+
title: titleFromDescription(input.title),
|
|
299
|
+
description: clip(input.description || input.title, MAX_DESCRIPTION),
|
|
300
|
+
mainline: false,
|
|
301
|
+
isolated: true,
|
|
302
|
+
messages: [],
|
|
303
|
+
createdAt: now,
|
|
304
|
+
updatedAt: now,
|
|
305
|
+
};
|
|
306
|
+
await persistTopic(client, topic);
|
|
307
|
+
await upsertUniqueEdges(client, [
|
|
308
|
+
{ from: topic.id, to: parent.rootId, relation: "part_of" },
|
|
309
|
+
{ from: topic.id, to: parent.id, relation: "co_occurs" },
|
|
310
|
+
]);
|
|
311
|
+
const root = await loadRoot(client, parent.rootId);
|
|
312
|
+
if (root) {
|
|
313
|
+
await persistRoot(client, { ...root, activeTopicId: topic.id, updatedAt: now });
|
|
314
|
+
}
|
|
315
|
+
return topic;
|
|
316
|
+
}
|
|
317
|
+
async function appendTopicMessage(client, input) {
|
|
318
|
+
const query = clip(input.query ?? "", MAX_MESSAGE);
|
|
319
|
+
const reply = clip(input.assistantReply ?? "", MAX_MESSAGE);
|
|
320
|
+
const now = input.now ?? Date.now();
|
|
321
|
+
let topic = input.topicId
|
|
322
|
+
? await activateTopic(client, input.topicId, now)
|
|
323
|
+
: await loadActiveTopic(client, input.workspaceRoot, input.taskHint);
|
|
324
|
+
if (!topic)
|
|
325
|
+
return undefined;
|
|
326
|
+
const filled = fillPendingAssistant(topic, query, reply, now);
|
|
327
|
+
if (filled) {
|
|
328
|
+
await persistTopic(client, filled);
|
|
329
|
+
return { topic: filled, forked: false, filled: true };
|
|
330
|
+
}
|
|
331
|
+
if (query.trim().length < 4)
|
|
332
|
+
return undefined;
|
|
333
|
+
const explicitClick = Boolean(input.topicId);
|
|
334
|
+
const shouldFork = input.allowAutoFork !== false &&
|
|
335
|
+
!explicitClick &&
|
|
336
|
+
topic.mainline &&
|
|
337
|
+
topic.messages.filter((message) => message.role === "user").length >= 1 &&
|
|
338
|
+
scoreOverlap(topicTokens(topic), query) < DRIFT_OVERLAP_THRESHOLD;
|
|
339
|
+
if (shouldFork) {
|
|
340
|
+
const forked = await forkIsolatedTopic(client, {
|
|
341
|
+
fromTopicId: topic.id,
|
|
342
|
+
title: query,
|
|
343
|
+
description: query,
|
|
344
|
+
now,
|
|
345
|
+
});
|
|
346
|
+
if (forked) {
|
|
347
|
+
topic = forked;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const messages = [
|
|
351
|
+
...topic.messages,
|
|
352
|
+
{ role: "user", content: query, at: now },
|
|
353
|
+
...(reply.trim() ? [{ role: "assistant", content: reply, at: now }] : []),
|
|
354
|
+
].slice(-MAX_MESSAGES_PER_TOPIC);
|
|
355
|
+
const updated = { ...topic, messages, updatedAt: now };
|
|
356
|
+
await persistTopic(client, updated);
|
|
357
|
+
return { topic: updated, forked: shouldFork && updated.isolated, filled: false };
|
|
358
|
+
}
|
|
359
|
+
function topicPendingReply(topic) {
|
|
360
|
+
return topic.messages[topic.messages.length - 1]?.role === "user";
|
|
361
|
+
}
|
|
362
|
+
function fillPendingAssistant(topic, query, reply, now) {
|
|
363
|
+
if (!reply.trim())
|
|
364
|
+
return undefined;
|
|
365
|
+
const messages = topic.messages;
|
|
366
|
+
const last = messages[messages.length - 1];
|
|
367
|
+
const prev = messages[messages.length - 2];
|
|
368
|
+
if (last?.role === "user") {
|
|
369
|
+
if (query.trim().length >= 4 && normalizeMessage(query) !== normalizeMessage(last.content)) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
...topic,
|
|
374
|
+
messages: [...messages, { role: "assistant", content: reply, at: now }].slice(-MAX_MESSAGES_PER_TOPIC),
|
|
375
|
+
updatedAt: now,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (last?.role === "assistant" &&
|
|
379
|
+
prev?.role === "user" &&
|
|
380
|
+
query.trim().length >= 4 &&
|
|
381
|
+
normalizeMessage(query) === normalizeMessage(prev.content)) {
|
|
382
|
+
return {
|
|
383
|
+
...topic,
|
|
384
|
+
messages: [...messages.slice(0, -1), { role: "assistant", content: reply, at: now }].slice(-MAX_MESSAGES_PER_TOPIC),
|
|
385
|
+
updatedAt: now,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
function normalizeMessage(text) {
|
|
391
|
+
return text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
392
|
+
}
|
|
393
|
+
async function loadActiveTopic(client, workspaceRoot, taskHint) {
|
|
394
|
+
if (taskHint?.trim()) {
|
|
395
|
+
const root = await loadRoot(client, workbenchRootIdFor(taskHint, workspaceRoot));
|
|
396
|
+
if (root) {
|
|
397
|
+
return loadTopic(client, root.activeTopicId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const roots = (await collectWorkbenchNodes(client)).filter(isWorkbenchRootNode);
|
|
401
|
+
if (roots.length === 0)
|
|
402
|
+
return undefined;
|
|
403
|
+
roots.sort((a, b) => {
|
|
404
|
+
const ar = deserializeRoot(a)?.updatedAt ?? 0;
|
|
405
|
+
const br = deserializeRoot(b)?.updatedAt ?? 0;
|
|
406
|
+
return br - ar;
|
|
407
|
+
});
|
|
408
|
+
const root = deserializeRoot(roots[0]);
|
|
409
|
+
if (!root)
|
|
410
|
+
return undefined;
|
|
411
|
+
return loadTopic(client, root.activeTopicId);
|
|
412
|
+
}
|
|
413
|
+
async function loadWorkbenchContext(client, topicId) {
|
|
414
|
+
const active = await loadTopic(client, topicId);
|
|
415
|
+
if (!active)
|
|
416
|
+
return undefined;
|
|
417
|
+
const root = await loadRoot(client, active.rootId);
|
|
418
|
+
if (!root)
|
|
419
|
+
return undefined;
|
|
420
|
+
const ancestors = await loadAncestors(client, active);
|
|
421
|
+
const promptLines = formatWorkbenchPrompt({
|
|
422
|
+
rootId: root.id,
|
|
423
|
+
task: root.task,
|
|
424
|
+
active,
|
|
425
|
+
ancestors,
|
|
426
|
+
isolated: active.isolated,
|
|
427
|
+
promptLines: [],
|
|
428
|
+
});
|
|
429
|
+
const outline = (await loadWorkbenchOutlines(client)).find((item) => item.rootId === root.id);
|
|
430
|
+
return {
|
|
431
|
+
rootId: root.id,
|
|
432
|
+
task: root.task,
|
|
433
|
+
active,
|
|
434
|
+
ancestors,
|
|
435
|
+
isolated: active.isolated,
|
|
436
|
+
promptLines,
|
|
437
|
+
...(outline ? { outline } : {}),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function formatWorkbenchPrompt(view) {
|
|
441
|
+
const lines = [
|
|
442
|
+
`Workbench: ${clip(view.task, 80)}`,
|
|
443
|
+
`Active: ${view.active.isolated ? "旁支" : "主线"} 「${view.active.title}」 (${view.active.id})`,
|
|
444
|
+
];
|
|
445
|
+
if (view.ancestors.length > 0) {
|
|
446
|
+
lines.push(`Path: ${view.ancestors.map((item) => item.title).join(" → ")} → ${view.active.title}`);
|
|
447
|
+
}
|
|
448
|
+
if (view.isolated) {
|
|
449
|
+
lines.push(`Isolated: 当前不在主线。点回主线节点并传入 topicId 可恢复主干上下文。`);
|
|
450
|
+
}
|
|
451
|
+
const recent = view.active.messages.slice(-6);
|
|
452
|
+
for (const message of recent) {
|
|
453
|
+
lines.push(`${message.role === "user" ? "Q" : "A"}: ${clip(message.content, 100)}`);
|
|
454
|
+
}
|
|
455
|
+
lines.push(`Resume: graphflow_context({ topicId: "${view.active.id}" })`);
|
|
456
|
+
return lines;
|
|
457
|
+
}
|
|
458
|
+
function scoreOverlap(previousTokens, query) {
|
|
459
|
+
const next = tokenize(query);
|
|
460
|
+
if (previousTokens.length === 0 || next.length === 0)
|
|
461
|
+
return 1;
|
|
462
|
+
const prev = new Set(previousTokens);
|
|
463
|
+
let hit = 0;
|
|
464
|
+
for (const token of next) {
|
|
465
|
+
if (prev.has(token))
|
|
466
|
+
hit += 1;
|
|
467
|
+
}
|
|
468
|
+
return hit / next.length;
|
|
469
|
+
}
|
|
470
|
+
function topicTokens(topic) {
|
|
471
|
+
const blob = [topic.title, topic.description, ...topic.messages.map((message) => message.content)].join(" ");
|
|
472
|
+
return tokenize(blob);
|
|
473
|
+
}
|
|
474
|
+
async function loadAncestors(client, topic) {
|
|
475
|
+
if (typeof client.getNeighbors !== "function")
|
|
476
|
+
return [];
|
|
477
|
+
const chain = [];
|
|
478
|
+
const seen = new Set([topic.id]);
|
|
479
|
+
let frontier = [topic.id];
|
|
480
|
+
for (let depth = 0; depth < 6 && frontier.length > 0; depth += 1) {
|
|
481
|
+
const neighbors = await client.getNeighbors(frontier, ["depends_on"], "out");
|
|
482
|
+
const next = [];
|
|
483
|
+
for (const { node } of neighbors) {
|
|
484
|
+
if (seen.has(node.id))
|
|
485
|
+
continue;
|
|
486
|
+
const rec = parseWorkbenchTopic(node);
|
|
487
|
+
if (!rec)
|
|
488
|
+
continue;
|
|
489
|
+
seen.add(node.id);
|
|
490
|
+
chain.push({ id: rec.id, title: rec.title, description: rec.description });
|
|
491
|
+
next.push(rec.id);
|
|
492
|
+
}
|
|
493
|
+
frontier = next;
|
|
494
|
+
}
|
|
495
|
+
return chain.reverse();
|
|
496
|
+
}
|
|
497
|
+
async function persistTopic(client, topic) {
|
|
498
|
+
const prefix = topic.isolated ? "旁支" : "主线";
|
|
499
|
+
await client.upsertNodes([
|
|
500
|
+
{
|
|
501
|
+
id: topic.id,
|
|
502
|
+
type: "Decision",
|
|
503
|
+
content: `workbench-topic ${prefix} ${topic.title} # ${clip(topic.description, 120)}`,
|
|
504
|
+
metadata: {
|
|
505
|
+
kind: exports.WORKBENCH_TOPIC_KIND,
|
|
506
|
+
record: JSON.stringify(topic),
|
|
507
|
+
mainline: topic.mainline,
|
|
508
|
+
isolated: topic.isolated,
|
|
509
|
+
title: topic.title,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
async function persistRoot(client, root) {
|
|
515
|
+
await client.upsertNodes([
|
|
516
|
+
{
|
|
517
|
+
id: root.id,
|
|
518
|
+
type: "Decision",
|
|
519
|
+
content: `workbench-root ${clip(root.task, 160)} active=${root.activeTopicId}`,
|
|
520
|
+
metadata: {
|
|
521
|
+
kind: exports.WORKBENCH_ROOT_KIND,
|
|
522
|
+
record: JSON.stringify(root),
|
|
523
|
+
activeTopicId: root.activeTopicId,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
}
|
|
528
|
+
async function loadTopic(client, topicId) {
|
|
529
|
+
const node = await loadNode(client, topicId);
|
|
530
|
+
return node ? parseWorkbenchTopic(node) : undefined;
|
|
531
|
+
}
|
|
532
|
+
async function loadRoot(client, rootId) {
|
|
533
|
+
const node = await loadNode(client, rootId);
|
|
534
|
+
return node && isWorkbenchRootNode(node) ? deserializeRoot(node) : undefined;
|
|
535
|
+
}
|
|
536
|
+
async function loadNode(client, id) {
|
|
537
|
+
if (typeof client.getNodesByIds === "function") {
|
|
538
|
+
const nodes = await client.getNodesByIds([id]);
|
|
539
|
+
return nodes.find((node) => node.id === id);
|
|
540
|
+
}
|
|
541
|
+
return (await collectWorkbenchNodes(client)).find((node) => node.id === id);
|
|
542
|
+
}
|
|
543
|
+
async function collectWorkbenchNodes(client) {
|
|
544
|
+
const snapshot = client.readSnapshot?.();
|
|
545
|
+
if (snapshot) {
|
|
546
|
+
return snapshot.nodes.filter((node) => isWorkbenchTopicNode(node) || isWorkbenchRootNode(node));
|
|
547
|
+
}
|
|
548
|
+
const byId = new Map();
|
|
549
|
+
for (const query of ["workbench-topic", "workbench-root", exports.WORKBENCH_TOPIC_PREFIX]) {
|
|
550
|
+
for (const node of await client.queryByKeyword(query)) {
|
|
551
|
+
if (isWorkbenchTopicNode(node) || isWorkbenchRootNode(node)) {
|
|
552
|
+
byId.set(node.id, node);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return Array.from(byId.values());
|
|
557
|
+
}
|
|
558
|
+
async function upsertUniqueEdges(client, edges) {
|
|
559
|
+
if (edges.length === 0)
|
|
560
|
+
return;
|
|
561
|
+
const snapshot = client.readSnapshot?.();
|
|
562
|
+
const existing = new Set((snapshot?.edges ?? []).map((edge) => `${edge.from}|${edge.relation}|${edge.to}`));
|
|
563
|
+
const fresh = edges.filter((edge) => !existing.has(`${edge.from}|${edge.relation}|${edge.to}`));
|
|
564
|
+
if (fresh.length === 0)
|
|
565
|
+
return;
|
|
566
|
+
await client.upsertEdges(fresh);
|
|
567
|
+
}
|
|
568
|
+
function deserializeTopic(node) {
|
|
569
|
+
const raw = typeof node.metadata?.record === "string" ? node.metadata.record : undefined;
|
|
570
|
+
if (!raw)
|
|
571
|
+
return undefined;
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(raw);
|
|
574
|
+
if (typeof parsed.id !== "string" || typeof parsed.title !== "string")
|
|
575
|
+
return undefined;
|
|
576
|
+
return {
|
|
577
|
+
id: parsed.id,
|
|
578
|
+
rootId: typeof parsed.rootId === "string" ? parsed.rootId : "",
|
|
579
|
+
title: parsed.title,
|
|
580
|
+
description: typeof parsed.description === "string" ? parsed.description : "",
|
|
581
|
+
mainline: parsed.mainline !== false,
|
|
582
|
+
isolated: parsed.isolated === true,
|
|
583
|
+
...(typeof parsed.planStepId === "string" ? { planStepId: parsed.planStepId } : {}),
|
|
584
|
+
messages: Array.isArray(parsed.messages)
|
|
585
|
+
? parsed.messages
|
|
586
|
+
.filter((item) => {
|
|
587
|
+
return Boolean(item &&
|
|
588
|
+
(item.role === "user" || item.role === "assistant") &&
|
|
589
|
+
typeof item.content === "string");
|
|
590
|
+
})
|
|
591
|
+
.map((item) => ({ role: item.role, content: item.content, at: item.at ?? 0 }))
|
|
592
|
+
: [],
|
|
593
|
+
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : 0,
|
|
594
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function deserializeRoot(node) {
|
|
602
|
+
const raw = typeof node.metadata?.record === "string" ? node.metadata.record : undefined;
|
|
603
|
+
if (!raw)
|
|
604
|
+
return undefined;
|
|
605
|
+
try {
|
|
606
|
+
const parsed = JSON.parse(raw);
|
|
607
|
+
if (typeof parsed.id !== "string" || typeof parsed.task !== "string")
|
|
608
|
+
return undefined;
|
|
609
|
+
return {
|
|
610
|
+
id: parsed.id,
|
|
611
|
+
task: parsed.task,
|
|
612
|
+
activeTopicId: typeof parsed.activeTopicId === "string" ? parsed.activeTopicId : "",
|
|
613
|
+
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : 0,
|
|
614
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function titleFromDescription(description) {
|
|
622
|
+
const cleaned = description.replace(/^\s*(分析与设计|实现|测试设计|验证)\s*[::]\s*/u, "").trim();
|
|
623
|
+
return clip(cleaned || description, MAX_TITLE);
|
|
624
|
+
}
|
|
625
|
+
function normalizeTask(task) {
|
|
626
|
+
return task.trim().toLowerCase();
|
|
627
|
+
}
|
|
628
|
+
function clip(text, max) {
|
|
629
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
630
|
+
if (normalized.length <= max)
|
|
631
|
+
return normalized;
|
|
632
|
+
return `${normalized.slice(0, max - 1)}…`;
|
|
633
|
+
}
|
|
634
|
+
function tokenize(text) {
|
|
635
|
+
const out = new Set();
|
|
636
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9_\u4e00-\u9fff]+/)) {
|
|
637
|
+
if (raw.length >= 2)
|
|
638
|
+
out.add(raw);
|
|
639
|
+
}
|
|
640
|
+
return Array.from(out);
|
|
641
|
+
}
|
|
642
|
+
//# sourceMappingURL=workbench-topic.js.map
|