akm-cli 0.9.16-alpha.1 → 0.9.16
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 +56 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/repository.js +3 -12
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/embedding.js +30 -7
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/html-render.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/stdout.js +24 -0
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/output/text.js +4 -3
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- package/dist/storage/repositories/units-repository.js +0 -510
|
@@ -2,162 +2,84 @@
|
|
|
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
4
|
import { stableFtsScore } from "../../core/lexical-score.js";
|
|
5
|
-
import { getEntryById } from "../../storage/repositories/index-entries-repository.js";
|
|
6
5
|
import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
|
|
7
|
-
import { groupUnitHitsByEntry } from "../../storage/repositories/units-repository.js";
|
|
8
6
|
import { buildLexicalQueryPlan } from "./fts-query.js";
|
|
9
7
|
import { lexicalNameTokens, structuralNameTokenMatch } from "./name-match.js";
|
|
10
8
|
import { applyBeliefStateScoreCeiling, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* Lower bounds keep a lexical hit competitive with a vector-only neighbour;
|
|
11
|
+
* the upper bound deliberately leaves room for the ranking contributors that
|
|
12
|
+
* run after retrieval (notably the bounded graph boost). This is a
|
|
13
|
+
* calibration for the one search pipeline, not a claim that BM25 is
|
|
14
|
+
* comparable across different queries or FTS tables.
|
|
17
15
|
*/
|
|
18
|
-
const LEXICAL_WEIGHT = 0.7;
|
|
19
|
-
const SEMANTIC_WEIGHT = 0.3;
|
|
20
16
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* scores on.
|
|
17
|
+
* Convert FTS5's negative BM25 value into the lexical contribution used by
|
|
18
|
+
* this pipeline. The transform is fixed and monotone: it depends only on a
|
|
19
|
+
* row's own BM25 value, so appending weaker candidates cannot rewrite an
|
|
20
|
+
* existing row's score. FTS5 commonly emits relevance near 1e-6 for broad
|
|
21
|
+
* queries, so first put relevance on a log scale around that observed value.
|
|
22
|
+
* The shape constant intentionally makes the curve approach its ceiling
|
|
23
|
+
* slowly: rare-term scores retain separation instead of all reading as 0.8.
|
|
24
|
+
*
|
|
25
|
+
* FTS5 produces finite non-positive values in normal operation. Keeping the
|
|
26
|
+
* defensive cases here finite makes this boundary safe if a driver or fixture
|
|
27
|
+
* hands us an invalid value: `-Infinity` is the strongest possible match,
|
|
28
|
+
* while NaN, +Infinity, and positive scores contribute no lexical evidence.
|
|
34
29
|
*/
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const hashes = [...new Set(hits.map((hit) => hit.unitHash))];
|
|
40
|
-
const placeholders = hashes.map(() => "?").join(",");
|
|
41
|
-
const rows = db
|
|
42
|
-
.prepare(`SELECT entry_id AS entryId, fragment_id AS fragmentId, unit_hash AS unitHash FROM entry_units WHERE unit_hash IN (${placeholders})`)
|
|
43
|
-
.all(...hashes);
|
|
44
|
-
const ownersByHash = new Map();
|
|
45
|
-
for (const row of rows) {
|
|
46
|
-
const owners = ownersByHash.get(row.unitHash) ?? [];
|
|
47
|
-
owners.push({ entryId: row.entryId, fragmentId: row.fragmentId });
|
|
48
|
-
ownersByHash.set(row.unitHash, owners);
|
|
49
|
-
}
|
|
50
|
-
for (const hit of hits) {
|
|
51
|
-
for (const owner of ownersByHash.get(hit.unitHash) ?? []) {
|
|
52
|
-
const existing = best.get(owner.entryId);
|
|
53
|
-
if (!existing || hit.bm25 < existing.bm25) {
|
|
54
|
-
best.set(owner.entryId, {
|
|
55
|
-
bm25: hit.bm25,
|
|
56
|
-
unitHash: hit.unitHash,
|
|
57
|
-
fragmentId: owner.fragmentId,
|
|
58
|
-
lexicalMatch: hit.lexicalMatch,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
30
|
+
export function normalizeFtsScores(results) {
|
|
31
|
+
const ftsScoreMap = new Map();
|
|
32
|
+
for (const result of results) {
|
|
33
|
+
ftsScoreMap.set(result.id, { score: result.lexicalScore ?? stableFtsScore(result.bm25Score), result });
|
|
62
34
|
}
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Which unit is reported as `matchedUnit`/`fragmentId`: a fixed priority
|
|
67
|
-
* order by evidence strength — card (name/description/tags/hints, the
|
|
68
|
-
* strongest, most structured signal), then fragment, then a vector
|
|
69
|
-
* neighbor — never a magnitude comparison across the three. At least one of
|
|
70
|
-
* the three is defined for every `entryId` this is called with.
|
|
71
|
-
*/
|
|
72
|
-
function pickFusionWinner(cardHit, fragmentHit, semanticHit) {
|
|
73
|
-
if (cardHit)
|
|
74
|
-
return { unitHash: cardHit.unitHash, fragmentId: cardHit.fragmentId };
|
|
75
|
-
if (fragmentHit)
|
|
76
|
-
return { unitHash: fragmentHit.unitHash, fragmentId: fragmentHit.fragmentId };
|
|
77
|
-
const winner = semanticHit;
|
|
78
|
-
return { unitHash: winner.hash, fragmentId: winner.fragmentId };
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Cosine similarity from a `units_vec` L2 distance over normalized vectors —
|
|
82
|
-
* exactly the retired `tryVecScores`' own conversion, guarded the same way:
|
|
83
|
-
* `1 - distance²/2`, clamped at 0, non-finite results treated as no evidence.
|
|
84
|
-
*/
|
|
85
|
-
function semanticCosine(distance) {
|
|
86
|
-
const raw = 1 - (distance * distance) / 2;
|
|
87
|
-
return Number.isFinite(raw) ? Math.max(0, raw) : 0;
|
|
35
|
+
return ftsScoreMap;
|
|
88
36
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* priority, not magnitude (see `pickFusionWinner`).
|
|
117
|
-
*/
|
|
118
|
-
export function fuseByEntry(db, cardLexical, fragmentLexical, semantic, opts = {}) {
|
|
119
|
-
const cardByEntry = groupLexicalHitsByEntry(db, cardLexical);
|
|
120
|
-
const fragmentByEntry = groupLexicalHitsByEntry(db, fragmentLexical);
|
|
121
|
-
const semanticByEntry = groupUnitHitsByEntry(db, semantic);
|
|
122
|
-
const includeTypes = opts.typeFilter && opts.typeFilter.length > 0 ? new Set(opts.typeFilter) : null;
|
|
123
|
-
const excludeTypes = opts.excludeTypes && opts.excludeTypes.length > 0 ? new Set(opts.excludeTypes) : null;
|
|
124
|
-
const entryIds = new Set([...cardByEntry.keys(), ...fragmentByEntry.keys(), ...semanticByEntry.keys()]);
|
|
125
|
-
const results = [];
|
|
126
|
-
for (const entryId of entryIds) {
|
|
127
|
-
const cardHit = cardByEntry.get(entryId);
|
|
128
|
-
const fragmentHit = fragmentByEntry.get(entryId);
|
|
129
|
-
const semanticHit = semanticByEntry.get(entryId);
|
|
130
|
-
const lexicalHit = cardHit ?? fragmentHit;
|
|
131
|
-
const lexicalScore = Math.max(cardHit ? stableFtsScore(cardHit.bm25, "parent") : 0, fragmentHit ? stableFtsScore(fragmentHit.bm25, "fragment") : 0);
|
|
132
|
-
const semanticScore = semanticHit ? semanticCosine(semanticHit.distance) : undefined;
|
|
133
|
-
const score = lexicalHit
|
|
134
|
-
? semanticScore === undefined
|
|
135
|
-
? lexicalScore
|
|
136
|
-
: lexicalScore * LEXICAL_WEIGHT + semanticScore * SEMANTIC_WEIGHT
|
|
137
|
-
: (semanticScore ?? 0) * SEMANTIC_WEIGHT;
|
|
138
|
-
const { unitHash, fragmentId } = pickFusionWinner(cardHit, fragmentHit, semanticHit);
|
|
139
|
-
const found = getEntryById(db, entryId);
|
|
37
|
+
export function combineSearchScores(options) {
|
|
38
|
+
const FTS_WEIGHT = 0.7;
|
|
39
|
+
const VEC_WEIGHT = 0.3;
|
|
40
|
+
const excludeTypeSet = options.excludeTypes && options.excludeTypes.length > 0 ? new Set(options.excludeTypes) : null;
|
|
41
|
+
const scored = [];
|
|
42
|
+
const seenIds = new Set();
|
|
43
|
+
for (const [id, { score: ftsScore, result }] of options.ftsScoreMap) {
|
|
44
|
+
seenIds.add(id);
|
|
45
|
+
const embedScore = options.embedScoreMap.get(id);
|
|
46
|
+
const combinedScore = embedScore !== undefined ? ftsScore * FTS_WEIGHT + embedScore * VEC_WEIGHT : ftsScore;
|
|
47
|
+
scored.push({
|
|
48
|
+
id,
|
|
49
|
+
entry: result.entry,
|
|
50
|
+
filePath: result.filePath,
|
|
51
|
+
score: combinedScore,
|
|
52
|
+
rankingMode: embedScore !== undefined ? "hybrid" : "fts",
|
|
53
|
+
lexicalMatch: result.lexicalMatch,
|
|
54
|
+
itemRef: result.itemRef,
|
|
55
|
+
bundleId: result.bundleId,
|
|
56
|
+
conceptId: result.conceptId,
|
|
57
|
+
fragmentId: result.fragmentId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const [id, cosine] of options.embedScoreMap) {
|
|
61
|
+
if (seenIds.has(id))
|
|
62
|
+
continue;
|
|
63
|
+
const found = options.getEntryById(id);
|
|
140
64
|
if (!found)
|
|
141
65
|
continue;
|
|
142
|
-
if (
|
|
66
|
+
if (options.typeFilter && found.entry.type !== options.typeFilter)
|
|
143
67
|
continue;
|
|
144
|
-
|
|
68
|
+
// #627 — drop vector-only neighbors whose type is excluded on the default path.
|
|
69
|
+
if (excludeTypeSet?.has(found.entry.type))
|
|
145
70
|
continue;
|
|
146
|
-
|
|
147
|
-
id
|
|
71
|
+
scored.push({
|
|
72
|
+
id,
|
|
148
73
|
entry: found.entry,
|
|
149
74
|
filePath: found.filePath,
|
|
150
|
-
score,
|
|
151
|
-
rankingMode:
|
|
75
|
+
score: cosine * VEC_WEIGHT,
|
|
76
|
+
rankingMode: "semantic",
|
|
152
77
|
itemRef: found.itemRef,
|
|
153
78
|
bundleId: found.bundleId,
|
|
154
79
|
conceptId: found.conceptId,
|
|
155
|
-
...(lexicalHit ? { lexicalMatch: lexicalHit.lexicalMatch } : {}),
|
|
156
|
-
...(fragmentId ? { fragmentId } : {}),
|
|
157
|
-
matchedUnit: { unitHash, fragmentId, kind: unitKindFromFragmentId(fragmentId) },
|
|
158
80
|
});
|
|
159
81
|
}
|
|
160
|
-
return
|
|
82
|
+
return scored;
|
|
161
83
|
}
|
|
162
84
|
export function applyRankingRules(options) {
|
|
163
85
|
const queryTokens = buildLexicalQueryPlan(options.query).tokens.map((token) => token.toLowerCase());
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
7
|
-
import { bundleComponentConfig, bundlesToSourceEntries, getSources, loadConfig } from "../../core/config/config.js";
|
|
7
|
+
import { bundleComponentConfig, bundleKeyForContentRoot, bundlesToSourceEntries, getSources, isBundleEnabled, loadConfig, } from "../../core/config/config.js";
|
|
8
|
+
import { ConfigError } from "../../core/errors.js";
|
|
8
9
|
import { getUnresolvedSourcesDir } from "../../core/paths.js";
|
|
9
10
|
import { resolveGitContentRoot, resolveWritable } from "../../core/write-source.js";
|
|
10
11
|
import { lockContentRootFor } from "../../integrations/lockfile.js";
|
|
@@ -23,7 +24,7 @@ import { warn } from "../../core/warn.js";
|
|
|
23
24
|
* 2. The configured `defaultBundle`, after component-root validation.
|
|
24
25
|
* 3. Remaining configured bundles in installation-priority order.
|
|
25
26
|
*
|
|
26
|
-
* Disabled entries (`enabled: false`) are
|
|
27
|
+
* Disabled entries (`enabled: false`) are excluded before materialization.
|
|
27
28
|
* Missing configured roots remain in the result so the
|
|
28
29
|
* indexer can classify their scan as incomplete instead of mistaking them for
|
|
29
30
|
* removed sources.
|
|
@@ -39,23 +40,39 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
39
40
|
: config.defaultBundle
|
|
40
41
|
? undefined
|
|
41
42
|
: resolveStashDir();
|
|
43
|
+
const implicitConfiguredBundle = implicitStashDir ? bundleKeyForContentRoot(config, implicitStashDir) : undefined;
|
|
44
|
+
if (implicitConfiguredBundle && !isBundleEnabled(config, implicitConfiguredBundle)) {
|
|
45
|
+
throw new ConfigError(`The requested working source is disabled as bundle ${JSON.stringify(implicitConfiguredBundle)}.`, "INVALID_CONFIG_FILE");
|
|
46
|
+
}
|
|
42
47
|
// Explicit and environment overrides stay first. Without either override,
|
|
43
48
|
// the configured default enters through the validated loop below.
|
|
44
|
-
const sources = implicitStashDir ? [{ path: implicitStashDir, writable: true }] : [];
|
|
45
|
-
const
|
|
46
|
-
|
|
49
|
+
const sources = implicitStashDir ? [{ path: implicitStashDir, writable: true, isDefault: true }] : [];
|
|
50
|
+
const sourceIdentity = (dir) => {
|
|
51
|
+
const resolved = path.resolve(dir);
|
|
52
|
+
try {
|
|
53
|
+
return fs.realpathSync.native(resolved);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const seen = new Set(implicitStashDir ? [sourceIdentity(implicitStashDir)] : []);
|
|
60
|
+
const addSource = (dir, registryId, writable, type, adapterId, unresolved = false, isDefault = false) => {
|
|
47
61
|
const resolved = path.resolve(dir);
|
|
48
|
-
|
|
62
|
+
const identity = sourceIdentity(resolved);
|
|
63
|
+
if (seen.has(identity)) {
|
|
49
64
|
// Already in the source list — typically the primary stash injected at
|
|
50
65
|
// sources[0] before this loop. Enrich that entry with whatever metadata
|
|
51
66
|
// the matching config source carries so `--from <config-name>` can
|
|
52
67
|
// find it via registryId. Without this, the primary stash entry stays
|
|
53
68
|
// identity-less and a user-named primary source ("name": "my-stash")
|
|
54
69
|
// would validate but match zero entries when filtering.
|
|
55
|
-
const existing = sources.find((
|
|
70
|
+
const existing = sources.find((source) => sourceIdentity(source.path) === identity);
|
|
56
71
|
if (existing && existing.type === undefined) {
|
|
57
72
|
if (registryId)
|
|
58
73
|
existing.registryId = registryId;
|
|
74
|
+
if (isDefault)
|
|
75
|
+
existing.isDefault = true;
|
|
59
76
|
existing.type = type;
|
|
60
77
|
existing.writable = writable;
|
|
61
78
|
existing.adapterId = adapterId;
|
|
@@ -64,13 +81,14 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
64
81
|
existing.unresolved = true;
|
|
65
82
|
return;
|
|
66
83
|
}
|
|
67
|
-
seen.add(
|
|
84
|
+
seen.add(identity);
|
|
68
85
|
if (isSuspiciousStashRoot(dir)) {
|
|
69
86
|
warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
|
|
70
87
|
}
|
|
71
88
|
sources.push({
|
|
72
89
|
path: resolved,
|
|
73
90
|
...(registryId ? { registryId } : {}),
|
|
91
|
+
...(isDefault ? { isDefault: true } : {}),
|
|
74
92
|
writable,
|
|
75
93
|
type,
|
|
76
94
|
...(adapterId ? { adapterId } : {}),
|
|
@@ -88,17 +106,17 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
88
106
|
const contentRoot = resolveEntryContentDir(entry);
|
|
89
107
|
if (contentRoot == null) {
|
|
90
108
|
const unresolvedPath = path.join(getUnresolvedSourcesDir(implicitStashDir ?? process.cwd()), entry.name ?? entry.type);
|
|
91
|
-
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
|
|
109
|
+
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true, entry.name === config.defaultBundle);
|
|
92
110
|
continue;
|
|
93
111
|
}
|
|
94
112
|
const dir = path.resolve(contentRoot, component?.root ?? ".");
|
|
95
113
|
if (!isWithin(dir, contentRoot)) {
|
|
96
114
|
warn(`Warning: component root "${component?.root}" escapes bundle "${entry.name}"; skipping source.`);
|
|
97
115
|
const unresolvedPath = path.join(getUnresolvedSourcesDir(contentRoot), entry.name ?? entry.type);
|
|
98
|
-
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
|
|
116
|
+
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true, entry.name === config.defaultBundle);
|
|
99
117
|
continue;
|
|
100
118
|
}
|
|
101
|
-
addSource(dir, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter);
|
|
119
|
+
addSource(dir, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, false, entry.name === config.defaultBundle);
|
|
102
120
|
}
|
|
103
121
|
return sources;
|
|
104
122
|
}
|
|
@@ -581,8 +581,9 @@ function lowerLlm(request, base) {
|
|
|
581
581
|
(typeof tools === "object" && tools !== null && !Array.isArray(tools) && Object.keys(tools).length === 0);
|
|
582
582
|
if (empty)
|
|
583
583
|
translated.add("tools");
|
|
584
|
-
else
|
|
585
|
-
|
|
584
|
+
else {
|
|
585
|
+
throw new ConfigError("The direct LLM transport cannot enforce the resolved tool policy.", "INVALID_CONFIG_FILE");
|
|
586
|
+
}
|
|
586
587
|
}
|
|
587
588
|
if (own(request.runtime, "workspace")) {
|
|
588
589
|
if (request.runtime.workspace === null || request.runtime.workspace === "")
|
|
@@ -9,6 +9,37 @@ import { loadModelMap } from "./model-map.js";
|
|
|
9
9
|
function own(value, key) {
|
|
10
10
|
return Object.hasOwn(value, key);
|
|
11
11
|
}
|
|
12
|
+
function requestedToolNames(tools) {
|
|
13
|
+
if (tools === null)
|
|
14
|
+
return Object.freeze([]);
|
|
15
|
+
if (typeof tools === "string") {
|
|
16
|
+
return Object.freeze(tools
|
|
17
|
+
.split(",")
|
|
18
|
+
.map((tool) => tool.trim())
|
|
19
|
+
.filter(Boolean));
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(tools))
|
|
22
|
+
return Object.freeze(tools.map((tool) => tool.trim()).filter(Boolean));
|
|
23
|
+
const structured = tools;
|
|
24
|
+
// The portable structured spelling is a boolean allow/deny map. Opaque or
|
|
25
|
+
// nested provider policy cannot be reduced to tool names without guessing,
|
|
26
|
+
// so a host name allowlist must deny it (an explicit "*" may still allow it).
|
|
27
|
+
if (Object.values(structured).some((value) => typeof value !== "boolean"))
|
|
28
|
+
return undefined;
|
|
29
|
+
return Object.freeze(Object.keys(structured).filter((tool) => structured[tool] === true));
|
|
30
|
+
}
|
|
31
|
+
/** The production authorizer: executable assets may only narrow a host-owned ceiling. */
|
|
32
|
+
export function toolAuthorizerFromConfig(config) {
|
|
33
|
+
const allowed = new Set(config.execution?.allowedTools ?? []);
|
|
34
|
+
return (input) => {
|
|
35
|
+
const requested = requestedToolNames(input.tools);
|
|
36
|
+
const permitted = allowed.has("*") || requested?.every((tool) => allowed.has(tool)) === true;
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
status: permitted ? "allowed" : "denied",
|
|
39
|
+
policy: "config-execution-allowed-tools",
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}
|
|
12
43
|
/** Pure common cascade composition once caller-specific sources are rendered. */
|
|
13
44
|
export function planPreparedExecution(options) {
|
|
14
45
|
return planExecutionCascade({
|
|
@@ -56,7 +87,7 @@ export function prepareResolvedExecution(options) {
|
|
|
56
87
|
engines: executionEngineDefinitionsFromConfig(config),
|
|
57
88
|
modelMap: options.modelMap ?? loadModelMap({ engines: config.engines }).map,
|
|
58
89
|
invocationKind: options.invocationKind,
|
|
59
|
-
|
|
90
|
+
authorizeTools: options.authorizeTools ?? toolAuthorizerFromConfig(config),
|
|
60
91
|
});
|
|
61
92
|
return Object.freeze({
|
|
62
93
|
plan,
|
|
@@ -324,7 +324,7 @@ export function buildReflectPrompt(input) {
|
|
|
324
324
|
sections.push(lines.join("\n"));
|
|
325
325
|
}
|
|
326
326
|
if (input.avoidPatterns && input.avoidPatterns.length > 0) {
|
|
327
|
-
sections.push(`## Avoid These Patterns\nPrevious assets in this run produced these errors — do not repeat them:\n${input.avoidPatterns.map((e) => `- ${e}`).join("\n")}`);
|
|
327
|
+
sections.push(`## Avoid These Patterns\nRun-only guidance: do not copy this heading, explanation, or any diagnostic below into the proposed asset.\nPrevious assets in this run produced these errors — do not repeat them:\n${input.avoidPatterns.map((e) => `- ${e}`).join("\n")}`);
|
|
328
328
|
}
|
|
329
329
|
// R-1 / #372: Self-Refine (arXiv:2303.17651) — inject prior draft as critique target.
|
|
330
330
|
// On refinement iterations (iter > 0), the agent is shown its previous proposal
|
|
@@ -162,8 +162,9 @@ export function createAgentRequestLowerer(options) {
|
|
|
162
162
|
const tools = request.tools;
|
|
163
163
|
if (translatesTools(options.tools, tools))
|
|
164
164
|
translated.add("tools");
|
|
165
|
-
else
|
|
166
|
-
|
|
165
|
+
else {
|
|
166
|
+
throw new ConfigError(`The ${options.adapter} transport cannot enforce the resolved tool policy.`, "INVALID_CONFIG_FILE");
|
|
167
|
+
}
|
|
167
168
|
dispatch.tools = tools;
|
|
168
169
|
}
|
|
169
170
|
if (own(request.runtime, "settings")) {
|
package/dist/llm/client.js
CHANGED
|
@@ -444,11 +444,12 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
|
|
|
444
444
|
* attempts the schema request fresh every time and falls back once per call
|
|
445
445
|
* on a 4xx — see the in-memory tracker below.
|
|
446
446
|
*/
|
|
447
|
-
export async function probeLlmReachable(config) {
|
|
447
|
+
export async function probeLlmReachable(config, timeoutMs) {
|
|
448
448
|
try {
|
|
449
449
|
const raw = await chatCompletion(config, [{ role: "user", content: "Respond with just the word: ok" }], {
|
|
450
450
|
maxTokens: 16,
|
|
451
451
|
temperature: 0,
|
|
452
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
452
453
|
});
|
|
453
454
|
return raw.length > 0 ? { reachable: true } : { reachable: false, error: "empty response" };
|
|
454
455
|
}
|
|
@@ -456,15 +457,6 @@ export async function probeLlmReachable(config) {
|
|
|
456
457
|
return { reachable: false, error: err instanceof Error ? err.message : String(err) };
|
|
457
458
|
}
|
|
458
459
|
}
|
|
459
|
-
/**
|
|
460
|
-
* Default bound for a best-effort capability/reachability probe (#914):
|
|
461
|
-
* generous enough for a cold local model server to answer a route-existence
|
|
462
|
-
* check, short enough that `akm health --probe` and the provider-limits
|
|
463
|
-
* probe (`src/llm/embedders/provider-limits.ts`, index-units) do not stall a
|
|
464
|
-
* run on a dead endpoint. Shared so both probes bound themselves to the same
|
|
465
|
-
* value instead of drifting apart.
|
|
466
|
-
*/
|
|
467
|
-
export const HEALTH_PROBE_TIMEOUT_MS = 3_000;
|
|
468
460
|
/**
|
|
469
461
|
* Reachability probe for `akm health` (#914): one GET against the
|
|
470
462
|
* OpenAI-compatible `/models` route, bounded by `timeoutMs`. Any HTTP
|
|
@@ -472,7 +464,7 @@ export const HEALTH_PROBE_TIMEOUT_MS = 3_000;
|
|
|
472
464
|
* answers, not whether the route exists or the credential is right — so a
|
|
473
465
|
* cold local server is never asked to load a model just to be checked.
|
|
474
466
|
*/
|
|
475
|
-
export async function probeLlmEndpoint(config, timeoutMs =
|
|
467
|
+
export async function probeLlmEndpoint(config, timeoutMs = 3_000) {
|
|
476
468
|
try {
|
|
477
469
|
await fetch(`${config.endpoint.replace(/\/+$/, "")}/models`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
478
470
|
return { reachable: true };
|
package/dist/llm/embedder.js
CHANGED
|
@@ -104,17 +104,10 @@ async function embedOnce(text, embeddingConfig, signal) {
|
|
|
104
104
|
* `onBatch`, when given, fires once per provider/local batch as it completes
|
|
105
105
|
* (#954) so a caller can commit each batch's rows durably as they land
|
|
106
106
|
* rather than buffering the whole call — see `EmbeddingBatchCommit`.
|
|
107
|
-
*
|
|
108
|
-
* `packing`, when given, threads a remote request's window/exact-token-
|
|
109
|
-
* counter/Ollama `num_ctx` in from the provider's own probed limits
|
|
110
|
-
* (`probeProviderLimits`, `src/llm/embedders/provider-limits.ts`) instead of
|
|
111
|
-
* the retired `embedding.maxTokens`/`batchSize`/`contextLength` config keys
|
|
112
|
-
* (index redesign, B5) — see `EmbeddingRequestPacking`. Only the remote
|
|
113
|
-
* branch below consumes it; local/deterministic embedding ignores it.
|
|
114
107
|
*/
|
|
115
|
-
export async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch
|
|
108
|
+
export async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
|
|
116
109
|
if (embedderOverrides?.embedBatch) {
|
|
117
|
-
return embedderOverrides.embedBatch(texts, embeddingConfig, signal, onSkip, onBatch
|
|
110
|
+
return embedderOverrides.embedBatch(texts, embeddingConfig, signal, onSkip, onBatch);
|
|
118
111
|
}
|
|
119
112
|
if (texts.length === 0)
|
|
120
113
|
return [];
|
|
@@ -128,7 +121,7 @@ export async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch
|
|
|
128
121
|
return embeddings;
|
|
129
122
|
}
|
|
130
123
|
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
|
|
131
|
-
return new RemoteEmbedder(embeddingConfig).embedBatch(texts, signal, onSkip, onBatch
|
|
124
|
+
return new RemoteEmbedder(embeddingConfig).embedBatch(texts, signal, onSkip, onBatch);
|
|
132
125
|
}
|
|
133
126
|
// Local transformer: use the batched path (chunks of 32 via LocalEmbedder).
|
|
134
127
|
// When a localModel override is set we cannot share the singleton (which uses
|