@hadooppei/hwcode 1.0.8 → 1.0.9
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/.pi/extensions/knowledge.ts +137 -192
- package/.pi/lib/knowledge/review-worker.ts +238 -42
- package/.pi/lib/knowledge/session-scanner.ts +155 -0
- package/.pi/lib/knowledge/store.ts +237 -123
- package/.pi/lib/knowledge/types.ts +37 -5
- package/.pi/lib/knowledge/worker-protocol.ts +15 -7
- package/.pi/lib/runtime/defaults.ts +5 -2
- package/.pi/lib/runtime/paths.ts +16 -11
- package/README.md +19 -16
- package/package.json +1 -1
|
@@ -1,212 +1,148 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import { Worker } from "node:worker_threads";
|
|
3
4
|
|
|
4
5
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
6
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
|
-
buildKnowledgeExtractionPrompt,
|
|
9
|
+
buildKnowledgeExtractionPrompt, KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
9
10
|
} from "../lib/knowledge/extractor.ts";
|
|
10
11
|
import { matchKnowledge } from "../lib/knowledge/matcher.ts";
|
|
11
|
-
import {
|
|
12
|
-
loadKnowledgeById, loadKnowledgeSnapshot, projectKnowledgeKey,
|
|
13
|
-
} from "../lib/knowledge/store.ts";
|
|
14
|
-
import type { KnowledgeReviewState, KnowledgeSnapshot } from "../lib/knowledge/types.ts";
|
|
12
|
+
import { loadKnowledgeById, loadKnowledgeSnapshot, projectKnowledgeKey } from "../lib/knowledge/store.ts";
|
|
15
13
|
import type { KnowledgeWorkerInput, KnowledgeWorkerOutput } from "../lib/knowledge/worker-protocol.ts";
|
|
16
14
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../lib/runtime/defaults.ts";
|
|
17
15
|
import { getWorkingDirectory } from "../lib/working-directory.ts";
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
interface PendingReview {
|
|
17
|
+
interface ActiveReview {
|
|
22
18
|
requestId: string;
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
leaderToken: string;
|
|
20
|
+
controller: AbortController;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}).filter(Boolean).join("\n");
|
|
23
|
+
interface ProcessKnowledgeRuntime {
|
|
24
|
+
worker?: Worker;
|
|
25
|
+
context?: ExtensionContext;
|
|
26
|
+
sessionsRoot?: string;
|
|
27
|
+
modelAvailable?: boolean;
|
|
28
|
+
activeReview?: ActiveReview;
|
|
29
|
+
capabilityTimer?: NodeJS.Timeout;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (data.type === "compaction" || data.type === "branch_summary") {
|
|
41
|
-
return typeof data.summary === "string" ? `[summary]\n${data.summary}` : "";
|
|
42
|
-
}
|
|
43
|
-
if (data.type !== "message" || !data.message || typeof data.message !== "object") return "";
|
|
44
|
-
const message = data.message as Record<string, unknown>;
|
|
45
|
-
const role = typeof message.role === "string" ? message.role : "message";
|
|
46
|
-
const content = textContent(message.content);
|
|
47
|
-
return content ? `[${role}]\n${content}` : "";
|
|
48
|
-
}
|
|
32
|
+
const RUNTIME_SYMBOL = Symbol.for("hwcode.knowledge.runtime.v3");
|
|
33
|
+
const processGlobals = globalThis as unknown as Record<PropertyKey, unknown>;
|
|
34
|
+
const runtime = (processGlobals[RUNTIME_SYMBOL] ??= {}) as ProcessKnowledgeRuntime;
|
|
49
35
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
const id = (entry as Record<string, unknown>).id;
|
|
53
|
-
return typeof id === "string" ? id : undefined;
|
|
36
|
+
function post(message: KnowledgeWorkerInput): void {
|
|
37
|
+
runtime.worker?.postMessage(message);
|
|
54
38
|
}
|
|
55
39
|
|
|
56
|
-
function
|
|
57
|
-
return ctx.
|
|
40
|
+
function modelAvailable(ctx: ExtensionContext | undefined): boolean {
|
|
41
|
+
return Boolean(ctx?.model && ctx.modelRegistry.hasConfiguredAuth(ctx.model));
|
|
58
42
|
}
|
|
59
43
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
44
|
+
function updateCapability(): void {
|
|
45
|
+
if (!runtime.worker && runtime.context) ensureWorker();
|
|
46
|
+
if (!runtime.worker || !runtime.sessionsRoot) return;
|
|
47
|
+
const available = modelAvailable(runtime.context);
|
|
48
|
+
if (runtime.modelAvailable === available) return;
|
|
49
|
+
runtime.modelAvailable = available;
|
|
50
|
+
post({ type: "configure", modelAvailable: available, sessionsRoot: runtime.sessionsRoot });
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
const lastIndex = lastReviewedEntryId ? entries.findIndex((entry) => entryId(entry) === lastReviewedEntryId) : -1;
|
|
68
|
-
const pending = entries.slice(lastIndex + 1);
|
|
69
|
-
const lastEntryId = pending.length > 0 ? entryId(pending[pending.length - 1]) : undefined;
|
|
70
|
-
const parts: string[] = [];
|
|
71
|
-
let chars = 0;
|
|
72
|
-
for (const entry of pending.slice().reverse()) {
|
|
73
|
-
const part = entryMessageText(entry);
|
|
74
|
-
if (!part) continue;
|
|
75
|
-
const remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars - chars;
|
|
76
|
-
if (remaining <= 0) break;
|
|
77
|
-
const clipped = part.slice(0, remaining);
|
|
78
|
-
parts.unshift(clipped);
|
|
79
|
-
chars += clipped.length;
|
|
80
|
-
if (clipped.length < part.length) break;
|
|
81
|
-
}
|
|
82
|
-
return { text: parts.join("\n\n"), lastEntryId };
|
|
53
|
+
function cancelActiveReview(): void {
|
|
54
|
+
runtime.activeReview?.controller.abort();
|
|
83
55
|
}
|
|
84
56
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
57
|
+
async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review_request" }>): Promise<void> {
|
|
58
|
+
const ctx = runtime.context;
|
|
59
|
+
if (!ctx?.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) {
|
|
60
|
+
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "no-configured-model" });
|
|
61
|
+
updateCapability();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
65
|
+
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: "model-executor-is-busy" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
runtime.activeReview = { requestId: message.requestId, leaderToken: message.leaderToken, controller };
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
|
|
71
|
+
timeout.unref();
|
|
72
|
+
try {
|
|
73
|
+
const response = await ctx.modelRegistry.complete(
|
|
74
|
+
ctx.model,
|
|
75
|
+
{
|
|
76
|
+
systemPrompt: KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
77
|
+
messages: [{
|
|
78
|
+
role: "user",
|
|
79
|
+
content: [{ type: "text", text: buildKnowledgeExtractionPrompt(message.task.projectRoot, message.task.delta) }],
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
}],
|
|
82
|
+
},
|
|
83
|
+
{ signal: controller.signal, reasoningEffort: "low", cacheRetention: "none", sessionId: randomUUID() },
|
|
84
|
+
);
|
|
85
|
+
if (controller.signal.aborted) throw new Error("knowledge-review-aborted-or-timed-out");
|
|
86
|
+
const raw = response.content.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
87
|
+
.map((item) => item.text).join("\n");
|
|
88
|
+
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, raw });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
post({
|
|
91
|
+
type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
if (runtime.activeReview?.requestId === message.requestId) runtime.activeReview = undefined;
|
|
91
97
|
}
|
|
92
|
-
return { version: 1 };
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let activeProjectKey = "";
|
|
100
|
-
let snapshot: KnowledgeSnapshot = { rulesPrompt: "", memoryPrompt: "", catalog: { version: 2, updatedAt: "", items: [] } };
|
|
101
|
-
let reviewState: KnowledgeReviewState = { version: 1 };
|
|
102
|
-
let pendingReview: PendingReview | undefined;
|
|
103
|
-
let reviewController: AbortController | undefined;
|
|
104
|
-
|
|
105
|
-
const post = (message: KnowledgeWorkerInput) => worker?.postMessage(message);
|
|
106
|
-
|
|
107
|
-
function cancelReview(reason: string): void {
|
|
108
|
-
reviewController?.abort();
|
|
109
|
-
reviewController = undefined;
|
|
110
|
-
if (pendingReview) {
|
|
111
|
-
post({ type: "review_result", requestId: pendingReview.requestId, error: reason });
|
|
112
|
-
pendingReview = undefined;
|
|
113
|
-
}
|
|
100
|
+
function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
|
|
101
|
+
if (message.type === "review_request") { void runReview(message); return; }
|
|
102
|
+
if (message.type === "review_cancel" && runtime.activeReview?.requestId === message.requestId) {
|
|
103
|
+
runtime.activeReview.controller.abort();
|
|
114
104
|
}
|
|
105
|
+
}
|
|
115
106
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
107
|
+
function ensureWorker(): Worker {
|
|
108
|
+
if (runtime.worker) return runtime.worker;
|
|
109
|
+
const worker = new Worker(new URL("../lib/knowledge/review-worker.ts", import.meta.url));
|
|
110
|
+
worker.unref();
|
|
111
|
+
runtime.worker = worker;
|
|
112
|
+
worker.on("message", (message: KnowledgeWorkerOutput) => handleWorkerMessage(message));
|
|
113
|
+
worker.on("error", () => {
|
|
114
|
+
cancelActiveReview();
|
|
115
|
+
if (runtime.worker === worker) {
|
|
116
|
+
runtime.worker = undefined;
|
|
117
|
+
runtime.modelAvailable = undefined;
|
|
126
118
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const controller = new AbortController();
|
|
134
|
-
reviewController = controller;
|
|
135
|
-
try {
|
|
136
|
-
const response = await ctx.modelRegistry.complete(
|
|
137
|
-
ctx.model,
|
|
138
|
-
{
|
|
139
|
-
systemPrompt: KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
140
|
-
messages: [{
|
|
141
|
-
role: "user",
|
|
142
|
-
content: [{ type: "text", text: buildKnowledgeExtractionPrompt(activeProjectRoot, delta.text) }],
|
|
143
|
-
timestamp: Date.now(),
|
|
144
|
-
}],
|
|
145
|
-
},
|
|
146
|
-
{ signal: controller.signal, reasoningEffort: "low", cacheRetention: "none", sessionId: randomUUID() },
|
|
147
|
-
);
|
|
148
|
-
if (controller.signal.aborted) return;
|
|
149
|
-
const raw = response.content.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
150
|
-
.map((item) => item.text).join("\n");
|
|
151
|
-
post({ type: "review_result", requestId, raw });
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (!controller.signal.aborted) {
|
|
154
|
-
post({ type: "review_result", requestId, error: error instanceof Error ? error.message : String(error) });
|
|
155
|
-
pendingReview = undefined;
|
|
156
|
-
}
|
|
157
|
-
} finally {
|
|
158
|
-
if (reviewController === controller) reviewController = undefined;
|
|
119
|
+
});
|
|
120
|
+
worker.on("exit", () => {
|
|
121
|
+
if (runtime.worker === worker) {
|
|
122
|
+
runtime.worker = undefined;
|
|
123
|
+
runtime.modelAvailable = undefined;
|
|
159
124
|
}
|
|
160
|
-
}
|
|
125
|
+
});
|
|
126
|
+
runtime.capabilityTimer ??= setInterval(updateCapability, KNOWLEDGE_RUNTIME_DEFAULTS.review.capabilityPollMs);
|
|
127
|
+
runtime.capabilityTimer.unref();
|
|
128
|
+
return worker;
|
|
129
|
+
}
|
|
161
130
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
version: 1,
|
|
171
|
-
lastReviewedEntryId: pendingReview.lastEntryId,
|
|
172
|
-
lastReviewedAt: new Date().toISOString(),
|
|
173
|
-
lastDeltaDigest: pendingReview.deltaDigest,
|
|
174
|
-
lastResult: result,
|
|
175
|
-
};
|
|
176
|
-
pi.appendEntry<KnowledgeReviewState>(KNOWLEDGE_REVIEW_STATE_TYPE, reviewState);
|
|
177
|
-
snapshot = loadKnowledgeSnapshot(activeProjectKey);
|
|
178
|
-
pendingReview = undefined;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (message.type === "review_failed" && pendingReview?.requestId === message.requestId) {
|
|
182
|
-
pendingReview = undefined;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
131
|
+
function configureForContext(ctx: ExtensionContext): void {
|
|
132
|
+
runtime.context = ctx;
|
|
133
|
+
runtime.sessionsRoot = dirname(ctx.sessionManager.getSessionDir());
|
|
134
|
+
ensureWorker();
|
|
135
|
+
const available = modelAvailable(ctx);
|
|
136
|
+
runtime.modelAvailable = available;
|
|
137
|
+
post({ type: "configure", modelAvailable: available, sessionsRoot: runtime.sessionsRoot });
|
|
138
|
+
}
|
|
185
139
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
void worker.terminate();
|
|
191
|
-
}
|
|
192
|
-
activeContext = ctx;
|
|
193
|
-
activeProjectRoot = getWorkingDirectory(ctx.sessionManager);
|
|
194
|
-
activeProjectKey = projectKnowledgeKey(activeProjectRoot);
|
|
195
|
-
reviewState = loadReviewState(ctx);
|
|
196
|
-
snapshot = loadKnowledgeSnapshot(activeProjectKey);
|
|
197
|
-
const startedWorker = new Worker(new URL("../lib/knowledge/review-worker.ts", import.meta.url));
|
|
198
|
-
worker = startedWorker;
|
|
199
|
-
startedWorker.on("message", (message: KnowledgeWorkerOutput) => handleWorkerMessage(message));
|
|
200
|
-
startedWorker.on("error", () => { if (worker === startedWorker) worker = undefined; });
|
|
201
|
-
const latest = lastReviewableEntryId(ctx);
|
|
202
|
-
post({
|
|
203
|
-
type: "configure",
|
|
204
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
205
|
-
projectKey: activeProjectKey,
|
|
206
|
-
dirty: Boolean(latest && latest !== reviewState.lastReviewedEntryId),
|
|
207
|
-
});
|
|
208
|
-
}
|
|
140
|
+
function currentProject(ctx: ExtensionContext): { root: string; key: string } {
|
|
141
|
+
const root = getWorkingDirectory(ctx.sessionManager);
|
|
142
|
+
return { root, key: projectKnowledgeKey(root) };
|
|
143
|
+
}
|
|
209
144
|
|
|
145
|
+
export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
|
|
210
146
|
pi.registerTool(defineTool({
|
|
211
147
|
name: "hwcode_knowledge_lookup",
|
|
212
148
|
label: "HWCode Knowledge Lookup",
|
|
@@ -218,14 +154,18 @@ export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
|
|
|
218
154
|
}),
|
|
219
155
|
executionMode: "sequential",
|
|
220
156
|
async execute(_id, params) {
|
|
157
|
+
const ctx = runtime.context;
|
|
158
|
+
if (!ctx) return { content: [{ type: "text", text: "Knowledge context is not available." }], isError: true, details: {} };
|
|
159
|
+
const { key } = currentProject(ctx);
|
|
221
160
|
if (params.id) {
|
|
222
|
-
const found = loadKnowledgeById(params.id,
|
|
161
|
+
const found = loadKnowledgeById(params.id, key);
|
|
223
162
|
if (!found) return { content: [{ type: "text", text: `No applicable knowledge found with ID: ${params.id}` }], isError: true, details: {} };
|
|
224
163
|
return { content: [{ type: "text", text: found.content }], details: {} };
|
|
225
164
|
}
|
|
226
165
|
if (params.query) {
|
|
166
|
+
const snapshot = loadKnowledgeSnapshot(key);
|
|
227
167
|
const matches = matchKnowledge(params.query, snapshot.catalog)
|
|
228
|
-
.filter((item) => item.scope === "global" || item.scope === `project:${
|
|
168
|
+
.filter((item) => item.scope === "global" || item.scope === `project:${key}`);
|
|
229
169
|
if (matches.length === 0) return { content: [{ type: "text", text: `No relevant knowledge found for: ${params.query}` }], details: {} };
|
|
230
170
|
return {
|
|
231
171
|
content: [{ type: "text", text: matches.map((item) => `- [${item.id}] ${item.title}: ${item.summary}`).join("\n") }],
|
|
@@ -236,10 +176,18 @@ export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
|
|
|
236
176
|
},
|
|
237
177
|
}));
|
|
238
178
|
|
|
239
|
-
pi.on("session_start", (_event, ctx) =>
|
|
179
|
+
pi.on("session_start", (_event, ctx) => configureForContext(ctx));
|
|
180
|
+
|
|
181
|
+
pi.on("model_select", (_event, ctx) => {
|
|
182
|
+
runtime.context = ctx;
|
|
183
|
+
updateCapability();
|
|
184
|
+
});
|
|
240
185
|
|
|
241
186
|
pi.on("before_agent_start", (event, ctx) => {
|
|
242
|
-
|
|
187
|
+
runtime.context = ctx;
|
|
188
|
+
updateCapability();
|
|
189
|
+
const { key } = currentProject(ctx);
|
|
190
|
+
const snapshot = loadKnowledgeSnapshot(key);
|
|
243
191
|
const sections = [
|
|
244
192
|
snapshot.rulesPrompt ? `<hwcode_rules>\n${snapshot.rulesPrompt}\n</hwcode_rules>` : "",
|
|
245
193
|
snapshot.memoryPrompt ? `<hwcode_knowledge_index>\n${snapshot.memoryPrompt}\n</hwcode_knowledge_index>` : "",
|
|
@@ -249,31 +197,28 @@ export default function registerKnowledgeExtension(pi: ExtensionAPI): void {
|
|
|
249
197
|
});
|
|
250
198
|
|
|
251
199
|
pi.on("input", (_event, ctx) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
200
|
+
runtime.context = ctx;
|
|
201
|
+
cancelActiveReview();
|
|
202
|
+
updateCapability();
|
|
255
203
|
return undefined;
|
|
256
204
|
});
|
|
257
205
|
|
|
258
206
|
pi.on("agent_start", (_event, ctx) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
post({ type: "activity", state: "busy", dirty: true });
|
|
207
|
+
runtime.context = ctx;
|
|
208
|
+
cancelActiveReview();
|
|
262
209
|
});
|
|
263
210
|
|
|
264
211
|
pi.on("agent_settled", (_event, ctx) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
post({ type: "
|
|
212
|
+
runtime.context = ctx;
|
|
213
|
+
updateCapability();
|
|
214
|
+
post({ type: "scan_now" });
|
|
268
215
|
});
|
|
269
216
|
|
|
270
|
-
pi.on("session_shutdown",
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
217
|
+
pi.on("session_shutdown", (event) => {
|
|
218
|
+
cancelActiveReview();
|
|
219
|
+
if (event.reason === "quit") {
|
|
220
|
+
runtime.context = undefined;
|
|
221
|
+
updateCapability();
|
|
275
222
|
}
|
|
276
|
-
worker = undefined;
|
|
277
|
-
activeContext = undefined;
|
|
278
223
|
});
|
|
279
224
|
}
|