akm-cli 0.9.0-rc.1 → 0.9.0-rc.2
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 +223 -9
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +358 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +116 -1
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -52
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/runner-dispatch.js +76 -13
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +4 -6
- package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +0 -2
- package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +1 -24
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +0 -6
- package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +3 -1
- package/dist/schemas/akm-config.json +11013 -8600
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +57 -13
- package/dist/scripts/migrate-storage.js +8591 -509
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +9 -8
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +65 -13
- package/dist/workflows/exec/brief.js +18 -24
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +124 -65
- package/dist/workflows/exec/report.js +93 -33
- package/dist/workflows/exec/run-workflow.js +38 -25
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +91 -35
- package/dist/workflows/ir/compile.js +13 -26
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +542 -1
- package/dist/workflows/parser.js +7 -0
- package/dist/workflows/program/parser.js +132 -23
- package/dist/workflows/program/project.js +3 -4
- package/dist/workflows/program/schema.js +2 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +54 -47
- package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
- package/dist/workflows/validator.js +25 -0
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -68,8 +68,7 @@
|
|
|
68
68
|
* `pattern: "local-runner"`, `structuredOutput: "native-json"`, and no
|
|
69
69
|
* `resume`). Exported standalone so that task only adds a registry entry.
|
|
70
70
|
*/
|
|
71
|
-
import { assertNotFlag } from "../../agent/builder-shared.js";
|
|
72
|
-
import { resolveModel } from "../../agent/model-aliases.js";
|
|
71
|
+
import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
|
|
73
72
|
/** Canonical harness/platform id used for model-alias resolution. */
|
|
74
73
|
export const OPENHANDS_PLATFORM = "openhands";
|
|
75
74
|
/**
|
|
@@ -117,7 +116,7 @@ export const openhandsBuilder = {
|
|
|
117
116
|
args.push(`--task=${buildTaskPayload(req)}`);
|
|
118
117
|
let env;
|
|
119
118
|
if (req.model) {
|
|
120
|
-
const resolved =
|
|
119
|
+
const resolved = resolveDispatchModel(req, profile, OPENHANDS_PLATFORM);
|
|
121
120
|
// Model travels via env, not argv — OpenHands' documented channel.
|
|
122
121
|
env = { [OPENHANDS_MODEL_ENV]: resolved };
|
|
123
122
|
}
|
|
@@ -48,8 +48,7 @@
|
|
|
48
48
|
* are registry-side). Exported standalone so that task only adds a registry
|
|
49
49
|
* entry.
|
|
50
50
|
*/
|
|
51
|
-
import { assertNotFlag } from "../../agent/builder-shared.js";
|
|
52
|
-
import { resolveModel } from "../../agent/model-aliases.js";
|
|
51
|
+
import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
|
|
53
52
|
/** Canonical harness/platform id used for model-alias resolution. */
|
|
54
53
|
export const PI_PLATFORM = "pi";
|
|
55
54
|
/**
|
|
@@ -87,7 +86,7 @@ export const piBuilder = {
|
|
|
87
86
|
args.push("--system-prompt", req.systemPrompt);
|
|
88
87
|
}
|
|
89
88
|
if (req.model) {
|
|
90
|
-
const resolved =
|
|
89
|
+
const resolved = resolveDispatchModel(req, profile, PI_PLATFORM);
|
|
91
90
|
args.push("--model", resolved);
|
|
92
91
|
}
|
|
93
92
|
if (req.schema) {
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared base for harness descriptors (#566).
|
|
6
6
|
*
|
|
7
|
-
* Provides
|
|
8
|
-
* ONE place instead of being duplicated across the old `guessAgentPlatform()`
|
|
9
|
-
* (config-migration) and the `name.includes("claude")` heuristic (setup). A
|
|
10
|
-
* concrete harness sets `id`/`aliases`/`capabilities` and, when its v1 profile
|
|
11
|
-
* names could be decorated (e.g. `"opencode-sdk-fast"`), `v1ProfilePrefixes`.
|
|
7
|
+
* Provides shared optional descriptor fields for concrete harnesses.
|
|
12
8
|
*/
|
|
13
9
|
export class BaseHarness {
|
|
14
10
|
runtimeId;
|
|
@@ -21,31 +17,4 @@ export class BaseHarness {
|
|
|
21
17
|
presenceEnv;
|
|
22
18
|
resultExtractor;
|
|
23
19
|
sessionLogProvider;
|
|
24
|
-
/**
|
|
25
|
-
* Lowercase prefixes that a decorated v1 profile name may start with and
|
|
26
|
-
* still belong to this harness (e.g. `["opencode-sdk"]`). The canonical id
|
|
27
|
-
* and aliases are always matched in addition to these; an empty list means
|
|
28
|
-
* only exact id/alias matches.
|
|
29
|
-
*/
|
|
30
|
-
v1ProfilePrefixes = [];
|
|
31
|
-
matchesV1ProfileName(name) {
|
|
32
|
-
// Only harnesses with a v1→v2 mapping participate; others never claim a
|
|
33
|
-
// legacy profile name (so unknown names are dropped, not misclassified).
|
|
34
|
-
if (!this.capabilities.v1Migration)
|
|
35
|
-
return false;
|
|
36
|
-
const lower = name.trim().toLowerCase();
|
|
37
|
-
if (!lower)
|
|
38
|
-
return false;
|
|
39
|
-
if (lower === this.id.toLowerCase())
|
|
40
|
-
return true;
|
|
41
|
-
for (const alias of this.aliases) {
|
|
42
|
-
if (lower === alias.toLowerCase())
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
for (const prefix of this.v1ProfilePrefixes) {
|
|
46
|
-
if (lower.startsWith(prefix.toLowerCase()))
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
20
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { writeFileAtomic } from "../core/common.js";
|
|
7
|
-
import { rethrowIfTestIsolationError } from "../core/errors.js";
|
|
8
|
-
import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock.js";
|
|
7
|
+
import { ConfigError, rethrowIfTestIsolationError } from "../core/errors.js";
|
|
8
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../core/file-lock.js";
|
|
9
|
+
import { acquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
|
|
9
10
|
import { getDataDir, getLockfileLockPath, getLockfilePath } from "../core/paths.js";
|
|
10
11
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
11
12
|
const LOCK_MAX_RETRIES = 3;
|
|
@@ -15,23 +16,26 @@ async function acquireLockSentinel() {
|
|
|
15
16
|
// Ensure the directory exists before attempting to create the sentinel.
|
|
16
17
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
17
18
|
for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const releaseBarrier = acquireMaintenanceBarrier();
|
|
20
|
+
try {
|
|
21
|
+
const ownership = tryAcquireLockSync(sentinelPath, createLockPayload());
|
|
22
|
+
if (ownership) {
|
|
23
|
+
return () => releaseLock(ownership);
|
|
24
|
+
}
|
|
25
|
+
const probe = probeLock(sentinelPath);
|
|
26
|
+
if (probe.state === "stale" && reclaimStaleLock(sentinelPath, probe)) {
|
|
27
|
+
continue; // Reclaimed — retry immediately.
|
|
28
|
+
}
|
|
20
29
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
continue; // Reclaimed — retry immediately.
|
|
30
|
+
finally {
|
|
31
|
+
releaseBarrier();
|
|
24
32
|
}
|
|
25
33
|
// Another process holds the lock — wait briefly before retrying.
|
|
26
34
|
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
27
35
|
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function releaseLockSentinel() {
|
|
34
|
-
releaseLock(getLockfileLockPath());
|
|
38
|
+
throw new ConfigError(`Could not acquire lockfile sentinel at ${sentinelPath}; refusing to write without exclusive ownership.`, "INVALID_CONFIG_FILE");
|
|
35
39
|
}
|
|
36
40
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
37
41
|
export function readLockfile() {
|
|
@@ -50,36 +54,43 @@ export function readLockfile() {
|
|
|
50
54
|
return [];
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
function writeLockfileUnlocked(entries) {
|
|
54
58
|
// Always write to $DATA — never to the legacy $CONFIG location.
|
|
55
59
|
const lockfilePath = getLockfilePath();
|
|
56
60
|
const dir = path.dirname(lockfilePath);
|
|
57
61
|
fs.mkdirSync(dir, { recursive: true });
|
|
58
62
|
writeFileAtomic(lockfilePath, `${JSON.stringify(entries, null, 2)}\n`);
|
|
59
63
|
}
|
|
64
|
+
export async function writeLockfile(entries) {
|
|
65
|
+
const release = await acquireLockSentinel();
|
|
66
|
+
try {
|
|
67
|
+
writeLockfileUnlocked(entries);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
60
73
|
export async function upsertLockEntry(entry) {
|
|
61
|
-
const
|
|
74
|
+
const release = await acquireLockSentinel();
|
|
62
75
|
try {
|
|
63
76
|
const entries = readLockfile();
|
|
64
77
|
const withoutExisting = entries.filter((e) => e.id !== entry.id);
|
|
65
|
-
|
|
78
|
+
writeLockfileUnlocked([...withoutExisting, entry]);
|
|
66
79
|
}
|
|
67
80
|
finally {
|
|
68
|
-
|
|
69
|
-
releaseLockSentinel();
|
|
81
|
+
release();
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
export async function removeLockEntry(id) {
|
|
73
85
|
if (!fs.existsSync(getDataDir()))
|
|
74
86
|
return;
|
|
75
|
-
const
|
|
87
|
+
const release = await acquireLockSentinel();
|
|
76
88
|
try {
|
|
77
89
|
const entries = readLockfile();
|
|
78
|
-
|
|
90
|
+
writeLockfileUnlocked(entries.filter((e) => e.id !== id));
|
|
79
91
|
}
|
|
80
92
|
finally {
|
|
81
|
-
|
|
82
|
-
releaseLockSentinel();
|
|
93
|
+
release();
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
package/dist/llm/client.js
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { fetchWithTimeout } from "../core/common.js";
|
|
14
14
|
import { resolveSecret } from "../core/config/config.js";
|
|
15
|
+
import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
|
|
15
16
|
import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse.js";
|
|
17
|
+
import { redactSensitiveText } from "../core/redaction.js";
|
|
16
18
|
import { warnVerbose } from "../core/warn.js";
|
|
19
|
+
import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
|
|
17
20
|
import { emitLlmUsage, extractUsageTokens } from "./usage-telemetry.js";
|
|
18
21
|
// Re-export shared parse utilities so existing importers of `client.ts` continue
|
|
19
22
|
// to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
|
|
20
23
|
export { escapeJsonStringControls, parseEmbeddedJsonResponse, parseJsonResponse, stripCodeFences, stripThinkBlocks, } from "../core/parse.js";
|
|
21
24
|
/** Maximum length of an LLM error response body included in thrown errors. */
|
|
22
25
|
const ERROR_BODY_MAX_LEN = 200;
|
|
26
|
+
/** Stable OpenAI-compatible response-schema name used for every structured call. */
|
|
27
|
+
const JSON_SCHEMA_RESPONSE_NAME = "akm_response";
|
|
23
28
|
/**
|
|
24
29
|
* Redact credential-shaped substrings from an upstream error body before
|
|
25
30
|
* including it in a thrown Error. The body is also trimmed to a fixed length
|
|
@@ -178,7 +183,11 @@ export async function chatCompletion(config, messages, options) {
|
|
|
178
183
|
return chatCompletionReal(config, messages, options);
|
|
179
184
|
}
|
|
180
185
|
async function chatCompletionReal(config, messages, options) {
|
|
181
|
-
const effectiveTimeoutMs = options
|
|
186
|
+
const effectiveTimeoutMs = options && Object.hasOwn(options, "timeoutMs")
|
|
187
|
+
? (options.timeoutMs ?? null)
|
|
188
|
+
: Object.hasOwn(config, "timeoutMs")
|
|
189
|
+
? (config.timeoutMs ?? null)
|
|
190
|
+
: DEFAULT_LLM_TIMEOUT_MS;
|
|
182
191
|
const started = Date.now();
|
|
183
192
|
try {
|
|
184
193
|
return await chatCompletionAttempt(config, messages, options, effectiveTimeoutMs);
|
|
@@ -189,8 +198,9 @@ async function chatCompletionReal(config, messages, options) {
|
|
|
189
198
|
// Timeout-budget guard: if the first attempt already burned most of the
|
|
190
199
|
// budget, a second attempt cannot complete — skip the retry.
|
|
191
200
|
const elapsed = Date.now() - started;
|
|
192
|
-
const remaining = effectiveTimeoutMs - elapsed;
|
|
193
|
-
if (
|
|
201
|
+
const remaining = effectiveTimeoutMs === null ? null : effectiveTimeoutMs - elapsed;
|
|
202
|
+
if (effectiveTimeoutMs !== null &&
|
|
203
|
+
(elapsed >= effectiveTimeoutMs * RETRY_BUDGET_FRACTION || (remaining !== null && remaining <= 0))) {
|
|
194
204
|
throw err;
|
|
195
205
|
}
|
|
196
206
|
// Signal the caller so it can bump `retryAttempts` (NOT `failureCount`).
|
|
@@ -210,6 +220,11 @@ async function chatCompletionReal(config, messages, options) {
|
|
|
210
220
|
* transient failures.
|
|
211
221
|
*/
|
|
212
222
|
async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
223
|
+
if (config.extraParams !== undefined) {
|
|
224
|
+
const issue = validateExtraParams(config.extraParams)[0];
|
|
225
|
+
if (issue)
|
|
226
|
+
throw new Error(formatExtraParamsIssue("LLM extraParams", issue));
|
|
227
|
+
}
|
|
213
228
|
const headers = { "Content-Type": "application/json" };
|
|
214
229
|
const resolvedKey = resolveSecret(config.apiKey);
|
|
215
230
|
if (resolvedKey) {
|
|
@@ -220,7 +235,12 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
220
235
|
// guess is wrong. Users who need a cap can set llm.maxTokens in config.
|
|
221
236
|
const resolvedMaxTokens = options?.maxTokens ?? config.maxTokens;
|
|
222
237
|
const responseFormat = options?.responseSchema && config.supportsJsonSchema
|
|
223
|
-
? {
|
|
238
|
+
? {
|
|
239
|
+
response_format: {
|
|
240
|
+
type: "json_schema",
|
|
241
|
+
json_schema: { name: JSON_SCHEMA_RESPONSE_NAME, schema: options.responseSchema, strict: true },
|
|
242
|
+
},
|
|
243
|
+
}
|
|
224
244
|
: {};
|
|
225
245
|
// Wall-clock start for per-call usage telemetry (#576). Captured here so the
|
|
226
246
|
// emitted duration covers the full request/response/parse cycle of a single
|
|
@@ -252,22 +272,22 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
252
272
|
// caller-driven cancellations. Map both to typed LlmCallError.
|
|
253
273
|
const msg = err instanceof Error ? err.message : String(err);
|
|
254
274
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
255
|
-
throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
|
|
275
|
+
throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
|
|
256
276
|
}
|
|
257
277
|
if (msg.includes("timed out")) {
|
|
258
|
-
throw new LlmCallError(`Request timed out after ${timeoutMs}ms`, "timeout");
|
|
278
|
+
throw new LlmCallError(`Request timed out${timeoutMs === null ? "" : ` after ${timeoutMs}ms`}`, "timeout");
|
|
259
279
|
}
|
|
260
280
|
throw new LlmCallError(`Network error: ${msg}`, "network_error");
|
|
261
281
|
}
|
|
262
282
|
if (!response.ok) {
|
|
263
283
|
const rawBody = await response.text().catch(() => "");
|
|
264
|
-
const safeBody = redactErrorBody(rawBody);
|
|
284
|
+
const safeBody = redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : []);
|
|
265
285
|
const status = response.status;
|
|
266
286
|
if (status === 429) {
|
|
267
287
|
throw new LlmCallError(`LLM request rate limited (429) ${config.endpoint}: ${safeBody}`, "rate_limited", status);
|
|
268
288
|
}
|
|
269
289
|
if (status >= 500 && isHtmlResponse(rawBody)) {
|
|
270
|
-
throw new LlmCallError(`LLM provider returned HTML instead of JSON (${status}) ${config.endpoint}: ${htmlExcerpt(rawBody)}`, "provider_html_error", status);
|
|
290
|
+
throw new LlmCallError(`LLM provider returned HTML instead of JSON (${status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", status);
|
|
271
291
|
}
|
|
272
292
|
if (status >= 500) {
|
|
273
293
|
throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
|
|
@@ -279,14 +299,14 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
279
299
|
// we can categorize an HTML page distinctly from a malformed-JSON parse_error.
|
|
280
300
|
const rawOkBody = await response.text();
|
|
281
301
|
if (isHtmlResponse(rawOkBody)) {
|
|
282
|
-
throw new LlmCallError(`LLM provider returned HTML instead of JSON (${response.status}) ${config.endpoint}: ${htmlExcerpt(rawOkBody)}`, "provider_html_error", response.status);
|
|
302
|
+
throw new LlmCallError(`LLM provider returned HTML instead of JSON (${response.status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", response.status);
|
|
283
303
|
}
|
|
284
304
|
let json;
|
|
285
305
|
try {
|
|
286
306
|
json = JSON.parse(rawOkBody);
|
|
287
307
|
}
|
|
288
308
|
catch {
|
|
289
|
-
throw new LlmCallError(`LLM response was not valid JSON ${config.endpoint}: ${redactErrorBody(rawOkBody)}`, "parse_error", response.status);
|
|
309
|
+
throw new LlmCallError(`LLM response was not valid JSON ${config.endpoint}: ${redactSensitiveText(redactErrorBody(rawOkBody), resolvedKey ? [resolvedKey] : [])}`, "parse_error", response.status);
|
|
290
310
|
}
|
|
291
311
|
// Per-call usage telemetry (#576). Best-effort and fully isolated: a missing
|
|
292
312
|
// or garbled usage block still records duration + model, and a throwing sink
|
|
@@ -300,7 +320,7 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
300
320
|
});
|
|
301
321
|
const content = (json.choices?.[0]?.message?.content ?? "").trim();
|
|
302
322
|
const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
|
|
303
|
-
return content || reasoning;
|
|
323
|
+
return redactSensitiveText(content || reasoning, resolvedKey ? [resolvedKey] : []);
|
|
304
324
|
}
|
|
305
325
|
/**
|
|
306
326
|
* Strip `<think>` blocks, code fences, and escape control characters in JSON
|
|
@@ -325,6 +345,16 @@ export async function isLlmAvailable(config) {
|
|
|
325
345
|
}
|
|
326
346
|
}
|
|
327
347
|
// ── Capability probe ────────────────────────────────────────────────────────
|
|
348
|
+
const CAPABILITY_PROBE_JSON_SCHEMA = {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
ok: { type: "boolean", const: true },
|
|
352
|
+
ingest: { type: "boolean", const: true },
|
|
353
|
+
lint: { type: "boolean", const: true },
|
|
354
|
+
},
|
|
355
|
+
required: ["ok", "ingest", "lint"],
|
|
356
|
+
additionalProperties: false,
|
|
357
|
+
};
|
|
328
358
|
/**
|
|
329
359
|
* Ask the model to emit a strict JSON object so we know whether the knowledge
|
|
330
360
|
* wiki ingest/lint flows can rely on structured output. Failure is non-fatal —
|
|
@@ -332,7 +362,7 @@ export async function isLlmAvailable(config) {
|
|
|
332
362
|
*/
|
|
333
363
|
export async function probeLlmCapabilities(config) {
|
|
334
364
|
try {
|
|
335
|
-
const raw = await chatCompletion(config, [
|
|
365
|
+
const raw = await chatCompletion({ ...config, supportsJsonSchema: true }, [
|
|
336
366
|
{
|
|
337
367
|
role: "system",
|
|
338
368
|
content: "You return only valid JSON. No prose, no markdown fences.",
|
|
@@ -341,13 +371,17 @@ export async function probeLlmCapabilities(config) {
|
|
|
341
371
|
role: "user",
|
|
342
372
|
content: 'Return exactly this JSON object and nothing else: {"ok": true, "ingest": true, "lint": true}',
|
|
343
373
|
},
|
|
344
|
-
], { maxTokens: 64, temperature: 0 });
|
|
374
|
+
], { maxTokens: 64, temperature: 0, responseSchema: CAPABILITY_PROBE_JSON_SCHEMA });
|
|
345
375
|
if (!raw)
|
|
346
376
|
return { reachable: false, structuredOutput: false, error: "empty response" };
|
|
347
377
|
const parsed = parseJsonResponse(raw);
|
|
348
378
|
return {
|
|
349
379
|
reachable: true,
|
|
350
|
-
structuredOutput: Boolean(parsed &&
|
|
380
|
+
structuredOutput: Boolean(parsed &&
|
|
381
|
+
Object.keys(parsed).length === 3 &&
|
|
382
|
+
parsed.ok === true &&
|
|
383
|
+
parsed.ingest === true &&
|
|
384
|
+
parsed.lint === true),
|
|
351
385
|
};
|
|
352
386
|
}
|
|
353
387
|
catch (err) {
|
package/dist/llm/feature-gate.js
CHANGED
|
@@ -3,54 +3,39 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
5
|
* For each feature key, return the effective enabled state by reading the
|
|
6
|
-
*
|
|
6
|
+
* 0.9.0 config shape. Defaults match the legacy `LlmFeatureFlags` docstrings.
|
|
7
7
|
*/
|
|
8
8
|
// Defaults below mirror the legacy LlmFeatureFlags docstrings so existing
|
|
9
9
|
// behaviour is preserved when a config is silent on a flag.
|
|
10
10
|
const FEATURE_LOCATION = {
|
|
11
|
-
// Legacy default: false → memory_consolidation only runs when explicitly enabled
|
|
12
|
-
// (either via the user's improve profile or the built-in `default` profile).
|
|
13
|
-
memory_consolidation: (cfg) => cfg.profiles?.improve?.default?.processes?.consolidate?.enabled ?? false,
|
|
14
|
-
// 0.8.0 unified gate: replaces the legacy `feedback_distillation` key.
|
|
15
|
-
// The orchestration gate (planner) and the LLM-call gate now share the same
|
|
16
|
-
// source of truth: `processes.distill.enabled`. Default: true (matches the
|
|
17
|
-
// built-in `default` profile).
|
|
18
|
-
distill: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.enabled ?? true,
|
|
19
11
|
// Legacy default: true
|
|
20
|
-
memory_inference: (cfg) => cfg.
|
|
12
|
+
memory_inference: (cfg) => cfg.index?.memory?.enabled ?? true,
|
|
21
13
|
// Legacy default: true
|
|
22
|
-
graph_extraction: (cfg) => cfg.
|
|
14
|
+
graph_extraction: (cfg) => cfg.index?.graph?.enabled ?? true,
|
|
23
15
|
// Legacy default: false
|
|
24
16
|
metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
|
|
25
|
-
// Default ON since R3 (docs/design/improve-self-learning-analysis.md G5):
|
|
26
|
-
// distill is a primary acquisition path, so the gate guards minted content by
|
|
27
|
-
// default. The judge fails CLOSED (07 P0-2): no LLM / timeout / parse failure
|
|
28
|
-
// reject the proposal rather than passing it through — an unjudgeable proposal
|
|
29
|
-
// must not slip into the stash. Opt out via
|
|
30
|
-
// profiles.improve.default.processes.distill.qualityGate.enabled: false.
|
|
31
|
-
lesson_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.qualityGate?.enabled ?? true,
|
|
32
|
-
// Legacy default: false
|
|
33
|
-
proposal_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.reflect?.qualityGate?.enabled ?? false,
|
|
34
|
-
// Legacy default: false
|
|
35
|
-
memory_contradiction_detection: (cfg) => cfg.profiles?.improve?.default?.processes?.consolidate?.contradictionDetection?.enabled ?? false,
|
|
36
17
|
// Always on at the LLM-wrapper level. Enablement is decided ONCE at the
|
|
37
18
|
// extract entry point (`akmExtract`): the `extract.enabled` process toggle
|
|
38
|
-
// gates extract as a STAGE of `akm improve` (the active improve
|
|
19
|
+
// gates extract as a STAGE of `akm improve` (the active improve strategy, per
|
|
39
20
|
// #593/#594), while an explicit `akm extract` command always runs. Gating the
|
|
40
21
|
// inner LLM calls on `default.processes.extract.enabled` here was a footgun —
|
|
41
|
-
// dropping extract from the daily improve
|
|
22
|
+
// dropping extract from the daily improve strategy silently disabled the
|
|
42
23
|
// standalone `akm extract` command. (cfg unused — kept for resolver signature.)
|
|
43
24
|
session_extraction: (_cfg) => true,
|
|
44
25
|
};
|
|
45
26
|
/**
|
|
46
27
|
* Pure predicate: is the named feature gate enabled in `config`?
|
|
47
28
|
*
|
|
48
|
-
* Reads from the unified 0.
|
|
29
|
+
* Reads from the unified 0.9.0 config shape. Defaults follow the legacy
|
|
49
30
|
* `LlmFeatureFlags` docstring defaults.
|
|
50
31
|
*/
|
|
51
|
-
export function isLlmFeatureEnabled(config, feature) {
|
|
32
|
+
export function isLlmFeatureEnabled(config, feature, improveEnabled) {
|
|
52
33
|
if (!config)
|
|
53
34
|
return false;
|
|
35
|
+
// Improve resolves enablement from its selected strategy and passes the
|
|
36
|
+
// decision in. This module does not own a second improve config lookup.
|
|
37
|
+
if (improveEnabled !== undefined)
|
|
38
|
+
return improveEnabled;
|
|
54
39
|
const resolver = FEATURE_LOCATION[feature];
|
|
55
40
|
if (!resolver)
|
|
56
41
|
return false;
|
|
@@ -67,13 +52,13 @@ const DEFAULT_TIMEOUT_MS = 600_000;
|
|
|
67
52
|
*/
|
|
68
53
|
export async function tryLlmFeature(feature, config, fn, fallback, opts) {
|
|
69
54
|
const resolveFallback = async () => typeof fallback === "function" ? await fallback() : fallback;
|
|
70
|
-
if (!isLlmFeatureEnabled(config, feature)) {
|
|
55
|
+
if (!isLlmFeatureEnabled(config, feature, opts?.enabled)) {
|
|
71
56
|
opts?.onFallback?.({ feature, reason: "disabled" });
|
|
72
57
|
return resolveFallback();
|
|
73
58
|
}
|
|
74
|
-
const timeoutMs = opts
|
|
59
|
+
const timeoutMs = opts && Object.hasOwn(opts, "timeoutMs") ? (opts.timeoutMs ?? null) : DEFAULT_TIMEOUT_MS;
|
|
75
60
|
try {
|
|
76
|
-
if (timeoutMs <= 0) {
|
|
61
|
+
if (timeoutMs === null || timeoutMs <= 0) {
|
|
77
62
|
return await fn();
|
|
78
63
|
}
|
|
79
64
|
return await runWithTimeout(fn, timeoutMs, feature);
|
|
@@ -96,7 +81,7 @@ export async function tryLlmFeature(feature, config, fn, fallback, opts) {
|
|
|
96
81
|
export function isProcessEnabled(section, processName, config) {
|
|
97
82
|
if (!config)
|
|
98
83
|
return false;
|
|
99
|
-
//
|
|
84
|
+
// Index passes are first-class 0.9 entries.
|
|
100
85
|
if (section === "index") {
|
|
101
86
|
if (processName === "metadata_enhance" || processName === "metadataEnhance") {
|
|
102
87
|
return config.index?.metadataEnhance?.enabled ?? true;
|
|
@@ -108,23 +93,6 @@ export function isProcessEnabled(section, processName, config) {
|
|
|
108
93
|
return isLlmFeatureEnabled(config, "graph_extraction");
|
|
109
94
|
}
|
|
110
95
|
}
|
|
111
|
-
if (section === "improve") {
|
|
112
|
-
const processes = config.profiles?.improve?.default?.processes;
|
|
113
|
-
const entry = processes?.[processName];
|
|
114
|
-
if (entry && typeof entry.enabled === "boolean")
|
|
115
|
-
return entry.enabled;
|
|
116
|
-
// Fallback to default-enabled state for known processes.
|
|
117
|
-
switch (processName) {
|
|
118
|
-
case "reflect":
|
|
119
|
-
case "distill":
|
|
120
|
-
case "consolidate":
|
|
121
|
-
case "memoryInference":
|
|
122
|
-
case "graphExtraction":
|
|
123
|
-
return true;
|
|
124
|
-
default:
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
96
|
return false;
|
|
129
97
|
}
|
|
130
98
|
/** Specific error class so call sites and the wrapper can tell timeouts apart from generic throws. */
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `mock.module("../src/llm/graph-extract", ...)` without hitting a network.
|
|
17
17
|
*
|
|
18
18
|
* Locked v1 contract (#208): the LLM connection always comes from the
|
|
19
|
-
*
|
|
19
|
+
* selected named LLM engine. Callers obtain
|
|
20
20
|
* the connection via `resolveIndexPassLLM("graph", config)` and pass it
|
|
21
21
|
* straight through.
|
|
22
22
|
*/
|
package/dist/llm/index-passes.js
CHANGED
|
@@ -1,50 +1,16 @@
|
|
|
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
|
-
import {
|
|
4
|
+
import { materializeLlmConnection, resolveLlmEngineUse } from "../integrations/agent/engine-resolution.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* no improve-profile counterpart return undefined and resolve via the
|
|
9
|
-
* default LLM only.
|
|
6
|
+
* Resolve standalone index passes from the index section only. Improve
|
|
7
|
+
* strategies own improve-triggered calls and are intentionally not consulted.
|
|
10
8
|
*/
|
|
11
|
-
function improveProcessKeyForPass(passName) {
|
|
12
|
-
switch (passName) {
|
|
13
|
-
case "memory":
|
|
14
|
-
return "memoryInference";
|
|
15
|
-
case "graph":
|
|
16
|
-
return "graphExtraction";
|
|
17
|
-
default:
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
9
|
export function resolveIndexPassLLM(passName, config) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (passConfig?.llm === false)
|
|
10
|
+
const pass = config.index?.[passName];
|
|
11
|
+
if (pass?.enabled === false)
|
|
25
12
|
return undefined;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// actually take effect on the graph pass instead of being silently
|
|
30
|
-
// ignored.
|
|
31
|
-
const processKey = improveProcessKeyForPass(passName);
|
|
32
|
-
if (processKey) {
|
|
33
|
-
const processConfig = config.profiles?.improve?.default?.processes?.[processKey];
|
|
34
|
-
// Honor enabled === false here too — an explicit disable wins.
|
|
35
|
-
if (processConfig?.enabled === false)
|
|
36
|
-
return undefined;
|
|
37
|
-
const profileName = processConfig?.profile;
|
|
38
|
-
if (profileName) {
|
|
39
|
-
const profile = config.profiles?.llm?.[profileName];
|
|
40
|
-
if (profile)
|
|
41
|
-
return profile;
|
|
42
|
-
// A named-but-missing profile is a configuration error in spirit, but
|
|
43
|
-
// we fall through to default rather than throwing — callers gracefully
|
|
44
|
-
// treat `undefined` as "pass disabled" and emitting a hard throw here
|
|
45
|
-
// would take the whole improve run down on a typo.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// Gate 3 — fall back to the default LLM profile.
|
|
49
|
-
return getDefaultLlmConfig(config);
|
|
13
|
+
const defaults = config.index?.defaults;
|
|
14
|
+
const resolved = resolveLlmEngineUse(config, [defaults ?? {}, pass ?? {}], { optional: true });
|
|
15
|
+
return resolved ? materializeLlmConnection(resolved) : undefined;
|
|
50
16
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `mock.module("../src/llm/memory-infer", ...)` without hitting a network.
|
|
15
15
|
*
|
|
16
16
|
* Locked v1 contract (#208): the LLM connection always comes from the
|
|
17
|
-
*
|
|
17
|
+
* selected named LLM engine. Callers obtain
|
|
18
18
|
* the connection via `resolveIndexPassLLM("memory", config)` and pass it
|
|
19
19
|
* straight through.
|
|
20
20
|
*/
|
|
@@ -31,6 +31,10 @@ function toEventMetadata(record) {
|
|
|
31
31
|
const metadata = { durationMs: record.durationMs };
|
|
32
32
|
if (record.stage !== undefined)
|
|
33
33
|
metadata.stage = record.stage;
|
|
34
|
+
if (record.engine !== undefined)
|
|
35
|
+
metadata.engine = record.engine;
|
|
36
|
+
if (record.process !== undefined)
|
|
37
|
+
metadata.process = record.process;
|
|
34
38
|
if (record.model !== undefined)
|
|
35
39
|
metadata.model = record.model;
|
|
36
40
|
if (record.finishReason !== undefined)
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `undefined` outside any `withLlmStage` scope.
|
|
25
25
|
*/
|
|
26
26
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
-
const
|
|
27
|
+
const attributionStorage = new AsyncLocalStorage();
|
|
28
28
|
let usageSink;
|
|
29
29
|
/**
|
|
30
30
|
* Run `fn` with `stage` as the ambient LLM stage. Any `chatCompletion` call
|
|
@@ -32,12 +32,12 @@ let usageSink;
|
|
|
32
32
|
* helpers and nested `withLlmStage` calls — the innermost wins) is attributed
|
|
33
33
|
* to `stage`. Returns whatever `fn` returns; never alters control flow.
|
|
34
34
|
*/
|
|
35
|
-
export function withLlmStage(stage, fn) {
|
|
36
|
-
return
|
|
35
|
+
export function withLlmStage(stage, fn, attribution = {}) {
|
|
36
|
+
return attributionStorage.run({ ...attributionStorage.getStore(), ...attribution, stage }, fn);
|
|
37
37
|
}
|
|
38
38
|
/** The ambient LLM stage for the current async context, or `undefined` outside any {@link withLlmStage} scope. */
|
|
39
39
|
export function currentLlmStage() {
|
|
40
|
-
return
|
|
40
|
+
return attributionStorage.getStore()?.stage;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Install the process-wide usage sink. Replaces any previously installed sink.
|
|
@@ -69,7 +69,13 @@ export function emitLlmUsage(record) {
|
|
|
69
69
|
if (!sink)
|
|
70
70
|
return;
|
|
71
71
|
try {
|
|
72
|
-
|
|
72
|
+
const ambient = attributionStorage.getStore();
|
|
73
|
+
sink({
|
|
74
|
+
...record,
|
|
75
|
+
stage: record.stage ?? ambient?.stage,
|
|
76
|
+
engine: record.engine ?? ambient?.engine,
|
|
77
|
+
process: record.process ?? ambient?.process,
|
|
78
|
+
});
|
|
73
79
|
}
|
|
74
80
|
catch {
|
|
75
81
|
// Telemetry must never break a real run.
|
|
@@ -78,6 +84,30 @@ export function emitLlmUsage(record) {
|
|
|
78
84
|
function asFiniteNonNegative(value) {
|
|
79
85
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
80
86
|
}
|
|
87
|
+
function asNonEmptyString(value) {
|
|
88
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Decode durable event metadata with the same validation used by health aggregation. */
|
|
91
|
+
export function decodeLlmUsageRecord(value) {
|
|
92
|
+
if (!value || typeof value !== "object")
|
|
93
|
+
return undefined;
|
|
94
|
+
const raw = value;
|
|
95
|
+
const durationMs = asFiniteNonNegative(raw.durationMs);
|
|
96
|
+
if (durationMs === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
const record = { durationMs };
|
|
99
|
+
for (const key of ["stage", "engine", "process", "model", "finishReason"]) {
|
|
100
|
+
const decoded = asNonEmptyString(raw[key]);
|
|
101
|
+
if (decoded !== undefined)
|
|
102
|
+
record[key] = decoded;
|
|
103
|
+
}
|
|
104
|
+
for (const key of ["promptTokens", "completionTokens", "totalTokens", "reasoningTokens"]) {
|
|
105
|
+
const decoded = asFiniteNonNegative(raw[key]);
|
|
106
|
+
if (decoded !== undefined)
|
|
107
|
+
record[key] = decoded;
|
|
108
|
+
}
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
81
111
|
/**
|
|
82
112
|
* Project a provider `usage` block into the token fields of an
|
|
83
113
|
* {@link LlmUsageRecord}. Missing or garbled values are omitted (not zeroed)
|