@hadooppei/hwcode 1.0.19 → 1.0.22
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/evidence.js +19 -10
- package/.pi/dist/lib/knowledge/extractor.js +41 -101
- package/.pi/dist/lib/knowledge/review-worker.js +94 -18
- package/.pi/dist/lib/knowledge/sanitize.js +8 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +56 -12
- package/.pi/dist/lib/runtime/defaults.js +5 -1
- package/.pi/extensions/knowledge.ts +60 -19
- package/.pi/extensions/workflows/cloud/provider-tools.ts +37 -7
- package/.pi/lib/knowledge/evidence.ts +19 -9
- package/.pi/lib/knowledge/extractor.ts +39 -78
- package/.pi/lib/knowledge/review-status.ts +18 -0
- package/.pi/lib/knowledge/review-worker.ts +91 -18
- package/.pi/lib/knowledge/sanitize.ts +10 -0
- package/.pi/lib/knowledge/session-scanner.ts +55 -11
- package/.pi/lib/knowledge/worker-protocol.ts +1 -1
- package/.pi/lib/runtime/defaults.ts +5 -1
- package/.pi/lib/workflows/cloud/process.ts +19 -0
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import { dirname } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -22,6 +21,7 @@ interface ActiveReview {
|
|
|
22
21
|
leaderToken: string;
|
|
23
22
|
controller: AbortController;
|
|
24
23
|
abortReason?: string;
|
|
24
|
+
diagnostic?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
interface ProcessKnowledgeRuntime {
|
|
@@ -53,7 +53,7 @@ function compactReviewEvidence(task: KnowledgeReviewTask): {
|
|
|
53
53
|
let characters = 0;
|
|
54
54
|
const add = (item: KnowledgeEvidence | undefined): void => {
|
|
55
55
|
if (!item || seen.has(item.id) || selected.length >= KNOWLEDGE_RUNTIME_DEFAULTS.review.maxEvidenceItems) return;
|
|
56
|
-
const compact = { ...item, excerpt: item.excerpt.slice(0,
|
|
56
|
+
const compact = { ...item, excerpt: item.excerpt.slice(0, 800) };
|
|
57
57
|
const size = JSON.stringify(compact).length;
|
|
58
58
|
if (characters + size > KNOWLEDGE_RUNTIME_DEFAULTS.review.maxEvidenceChars) return;
|
|
59
59
|
selected.push(compact);
|
|
@@ -117,7 +117,7 @@ function reportWorkerIssue(error: unknown): void {
|
|
|
117
117
|
const prior = runtime.lastWorkerIssue;
|
|
118
118
|
if (prior?.message === message && now - prior.reportedAt < 60_000) return;
|
|
119
119
|
runtime.lastWorkerIssue = { message, reportedAt: now };
|
|
120
|
-
const rendered = `HWCode knowledge worker failed: ${message}`;
|
|
120
|
+
const rendered = `HWCode knowledge worker failed: ${message.slice(0, 600)}`;
|
|
121
121
|
if (runtime.context?.hasUI) runtime.context.ui.notify(rendered, "error");
|
|
122
122
|
else console.error(rendered);
|
|
123
123
|
}
|
|
@@ -162,7 +162,7 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
162
162
|
runtime.activeReview = review;
|
|
163
163
|
timeout = setTimeout(() => {
|
|
164
164
|
if (controller.signal.aborted) return;
|
|
165
|
-
review!.abortReason =
|
|
165
|
+
review!.abortReason = `knowledge-review-model-timeout after ${KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs}ms${review!.diagnostic ? `; ${review!.diagnostic}` : ""}`;
|
|
166
166
|
controller.abort();
|
|
167
167
|
}, KNOWLEDGE_RUNTIME_DEFAULTS.review.modelTimeoutMs);
|
|
168
168
|
timeout.unref();
|
|
@@ -182,13 +182,20 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
182
182
|
seenRelated.add(entry.id);
|
|
183
183
|
};
|
|
184
184
|
const episodeMatches = (message.task.episodes ?? []).map((episode) => (
|
|
185
|
-
matchKnowledge(
|
|
185
|
+
matchKnowledge(
|
|
186
|
+
`${episode.title}\n${episode.query}`,
|
|
187
|
+
applicableCatalog,
|
|
188
|
+
KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingContextItems,
|
|
189
|
+
)
|
|
186
190
|
));
|
|
187
|
-
for (const matches of episodeMatches
|
|
191
|
+
for (const matches of episodeMatches) addRelated(matches[0]);
|
|
188
192
|
for (const id of message.task.loadedKnowledgeIds ?? []) {
|
|
189
193
|
addRelated(applicableCatalog.items.find((entry) => entry.id === id));
|
|
190
194
|
}
|
|
191
|
-
|
|
195
|
+
const matchDepth = Math.max(0, ...episodeMatches.map((matches) => matches.length));
|
|
196
|
+
for (let rank = 1; rank < matchDepth; rank++) {
|
|
197
|
+
for (const matches of episodeMatches) addRelated(matches[rank]);
|
|
198
|
+
}
|
|
192
199
|
for (const entry of matchKnowledge(
|
|
193
200
|
message.task.recallQuery || message.task.delta,
|
|
194
201
|
applicableCatalog,
|
|
@@ -210,6 +217,28 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
210
217
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxExistingBodyChars);
|
|
211
218
|
return content ? { ...reference, content } : reference;
|
|
212
219
|
});
|
|
220
|
+
const prompt = buildKnowledgeExtractionPrompt(
|
|
221
|
+
message.task.projectRoot,
|
|
222
|
+
message.task.delta.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxNarrativeChars),
|
|
223
|
+
message.task.context,
|
|
224
|
+
related,
|
|
225
|
+
promptContext.evidence,
|
|
226
|
+
promptContext.episodes,
|
|
227
|
+
);
|
|
228
|
+
const diagnostic = [
|
|
229
|
+
`reviewKey=${message.task.reviewKey}`,
|
|
230
|
+
`model=${ctx.model.provider}/${ctx.model.id}`,
|
|
231
|
+
`promptChars=${prompt.length}`,
|
|
232
|
+
`deltaChars=${message.task.delta.length}`,
|
|
233
|
+
`evidenceItems=${promptContext.evidence.length}`,
|
|
234
|
+
`episodeItems=${promptContext.episodes.length}`,
|
|
235
|
+
`existingItems=${related.length}`,
|
|
236
|
+
];
|
|
237
|
+
const requestStartedAt = Date.now();
|
|
238
|
+
const setRequestPhase = (phase: string): void => {
|
|
239
|
+
review!.diagnostic = [...diagnostic, `phase=${phase}`, `requestElapsedMs=${Date.now() - requestStartedAt}`].join("; ");
|
|
240
|
+
};
|
|
241
|
+
setRequestPhase("preparing-request");
|
|
213
242
|
const response = await ctx.modelRegistry.complete(
|
|
214
243
|
ctx.model,
|
|
215
244
|
{
|
|
@@ -218,29 +247,41 @@ async function runReview(message: Extract<KnowledgeWorkerOutput, { type: "review
|
|
|
218
247
|
role: "user",
|
|
219
248
|
content: [{
|
|
220
249
|
type: "text",
|
|
221
|
-
text:
|
|
222
|
-
message.task.projectRoot,
|
|
223
|
-
message.task.delta.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxNarrativeChars),
|
|
224
|
-
message.task.context,
|
|
225
|
-
related,
|
|
226
|
-
promptContext.evidence,
|
|
227
|
-
promptContext.episodes,
|
|
228
|
-
),
|
|
250
|
+
text: prompt,
|
|
229
251
|
}],
|
|
230
252
|
timestamp: Date.now(),
|
|
231
253
|
}],
|
|
232
254
|
},
|
|
233
|
-
{
|
|
255
|
+
{
|
|
256
|
+
signal: controller.signal,
|
|
257
|
+
reasoningEffort: "low",
|
|
258
|
+
cacheRetention: "none",
|
|
259
|
+
temperature: 0,
|
|
260
|
+
maxTokens: KNOWLEDGE_RUNTIME_DEFAULTS.review.maxOutputTokens,
|
|
261
|
+
timeoutMs: KNOWLEDGE_RUNTIME_DEFAULTS.review.requestTimeoutMs,
|
|
262
|
+
maxRetries: 0,
|
|
263
|
+
onPayload: () => { setRequestPhase("awaiting-http-response"); },
|
|
264
|
+
onResponse: (providerResponse: { status: number }) => {
|
|
265
|
+
setRequestPhase(`streaming-response-http-${providerResponse.status}`);
|
|
266
|
+
},
|
|
267
|
+
},
|
|
234
268
|
);
|
|
235
269
|
if (controller.signal.aborted) throw new Error(review.abortReason ?? "knowledge-review-interrupted");
|
|
270
|
+
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
271
|
+
throw new Error(response.errorMessage ?? `knowledge-review-model-${response.stopReason}`);
|
|
272
|
+
}
|
|
236
273
|
const raw = response.content.filter((item): item is { type: "text"; text: string } => item.type === "text")
|
|
237
274
|
.map((item) => item.text).join("\n");
|
|
238
275
|
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, raw });
|
|
239
276
|
} catch (error) {
|
|
240
|
-
const
|
|
277
|
+
const baseReason = review?.controller.signal.aborted
|
|
241
278
|
? review.abortReason ?? "knowledge-review-interrupted"
|
|
242
279
|
: error instanceof Error ? error.message : String(error);
|
|
243
|
-
|
|
280
|
+
const expectedInterruption = baseReason === "model-executor-is-busy" || isExpectedReviewInterruption(baseReason);
|
|
281
|
+
const reason = review?.diagnostic && !expectedInterruption && !baseReason.includes("reviewKey=")
|
|
282
|
+
? `${baseReason}; ${review.diagnostic}`
|
|
283
|
+
: baseReason;
|
|
284
|
+
if (expectedInterruption) {
|
|
244
285
|
post({ type: "review_deferred", leaderToken: message.leaderToken, requestId: message.requestId, reason });
|
|
245
286
|
} else {
|
|
246
287
|
post({ type: "review_result", leaderToken: message.leaderToken, requestId: message.requestId, error: reason });
|
|
@@ -263,7 +304,7 @@ function handleWorkerMessage(message: KnowledgeWorkerOutput): void {
|
|
|
263
304
|
cancelActiveReview(message.reason);
|
|
264
305
|
return;
|
|
265
306
|
}
|
|
266
|
-
if (message.type === "review_failed") reportWorkerIssue(message.error);
|
|
307
|
+
if (message.type === "review_failed" && message.terminal !== false) reportWorkerIssue(message.error);
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
function ensureWorker(): Worker | undefined {
|
|
@@ -8,7 +8,9 @@ import { toolError, toolOk } from "../../../lib/tool-result.ts";
|
|
|
8
8
|
import { evaluateCommandArgumentsAccess } from "../../../lib/workspace/access-policy.ts";
|
|
9
9
|
import { prepareCloudExecution } from "../../../lib/workflows/cloud/adapters.ts";
|
|
10
10
|
import { cloudExecutionBlockReason, recordStrategyFailure } from "../../../lib/workflows/cloud/execution.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cloudProviderFailureReason, runProcess, truncateOutput, type ProcessResult,
|
|
13
|
+
} from "../../../lib/workflows/cloud/process.ts";
|
|
12
14
|
import { CLOUD_EXECUTABLES, classifyCloudOperation, getCloudProvider, redactCredentialValues, type CloudOperation } from "../../../lib/workflows/cloud/providers.ts";
|
|
13
15
|
import { cloudArtifactDirectory } from "../../../lib/workflows/cloud/workspace.ts";
|
|
14
16
|
import { WORKFLOW_EXTERNAL_AUDIT_TYPE, cloudDetails } from "../../../lib/workflows/state.ts";
|
|
@@ -106,16 +108,44 @@ export function registerCloudProviderTools(pi: ExtensionAPI, runtime: CloudExten
|
|
|
106
108
|
: await runProcess(params.command, prepared.args, { cwd: cloudArtifactDirectory(activeState), env: prepared.env, signal });
|
|
107
109
|
} finally { prepared.cleanup?.(); }
|
|
108
110
|
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const redactedStdout = redactCredentialValues(result.stdout, credentials);
|
|
112
|
+
const redactedStderr = redactCredentialValues(result.stderr, credentials);
|
|
113
|
+
const providerFailure = cloudProviderFailureReason(redactedStdout, redactedStderr);
|
|
114
|
+
const stdout = truncateOutput(redactedStdout);
|
|
115
|
+
const stderr = truncateOutput(redactedStderr);
|
|
116
|
+
const failureReason = result.code !== 0
|
|
117
|
+
? stderr.trim() || stdout.trim() || `exit code ${result.code}`
|
|
118
|
+
: providerFailure;
|
|
119
|
+
pi.appendEntry(CLOUD_AUDIT_TYPE, {
|
|
120
|
+
vendor: details.vendor,
|
|
121
|
+
command: params.command,
|
|
122
|
+
operation,
|
|
123
|
+
intent: params.intent,
|
|
124
|
+
approach: params.approach,
|
|
125
|
+
exitCode: result.code,
|
|
126
|
+
outcome: failureReason ? "failed" : "succeeded",
|
|
127
|
+
providerFailure: providerFailure ? safeStepText(providerFailure, credentials) : undefined,
|
|
128
|
+
executedAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
if (failureReason) {
|
|
131
|
+
const failed = recordStrategyFailure(details, params.approach, params.command, failureReason, CLOUD_RUNTIME_DEFAULTS.workflow.maxFailedApproaches);
|
|
114
132
|
runtime.replaceDetails(activeState, failed, failed.terminalFailure ? "failed" : "executing");
|
|
115
133
|
const suffix = failed.terminalFailure
|
|
116
134
|
? "\n\nTERMINAL FAILURE: Three materially different approaches failed. Explain causes and summarize progress and changes. Do not try another cloud operation."
|
|
117
135
|
: `\n\nDistinct failed approaches: ${failed.failedApproaches.length}/${CLOUD_RUNTIME_DEFAULTS.workflow.maxFailedApproaches}.`;
|
|
118
|
-
|
|
136
|
+
const heading = result.code !== 0
|
|
137
|
+
? `Cloud command failed with exit code ${result.code}.`
|
|
138
|
+
: "Cloud provider reported a failure despite exit code 0.";
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text" as const, text: `${heading}\n${stderr || stdout || providerFailure}${suffix}` }],
|
|
141
|
+
isError: true,
|
|
142
|
+
details: {
|
|
143
|
+
operation,
|
|
144
|
+
exitCode: result.code,
|
|
145
|
+
providerFailure: Boolean(providerFailure),
|
|
146
|
+
terminalFailure: failed.terminalFailure,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
const successfulSteps = [...details.successfulSteps, {
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
import { sanitizeKnowledgeText } from "./sanitize.ts";
|
|
5
|
+
import { containsInjectedSkill, sanitizeKnowledgeText } from "./sanitize.ts";
|
|
6
6
|
import type { KnowledgeEpisode, KnowledgeEvidence, KnowledgeEvidenceKind } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const FAILURE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|timeout|timed out|unreachable|servfail|refused)\b|not supported|失败|错误|不支持|超时|无法|不可达)/iu;
|
|
@@ -99,8 +99,13 @@ function boundedExcerpt(value: string, limit = 900): string {
|
|
|
99
99
|
const clean = sanitizeKnowledgeText(value).replace(/\n{3,}/gu, "\n\n").trim();
|
|
100
100
|
if (clean.length <= limit) return clean;
|
|
101
101
|
const lines = clean.split("\n");
|
|
102
|
-
const
|
|
103
|
-
|
|
102
|
+
const priority = (line: string): number => REVERSAL_RE.test(line) ? 0
|
|
103
|
+
: FAILURE_RE.test(line) && VERIFICATION_RE.test(line) ? 1
|
|
104
|
+
: FAILURE_RE.test(line) ? 2 : 3;
|
|
105
|
+
const significant = lines.filter((line) => (
|
|
106
|
+
REVERSAL_RE.test(line) || FAILURE_RE.test(line) || VERIFICATION_RE.test(line)
|
|
107
|
+
)).sort((left, right) => priority(left) - priority(right));
|
|
108
|
+
const selected = [...lines.slice(0, 2), ...significant.slice(0, 7), ...lines.slice(-2)];
|
|
104
109
|
return [...new Set(selected)].join("\n").slice(0, limit);
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -163,9 +168,8 @@ function evidenceId(entryId: string, suffix: string): string {
|
|
|
163
168
|
return `ev-${digest(`${entryId}\0${suffix}`)}`;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
function outcomeFor(message: Record<string, unknown
|
|
167
|
-
|
|
168
|
-
return "success";
|
|
171
|
+
function outcomeFor(message: Record<string, unknown>): "success" | "failure" {
|
|
172
|
+
return message.isError === true ? "failure" : "success";
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
function userKind(text: string): KnowledgeEvidenceKind {
|
|
@@ -200,6 +204,9 @@ export function buildKnowledgeEvidence(entries: SessionEntry[]): KnowledgeEviden
|
|
|
200
204
|
const role = typeof message.role === "string" ? message.role : "";
|
|
201
205
|
const text = textContent(content).trim();
|
|
202
206
|
if (role === "user" && text) {
|
|
207
|
+
// Skill bodies and workflow activation envelopes are injected as user-role messages by
|
|
208
|
+
// the host. They are execution context, not an explicit human directive to persist.
|
|
209
|
+
if (containsInjectedSkill(text)) return;
|
|
203
210
|
evidence.push({
|
|
204
211
|
id: evidenceId(entryId, "user"), entryId, timestamp, kind: userKind(text),
|
|
205
212
|
subject: "User requirement or correction", outcome: "unknown", excerpt: boundedExcerpt(text, 700),
|
|
@@ -209,11 +216,14 @@ export function buildKnowledgeEvidence(entries: SessionEntry[]): KnowledgeEviden
|
|
|
209
216
|
if (role === "toolResult" && text) {
|
|
210
217
|
const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
211
218
|
const call = calls.get(toolCallId);
|
|
212
|
-
|
|
219
|
+
// Recalled knowledge is comparison context, not fresh execution evidence. Treating a
|
|
220
|
+
// lookup result as verification would let knowledge reinforce itself merely by loading it.
|
|
221
|
+
if (call?.name === "hwcode_knowledge_lookup") return;
|
|
222
|
+
const outcome = outcomeFor(message);
|
|
213
223
|
const callText = call ? `[call] ${call.name}: ${compactArguments(call.args)}\n` : "";
|
|
214
224
|
evidence.push({
|
|
215
225
|
id: evidenceId(entryId, toolCallId || "tool"), entryId, timestamp,
|
|
216
|
-
kind: outcome === "failure" ? "tool-failure" :
|
|
226
|
+
kind: outcome === "failure" ? "tool-failure" : "tool-success",
|
|
217
227
|
subject: evidenceSubject(call, text), operation: operationName(call), outcome,
|
|
218
228
|
excerpt: boundedExcerpt(`${callText}[result] ${text}`),
|
|
219
229
|
});
|
|
@@ -222,7 +232,7 @@ export function buildKnowledgeEvidence(entries: SessionEntry[]): KnowledgeEviden
|
|
|
222
232
|
if (role === "assistant" && text && (FAILURE_RE.test(text) || VERIFICATION_RE.test(text) || REVERSAL_RE.test(text))) {
|
|
223
233
|
evidence.push({
|
|
224
234
|
id: evidenceId(entryId, "assistant"), entryId, timestamp,
|
|
225
|
-
kind:
|
|
235
|
+
kind: "assistant-claim",
|
|
226
236
|
subject: "Assistant diagnosis or conclusion", outcome: "unknown", excerpt: boundedExcerpt(text, 800),
|
|
227
237
|
});
|
|
228
238
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
4
|
+
import { compactKnowledgeText } from "./sanitize.ts";
|
|
4
5
|
import type { KnowledgeEpisode, KnowledgeEpisodeDecision, KnowledgeEvidence } from "./types.ts";
|
|
5
6
|
|
|
6
7
|
export interface ExistingKnowledgeContext {
|
|
@@ -32,9 +33,11 @@ Treat failed, unsupported, invalid, or corrected operations as negative evidence
|
|
|
32
33
|
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.
|
|
33
34
|
Rank knowledge by future time saved, repeated failures, hypothesis reversals, user corrections, verified resolution, reuse, and novelty. Do not favor the final task result merely because it completed the objective. Every supplied episode scoring 45 or higher must have an episodeDecisions entry, even when skipped.
|
|
34
35
|
Relevant existing knowledge may be supplied. Cheap recall only proposes related items; it does not prove duplication. Compare execution environment, symptom, and root cause. Use action "reinforce" only when the exact target already covers every claim and there is no new content. Use "extend" for new compatible claims, "correct" when direct evidence disproves old content, "retire" when the whole target is invalid, and "add" for a distinct environment or root cause. Never turn lexical similarity into reinforcement.
|
|
36
|
+
Prefer the most specific existing topic that already covers a claim over a broad conventions or SOP topic.
|
|
35
37
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
36
38
|
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.
|
|
37
39
|
Every technical claim must have a stable claim key and cite evidenceIds from the supplied evidence ledger. Tool success, tool failure, and verification evidence outrank assistant narration. Do not support a service-wide or universal claim with one operation-specific failure. Evidence for an unrelated parameter error cannot support an API-version claim.
|
|
40
|
+
Keep each candidate atomic: put separable claim sets in separate candidates, omit any claim without direct cited evidence, and prioritize the highest-value supported candidates. One unsupported claim rejects its entire candidate.
|
|
38
41
|
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.
|
|
39
42
|
The body must not repeat the title as a Markdown H1. The storage renderer supplies the H1. Replace any necessary example resource value with an obvious placeholder instead of a real project, namespace, image, network, or resource name.
|
|
40
43
|
Return: {"episodeDecisions":[{"episodeId":"ep-id","decision":"add|reinforce|extend|correct|retire|skip","targetId":"existing-id or omitted","reason":"..."}],"candidates":[{"key":"stable semantic key","identityKey":"platform/runtime/problem/root-cause","targetId":"existing-id or null","episodeIds":["ep-id"],"title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body or only the new section for extend","claims":[{"key":"stable claim key","text":"one precise claim","evidenceIds":["ev-id"]}],"evidenceIds":["ev-id"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|extend|correct|retire","explicitUserDirective":false,"durability":"stable"}]}
|
|
@@ -65,25 +68,21 @@ export function buildKnowledgeExtractionPrompt(
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export function parseKnowledgeReviewEnvelope(text: string): KnowledgeReviewEnvelope {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
84
|
-
} catch {
|
|
85
|
-
return { episodeDecisions: [], candidates: parseCandidateEnvelope(text) };
|
|
86
|
-
}
|
|
71
|
+
const parsed = parseEnvelopeObject(text);
|
|
72
|
+
if (!Array.isArray(parsed.candidates)) throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
73
|
+
const validDecisions = new Set(["add", "reinforce", "extend", "correct", "retire", "skip"]);
|
|
74
|
+
const decisions = Array.isArray(parsed.episodeDecisions)
|
|
75
|
+
? parsed.episodeDecisions.filter((item): item is KnowledgeEpisodeDecision => {
|
|
76
|
+
if (!item || typeof item !== "object") return false;
|
|
77
|
+
const value = item as Record<string, unknown>;
|
|
78
|
+
return typeof value.episodeId === "string" && validDecisions.has(String(value.decision))
|
|
79
|
+
&& typeof value.reason === "string";
|
|
80
|
+
})
|
|
81
|
+
: [];
|
|
82
|
+
return {
|
|
83
|
+
episodeDecisions: decisions,
|
|
84
|
+
candidates: parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates),
|
|
85
|
+
};
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
function parseEnvelopeObject(text: string): Record<string, unknown> {
|
|
@@ -91,68 +90,30 @@ function parseEnvelopeObject(text: string): Record<string, unknown> {
|
|
|
91
90
|
const unfenced = trimmed.replace(/^```(?:json)?\s*/iu, "").replace(/\s*```$/u, "");
|
|
92
91
|
const start = unfenced.indexOf("{");
|
|
93
92
|
const end = unfenced.lastIndexOf("}");
|
|
94
|
-
if (start < 0 || end <= start)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const end = unfenced.lastIndexOf("}");
|
|
103
|
-
if (start < 0 || end <= start) throw new Error("Knowledge reviewer did not return a JSON object");
|
|
104
|
-
let strictError: unknown;
|
|
93
|
+
if (start < 0 || end <= start) {
|
|
94
|
+
throw new Error([
|
|
95
|
+
"Knowledge reviewer did not return a complete JSON object",
|
|
96
|
+
`responseChars=${unfenced.length}`,
|
|
97
|
+
`responseExcerpt=${JSON.stringify(compactKnowledgeText(unfenced, 1_200))}`,
|
|
98
|
+
].join("; "));
|
|
99
|
+
}
|
|
100
|
+
const payload = unfenced.slice(start, end + 1);
|
|
105
101
|
try {
|
|
106
|
-
|
|
107
|
-
if (!Array.isArray(parsed.candidates)) throw new Error("Knowledge reviewer response is missing candidates[]");
|
|
108
|
-
return parsed.candidates.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates);
|
|
102
|
+
return JSON.parse(payload) as Record<string, unknown>;
|
|
109
103
|
} catch (error) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (escaped) escaped = false;
|
|
123
|
-
else if (character === "\\") escaped = true;
|
|
124
|
-
else if (character === '"') inString = false;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (character === '"') { inString = true; continue; }
|
|
128
|
-
if (character !== "{") continue;
|
|
129
|
-
const objectEnd = balancedObjectEnd(unfenced, index);
|
|
130
|
-
if (objectEnd < 0) break;
|
|
131
|
-
try { candidates.push(JSON.parse(unfenced.slice(index, objectEnd + 1))); } catch { /* Salvage other complete candidates. */ }
|
|
132
|
-
index = objectEnd;
|
|
133
|
-
}
|
|
134
|
-
if (candidates.length > 0) return candidates;
|
|
135
|
-
}
|
|
136
|
-
throw strictError;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function balancedObjectEnd(value: string, start: number): number {
|
|
140
|
-
let depth = 0;
|
|
141
|
-
let inString = false;
|
|
142
|
-
let escaped = false;
|
|
143
|
-
for (let index = start; index < value.length; index++) {
|
|
144
|
-
const character = value[index];
|
|
145
|
-
if (inString) {
|
|
146
|
-
if (escaped) escaped = false;
|
|
147
|
-
else if (character === "\\") escaped = true;
|
|
148
|
-
else if (character === '"') inString = false;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (character === '"') { inString = true; continue; }
|
|
152
|
-
if (character === "{") depth++;
|
|
153
|
-
else if (character === "}" && --depth === 0) return index;
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
const offsetMatch = /position\s+(\d+)/iu.exec(message);
|
|
106
|
+
const offset = offsetMatch ? Number.parseInt(offsetMatch[1]!, 10) : -1;
|
|
107
|
+
const excerptStart = offset >= 0 ? Math.max(0, offset - 600) : 0;
|
|
108
|
+
const excerptEnd = offset >= 0 ? Math.min(payload.length, offset + 600) : Math.min(payload.length, 1_200);
|
|
109
|
+
const excerpt = compactKnowledgeText(payload.slice(excerptStart, excerptEnd), 1_200);
|
|
110
|
+
throw new Error([
|
|
111
|
+
`Knowledge reviewer returned invalid JSON: ${message}`,
|
|
112
|
+
`responseChars=${payload.length}`,
|
|
113
|
+
offset >= 0 ? `errorOffset=${offset}` : undefined,
|
|
114
|
+
`responseExcerpt=${JSON.stringify(excerpt)}`,
|
|
115
|
+
].filter(Boolean).join("; "));
|
|
154
116
|
}
|
|
155
|
-
return -1;
|
|
156
117
|
}
|
|
157
118
|
|
|
158
119
|
export function knowledgeDeltaDigest(delta: string): string {
|
|
@@ -40,6 +40,24 @@ export interface KnowledgeReviewStatus {
|
|
|
40
40
|
result?: PersistKnowledgeResult;
|
|
41
41
|
error?: string;
|
|
42
42
|
};
|
|
43
|
+
reviewFailures?: Record<string, {
|
|
44
|
+
sessionKey: string;
|
|
45
|
+
sessionFileHash: string;
|
|
46
|
+
attempts: Array<{
|
|
47
|
+
attempt: number;
|
|
48
|
+
failedAt: string;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
error: string;
|
|
51
|
+
}>;
|
|
52
|
+
}>;
|
|
53
|
+
blockedReviews?: Record<string, {
|
|
54
|
+
reviewKey: string;
|
|
55
|
+
sessionKey: string;
|
|
56
|
+
sessionFileHash: string;
|
|
57
|
+
blockedAt: string;
|
|
58
|
+
attempts: number;
|
|
59
|
+
error: string;
|
|
60
|
+
}>;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
export function writeKnowledgeReviewStatus(status: KnowledgeReviewStatus, home: string): void {
|