akm-cli 0.9.0-beta.50 → 0.9.0-beta.52
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 +2 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/wiki/ingest-workflow-template.md +34 -12
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli.js +12 -6
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +17 -2
- package/dist/commands/env/secret-cli.js +24 -2
- package/dist/commands/health/checks.js +1 -1
- package/dist/commands/improve/improve-auto-accept.js +30 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-result-file.js +9 -2
- package/dist/commands/improve/preparation.js +10 -2
- package/dist/commands/improve/recombine.js +52 -15
- package/dist/commands/lint/env-key-rules.js +4 -0
- package/dist/commands/read/knowledge.js +5 -2
- package/dist/commands/read/search-cli.js +2 -4
- package/dist/commands/read/search.js +9 -6
- package/dist/commands/read/show.js +19 -5
- package/dist/commands/sources/init.js +13 -8
- package/dist/commands/sources/installed-stashes.js +6 -2
- package/dist/commands/sources/schema-repair.js +33 -47
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/tasks/tasks.js +38 -10
- package/dist/core/asset/asset-registry.js +1 -1
- package/dist/core/asset/asset-spec.js +4 -2
- package/dist/core/config/config-migration.js +12 -11
- package/dist/indexer/passes/memory-inference.js +3 -2
- package/dist/indexer/search/db-search.js +6 -4
- package/dist/indexer/search/search-source.js +15 -2
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -135
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/renderers.js +1 -1
- package/dist/scripts/migrate-storage.js +463 -347
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/git-install.js +10 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/website.js +8 -3
- package/dist/sources/website-ingest.js +136 -20
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +15 -11
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +8 -8
|
@@ -41,7 +41,7 @@ export const ACTION_BUILDERS = {
|
|
|
41
41
|
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
42
42
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
43
43
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
44
|
-
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
44
|
+
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
|
|
45
45
|
secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
|
|
46
46
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
47
47
|
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildWorkflowAction } from "../../output/renderers.js";
|
|
6
|
-
import { toPosix } from "../common.js";
|
|
7
6
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
|
|
7
|
+
function toPosix(input) {
|
|
8
|
+
return input.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
8
10
|
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
9
11
|
const markdownSpec = {
|
|
10
12
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
@@ -89,7 +91,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
89
91
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
90
92
|
},
|
|
91
93
|
rendererName: "env-file",
|
|
92
|
-
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
94
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
|
|
93
95
|
},
|
|
94
96
|
// Secrets — a single sensitive value used on its own for authentication (a
|
|
95
97
|
// PEM key, API token, TLS cert). Unlike `env` (a group of related .env
|
|
@@ -169,7 +169,8 @@ function migrateProcessEntryToImprove(result, processName, legacy) {
|
|
|
169
169
|
* (canonical string sentinel for this release) or when `configVersion` is a
|
|
170
170
|
* number ≥ 2 AND the config carries no recognized legacy keys.
|
|
171
171
|
*/
|
|
172
|
-
export function migrateConfigShape(raw) {
|
|
172
|
+
export function migrateConfigShape(raw, opts) {
|
|
173
|
+
const emitWarn = opts?.warn ?? warn;
|
|
173
174
|
const hasLegacyKeys = Object.hasOwn(raw, "features") ||
|
|
174
175
|
(isObj(raw.llm) && (Object.hasOwn(raw.llm, "endpoint") || Object.hasOwn(raw.llm, "features"))) ||
|
|
175
176
|
isObj(raw.agent) ||
|
|
@@ -199,11 +200,11 @@ export function migrateConfigShape(raw) {
|
|
|
199
200
|
if (Array.isArray(result.stashes)) {
|
|
200
201
|
if (!Array.isArray(result.sources)) {
|
|
201
202
|
result.sources = result.stashes;
|
|
202
|
-
|
|
203
|
+
emitWarn("[akm config-migrate] Legacy `stashes[]` config key renamed to `sources[]`. " +
|
|
203
204
|
"Re-save your config to remove the deprecation notice.");
|
|
204
205
|
}
|
|
205
206
|
else {
|
|
206
|
-
|
|
207
|
+
emitWarn("[akm config-migrate] Both `stashes[]` and `sources[]` present; `stashes[]` dropped (sources takes precedence).");
|
|
207
208
|
}
|
|
208
209
|
delete result.stashes;
|
|
209
210
|
changed = true;
|
|
@@ -218,7 +219,7 @@ export function migrateConfigShape(raw) {
|
|
|
218
219
|
for (const entry of sources) {
|
|
219
220
|
if (isObj(entry) && entry.type === "openviking") {
|
|
220
221
|
const name = typeof entry.name === "string" && entry.name ? entry.name : "unnamed";
|
|
221
|
-
|
|
222
|
+
emitWarn(`[akm config-migrate] Source "${name}" (type: openviking) is no longer supported. ` +
|
|
222
223
|
"Remove it from your config, or replace with a `website`/`git` source. " +
|
|
223
224
|
"Entry dropped from sources[].");
|
|
224
225
|
renamed = true;
|
|
@@ -349,11 +350,11 @@ export function migrateConfigShape(raw) {
|
|
|
349
350
|
const camelKey = toCamelCase(legacyKey);
|
|
350
351
|
if (typeof legacyVal === "boolean" || isObj(legacyVal)) {
|
|
351
352
|
migrateProcessEntryToImprove(result, camelKey, legacyVal);
|
|
352
|
-
|
|
353
|
+
emitWarn(`[akm config-migrate] Unknown features.improve.${legacyKey} migrated to ` +
|
|
353
354
|
`profiles.improve.default.processes.${camelKey}. Please verify the new location.`);
|
|
354
355
|
}
|
|
355
356
|
else {
|
|
356
|
-
|
|
357
|
+
emitWarn(`[akm config-migrate] features.improve.${legacyKey} has an unrecognized value type ` +
|
|
357
358
|
`(${typeof legacyVal}); dropping. Please re-add it under profiles.improve.* manually.`);
|
|
358
359
|
}
|
|
359
360
|
}
|
|
@@ -404,11 +405,11 @@ export function migrateConfigShape(raw) {
|
|
|
404
405
|
const camelKey = toCamelCase(legacyKey);
|
|
405
406
|
const ok = migrateGenericGateToSection(result, "index", camelKey, legacyVal);
|
|
406
407
|
if (ok) {
|
|
407
|
-
|
|
408
|
+
emitWarn(`[akm config-migrate] Unknown features.index.${legacyKey} migrated to ` +
|
|
408
409
|
`index.${camelKey}. Please verify the new location is correct.`);
|
|
409
410
|
}
|
|
410
411
|
else {
|
|
411
|
-
|
|
412
|
+
emitWarn(`[akm config-migrate] features.index.${legacyKey} has an unrecognized value shape; ` +
|
|
412
413
|
`dropping. Please re-add it under index.${camelKey} manually if needed.`);
|
|
413
414
|
}
|
|
414
415
|
}
|
|
@@ -434,11 +435,11 @@ export function migrateConfigShape(raw) {
|
|
|
434
435
|
const camelKey = toCamelCase(legacyKey);
|
|
435
436
|
const ok = migrateGenericGateToSection(result, "search", camelKey, legacyVal);
|
|
436
437
|
if (ok) {
|
|
437
|
-
|
|
438
|
+
emitWarn(`[akm config-migrate] Unknown features.search.${legacyKey} migrated to ` +
|
|
438
439
|
`search.${camelKey}. Please verify the new location is correct.`);
|
|
439
440
|
}
|
|
440
441
|
else {
|
|
441
|
-
|
|
442
|
+
emitWarn(`[akm config-migrate] features.search.${legacyKey} has an unrecognized value shape; ` +
|
|
442
443
|
`dropping. Please re-add it under search.${camelKey} manually if needed.`);
|
|
443
444
|
}
|
|
444
445
|
}
|
|
@@ -555,7 +556,7 @@ export function migrateConfigShape(raw) {
|
|
|
555
556
|
changed = true;
|
|
556
557
|
}
|
|
557
558
|
if (improveObj.preset !== undefined) {
|
|
558
|
-
|
|
559
|
+
emitWarn("[akm config-migrate] defaults.improve.preset is no longer supported. " +
|
|
559
560
|
"Use `--profile <name>` (built-ins: default, quick, thorough, memory-focus) instead.");
|
|
560
561
|
}
|
|
561
562
|
delete defaultsRaw.improve;
|
|
@@ -72,6 +72,7 @@ const FM_CAPTURE_MODE = "captureMode";
|
|
|
72
72
|
*/
|
|
73
73
|
export async function runMemoryInferencePass(ctx) {
|
|
74
74
|
const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
|
|
75
|
+
const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
|
|
75
76
|
const result = {
|
|
76
77
|
considered: 0,
|
|
77
78
|
cacheHits: 0,
|
|
@@ -172,14 +173,14 @@ export async function runMemoryInferencePass(ctx) {
|
|
|
172
173
|
retryAttempts += 1;
|
|
173
174
|
};
|
|
174
175
|
const derived = db
|
|
175
|
-
? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () =>
|
|
176
|
+
? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
|
|
176
177
|
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
177
178
|
}, inferTelemetry, onRetryAttempt), validate, undefined, "", {
|
|
178
179
|
onCacheHit: () => {
|
|
179
180
|
fromCache = true;
|
|
180
181
|
},
|
|
181
182
|
})
|
|
182
|
-
: await
|
|
183
|
+
: await compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
|
|
183
184
|
warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
|
|
184
185
|
}, inferTelemetry, onRetryAttempt);
|
|
185
186
|
if (!derived) {
|
|
@@ -66,6 +66,8 @@ export async function searchLocal(input) {
|
|
|
66
66
|
const beliefFilter = input.beliefFilter ?? "all";
|
|
67
67
|
const restrictToSources = input.restrictToSources === true;
|
|
68
68
|
const includeExcludedTypes = input.includeExcludedTypes === true;
|
|
69
|
+
const disableProjectContext = input.disableProjectContext === true;
|
|
70
|
+
const disableScopedUtility = input.disableScopedUtility === true;
|
|
69
71
|
const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
|
|
70
72
|
const allSourceDirs = sources.map((s) => s.path);
|
|
71
73
|
const rawStatus = readSemanticStatus();
|
|
@@ -115,7 +117,7 @@ export async function searchLocal(input) {
|
|
|
115
117
|
mode: "keyword",
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
|
-
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes);
|
|
120
|
+
const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
|
|
119
121
|
return {
|
|
120
122
|
hits,
|
|
121
123
|
tip: hits.length === 0
|
|
@@ -132,7 +134,7 @@ export async function searchLocal(input) {
|
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
// ── Database search ─────────────────────────────────────────────────────────
|
|
135
|
-
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false) {
|
|
137
|
+
async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false, disableProjectContext = false, disableScopedUtility = false) {
|
|
136
138
|
const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
|
|
137
139
|
// #627 — resolve the default type-exclusion policy. It applies ONLY on the
|
|
138
140
|
// untyped ('any') path and only when the caller did not opt back in via
|
|
@@ -247,7 +249,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
247
249
|
// Resolve project-context tokens from the current working directory once
|
|
248
250
|
// per search invocation. Returns null when running from home dir / /tmp,
|
|
249
251
|
// or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
|
|
250
|
-
const projectContext =
|
|
252
|
+
const projectContext = disableProjectContext ? null : resolveProjectContext(process.cwd());
|
|
251
253
|
// Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
|
|
252
254
|
// count query when the boost cannot make a difference (default ≤ 1.0 means
|
|
253
255
|
// boost^count == 1 — zero overhead for the common case).
|
|
@@ -267,7 +269,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
267
269
|
// AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
|
|
268
270
|
let scopeKey;
|
|
269
271
|
try {
|
|
270
|
-
scopeKey =
|
|
272
|
+
scopeKey = disableScopedUtility ? undefined : getCurrentWorkflowScopeKey();
|
|
271
273
|
}
|
|
272
274
|
catch {
|
|
273
275
|
// Non-fatal — ranking proceeds without scoped utility on any error.
|
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { resolveStashDir } from "../../core/common.js";
|
|
7
7
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
8
|
-
import { resolveSourceProviderFactory
|
|
8
|
+
import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
|
|
9
9
|
// Eager side-effect imports so all built-in source providers self-register
|
|
10
10
|
// before resolveEntryContentDir() runs.
|
|
11
11
|
import "../../sources/providers/index.js";
|
|
@@ -264,7 +264,20 @@ export async function ensureSourceCaches(config, options) {
|
|
|
264
264
|
// refreshes the same way — a bad source warns and is skipped without
|
|
265
265
|
// aborting the others. The git content/-subdir layout convention stays in
|
|
266
266
|
// resolveEntryContentDir.
|
|
267
|
-
for (const
|
|
267
|
+
for (const entry of getSources(cfg)) {
|
|
268
|
+
if (entry.enabled === false)
|
|
269
|
+
continue;
|
|
270
|
+
const factory = resolveSourceProviderFactory(entry.type);
|
|
271
|
+
if (!factory)
|
|
272
|
+
continue;
|
|
273
|
+
let provider;
|
|
274
|
+
try {
|
|
275
|
+
provider = factory(entry);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
warn(`Warning: failed to construct ${entry.type} source provider for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
268
281
|
if (!provider.sync)
|
|
269
282
|
continue;
|
|
270
283
|
try {
|
|
@@ -42,7 +42,7 @@ const TYPE_HINTS = {
|
|
|
42
42
|
memory: "memory assets are short factual notes the user wants persisted across sessions. Frontmatter usually includes `description`.",
|
|
43
43
|
workflow: "workflow assets are markdown describing a multi-step process. Include `# <Title>` and ordered `## Step N` sections.",
|
|
44
44
|
script: "script assets are executable text files. Include a shebang and minimal usage comment.",
|
|
45
|
-
env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd
|
|
45
|
+
env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd>`; prefer `--clean` in agent contexts so the child starts from a minimal inherited environment. AKM itself does not print values, but the child command can print its environment, so do not run `env`, `printenv`, shell tracing, or similar diagnostics when secrets are in scope. For a single sensitive value used on its own for authentication (token, key, cert) use a `secret` instead. Never echo values back to the user.",
|
|
46
46
|
wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
|
|
47
47
|
fact: "fact assets are durable stash-level facts (personal/team/project details, coding conventions, stash-meta). Frontmatter SHOULD include `description` and a `category` (personal|team|project|convention|meta); set `pinned: true` only for the small always-injected core. Keep each fact short, high-signal, and self-contained — it is durable context, not an episodic note.",
|
|
48
48
|
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* LLM helper for the `akm index` memory-inference pass (#201).
|
|
6
|
+
*
|
|
7
|
+
* Compresses a single memory body into one higher-signal derived memory. The
|
|
8
|
+
* pass itself (in `src/indexer/memory-inference.ts`) is responsible for
|
|
9
|
+
* deciding which memories are pending, persisting the derived memory with the
|
|
10
|
+
* correct frontmatter (`inferred: true`, `source: <parent-ref>`), and marking
|
|
11
|
+
* the parent as processed for idempotency.
|
|
12
|
+
*
|
|
13
|
+
* This module is intentionally tiny and stateless so tests can stub it via
|
|
14
|
+
* `mock.module("../src/llm/memory-infer", ...)` without hitting a network.
|
|
15
|
+
*
|
|
16
|
+
* Locked v1 contract (#208): the LLM connection always comes from the
|
|
17
|
+
* shared `akm.llm` block — never from a per-pass override. Callers obtain
|
|
18
|
+
* the connection via `resolveIndexPassLLM("memory", config)` and pass it
|
|
19
|
+
* straight through.
|
|
20
|
+
*/
|
|
21
|
+
import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" };
|
|
22
|
+
import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" };
|
|
23
|
+
import { toErrorMessage } from "../core/common.js";
|
|
24
|
+
import { warn } from "../core/warn.js";
|
|
25
|
+
import { parseEmbeddedJsonResponse } from "./client.js";
|
|
26
|
+
import { callStructured } from "./structured-call.js";
|
|
27
|
+
/** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
|
|
28
|
+
const MAX_BODY_CHARS = 4000;
|
|
29
|
+
const SYSTEM_PROMPT = memoryInferSystemPrompt;
|
|
30
|
+
const USER_PROMPT_PREFIX = memoryInferUserPrompt;
|
|
31
|
+
/**
|
|
32
|
+
* Strict JSON Schema for the derived-memory payload. Sent to providers that
|
|
33
|
+
* opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
|
|
34
|
+
* silently drops the schema for providers that don't.
|
|
35
|
+
*
|
|
36
|
+
* Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to
|
|
37
|
+
* the memory-inference path. Mirrors the validation gate below
|
|
38
|
+
* (title/description/content + non-empty tags/searchHints) so a
|
|
39
|
+
* schema-compliant response is guaranteed to pass the downstream check
|
|
40
|
+
* — no more "incomplete derived memory payload from LLM; skipping memory"
|
|
41
|
+
* for shape-only failures.
|
|
42
|
+
*/
|
|
43
|
+
const DERIVED_MEMORY_JSON_SCHEMA = {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
title: { type: "string", minLength: 1 },
|
|
47
|
+
description: { type: "string", minLength: 1 },
|
|
48
|
+
content: { type: "string", minLength: 1 },
|
|
49
|
+
tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
|
|
50
|
+
searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 },
|
|
51
|
+
},
|
|
52
|
+
required: ["title", "description", "content", "tags", "searchHints"],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Compress a single memory body into one derived memory via the configured LLM.
|
|
57
|
+
*
|
|
58
|
+
* Returns `undefined` on any failure (timeout, invalid JSON, empty response).
|
|
59
|
+
* Errors are logged via `warn()` but never thrown — a failed split for one memory
|
|
60
|
+
* must not abort the rest of the index pass.
|
|
61
|
+
*
|
|
62
|
+
* Routes through `callStructured({ feature: "memory_inference", ... })` so the
|
|
63
|
+
* feature gate, error classification, and onFallback hook are honoured uniformly
|
|
64
|
+
* (Fix C5).
|
|
65
|
+
*/
|
|
66
|
+
export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt) {
|
|
67
|
+
const trimmedBody = body.trim();
|
|
68
|
+
if (!trimmedBody)
|
|
69
|
+
return undefined;
|
|
70
|
+
const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody.slice(0, MAX_BODY_CHARS)}`;
|
|
71
|
+
// Memory-inference is ALWAYS gated: no `akmConfig` ⇒ gate closed (no chat,
|
|
72
|
+
// `disabled` fallback), never the seam's ungated/propagate path (which is for
|
|
73
|
+
// direct callers like `enhanceMetadata`). This is the gate-closed branch
|
|
74
|
+
// `tryLlmFeature(_, undefined, _)` took before the migration.
|
|
75
|
+
if (!akmConfig) {
|
|
76
|
+
onFallback?.({ feature: "memory_inference", reason: "disabled" });
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return callStructured({
|
|
80
|
+
feature: "memory_inference",
|
|
81
|
+
akmConfig,
|
|
82
|
+
config: llmConfig,
|
|
83
|
+
messages: [
|
|
84
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
85
|
+
{ role: "user", content: userPrompt },
|
|
86
|
+
],
|
|
87
|
+
request: {
|
|
88
|
+
temperature: 0.1,
|
|
89
|
+
timeoutMs: llmConfig.timeoutMs,
|
|
90
|
+
signal,
|
|
91
|
+
responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
|
|
92
|
+
onRetryAttempt,
|
|
93
|
+
},
|
|
94
|
+
parse: (raw) => {
|
|
95
|
+
if (!raw)
|
|
96
|
+
return undefined;
|
|
97
|
+
const parsed = parseEmbeddedJsonResponse(raw);
|
|
98
|
+
if (!parsed) {
|
|
99
|
+
warn("memory inference: invalid JSON response from LLM; skipping memory.");
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
|
|
103
|
+
const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
|
|
104
|
+
const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
|
105
|
+
const tags = Array.isArray(parsed.tags)
|
|
106
|
+
? parsed.tags
|
|
107
|
+
.filter((t) => typeof t === "string")
|
|
108
|
+
.map((t) => t.trim())
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.slice(0, 8)
|
|
111
|
+
: [];
|
|
112
|
+
const searchHints = Array.isArray(parsed.searchHints)
|
|
113
|
+
? parsed.searchHints
|
|
114
|
+
.filter((h) => typeof h === "string")
|
|
115
|
+
.map((h) => h.trim())
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.slice(0, 6)
|
|
118
|
+
: [];
|
|
119
|
+
if (!title || !description || !content || tags.length === 0 || searchHints.length === 0) {
|
|
120
|
+
warn("memory inference: incomplete derived memory payload from LLM; skipping memory.");
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return { title, description, tags, searchHints, content };
|
|
124
|
+
},
|
|
125
|
+
onError: (cls, err) => {
|
|
126
|
+
if (cls === "html") {
|
|
127
|
+
if (telemetry)
|
|
128
|
+
telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1;
|
|
129
|
+
warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
warn(`memory inference failed: ${toErrorMessage(err)}`);
|
|
133
|
+
return undefined;
|
|
134
|
+
},
|
|
135
|
+
fallback: undefined,
|
|
136
|
+
onFallback,
|
|
137
|
+
});
|
|
138
|
+
}
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -1,138 +1,4 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
|
|
5
|
-
* LLM helper for the `akm index` memory-inference pass (#201).
|
|
6
|
-
*
|
|
7
|
-
* Compresses a single memory body into one higher-signal derived memory. The
|
|
8
|
-
* pass itself (in `src/indexer/memory-inference.ts`) is responsible for
|
|
9
|
-
* deciding which memories are pending, persisting the derived memory with the
|
|
10
|
-
* correct frontmatter (`inferred: true`, `source: <parent-ref>`), and marking
|
|
11
|
-
* the parent as processed for idempotency.
|
|
12
|
-
*
|
|
13
|
-
* This module is intentionally tiny and stateless so tests can stub it via
|
|
14
|
-
* `mock.module("../src/llm/memory-infer", ...)` without hitting a network.
|
|
15
|
-
*
|
|
16
|
-
* Locked v1 contract (#208): the LLM connection always comes from the
|
|
17
|
-
* shared `akm.llm` block — never from a per-pass override. Callers obtain
|
|
18
|
-
* the connection via `resolveIndexPassLLM("memory", config)` and pass it
|
|
19
|
-
* straight through.
|
|
20
|
-
*/
|
|
21
|
-
import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" };
|
|
22
|
-
import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" };
|
|
23
|
-
import { toErrorMessage } from "../core/common.js";
|
|
24
|
-
import { warn } from "../core/warn.js";
|
|
25
|
-
import { parseEmbeddedJsonResponse } from "./client.js";
|
|
26
|
-
import { callStructured } from "./structured-call.js";
|
|
27
|
-
/** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
|
|
28
|
-
const MAX_BODY_CHARS = 4000;
|
|
29
|
-
const SYSTEM_PROMPT = memoryInferSystemPrompt;
|
|
30
|
-
const USER_PROMPT_PREFIX = memoryInferUserPrompt;
|
|
31
|
-
/**
|
|
32
|
-
* Strict JSON Schema for the derived-memory payload. Sent to providers that
|
|
33
|
-
* opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
|
|
34
|
-
* silently drops the schema for providers that don't.
|
|
35
|
-
*
|
|
36
|
-
* Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to
|
|
37
|
-
* the memory-inference path. Mirrors the validation gate below
|
|
38
|
-
* (title/description/content + non-empty tags/searchHints) so a
|
|
39
|
-
* schema-compliant response is guaranteed to pass the downstream check
|
|
40
|
-
* — no more "incomplete derived memory payload from LLM; skipping memory"
|
|
41
|
-
* for shape-only failures.
|
|
42
|
-
*/
|
|
43
|
-
const DERIVED_MEMORY_JSON_SCHEMA = {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
title: { type: "string", minLength: 1 },
|
|
47
|
-
description: { type: "string", minLength: 1 },
|
|
48
|
-
content: { type: "string", minLength: 1 },
|
|
49
|
-
tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
|
|
50
|
-
searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 },
|
|
51
|
-
},
|
|
52
|
-
required: ["title", "description", "content", "tags", "searchHints"],
|
|
53
|
-
additionalProperties: false,
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Compress a single memory body into one derived memory via the configured LLM.
|
|
57
|
-
*
|
|
58
|
-
* Returns `undefined` on any failure (timeout, invalid JSON, empty response).
|
|
59
|
-
* Errors are logged via `warn()` but never thrown — a failed split for one memory
|
|
60
|
-
* must not abort the rest of the index pass.
|
|
61
|
-
*
|
|
62
|
-
* Routes through `callStructured({ feature: "memory_inference", ... })` so the
|
|
63
|
-
* feature gate, error classification, and onFallback hook are honoured uniformly
|
|
64
|
-
* (Fix C5).
|
|
65
|
-
*/
|
|
66
|
-
export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt) {
|
|
67
|
-
const trimmedBody = body.trim();
|
|
68
|
-
if (!trimmedBody)
|
|
69
|
-
return undefined;
|
|
70
|
-
const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody.slice(0, MAX_BODY_CHARS)}`;
|
|
71
|
-
// Memory-inference is ALWAYS gated: no `akmConfig` ⇒ gate closed (no chat,
|
|
72
|
-
// `disabled` fallback), never the seam's ungated/propagate path (which is for
|
|
73
|
-
// direct callers like `enhanceMetadata`). This is the gate-closed branch
|
|
74
|
-
// `tryLlmFeature(_, undefined, _)` took before the migration.
|
|
75
|
-
if (!akmConfig) {
|
|
76
|
-
onFallback?.({ feature: "memory_inference", reason: "disabled" });
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
return callStructured({
|
|
80
|
-
feature: "memory_inference",
|
|
81
|
-
akmConfig,
|
|
82
|
-
config: llmConfig,
|
|
83
|
-
messages: [
|
|
84
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
85
|
-
{ role: "user", content: userPrompt },
|
|
86
|
-
],
|
|
87
|
-
request: {
|
|
88
|
-
temperature: 0.1,
|
|
89
|
-
timeoutMs: llmConfig.timeoutMs,
|
|
90
|
-
signal,
|
|
91
|
-
responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
|
|
92
|
-
onRetryAttempt,
|
|
93
|
-
},
|
|
94
|
-
parse: (raw) => {
|
|
95
|
-
if (!raw)
|
|
96
|
-
return undefined;
|
|
97
|
-
const parsed = parseEmbeddedJsonResponse(raw);
|
|
98
|
-
if (!parsed) {
|
|
99
|
-
warn("memory inference: invalid JSON response from LLM; skipping memory.");
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
|
|
103
|
-
const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
|
|
104
|
-
const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
|
105
|
-
const tags = Array.isArray(parsed.tags)
|
|
106
|
-
? parsed.tags
|
|
107
|
-
.filter((t) => typeof t === "string")
|
|
108
|
-
.map((t) => t.trim())
|
|
109
|
-
.filter(Boolean)
|
|
110
|
-
.slice(0, 8)
|
|
111
|
-
: [];
|
|
112
|
-
const searchHints = Array.isArray(parsed.searchHints)
|
|
113
|
-
? parsed.searchHints
|
|
114
|
-
.filter((h) => typeof h === "string")
|
|
115
|
-
.map((h) => h.trim())
|
|
116
|
-
.filter(Boolean)
|
|
117
|
-
.slice(0, 6)
|
|
118
|
-
: [];
|
|
119
|
-
if (!title || !description || !content || tags.length === 0 || searchHints.length === 0) {
|
|
120
|
-
warn("memory inference: incomplete derived memory payload from LLM; skipping memory.");
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
return { title, description, tags, searchHints, content };
|
|
124
|
-
},
|
|
125
|
-
onError: (cls, err) => {
|
|
126
|
-
if (cls === "html") {
|
|
127
|
-
if (telemetry)
|
|
128
|
-
telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1;
|
|
129
|
-
warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`);
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
warn(`memory inference failed: ${toErrorMessage(err)}`);
|
|
133
|
-
return undefined;
|
|
134
|
-
},
|
|
135
|
-
fallback: undefined,
|
|
136
|
-
onFallback,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
4
|
+
export { compressMemoryToDerivedMemory, } from "./memory-infer-impl.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
|
|
6
|
+
const { main } = await import("./scripts/migrate-storage.js");
|
|
7
|
+
|
|
8
|
+
await main();
|
package/dist/output/renderers.js
CHANGED
|
@@ -377,7 +377,7 @@ const envFileRenderer = {
|
|
|
377
377
|
type: "env",
|
|
378
378
|
name,
|
|
379
379
|
path: ctx.absPath,
|
|
380
|
-
action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected
|
|
380
|
+
action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected; prefer `--clean` to minimize inherited parent env. AKM itself does not print values, but child stdout/stderr is not redacted. `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
|
|
381
381
|
description: comments.length > 0 ? comments.join("\n") : undefined,
|
|
382
382
|
keys,
|
|
383
383
|
comments,
|