akm-cli 0.10.261008-beta → 0.10.261010-beta
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/CHANGELOG.md +97 -0
- package/STABILITY.md +25 -8
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/assets/prompts/memory-infer-system.md +1 -1
- package/dist/assets/prompts/memory-infer-user.md +3 -2
- package/dist/assets/ui/app.js +59 -2
- package/dist/assets/ui/asset.js +71 -6
- package/dist/assets/ui/bundles.js +80 -3
- package/dist/assets/ui/diff-view.js +1 -1
- package/dist/assets/ui/editor.js +103 -0
- package/dist/assets/ui/index.html +2 -0
- package/dist/assets/ui/lib.js +51 -1
- package/dist/assets/ui/markdown.js +86 -19
- package/dist/assets/ui/search.js +25 -5
- package/dist/assets/ui/styles.css +70 -0
- package/dist/commands/health/improve-metrics.js +11 -1
- package/dist/commands/health/md-report.js +3 -3
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/loop-stages.js +2 -2
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +78 -19
- package/dist/commands/read/knowledge.js +109 -77
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/commands/sources/bundle-cli.js +4 -6
- package/dist/commands/sources/installed-stashes.js +10 -19
- package/dist/commands/ui/api.js +95 -5
- package/dist/commands/ui/document-source.js +51 -0
- package/dist/commands/ui/git-read.js +18 -2
- package/dist/commands/ui/proposal-actions.js +5 -3
- package/dist/commands/ui/server.js +25 -13
- package/dist/commands/ui/ui-cli.js +3 -3
- package/dist/commands/ui/workspace-actions.js +264 -0
- package/dist/commands/workflow-cli.js +3 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/akm-markdown.js +19 -3
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/asset/markdown.js +14 -6
- package/dist/core/config/config.js +6 -4
- package/dist/core/non-task-input.js +67 -3
- package/dist/core/write-source.js +2 -2
- package/dist/indexer/index-writer-lock.js +5 -1
- package/dist/indexer/passes/memory-inference.js +28 -6
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/llm/memory-infer.js +23 -14
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1820 -329
- package/dist/scripts/akm-migrate.js +1820 -329
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/dist/workflows/exec/environment.js +4 -8
- package/dist/workflows/parser.js +13 -4
- package/docs/architecture/0.10-process-rules.md +9 -7
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/internals/storage-locations.md +1 -1
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/guides/author-workflows.md +78 -28
- package/docs/guides/bundles.md +16 -2
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +90 -18
- package/docs/reference/env-and-secrets.md +11 -0
- package/docs/reference/memory.md +7 -6
- package/docs/reference/ui-api.md +84 -8
- package/docs/reference/workflow-schema.md +63 -32
- package/package.json +2 -1
|
@@ -64,6 +64,12 @@ const FM_INFERRED = "inferred";
|
|
|
64
64
|
const FM_INFERENCE_PROCESSED = "inferenceProcessed";
|
|
65
65
|
const FM_SOURCE = "source";
|
|
66
66
|
const FM_CAPTURE_MODE = "captureMode";
|
|
67
|
+
/**
|
|
68
|
+
* Cache namespace for this pass. Bump it when the cached response semantics
|
|
69
|
+
* change (v3: a cached row may be an explicit no-change decision); rows under an
|
|
70
|
+
* older namespace are never read again, which is a disposable cache miss.
|
|
71
|
+
*/
|
|
72
|
+
const MEMORY_INFERENCE_CACHE_VARIANT = "memory-inference-v3";
|
|
67
73
|
function validateDerivedMemoryDraft(raw) {
|
|
68
74
|
if (!raw || typeof raw !== "object")
|
|
69
75
|
return undefined;
|
|
@@ -79,6 +85,10 @@ function validateDerivedMemoryDraft(raw) {
|
|
|
79
85
|
? { title, description, tags, searchHints, content }
|
|
80
86
|
: undefined;
|
|
81
87
|
}
|
|
88
|
+
/** Cache validator: a recognised no-change decision, a complete draft, or `undefined` (unusable → recompute). */
|
|
89
|
+
function validateMemoryInferDecision(raw) {
|
|
90
|
+
return memoryInfer.isMemoryInferNoChange(raw) ? memoryInfer.MEMORY_INFER_NO_CHANGE : validateDerivedMemoryDraft(raw);
|
|
91
|
+
}
|
|
82
92
|
function planPendingMemoryRecord(record, ctx) {
|
|
83
93
|
if (ctx.signal?.aborted)
|
|
84
94
|
return { kind: "aborted", record };
|
|
@@ -86,11 +96,11 @@ function planPendingMemoryRecord(record, ctx) {
|
|
|
86
96
|
return { kind: "existing-child", record };
|
|
87
97
|
if (ctx.db && !ctx.reEnrich) {
|
|
88
98
|
const cached = bestEffort(() => {
|
|
89
|
-
const entry = getLlmCacheEntry(ctx.db, record.filePath, computeBodyHash(record.body),
|
|
90
|
-
return entry ?
|
|
99
|
+
const entry = getLlmCacheEntry(ctx.db, record.filePath, computeBodyHash(record.body), MEMORY_INFERENCE_CACHE_VARIANT);
|
|
100
|
+
return entry ? validateMemoryInferDecision(JSON.parse(entry.resultJson)) : undefined;
|
|
91
101
|
}, "memory inference cache read corrupt — fall through to recompute");
|
|
92
102
|
if (cached)
|
|
93
|
-
return { kind: "cache-hit", record,
|
|
103
|
+
return { kind: "cache-hit", record, decision: cached };
|
|
94
104
|
}
|
|
95
105
|
return { kind: "model", record };
|
|
96
106
|
}
|
|
@@ -132,11 +142,11 @@ async function inferPendingMemoryRecord(plan, ctx) {
|
|
|
132
142
|
retryAttempts += 1;
|
|
133
143
|
};
|
|
134
144
|
const infer = () => compressMemoryToDerivedMemory(llmRunner, record.body, signal, featureConfig, (event) => warn(`[akm] LLM fallback for ${event.feature}: ${event.reason}`), inferTelemetry, onRetryAttempt, onNotices);
|
|
135
|
-
let derived = plan.kind === "cache-hit" ? plan.
|
|
145
|
+
let derived = plan.kind === "cache-hit" ? plan.decision : undefined;
|
|
136
146
|
try {
|
|
137
147
|
if (plan.kind === "model") {
|
|
138
148
|
derived = db
|
|
139
|
-
? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, infer,
|
|
149
|
+
? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, infer, validateMemoryInferDecision, undefined, MEMORY_INFERENCE_CACHE_VARIANT, {
|
|
140
150
|
onCacheHit: () => {
|
|
141
151
|
fromCache = true;
|
|
142
152
|
},
|
|
@@ -153,6 +163,12 @@ async function inferPendingMemoryRecord(plan, ctx) {
|
|
|
153
163
|
}
|
|
154
164
|
if (!derived)
|
|
155
165
|
return { skipped: true, fromCache, retryAttempts };
|
|
166
|
+
// An explicit decision, not a failure. The parent is left exactly as it is: stamping it
|
|
167
|
+
// `inferenceProcessed` would hide every later edit, and the cached decision already
|
|
168
|
+
// stops the retry for as long as the effective input is unchanged.
|
|
169
|
+
if (memoryInfer.isMemoryInferNoChange(derived)) {
|
|
170
|
+
return { skipped: false, splitParent: false, written: 0, noChange: true, fromCache, retryAttempts };
|
|
171
|
+
}
|
|
156
172
|
const writeOutcome = await writeDerivedMemory(record, derived);
|
|
157
173
|
if (writeOutcome.written > 0) {
|
|
158
174
|
markParentProcessed(record);
|
|
@@ -209,6 +225,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
209
225
|
retryAttempts: 0,
|
|
210
226
|
splitParents: 0,
|
|
211
227
|
writtenFacts: 0,
|
|
228
|
+
noChange: 0,
|
|
212
229
|
skippedNoFacts: 0,
|
|
213
230
|
skippedChildExists: 0,
|
|
214
231
|
skippedAborted: 0,
|
|
@@ -273,7 +290,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
273
290
|
assertRunnerCredentials(llmRunner);
|
|
274
291
|
let processed = 0;
|
|
275
292
|
const total = pending.length;
|
|
276
|
-
onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
|
|
293
|
+
onProgress?.({ processed, total, writtenFacts: 0, noChange: 0, skippedNoFacts: 0 });
|
|
277
294
|
let configFailure;
|
|
278
295
|
const perRecordResults = await concurrentMap(plans, (plan) => inferPendingMemoryRecord(plan, {
|
|
279
296
|
config,
|
|
@@ -305,6 +322,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
305
322
|
processed,
|
|
306
323
|
total,
|
|
307
324
|
writtenFacts: result.writtenFacts,
|
|
325
|
+
noChange: result.noChange,
|
|
308
326
|
skippedNoFacts: result.skippedNoFacts,
|
|
309
327
|
currentRef: pending[i]?.ref,
|
|
310
328
|
});
|
|
@@ -321,6 +339,9 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
321
339
|
// Intentionally NOT marked processed — a transient LLM failure should
|
|
322
340
|
// be retried on the next index run.
|
|
323
341
|
}
|
|
342
|
+
else if ("noChange" in res && res.noChange) {
|
|
343
|
+
result.noChange += 1;
|
|
344
|
+
}
|
|
324
345
|
else if (res.splitParent) {
|
|
325
346
|
result.splitParents += 1;
|
|
326
347
|
result.writtenFacts += res.written;
|
|
@@ -349,6 +370,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
|
|
|
349
370
|
processed,
|
|
350
371
|
total,
|
|
351
372
|
writtenFacts: result.writtenFacts,
|
|
373
|
+
noChange: result.noChange,
|
|
352
374
|
skippedNoFacts: result.skippedNoFacts,
|
|
353
375
|
currentRef: pending[i]?.ref,
|
|
354
376
|
});
|
|
@@ -165,6 +165,8 @@ export function validateStashEntry(entry) {
|
|
|
165
165
|
const evidenceSources = normalizeNonEmptyStringList(e.evidenceSources);
|
|
166
166
|
if (evidenceSources)
|
|
167
167
|
result.evidenceSources = evidenceSources;
|
|
168
|
+
if (typeof e.evidence === "string" && e.evidence.trim().length > 0)
|
|
169
|
+
result.evidence = e.evidence;
|
|
168
170
|
if (typeof e.derivedFrom === "string" && e.derivedFrom.trim().length > 0) {
|
|
169
171
|
result.derivedFrom = e.derivedFrom.trim();
|
|
170
172
|
}
|
|
@@ -242,6 +244,19 @@ export function applyScopeFrontmatter(entry, fmData) {
|
|
|
242
244
|
if (scope)
|
|
243
245
|
entry.scope = scope;
|
|
244
246
|
}
|
|
247
|
+
/** The same normalized tuple the index projects, including legacy nested scope. */
|
|
248
|
+
export function scopeFromFrontmatter(fm) {
|
|
249
|
+
const entry = { type: "memory", name: "scope" };
|
|
250
|
+
if (typeof fm.scope === "object" && fm.scope !== null && !Array.isArray(fm.scope)) {
|
|
251
|
+
entry.scope = normalizeScopeObject(fm.scope);
|
|
252
|
+
}
|
|
253
|
+
applyScopeFrontmatter(entry, fm);
|
|
254
|
+
return entry.scope ?? {};
|
|
255
|
+
}
|
|
256
|
+
/** Exact maintenance equality: absence is a value, never a wildcard. */
|
|
257
|
+
export function sameScope(a, b) {
|
|
258
|
+
return SCOPE_KEYS.every((key) => a[key] === b[key]);
|
|
259
|
+
}
|
|
245
260
|
function normalizeIntent(value) {
|
|
246
261
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
247
262
|
return undefined;
|
|
@@ -367,6 +382,8 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
367
382
|
const evidenceSources = normalizeNonEmptyStringList(fmData.evidenceSources);
|
|
368
383
|
if (evidenceSources)
|
|
369
384
|
entry.evidenceSources = evidenceSources;
|
|
385
|
+
if (typeof fmData.evidence === "string" && fmData.evidence.trim().length > 0)
|
|
386
|
+
entry.evidence = fmData.evidence;
|
|
370
387
|
// Phase 5A / Advantage D5: capture parent ref for derived memories.
|
|
371
388
|
// Memory-inference writes `source: "memories/<parent>"` and `inferred: true`
|
|
372
389
|
// (and a derived child name suffix `.derived`). We mirror that source ref into
|
|
@@ -138,6 +138,7 @@ export function indexDocumentToStashEntry(doc) {
|
|
|
138
138
|
entry.wikiRole = dj.wikiRole;
|
|
139
139
|
assignStringList(entry, "sources", dj.sources);
|
|
140
140
|
assignStringList(entry, "evidenceSources", dj.evidenceSources);
|
|
141
|
+
assignString(entry, "evidence", dj.evidence);
|
|
141
142
|
assignStringList(entry, "uses", dj.uses);
|
|
142
143
|
// D2 (#730): OKF v0.2 provenance promoteProposal stamps onto AKM-native
|
|
143
144
|
// writes, carried via DOCUMENT_JSON_CARRIED_FIELDS (akm-adapter.ts) —
|
|
@@ -22,7 +22,7 @@ import { embed } from "../../llm/embedder.js";
|
|
|
22
22
|
import { applyEmbeddingTemplate, resolveEmbeddingProfile } from "../../llm/embedders/profile.js";
|
|
23
23
|
import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
|
|
24
24
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
25
|
-
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getEntryRefsAndTypes, } from "../../storage/repositories/index-entries-repository.js";
|
|
25
|
+
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntriesByIds, getEntryById, getEntryCount, getEntryRefsAndTypes, } from "../../storage/repositories/index-entries-repository.js";
|
|
26
26
|
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
27
27
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
28
28
|
import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
@@ -221,8 +221,14 @@ async function searchDatabase(db, input) {
|
|
|
221
221
|
const typeFilter = searchType === "any" ? undefined : searchType;
|
|
222
222
|
const startedAt = Date.now();
|
|
223
223
|
// The query embedding request goes out first, so FTS runs while it is in flight.
|
|
224
|
-
const
|
|
225
|
-
|
|
224
|
+
const filterCandidates = (ids) => {
|
|
225
|
+
const entries = getEntriesByIds(db, ids)
|
|
226
|
+
.filter(hasIndexedProvenance)
|
|
227
|
+
.filter((row) => (typeFilter ? row.entry.type === typeFilter : !defaultExcludes.includes(row.entry.type)));
|
|
228
|
+
return new Set(applyEntryFilters(entries, filterOptions).map((row) => row.id));
|
|
229
|
+
};
|
|
230
|
+
const vectorChannel = startVectorChannel(db, query, config, filterCandidates);
|
|
231
|
+
const lexical = searchFts(db, query, CHANNEL_DEPTH, typeFilter, defaultExcludes, filterCandidates);
|
|
226
232
|
const vector = await vectorChannel;
|
|
227
233
|
const embedMs = Date.now() - startedAt;
|
|
228
234
|
const tRank0 = Date.now();
|
|
@@ -244,6 +250,7 @@ async function searchDatabase(db, input) {
|
|
|
244
250
|
sources,
|
|
245
251
|
config,
|
|
246
252
|
db,
|
|
253
|
+
eligible: (row) => applyEntryFilters([row], filterOptions).length > 0,
|
|
247
254
|
})));
|
|
248
255
|
const mode = vector.warning ? "fts-fallback" : vector.neighbors ? "semantic" : "keyword";
|
|
249
256
|
return { embedMs, rankMs, hits, mode, semanticWarning: vector.warning, feedbackWarning: feedback.warning };
|
|
@@ -371,6 +378,7 @@ async function enumerateEntries(opts) {
|
|
|
371
378
|
sources,
|
|
372
379
|
config,
|
|
373
380
|
db,
|
|
381
|
+
eligible: (row) => applyEntryFilters([row], opts).length > 0,
|
|
374
382
|
})));
|
|
375
383
|
}
|
|
376
384
|
/**
|
|
@@ -378,7 +386,7 @@ async function enumerateEntries(opts) {
|
|
|
378
386
|
* source-narrowing → scope → proposed-quality → derived-twin belief
|
|
379
387
|
* inheritance → belief filter.
|
|
380
388
|
*/
|
|
381
|
-
function applyEntryFilters(items, opts) {
|
|
389
|
+
export function applyEntryFilters(items, opts) {
|
|
382
390
|
const { filters } = opts;
|
|
383
391
|
// Source filter: when the caller narrowed `sources` via `--from <name>`,
|
|
384
392
|
// drop entries whose filePath does not live under any requested source. The
|
|
@@ -428,7 +436,7 @@ function inheritDerivedTwinBeliefStates(db, items) {
|
|
|
428
436
|
}
|
|
429
437
|
}
|
|
430
438
|
}
|
|
431
|
-
function matchBeliefFilter(beliefState, filter) {
|
|
439
|
+
export function matchBeliefFilter(beliefState, filter) {
|
|
432
440
|
if (filter === "all")
|
|
433
441
|
return true;
|
|
434
442
|
// 03: the belief filter applies to ANY flagged entry, not just memories, so
|
|
@@ -452,7 +460,7 @@ function matchBeliefFilter(beliefState, filter) {
|
|
|
452
460
|
* query overlaps it. A slow or failing embedder degrades the search to keyword
|
|
453
461
|
* ranking with a warning after `embedding.queryTimeoutMs`.
|
|
454
462
|
*/
|
|
455
|
-
function startVectorChannel(db, query, config) {
|
|
463
|
+
function startVectorChannel(db, query, config, filterCandidates) {
|
|
456
464
|
if (config.semanticSearchMode === "off")
|
|
457
465
|
return Promise.resolve({ neighbors: null });
|
|
458
466
|
// A real-time completeness fact, not a cached verdict: skip the round trip
|
|
@@ -472,7 +480,7 @@ function startVectorChannel(db, query, config) {
|
|
|
472
480
|
});
|
|
473
481
|
const queryText = applyEmbeddingTemplate(resolveEmbeddingProfile(config.embedding).queryTemplate, query);
|
|
474
482
|
return Promise.race([embed(queryText, config.embedding, controller.signal), timeout])
|
|
475
|
-
.then((vector) => ({ neighbors: searchVec(db, vector, CHANNEL_DEPTH) }))
|
|
483
|
+
.then((vector) => ({ neighbors: searchVec(db, vector, CHANNEL_DEPTH, undefined, filterCandidates) }))
|
|
476
484
|
.catch((error) => ({ neighbors: null, warning: buildVectorFallbackWarning(config, error) }))
|
|
477
485
|
.finally(() => clearTimeout(timer));
|
|
478
486
|
}
|
|
@@ -574,6 +582,7 @@ export async function buildDbHit(input) {
|
|
|
574
582
|
stashDir: entryStashDir,
|
|
575
583
|
bundleId: input.bundleId,
|
|
576
584
|
db: input.db,
|
|
585
|
+
eligible: input.eligible,
|
|
577
586
|
});
|
|
578
587
|
return hit;
|
|
579
588
|
}
|
|
@@ -17,7 +17,7 @@ export async function enrichSearchHit(hit, ctx) {
|
|
|
17
17
|
renderer?.enrichSearchHit?.(hit, ctx.stashDir);
|
|
18
18
|
}
|
|
19
19
|
if (ctx.type === "memory" && ctx.db)
|
|
20
|
-
surfaceDerivedMemory(hit, ctx.db, ctx.bundleId);
|
|
20
|
+
surfaceDerivedMemory(hit, ctx.db, ctx.bundleId, ctx.eligible);
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Phase 5A / Advantage D5 — when a parent memory has a `.derived` child
|
|
@@ -28,7 +28,7 @@ export async function enrichSearchHit(hit, ctx) {
|
|
|
28
28
|
* The parent ref is preserved on the hit — only the surface text is
|
|
29
29
|
* swapped, so links and provenance still point at the canonical parent.
|
|
30
30
|
*/
|
|
31
|
-
function surfaceDerivedMemory(hit, db, bundleId) {
|
|
31
|
+
function surfaceDerivedMemory(hit, db, bundleId, eligible) {
|
|
32
32
|
// Never recurse: a `.derived` hit is itself the child surface; leaving
|
|
33
33
|
// it untouched also avoids `<parent>.derived.derived` chains.
|
|
34
34
|
if (hit.name.toLowerCase().endsWith(".derived"))
|
|
@@ -38,7 +38,7 @@ function surfaceDerivedMemory(hit, db, bundleId) {
|
|
|
38
38
|
// carries. INTERNAL lookup key into `getDerivedForParent`: the `derived_from`
|
|
39
39
|
// column stores this same conceptId grammar.
|
|
40
40
|
const derived = getDerivedForParent(db, `memories/${hit.name}`, bundleId);
|
|
41
|
-
if (!derived)
|
|
41
|
+
if (!derived || (eligible && !eligible(derived)))
|
|
42
42
|
return;
|
|
43
43
|
// Swap description / tags from the derived child (SourceSearchHit carries no
|
|
44
44
|
// `searchHints`).
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -34,35 +34,40 @@ const PROMPT_PLACEHOLDERS = new Set([
|
|
|
34
34
|
"search phrase 2",
|
|
35
35
|
"2-3 sentence compressed body preserving key facts verbatim",
|
|
36
36
|
]);
|
|
37
|
+
export const MEMORY_INFER_NO_CHANGE = Object.freeze({ noChange: true });
|
|
38
|
+
export function isMemoryInferNoChange(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && value.noChange === true;
|
|
40
|
+
}
|
|
37
41
|
/**
|
|
38
42
|
* Strict JSON Schema for the derived-memory payload. Sent as `response_format`
|
|
39
43
|
* unless the engine sets `supportsJsonSchema: false`; the client drops it once
|
|
40
44
|
* for a provider that rejects it.
|
|
41
45
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* — no more "incomplete derived memory payload from LLM; skipping memory"
|
|
47
|
-
* for shape-only failures.
|
|
46
|
+
* Every property is required (a strict provider refuses otherwise), so the
|
|
47
|
+
* explicit no-change decision is `noChange: true` with the other fields empty.
|
|
48
|
+
* That also means the schema cannot demand non-empty fields: the parser below
|
|
49
|
+
* is the gate that requires a complete payload whenever `noChange` is false.
|
|
48
50
|
*/
|
|
49
51
|
export const DERIVED_MEMORY_JSON_SCHEMA = {
|
|
50
52
|
type: "object",
|
|
51
53
|
properties: {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
noChange: { type: "boolean" },
|
|
55
|
+
title: { type: "string" },
|
|
56
|
+
description: { type: "string" },
|
|
57
|
+
content: { type: "string" },
|
|
58
|
+
tags: { type: "array", items: { type: "string" }, maxItems: 8 },
|
|
59
|
+
searchHints: { type: "array", items: { type: "string" }, maxItems: 6 },
|
|
57
60
|
},
|
|
58
|
-
required: ["title", "description", "content", "tags", "searchHints"],
|
|
61
|
+
required: ["noChange", "title", "description", "content", "tags", "searchHints"],
|
|
59
62
|
additionalProperties: false,
|
|
60
63
|
};
|
|
61
64
|
/**
|
|
62
65
|
* Compress a single memory body into one derived memory via the configured LLM.
|
|
63
66
|
*
|
|
64
|
-
* Returns
|
|
65
|
-
*
|
|
67
|
+
* Returns the draft, or {@link MEMORY_INFER_NO_CHANGE} when the model explicitly
|
|
68
|
+
* decides the memory needs no derived memory. Returns `undefined` on any failure
|
|
69
|
+
* (timeout, invalid JSON, empty or incomplete response, HTML, abort, disabled
|
|
70
|
+
* pass), which is never the same thing as no-change. Errors are logged via `warn()` but never thrown — a failed split for one memory
|
|
66
71
|
* must not abort the rest of the index pass.
|
|
67
72
|
*
|
|
68
73
|
* Routes through `callStructured({ feature: "memory_inference", ... })` so the
|
|
@@ -105,6 +110,10 @@ export async function compressMemoryToDerivedMemory(llmRunner, body, signal, akm
|
|
|
105
110
|
warn("memory inference: invalid JSON response from LLM; skipping memory.");
|
|
106
111
|
return undefined;
|
|
107
112
|
}
|
|
113
|
+
// Only a literal boolean `true` is a decision; anything else falls through
|
|
114
|
+
// to the complete-payload checks, so a malformed reply is never restraint.
|
|
115
|
+
if (parsed.noChange === true)
|
|
116
|
+
return MEMORY_INFER_NO_CHANGE;
|
|
108
117
|
const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
|
|
109
118
|
const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
|
|
110
119
|
const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
|
@@ -5,7 +5,7 @@ import { capDescription, NORMAL_DESCRIPTION_LIMIT, pickFields } from "./helpers.
|
|
|
5
5
|
// Curation is a small, high-signal top-N. Even at `brief` we keep `followUp`
|
|
6
6
|
// (the actionable `akm show <ref>` command) and `reason` (why this asset was
|
|
7
7
|
// selected) — these are the point of curate, unlike a bulk search listing.
|
|
8
|
-
const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "followUp", "reason"];
|
|
8
|
+
const BRIEF_FIELDS = ["source", "type", "name", "ref", "id", "supportRefs", "beliefState", "followUp", "reason"];
|
|
9
9
|
const NORMAL_FIELDS = [
|
|
10
10
|
"source",
|
|
11
11
|
"type",
|
|
@@ -21,6 +21,7 @@ const NORMAL_FIELDS = [
|
|
|
21
21
|
"parameters",
|
|
22
22
|
"run",
|
|
23
23
|
"supportRefs",
|
|
24
|
+
"beliefState",
|
|
24
25
|
"followUp",
|
|
25
26
|
"reason",
|
|
26
27
|
"score",
|
|
@@ -37,6 +38,7 @@ const AGENT_FIELDS = [
|
|
|
37
38
|
"id",
|
|
38
39
|
"description",
|
|
39
40
|
"supportRefs",
|
|
41
|
+
"beliefState",
|
|
40
42
|
"followUp",
|
|
41
43
|
"reason",
|
|
42
44
|
"score",
|
|
@@ -386,6 +386,9 @@ export function shapeShowOutput(result, detail, shape = "human") {
|
|
|
386
386
|
"steps",
|
|
387
387
|
"keys",
|
|
388
388
|
"links",
|
|
389
|
+
"evidence",
|
|
390
|
+
"evidenceSources",
|
|
391
|
+
"provenance",
|
|
389
392
|
...FRAGMENT_PROVENANCE_FIELDS,
|
|
390
393
|
...FRAGMENT_CONTEXT_FIELDS,
|
|
391
394
|
]);
|
|
@@ -418,6 +421,9 @@ export function shapeShowOutput(result, detail, shape = "human") {
|
|
|
418
421
|
"activeRun",
|
|
419
422
|
"keys",
|
|
420
423
|
"links",
|
|
424
|
+
"evidence",
|
|
425
|
+
"evidenceSources",
|
|
426
|
+
"provenance",
|
|
421
427
|
...FRAGMENT_PROVENANCE_FIELDS,
|
|
422
428
|
...FRAGMENT_CONTEXT_FIELDS,
|
|
423
429
|
// ref, path, and editable are always projected — at every --detail level,
|
|
@@ -20,6 +20,13 @@ export function formatShowPlain(r, detail) {
|
|
|
20
20
|
}
|
|
21
21
|
if (r.description)
|
|
22
22
|
lines.push(`description: ${String(r.description)}`);
|
|
23
|
+
// Evidence is captured text: keep it on one line so it cannot forge other fields.
|
|
24
|
+
if (r.evidence)
|
|
25
|
+
lines.push(`evidence: ${String(r.evidence).replace(/\s+/g, " ")}`);
|
|
26
|
+
if (Array.isArray(r.evidenceSources))
|
|
27
|
+
lines.push(`evidenceSources: ${r.evidenceSources.join(", ")}`);
|
|
28
|
+
if (r.evidence && r.provenance)
|
|
29
|
+
lines.push(`provenance: ${JSON.stringify(r.provenance)}`);
|
|
23
30
|
if (r.workflowTitle)
|
|
24
31
|
lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
|
|
25
32
|
if (r.agent)
|