akm-cli 0.9.0-beta.44 → 0.9.0-beta.45
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/dist/commands/health.js
CHANGED
|
@@ -134,6 +134,7 @@ function createUnknownImproveMetrics() {
|
|
|
134
134
|
failures: 0,
|
|
135
135
|
htmlErrors: 0,
|
|
136
136
|
retryAttempts: 0,
|
|
137
|
+
nonArrayBatchFailures: 0,
|
|
137
138
|
durationMs: 0,
|
|
138
139
|
},
|
|
139
140
|
sessionExtraction: {
|
|
@@ -440,6 +441,7 @@ function projectRunMetrics(result) {
|
|
|
440
441
|
metrics.graphExtraction.failures += toFiniteNumber(telemetry.failureCount);
|
|
441
442
|
metrics.graphExtraction.htmlErrors += toFiniteNumber(telemetry.htmlErrorCount);
|
|
442
443
|
metrics.graphExtraction.retryAttempts += toFiniteNumber(telemetry.retryAttempts);
|
|
444
|
+
metrics.graphExtraction.nonArrayBatchFailures += toFiniteNumber(telemetry.nonArrayBatchFailures);
|
|
443
445
|
}
|
|
444
446
|
}
|
|
445
447
|
metrics.graphExtraction.durationMs += toFiniteNumber(result.graphExtractionDurationMs);
|
|
@@ -587,6 +589,7 @@ function mergeImproveMetrics(dst, src) {
|
|
|
587
589
|
dst.graphExtraction.truncations += src.graphExtraction.truncations;
|
|
588
590
|
dst.graphExtraction.failures += src.graphExtraction.failures;
|
|
589
591
|
dst.graphExtraction.htmlErrors += src.graphExtraction.htmlErrors;
|
|
592
|
+
dst.graphExtraction.nonArrayBatchFailures += src.graphExtraction.nonArrayBatchFailures;
|
|
590
593
|
dst.graphExtraction.durationMs += src.graphExtraction.durationMs;
|
|
591
594
|
dst.sessionExtraction.sessionsScanned += src.sessionExtraction.sessionsScanned;
|
|
592
595
|
dst.sessionExtraction.sessionsExtracted += src.sessionExtraction.sessionsExtracted;
|
|
@@ -990,6 +993,7 @@ const INTERESTING_DELTA_PATHS = [
|
|
|
990
993
|
"improve.graphExtraction.cacheHitRate",
|
|
991
994
|
"improve.graphExtraction.failures",
|
|
992
995
|
"improve.graphExtraction.htmlErrors",
|
|
996
|
+
"improve.graphExtraction.nonArrayBatchFailures",
|
|
993
997
|
"improve.sessionExtraction.sessionsScanned",
|
|
994
998
|
"improve.sessionExtraction.proposalsCreated",
|
|
995
999
|
"improve.autoAccept.promoted",
|
|
@@ -1655,6 +1659,7 @@ export function renderWindowCompareMd(windows, deltas) {
|
|
|
1655
1659
|
"improve.actions.reflect.failed",
|
|
1656
1660
|
"improve.actions.distill.llmFailed",
|
|
1657
1661
|
"improve.graphExtraction.failures",
|
|
1662
|
+
"improve.graphExtraction.nonArrayBatchFailures",
|
|
1658
1663
|
"improve.wallTime.medianMs",
|
|
1659
1664
|
"improve.wallTime.p95Ms",
|
|
1660
1665
|
"improve.memoryInference.skippedNoFacts",
|
package/dist/core/parse.js
CHANGED
|
@@ -98,16 +98,24 @@ export function parseJsonResponse(raw) {
|
|
|
98
98
|
* balanced `{ }` or `[ ]` structure in the text and attempts to parse that
|
|
99
99
|
* substring. Returns `undefined` if no valid JSON structure is found.
|
|
100
100
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* returned
|
|
101
|
+
* Shape preference is controlled by {@link ParseEmbeddedJsonOptions.expect}:
|
|
102
|
+
* - `"any"` (default): non-array results are preferred — a `{…}` object found
|
|
103
|
+
* first is returned immediately; arrays (`[…]`) are a fallback.
|
|
104
|
+
* - `"array"`: only top-level arrays are returned. The direct parse is
|
|
105
|
+
* accepted only if it is an array, and the scanner returns the first
|
|
106
|
+
* balanced `[…]` while skipping `{…}` openers entirely.
|
|
104
107
|
*/
|
|
105
|
-
export function parseEmbeddedJsonResponse(raw) {
|
|
108
|
+
export function parseEmbeddedJsonResponse(raw, options) {
|
|
109
|
+
const expectArray = options?.expect === "array";
|
|
106
110
|
const direct = parseJsonResponse(raw);
|
|
107
|
-
if (direct !== undefined)
|
|
111
|
+
if (direct !== undefined && (!expectArray || Array.isArray(direct)))
|
|
108
112
|
return direct;
|
|
109
113
|
const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
|
|
110
114
|
let arrayFallback;
|
|
115
|
+
// Scan only *top-level* balanced structures: once a `{…}`/`[…]` is matched we
|
|
116
|
+
// jump `start` past its closing bracket rather than re-scanning its interior.
|
|
117
|
+
// This keeps array mode from salvaging an array *nested inside* a leading
|
|
118
|
+
// object (e.g. the `entities` array of a bare `{entities,relations}` object).
|
|
111
119
|
for (let start = 0; start < text.length; start++) {
|
|
112
120
|
const opener = text[start];
|
|
113
121
|
if (opener !== "{" && opener !== "[")
|
|
@@ -116,6 +124,7 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
116
124
|
let depth = 0;
|
|
117
125
|
let inString = false;
|
|
118
126
|
let escaped = false;
|
|
127
|
+
let end = -1;
|
|
119
128
|
for (let i = start; i < text.length; i++) {
|
|
120
129
|
const ch = text[i];
|
|
121
130
|
if (inString) {
|
|
@@ -139,20 +148,31 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
139
148
|
if (ch === closer) {
|
|
140
149
|
depth -= 1;
|
|
141
150
|
if (depth === 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!Array.isArray(parsed)) {
|
|
145
|
-
return parsed;
|
|
146
|
-
}
|
|
147
|
-
arrayFallback ??= parsed;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
151
|
+
end = i;
|
|
152
|
+
break;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
if (end === -1)
|
|
157
|
+
continue; // never balanced — let the next opener try
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
160
|
+
if (Array.isArray(parsed)) {
|
|
161
|
+
// First valid array wins in array mode; in "any" mode it is the
|
|
162
|
+
// fallback returned only if no object is found.
|
|
163
|
+
if (expectArray)
|
|
164
|
+
return parsed;
|
|
165
|
+
arrayFallback ??= parsed;
|
|
166
|
+
}
|
|
167
|
+
else if (!expectArray) {
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
// Skip past this balanced structure so we don't descend into it.
|
|
171
|
+
start = end;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Malformed candidate — advance one char and try the next opener.
|
|
175
|
+
}
|
|
156
176
|
}
|
|
157
177
|
return arrayFallback;
|
|
158
178
|
}
|
|
@@ -372,6 +372,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
372
372
|
failureCount: 0,
|
|
373
373
|
htmlErrorCount: 0,
|
|
374
374
|
retryAttempts: 0,
|
|
375
|
+
nonArrayBatchFailures: 0,
|
|
375
376
|
};
|
|
376
377
|
const canReusePreviousGraph = previousGraph.telemetry?.extractorId === extractorId;
|
|
377
378
|
const runtimeTelemetry = {
|
|
@@ -625,6 +626,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
625
626
|
telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
|
|
626
627
|
telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
|
|
627
628
|
telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
|
|
629
|
+
telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
|
|
628
630
|
const qualityConsidered = mergedNodes.length;
|
|
629
631
|
const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
|
|
630
632
|
const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
@@ -433,6 +433,18 @@ function buildBatchSystemPrompt() {
|
|
|
433
433
|
"The array length MUST equal the number of assets provided. " +
|
|
434
434
|
'Use {"entities":[],"relations":[]} for assets with no extractable graph content.');
|
|
435
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Hardened system prompt for the single batch retry (#635). Used only after a
|
|
438
|
+
* first response failed array salvage — leans harder on "raw array only" so a
|
|
439
|
+
* model that wrapped the array in prose/fences corrects itself before we pay
|
|
440
|
+
* the per-asset fallback.
|
|
441
|
+
*/
|
|
442
|
+
function buildBatchRetrySystemPrompt() {
|
|
443
|
+
return (`${buildBatchSystemPrompt()} ` +
|
|
444
|
+
"Your previous response could NOT be parsed as a JSON array. " +
|
|
445
|
+
"Respond with ONLY the raw JSON array — start with '[' and end with ']'. " +
|
|
446
|
+
"No prose, no explanation, no markdown code fences, no preamble.");
|
|
447
|
+
}
|
|
436
448
|
function buildBatchUserPrompt(bodies) {
|
|
437
449
|
const count = bodies.length;
|
|
438
450
|
const assetBlocks = bodies.map((body, i) => `${BATCH_ASSET_SEPARATOR} ${i + 1} ===\n${body.trim()}`).join("\n\n");
|
|
@@ -542,7 +554,21 @@ export async function extractGraphFromBodies(llmConfig, bodies, signal, akmConfi
|
|
|
542
554
|
});
|
|
543
555
|
if (!raw)
|
|
544
556
|
return null;
|
|
545
|
-
|
|
557
|
+
// Array-preferring salvage (#635): the batch contract is a top-level
|
|
558
|
+
// JSON array. A leading/example `{…}` object in the response must not
|
|
559
|
+
// mask a valid `[…]` array as a false "non-array" failure.
|
|
560
|
+
let parsed = parseEmbeddedJsonResponse(raw, { expect: "array" });
|
|
561
|
+
if (!Array.isArray(parsed)) {
|
|
562
|
+
// One stricter-reprompt retry before paying the per-asset fallback
|
|
563
|
+
// (#635). Many genuine non-array responses recover when the model is
|
|
564
|
+
// told explicitly to emit only the raw array.
|
|
565
|
+
bumpTelemetry(options.telemetry, "retryAttempts");
|
|
566
|
+
const retryRaw = await chatCompletion(llmConfig, [
|
|
567
|
+
{ role: "system", content: buildBatchRetrySystemPrompt() },
|
|
568
|
+
{ role: "user", content: userPrompt },
|
|
569
|
+
], { temperature: 0, timeoutMs: llmConfig.timeoutMs, signal });
|
|
570
|
+
parsed = retryRaw ? parseEmbeddedJsonResponse(retryRaw, { expect: "array" }) : undefined;
|
|
571
|
+
}
|
|
546
572
|
if (!Array.isArray(parsed)) {
|
|
547
573
|
nonArrayResponse = true;
|
|
548
574
|
bumpTelemetry(options.telemetry, "nonArrayBatchFailures");
|
|
@@ -552,8 +578,9 @@ export async function extractGraphFromBodies(llmConfig, bodies, signal, akmConfi
|
|
|
552
578
|
batchState.batchingDisabled = true;
|
|
553
579
|
}
|
|
554
580
|
}
|
|
555
|
-
warn(`graph extraction (batch): LLM response was not a JSON array for ${nonEmptyBodies.length} asset(s)
|
|
556
|
-
`will fall back per-asset.
|
|
581
|
+
warn(`graph extraction (batch): LLM response was not a JSON array for ${nonEmptyBodies.length} asset(s) ` +
|
|
582
|
+
`even after a stricter retry; will fall back per-asset. ` +
|
|
583
|
+
`promptChars=${userPrompt.length}${formatContextHint(llmConfig)}`);
|
|
557
584
|
return null;
|
|
558
585
|
}
|
|
559
586
|
return parsed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|