@hadooppei/hwcode 1.0.11 → 1.0.12
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/dist/lib/knowledge/extractor.js +17 -6
- package/.pi/dist/lib/knowledge/session-scanner.js +46 -9
- package/.pi/dist/lib/knowledge/store.js +78 -7
- package/.pi/dist/lib/runtime/defaults.js +8 -4
- package/.pi/extensions/knowledge.ts +28 -2
- package/.pi/lib/knowledge/extractor.ts +31 -6
- package/.pi/lib/knowledge/session-scanner.ts +43 -9
- package/.pi/lib/knowledge/store.ts +73 -7
- package/.pi/lib/knowledge/types.ts +2 -0
- package/.pi/lib/runtime/defaults.ts +8 -4
- package/package.json +1 -1
|
@@ -8,14 +8,25 @@ Persist only knowledge that is likely to be useful in future sessions:
|
|
|
8
8
|
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
9
9
|
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
10
10
|
Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
|
|
11
|
-
|
|
12
|
-
Use
|
|
11
|
+
Treat failed, unsupported, invalid, or corrected operations as negative evidence. Never present an operation as valid when the supplied evidence says it failed; use only a verified alternative or record an explicit warning. Never invent commands or parameters absent from successful evidence.
|
|
12
|
+
Use storageHint "rule" only for one short imperative instruction of at most 240 characters. A rule must not contain incident narration, example resource names, IDs, timestamps, or evidence details. Use "topic" for multi-step SOPs and detailed experience.
|
|
13
|
+
Relevant existing knowledge may be supplied. If a candidate has the same intent as an existing item, do not add a translated, renamed, or reformatted duplicate. Set targetId to that exact existing ID and action to "reinforce" when the existing content remains correct, or "revise" only when the delta explicitly proves a correction. Use targetId null only for genuinely new knowledge.
|
|
13
14
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
The prior validation context is only for checking facts and contradictions; do not persist it again unless the new delta independently reinforces it. Prefer accumulated workflow successfulSteps and failedApproaches over assistant narration.
|
|
16
|
+
Each summary must be one complete sentence of at most 160 characters. Keep each topic body under 2200 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered. Return at most 3 candidates.
|
|
17
|
+
Return: {"candidates":[{"key":"stable semantic key","targetId":"existing-id or null","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
|
|
16
18
|
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
17
|
-
export function buildKnowledgeExtractionPrompt(projectRoot, delta) {
|
|
18
|
-
|
|
19
|
+
export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "", existing = []) {
|
|
20
|
+
const related = existing.length > 0 ? JSON.stringify(existing) : "[]";
|
|
21
|
+
return [
|
|
22
|
+
`Project root: ${projectRoot}`,
|
|
23
|
+
"",
|
|
24
|
+
"<relevant_existing_knowledge>", related, "</relevant_existing_knowledge>",
|
|
25
|
+
"",
|
|
26
|
+
"<prior_validation_context>", context, "</prior_validation_context>",
|
|
27
|
+
"",
|
|
28
|
+
"<conversation_delta>", delta, "</conversation_delta>",
|
|
29
|
+
].join("\n");
|
|
19
30
|
}
|
|
20
31
|
export function parseCandidateEnvelope(text) {
|
|
21
32
|
const trimmed = text.trim();
|
|
@@ -6,6 +6,8 @@ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
|
6
6
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
7
7
|
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
8
|
import { projectKnowledgeKey } from "./store.js";
|
|
9
|
+
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
10
|
+
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
9
11
|
function hash(value, length = 64) {
|
|
10
12
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
11
13
|
}
|
|
@@ -23,11 +25,11 @@ function textContent(content) {
|
|
|
23
25
|
}
|
|
24
26
|
function reviewPiece(entry, index) {
|
|
25
27
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
26
|
-
return { index, priority:
|
|
28
|
+
return { index, priority: 2, category: "summary", text: `[summary]\n${sanitizeKnowledgeText(entry.summary).slice(0, 6_000)}` };
|
|
27
29
|
}
|
|
28
30
|
if (entry.type === "custom" && entry.customType.startsWith("hwcode-workflow")) {
|
|
29
31
|
return {
|
|
30
|
-
index, priority:
|
|
32
|
+
index, priority: 1, category: "workflow",
|
|
31
33
|
text: `[workflow_state:${entry.customType}]\n${sanitizeKnowledgeText(JSON.stringify(entry.data ?? {})).slice(0, 6_000)}`,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -35,10 +37,11 @@ function reviewPiece(entry, index) {
|
|
|
35
37
|
return undefined;
|
|
36
38
|
const message = entry.message;
|
|
37
39
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
38
|
-
const content = sanitizeKnowledgeText(textContent(message.content));
|
|
40
|
+
const content = sanitizeKnowledgeText(textContent(message.content).replace(INJECTED_SKILL_RE, "[loaded skill omitted]"));
|
|
39
41
|
if (!content)
|
|
40
42
|
return undefined;
|
|
41
|
-
const priority = role === "user" ? 0 : role === "toolResult"
|
|
43
|
+
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
44
|
+
: role === "toolResult" ? 4 : 3;
|
|
42
45
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
43
46
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
44
47
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
@@ -50,16 +53,49 @@ function buildDelta(entries) {
|
|
|
50
53
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
51
54
|
};
|
|
52
55
|
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars;
|
|
53
|
-
for (const piece of pieces.slice().sort((left, right) =>
|
|
56
|
+
for (const piece of pieces.slice().sort((left, right) => {
|
|
57
|
+
if (left.priority !== right.priority)
|
|
58
|
+
return left.priority - right.priority;
|
|
59
|
+
if (left.category === right.category && left.category !== "user")
|
|
60
|
+
return right.index - left.index;
|
|
61
|
+
return left.index - right.index;
|
|
62
|
+
})) {
|
|
54
63
|
if (remaining <= 0)
|
|
55
64
|
break;
|
|
56
|
-
const
|
|
65
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
66
|
+
if (remaining <= separatorCharacters)
|
|
67
|
+
break;
|
|
68
|
+
const text = piece.text.slice(0, Math.min(remaining - separatorCharacters, categoryRemaining[piece.category]));
|
|
57
69
|
if (text)
|
|
58
70
|
selected.push({ ...piece, text });
|
|
59
|
-
remaining -= text.length;
|
|
71
|
+
remaining -= text.length + separatorCharacters;
|
|
60
72
|
categoryRemaining[piece.category] -= text.length;
|
|
61
73
|
}
|
|
62
|
-
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
74
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
75
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
76
|
+
}
|
|
77
|
+
function buildValidationContext(entries) {
|
|
78
|
+
const pieces = entries.map(reviewPiece).filter((piece) => Boolean(piece));
|
|
79
|
+
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
80
|
+
const evidence = pieces.filter((piece) => ((piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
81
|
+
&& FAILURE_EVIDENCE_RE.test(piece.text)));
|
|
82
|
+
if (latestWorkflow && !evidence.includes(latestWorkflow))
|
|
83
|
+
evidence.push(latestWorkflow);
|
|
84
|
+
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars;
|
|
85
|
+
const selected = [];
|
|
86
|
+
for (const piece of evidence.slice().sort((left, right) => right.index - left.index)) {
|
|
87
|
+
if (remaining <= 0)
|
|
88
|
+
break;
|
|
89
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
90
|
+
if (remaining <= separatorCharacters)
|
|
91
|
+
break;
|
|
92
|
+
const text = piece.text.slice(0, remaining - separatorCharacters);
|
|
93
|
+
if (text)
|
|
94
|
+
selected.push({ ...piece, text });
|
|
95
|
+
remaining -= text.length + separatorCharacters;
|
|
96
|
+
}
|
|
97
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
98
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars);
|
|
63
99
|
}
|
|
64
100
|
function projectRootForSession(header, branch) {
|
|
65
101
|
for (const entry of branch.slice().reverse()) {
|
|
@@ -128,6 +164,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
128
164
|
if (pending.length === 0)
|
|
129
165
|
return undefined;
|
|
130
166
|
const delta = buildDelta(pending);
|
|
167
|
+
const context = cursorIndex >= 0 ? buildValidationContext(branch.slice(0, cursorIndex + 1)) : "";
|
|
131
168
|
const firstEntryId = pending[0].id;
|
|
132
169
|
const lastEntryId = pending[pending.length - 1].id;
|
|
133
170
|
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
@@ -138,7 +175,7 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
138
175
|
return {
|
|
139
176
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
140
177
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
141
|
-
firstEntryId, lastEntryId, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
178
|
+
firstEntryId, lastEntryId, context, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
142
179
|
};
|
|
143
180
|
}
|
|
144
181
|
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
@@ -101,6 +101,57 @@ function normalizeSlug(value) {
|
|
|
101
101
|
function normalizedFingerprint(candidate) {
|
|
102
102
|
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
103
103
|
}
|
|
104
|
+
function comparisonTerms(value) {
|
|
105
|
+
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
106
|
+
.filter((term) => term.length >= 2));
|
|
107
|
+
}
|
|
108
|
+
function containment(left, right) {
|
|
109
|
+
const minimum = Math.min(left.size, right.size);
|
|
110
|
+
if (minimum === 0)
|
|
111
|
+
return 0;
|
|
112
|
+
let shared = 0;
|
|
113
|
+
for (const term of left)
|
|
114
|
+
if (right.has(term))
|
|
115
|
+
shared++;
|
|
116
|
+
return shared / minimum;
|
|
117
|
+
}
|
|
118
|
+
function semanticDuplicateScore(candidate, entry) {
|
|
119
|
+
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track)
|
|
120
|
+
return 0;
|
|
121
|
+
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
122
|
+
const keywordScore = containment(comparisonTerms(candidate.keywords.join(" ")), comparisonTerms(entry.keywords.join(" ")));
|
|
123
|
+
if (titleScore < 0.8 || keywordScore < 0.5)
|
|
124
|
+
return 0;
|
|
125
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
126
|
+
}
|
|
127
|
+
function semanticDuplicateIndex(candidate, catalog) {
|
|
128
|
+
let bestIndex = -1;
|
|
129
|
+
let bestScore = 0;
|
|
130
|
+
for (const [index, entry] of catalog.items.entries()) {
|
|
131
|
+
const score = semanticDuplicateScore(candidate, entry);
|
|
132
|
+
if (score > bestScore) {
|
|
133
|
+
bestIndex = index;
|
|
134
|
+
bestScore = score;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return bestIndex;
|
|
138
|
+
}
|
|
139
|
+
function unsupportedOperations(value) {
|
|
140
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)]
|
|
141
|
+
.map((match) => match[1].toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
function contradictsVerifiedFailures(body, validationText) {
|
|
144
|
+
const normalizedBody = body.toLowerCase();
|
|
145
|
+
for (const operation of unsupportedOperations(validationText)) {
|
|
146
|
+
const index = normalizedBody.search(new RegExp(`\\b${operation}\\b`, "u"));
|
|
147
|
+
if (index < 0)
|
|
148
|
+
continue;
|
|
149
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + operation.length + 80);
|
|
150
|
+
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context))
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
104
155
|
function contentHash(candidate) {
|
|
105
156
|
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
106
157
|
}
|
|
@@ -110,7 +161,7 @@ function asStringArray(value, maxItems) {
|
|
|
110
161
|
return [...new Set(value.filter((item) => typeof item === "string")
|
|
111
162
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
112
163
|
}
|
|
113
|
-
function normalizeCandidate(value, projectKey) {
|
|
164
|
+
function normalizeCandidate(value, projectKey, validationText) {
|
|
114
165
|
if (!value || typeof value !== "object")
|
|
115
166
|
return undefined;
|
|
116
167
|
const raw = value;
|
|
@@ -127,11 +178,16 @@ function normalizeCandidate(value, projectKey) {
|
|
|
127
178
|
const evidence = asStringArray(raw.evidence, 8);
|
|
128
179
|
if (!body || evidence.length === 0)
|
|
129
180
|
return undefined;
|
|
181
|
+
if (contradictsVerifiedFailures(body, validationText))
|
|
182
|
+
return undefined;
|
|
130
183
|
const requestedTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
131
184
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
185
|
+
&& body.split(/\n\s*\n/gu).length === 1
|
|
132
186
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
187
|
+
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
133
188
|
return {
|
|
134
|
-
key: compactKnowledgeText(raw.key, 160),
|
|
189
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
190
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
135
191
|
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
136
192
|
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
137
193
|
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
@@ -269,16 +325,30 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
269
325
|
const catalog = structuredClone(current.catalog);
|
|
270
326
|
const result = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
271
327
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
272
|
-
const candidate = normalizeCandidate(value, task.projectKey);
|
|
328
|
+
const candidate = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
|
|
273
329
|
if (!candidate) {
|
|
274
330
|
result.skipped++;
|
|
275
331
|
continue;
|
|
276
332
|
}
|
|
277
333
|
const fingerprint = normalizedFingerprint(candidate);
|
|
278
334
|
const hash = contentHash(candidate);
|
|
279
|
-
const
|
|
335
|
+
const targetIndex = candidate.targetId
|
|
336
|
+
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
337
|
+
: -1;
|
|
338
|
+
if (candidate.targetId && targetIndex < 0) {
|
|
339
|
+
result.skipped++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
let existingIndex = targetIndex >= 0
|
|
343
|
+
? targetIndex
|
|
344
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
345
|
+
let semanticReinforcement = false;
|
|
346
|
+
if (existingIndex < 0) {
|
|
347
|
+
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
348
|
+
semanticReinforcement = existingIndex >= 0;
|
|
349
|
+
}
|
|
280
350
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
281
|
-
if (existing
|
|
351
|
+
if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
|
|
282
352
|
existing.evidenceCount += candidate.evidence.length;
|
|
283
353
|
existing.updatedAt = new Date().toISOString();
|
|
284
354
|
result.updated++;
|
|
@@ -301,8 +371,9 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
301
371
|
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
302
372
|
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
303
373
|
const entry = {
|
|
304
|
-
id, fingerprint
|
|
305
|
-
|
|
374
|
+
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
375
|
+
title: candidate.title, summary: candidate.summary,
|
|
376
|
+
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
306
377
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
307
378
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
308
379
|
};
|
|
@@ -31,13 +31,17 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
31
31
|
idleMs: 60_000,
|
|
32
32
|
capabilityPollMs: 5_000,
|
|
33
33
|
modelTimeoutMs: 180_000,
|
|
34
|
-
maxDeltaChars:
|
|
35
|
-
|
|
34
|
+
maxDeltaChars: 12_000,
|
|
35
|
+
maxPriorContextChars: 4_000,
|
|
36
|
+
maxExistingContextItems: 5,
|
|
37
|
+
maxExistingBodyItems: 2,
|
|
38
|
+
maxExistingBodyChars: 600,
|
|
39
|
+
maxCandidates: 3,
|
|
36
40
|
minimumConfidence: 0.72,
|
|
37
41
|
}),
|
|
38
42
|
storage: Object.freeze({
|
|
39
|
-
maxRuleChars:
|
|
40
|
-
maxRuleFileLines:
|
|
43
|
+
maxRuleChars: 320,
|
|
44
|
+
maxRuleFileLines: 6,
|
|
41
45
|
maxRulesPromptChars: 12_000,
|
|
42
46
|
maxMemoryChars: 25_000,
|
|
43
47
|
maxMemoryLines: 200,
|
|
@@ -8,7 +8,7 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
8
8
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
buildKnowledgeExtractionPrompt, KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
11
|
+
buildKnowledgeExtractionPrompt, type ExistingKnowledgeContext, KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
12
12
|
} from "../lib/knowledge/extractor.ts";
|
|
13
13
|
import { matchKnowledge } from "../lib/knowledge/matcher.ts";
|
|
14
14
|
import { loadKnowledgeById, loadKnowledgeSnapshot, projectKnowledgeKey } from "../lib/knowledge/store.ts";
|
|
@@ -103,13 +103,39 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
103
103
|
runtime.activeReview = { requestId: message.requestId, leaderToken: message.leaderToken, controller };
|
|
104
104
|
timeout = setTimeout(() => controller.abort(), KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
|
|
105
105
|
timeout.unref();
|
|
106
|
+
const snapshot = loadKnowledgeSnapshot(message.task.projectKey);
|
|
107
|
+
const related: ExistingKnowledgeContext[] = matchKnowledge(
|
|
108
|
+
message.task.delta,
|
|
109
|
+
snapshot.catalog,
|
|
110
|
+
KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingContextItems,
|
|
111
|
+
).map((entry, index) => {
|
|
112
|
+
const reference: ExistingKnowledgeContext = {
|
|
113
|
+
id: entry.id,
|
|
114
|
+
title: entry.title,
|
|
115
|
+
summary: entry.summary,
|
|
116
|
+
keywords: entry.keywords,
|
|
117
|
+
track: entry.track,
|
|
118
|
+
};
|
|
119
|
+
if (index >= KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingBodyItems) return reference;
|
|
120
|
+
const content = loadKnowledgeById(entry.id, message.task.projectKey)?.content
|
|
121
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingBodyChars);
|
|
122
|
+
return content ? { ...reference, content } : reference;
|
|
123
|
+
});
|
|
106
124
|
const response = await ctx.modelRegistry.complete(
|
|
107
125
|
ctx.model,
|
|
108
126
|
{
|
|
109
127
|
systemPrompt: KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT,
|
|
110
128
|
messages: [{
|
|
111
129
|
role: "user",
|
|
112
|
-
content: [{
|
|
130
|
+
content: [{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: buildKnowledgeExtractionPrompt(
|
|
133
|
+
message.task.projectRoot,
|
|
134
|
+
message.task.delta,
|
|
135
|
+
message.task.context,
|
|
136
|
+
related,
|
|
137
|
+
),
|
|
138
|
+
}],
|
|
113
139
|
timestamp: Date.now(),
|
|
114
140
|
}],
|
|
115
141
|
},
|
|
@@ -2,6 +2,15 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
4
4
|
|
|
5
|
+
export interface ExistingKnowledgeContext {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
keywords: string[];
|
|
10
|
+
track: "rule" | "topic";
|
|
11
|
+
content?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export const KNOWLEDGE_EXTRACTION_SYSTEM_PROMPT = `You are HWCode's background knowledge reviewer.
|
|
6
15
|
Review only the supplied conversation delta. Return strict JSON and no markdown.
|
|
7
16
|
Persist only knowledge that is likely to be useful in future sessions:
|
|
@@ -10,15 +19,31 @@ Persist only knowledge that is likely to be useful in future sessions:
|
|
|
10
19
|
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
11
20
|
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
12
21
|
Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
|
|
13
|
-
|
|
14
|
-
Use
|
|
22
|
+
Treat failed, unsupported, invalid, or corrected operations as negative evidence. Never present an operation as valid when the supplied evidence says it failed; use only a verified alternative or record an explicit warning. Never invent commands or parameters absent from successful evidence.
|
|
23
|
+
Use storageHint "rule" only for one short imperative instruction of at most 240 characters. A rule must not contain incident narration, example resource names, IDs, timestamps, or evidence details. Use "topic" for multi-step SOPs and detailed experience.
|
|
24
|
+
Relevant existing knowledge may be supplied. If a candidate has the same intent as an existing item, do not add a translated, renamed, or reformatted duplicate. Set targetId to that exact existing ID and action to "reinforce" when the existing content remains correct, or "revise" only when the delta explicitly proves a correction. Use targetId null only for genuinely new knowledge.
|
|
15
25
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
The prior validation context is only for checking facts and contradictions; do not persist it again unless the new delta independently reinforces it. Prefer accumulated workflow successfulSteps and failedApproaches over assistant narration.
|
|
27
|
+
Each summary must be one complete sentence of at most 160 characters. Keep each topic body under 2200 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered. Return at most 3 candidates.
|
|
28
|
+
Return: {"candidates":[{"key":"stable semantic key","targetId":"existing-id or null","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
|
|
18
29
|
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
19
30
|
|
|
20
|
-
export function buildKnowledgeExtractionPrompt(
|
|
21
|
-
|
|
31
|
+
export function buildKnowledgeExtractionPrompt(
|
|
32
|
+
projectRoot: string,
|
|
33
|
+
delta: string,
|
|
34
|
+
context = "",
|
|
35
|
+
existing: ExistingKnowledgeContext[] = [],
|
|
36
|
+
): string {
|
|
37
|
+
const related = existing.length > 0 ? JSON.stringify(existing) : "[]";
|
|
38
|
+
return [
|
|
39
|
+
`Project root: ${projectRoot}`,
|
|
40
|
+
"",
|
|
41
|
+
"<relevant_existing_knowledge>", related, "</relevant_existing_knowledge>",
|
|
42
|
+
"",
|
|
43
|
+
"<prior_validation_context>", context, "</prior_validation_context>",
|
|
44
|
+
"",
|
|
45
|
+
"<conversation_delta>", delta, "</conversation_delta>",
|
|
46
|
+
].join("\n");
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
export function parseCandidateEnvelope(text: string): unknown[] {
|
|
@@ -19,6 +19,9 @@ interface ReviewPiece {
|
|
|
19
19
|
text: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
23
|
+
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
24
|
+
|
|
22
25
|
function hash(value: string, length = 64): string {
|
|
23
26
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
24
27
|
}
|
|
@@ -35,20 +38,21 @@ function textContent(content: unknown): string {
|
|
|
35
38
|
|
|
36
39
|
function reviewPiece(entry: SessionEntry, index: number): ReviewPiece | undefined {
|
|
37
40
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
38
|
-
return { index, priority:
|
|
41
|
+
return { index, priority: 2, category: "summary", text: `[summary]\n${sanitizeKnowledgeText(entry.summary).slice(0, 6_000)}` };
|
|
39
42
|
}
|
|
40
43
|
if (entry.type === "custom" && entry.customType.startsWith("hwcode-workflow")) {
|
|
41
44
|
return {
|
|
42
|
-
index, priority:
|
|
45
|
+
index, priority: 1, category: "workflow",
|
|
43
46
|
text: `[workflow_state:${entry.customType}]\n${sanitizeKnowledgeText(JSON.stringify(entry.data ?? {})).slice(0, 6_000)}`,
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
if (entry.type !== "message" || !entry.message || typeof entry.message !== "object") return undefined;
|
|
47
50
|
const message = entry.message as unknown as Record<string, unknown>;
|
|
48
51
|
const role = typeof message.role === "string" ? message.role : "message";
|
|
49
|
-
const content = sanitizeKnowledgeText(textContent(message.content));
|
|
52
|
+
const content = sanitizeKnowledgeText(textContent(message.content).replace(INJECTED_SKILL_RE, "[loaded skill omitted]"));
|
|
50
53
|
if (!content) return undefined;
|
|
51
|
-
const priority = role === "user" ? 0 : role === "toolResult"
|
|
54
|
+
const priority = role === "user" ? 0 : role === "toolResult" && FAILURE_EVIDENCE_RE.test(content) ? 2
|
|
55
|
+
: role === "toolResult" ? 4 : 3;
|
|
52
56
|
const cap = role === "user" ? 8_000 : role === "toolResult" ? 1_500 : 4_000;
|
|
53
57
|
const category = role === "user" ? "user" : role === "toolResult" ? "tool" : "assistant";
|
|
54
58
|
return { index, priority, category, text: `[${role}]\n${content.slice(0, cap)}` };
|
|
@@ -61,14 +65,43 @@ function buildDelta(entries: SessionEntry[]): string {
|
|
|
61
65
|
user: 14_000, workflow: 6_000, summary: 4_000, assistant: 8_000, tool: 4_000,
|
|
62
66
|
};
|
|
63
67
|
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars;
|
|
64
|
-
for (const piece of pieces.slice().sort((left, right) =>
|
|
68
|
+
for (const piece of pieces.slice().sort((left, right) => {
|
|
69
|
+
if (left.priority !== right.priority) return left.priority - right.priority;
|
|
70
|
+
if (left.category === right.category && left.category !== "user") return right.index - left.index;
|
|
71
|
+
return left.index - right.index;
|
|
72
|
+
})) {
|
|
65
73
|
if (remaining <= 0) break;
|
|
66
|
-
const
|
|
74
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
75
|
+
if (remaining <= separatorCharacters) break;
|
|
76
|
+
const text = piece.text.slice(0, Math.min(remaining - separatorCharacters, categoryRemaining[piece.category]));
|
|
67
77
|
if (text) selected.push({ ...piece, text });
|
|
68
|
-
remaining -= text.length;
|
|
78
|
+
remaining -= text.length + separatorCharacters;
|
|
69
79
|
categoryRemaining[piece.category] -= text.length;
|
|
70
80
|
}
|
|
71
|
-
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
81
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
82
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxDeltaChars);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildValidationContext(entries: SessionEntry[]): string {
|
|
86
|
+
const pieces = entries.map(reviewPiece).filter((piece): piece is ReviewPiece => Boolean(piece));
|
|
87
|
+
const latestWorkflow = pieces.filter((piece) => piece.category === "workflow").at(-1);
|
|
88
|
+
const evidence = pieces.filter((piece) => (
|
|
89
|
+
(piece.category === "tool" || piece.category === "assistant" || piece.category === "summary")
|
|
90
|
+
&& FAILURE_EVIDENCE_RE.test(piece.text)
|
|
91
|
+
));
|
|
92
|
+
if (latestWorkflow && !evidence.includes(latestWorkflow)) evidence.push(latestWorkflow);
|
|
93
|
+
let remaining = KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars;
|
|
94
|
+
const selected: ReviewPiece[] = [];
|
|
95
|
+
for (const piece of evidence.slice().sort((left, right) => right.index - left.index)) {
|
|
96
|
+
if (remaining <= 0) break;
|
|
97
|
+
const separatorCharacters = selected.length > 0 ? 2 : 0;
|
|
98
|
+
if (remaining <= separatorCharacters) break;
|
|
99
|
+
const text = piece.text.slice(0, remaining - separatorCharacters);
|
|
100
|
+
if (text) selected.push({ ...piece, text });
|
|
101
|
+
remaining -= text.length + separatorCharacters;
|
|
102
|
+
}
|
|
103
|
+
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
104
|
+
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars);
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
function projectRootForSession(header: SessionHeader, branch: SessionEntry[]): string {
|
|
@@ -120,6 +153,7 @@ export function readReviewTask(
|
|
|
120
153
|
const pending = branch.slice(cursorIndex + 1);
|
|
121
154
|
if (pending.length === 0) return undefined;
|
|
122
155
|
const delta = buildDelta(pending);
|
|
156
|
+
const context = cursorIndex >= 0 ? buildValidationContext(branch.slice(0, cursorIndex + 1)) : "";
|
|
123
157
|
const firstEntryId = pending[0].id;
|
|
124
158
|
const lastEntryId = pending[pending.length - 1].id;
|
|
125
159
|
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
@@ -130,7 +164,7 @@ export function readReviewTask(
|
|
|
130
164
|
return {
|
|
131
165
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
132
166
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
133
|
-
firstEntryId, lastEntryId, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
167
|
+
firstEntryId, lastEntryId, context, delta, deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
134
168
|
};
|
|
135
169
|
}
|
|
136
170
|
|
|
@@ -117,6 +117,56 @@ function normalizedFingerprint(candidate: KnowledgeCandidate): string {
|
|
|
117
117
|
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function comparisonTerms(value: string): Set<string> {
|
|
121
|
+
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
122
|
+
.filter((term) => term.length >= 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function containment(left: Set<string>, right: Set<string>): number {
|
|
126
|
+
const minimum = Math.min(left.size, right.size);
|
|
127
|
+
if (minimum === 0) return 0;
|
|
128
|
+
let shared = 0;
|
|
129
|
+
for (const term of left) if (right.has(term)) shared++;
|
|
130
|
+
return shared / minimum;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function semanticDuplicateScore(candidate: KnowledgeCandidate, entry: KnowledgeCatalogEntry): number {
|
|
134
|
+
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track) return 0;
|
|
135
|
+
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
136
|
+
const keywordScore = containment(
|
|
137
|
+
comparisonTerms(candidate.keywords.join(" ")),
|
|
138
|
+
comparisonTerms(entry.keywords.join(" ")),
|
|
139
|
+
);
|
|
140
|
+
if (titleScore < 0.8 || keywordScore < 0.5) return 0;
|
|
141
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: KnowledgeCatalog): number {
|
|
145
|
+
let bestIndex = -1;
|
|
146
|
+
let bestScore = 0;
|
|
147
|
+
for (const [index, entry] of catalog.items.entries()) {
|
|
148
|
+
const score = semanticDuplicateScore(candidate, entry);
|
|
149
|
+
if (score > bestScore) { bestIndex = index; bestScore = score; }
|
|
150
|
+
}
|
|
151
|
+
return bestIndex;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function unsupportedOperations(value: string): string[] {
|
|
155
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)]
|
|
156
|
+
.map((match) => match[1]!.toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function contradictsVerifiedFailures(body: string, validationText: string): boolean {
|
|
160
|
+
const normalizedBody = body.toLowerCase();
|
|
161
|
+
for (const operation of unsupportedOperations(validationText)) {
|
|
162
|
+
const index = normalizedBody.search(new RegExp(`\\b${operation}\\b`, "u"));
|
|
163
|
+
if (index < 0) continue;
|
|
164
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + operation.length + 80);
|
|
165
|
+
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context)) return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
function contentHash(candidate: KnowledgeCandidate): string {
|
|
121
171
|
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
122
172
|
}
|
|
@@ -127,7 +177,7 @@ function asStringArray(value: unknown, maxItems: number): string[] {
|
|
|
127
177
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
128
178
|
}
|
|
129
179
|
|
|
130
|
-
function normalizeCandidate(value: unknown, projectKey: string): KnowledgeCandidate | undefined {
|
|
180
|
+
function normalizeCandidate(value: unknown, projectKey: string, validationText: string): KnowledgeCandidate | undefined {
|
|
131
181
|
if (!value || typeof value !== "object") return undefined;
|
|
132
182
|
const raw = value as Record<string, unknown>;
|
|
133
183
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
@@ -139,11 +189,15 @@ function normalizeCandidate(value: unknown, projectKey: string): KnowledgeCandid
|
|
|
139
189
|
const body = compactKnowledgeText(raw.body, 20_000);
|
|
140
190
|
const evidence = asStringArray(raw.evidence, 8);
|
|
141
191
|
if (!body || evidence.length === 0) return undefined;
|
|
192
|
+
if (contradictsVerifiedFailures(body, validationText)) return undefined;
|
|
142
193
|
const requestedTrack: KnowledgeTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
143
194
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
195
|
+
&& body.split(/\n\s*\n/gu).length === 1
|
|
144
196
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
197
|
+
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
145
198
|
return {
|
|
146
|
-
key: compactKnowledgeText(raw.key, 160),
|
|
199
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
200
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
147
201
|
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
148
202
|
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
149
203
|
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
@@ -275,13 +329,24 @@ export function commitKnowledgeReview(
|
|
|
275
329
|
const catalog: KnowledgeCatalog = structuredClone(current.catalog);
|
|
276
330
|
const result: PersistKnowledgeResult = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
277
331
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
278
|
-
const candidate = normalizeCandidate(value, task.projectKey);
|
|
332
|
+
const candidate = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
|
|
279
333
|
if (!candidate) { result.skipped++; continue; }
|
|
280
334
|
const fingerprint = normalizedFingerprint(candidate);
|
|
281
335
|
const hash = contentHash(candidate);
|
|
282
|
-
const
|
|
336
|
+
const targetIndex = candidate.targetId
|
|
337
|
+
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
338
|
+
: -1;
|
|
339
|
+
if (candidate.targetId && targetIndex < 0) { result.skipped++; continue; }
|
|
340
|
+
let existingIndex = targetIndex >= 0
|
|
341
|
+
? targetIndex
|
|
342
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
343
|
+
let semanticReinforcement = false;
|
|
344
|
+
if (existingIndex < 0) {
|
|
345
|
+
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
346
|
+
semanticReinforcement = existingIndex >= 0;
|
|
347
|
+
}
|
|
283
348
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
284
|
-
if (existing
|
|
349
|
+
if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
|
|
285
350
|
existing.evidenceCount += candidate.evidence.length;
|
|
286
351
|
existing.updatedAt = new Date().toISOString();
|
|
287
352
|
result.updated++;
|
|
@@ -304,8 +369,9 @@ export function commitKnowledgeReview(
|
|
|
304
369
|
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
305
370
|
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
306
371
|
const entry: KnowledgeCatalogEntry = {
|
|
307
|
-
id, fingerprint
|
|
308
|
-
|
|
372
|
+
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
373
|
+
title: candidate.title, summary: candidate.summary,
|
|
374
|
+
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
309
375
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
310
376
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
311
377
|
};
|
|
@@ -5,6 +5,7 @@ export type KnowledgeDurability = "stable";
|
|
|
5
5
|
|
|
6
6
|
export interface KnowledgeCandidate {
|
|
7
7
|
key: string;
|
|
8
|
+
targetId?: string;
|
|
8
9
|
title: string;
|
|
9
10
|
summary: string;
|
|
10
11
|
keywords: string[];
|
|
@@ -86,6 +87,7 @@ export interface KnowledgeReviewTask {
|
|
|
86
87
|
projectKey: string;
|
|
87
88
|
firstEntryId: string;
|
|
88
89
|
lastEntryId: string;
|
|
90
|
+
context?: string;
|
|
89
91
|
delta: string;
|
|
90
92
|
deltaDigest: string;
|
|
91
93
|
fileSize: number;
|
|
@@ -32,13 +32,17 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
32
32
|
idleMs: 60_000,
|
|
33
33
|
capabilityPollMs: 5_000,
|
|
34
34
|
modelTimeoutMs: 180_000,
|
|
35
|
-
maxDeltaChars:
|
|
36
|
-
|
|
35
|
+
maxDeltaChars: 12_000,
|
|
36
|
+
maxPriorContextChars: 4_000,
|
|
37
|
+
maxExistingContextItems: 5,
|
|
38
|
+
maxExistingBodyItems: 2,
|
|
39
|
+
maxExistingBodyChars: 600,
|
|
40
|
+
maxCandidates: 3,
|
|
37
41
|
minimumConfidence: 0.72,
|
|
38
42
|
}),
|
|
39
43
|
storage: Object.freeze({
|
|
40
|
-
maxRuleChars:
|
|
41
|
-
maxRuleFileLines:
|
|
44
|
+
maxRuleChars: 320,
|
|
45
|
+
maxRuleFileLines: 6,
|
|
42
46
|
maxRulesPromptChars: 12_000,
|
|
43
47
|
maxMemoryChars: 25_000,
|
|
44
48
|
maxMemoryLines: 200,
|