akm-cli 0.9.0-beta.2 → 0.9.0-beta.27
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 +660 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +730 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +790 -0
- package/dist/commands/health.js +478 -15
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +634 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +145 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +33 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +280 -35
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +139 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +2079 -608
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +87 -0
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +51 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +369 -329
- package/dist/commands/read/curate.js +344 -80
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +255 -2
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +305 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +706 -42
- package/dist/indexer/db/db.js +364 -38
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +203 -3
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +71 -25
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +1 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -5
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +38 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +78 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1262 -591
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +485 -270
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +5 -2
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +9 -6
|
@@ -26,6 +26,15 @@ export class OpenCodeProvider {
|
|
|
26
26
|
isAvailable() {
|
|
27
27
|
return fs.existsSync(this.#baseDir);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Directory holding opencode's per-project session metadata files
|
|
31
|
+
* (`<base>/storage/session`). Returns `[]` when it does not exist on this
|
|
32
|
+
* machine. See {@link SessionLogHarness.watchRoots}.
|
|
33
|
+
*/
|
|
34
|
+
watchRoots() {
|
|
35
|
+
const sessionRoot = path.join(this.#baseDir, "storage", "session");
|
|
36
|
+
return fs.existsSync(sessionRoot) ? [sessionRoot] : [];
|
|
37
|
+
}
|
|
29
38
|
*readEvents(input) {
|
|
30
39
|
// Legacy behavior: stream raw log lines from the top-level dir and `log/`
|
|
31
40
|
// subdirectory. Kept to keep `getExecutionLogCandidates` working without
|
|
@@ -28,6 +28,22 @@ const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENO
|
|
|
28
28
|
export function getAvailableHarnesses() {
|
|
29
29
|
return HARNESSES.filter((harness) => harness.isAvailable());
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Map each available harness to its `{ harnessName, roots }` watch target,
|
|
33
|
+
* skipping harnesses that expose no roots (absent `watchRoots()` or an empty
|
|
34
|
+
* result). This is the one stable entry point the watcher uses so it never
|
|
35
|
+
* reaches into providers directly.
|
|
36
|
+
*/
|
|
37
|
+
export function getWatchTargets() {
|
|
38
|
+
const targets = [];
|
|
39
|
+
for (const harness of getAvailableHarnesses()) {
|
|
40
|
+
const roots = harness.watchRoots?.() ?? [];
|
|
41
|
+
if (roots.length === 0)
|
|
42
|
+
continue;
|
|
43
|
+
targets.push({ harnessName: harness.name, roots });
|
|
44
|
+
}
|
|
45
|
+
return targets;
|
|
46
|
+
}
|
|
31
47
|
export function normalizeSessionTopic(text) {
|
|
32
48
|
const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
33
49
|
if (normalized.length < 10)
|
package/dist/llm/client.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fetchWithTimeout } from "../core/common.js";
|
|
|
14
14
|
import { resolveSecret } from "../core/config/config.js";
|
|
15
15
|
import { escapeJsonStringControls, parseJsonResponse, stripCodeFences, stripThinkBlocks } from "../core/parse.js";
|
|
16
16
|
import { warnVerbose } from "../core/warn.js";
|
|
17
|
+
import { emitLlmUsage, extractUsageTokens } from "./usage-telemetry.js";
|
|
17
18
|
// Re-export shared parse utilities so existing importers of `client.ts` continue
|
|
18
19
|
// to resolve `parseJsonResponse` and `parseEmbeddedJsonResponse` from this module.
|
|
19
20
|
export { escapeJsonStringControls, parseEmbeddedJsonResponse, parseJsonResponse, stripCodeFences, stripThinkBlocks, } from "../core/parse.js";
|
|
@@ -118,9 +119,23 @@ function looksLikeContextOverflow(message) {
|
|
|
118
119
|
/**
|
|
119
120
|
* Decide whether a first-attempt {@link LlmCallError} is eligible for a single
|
|
120
121
|
* retry. Retryable: HTTP 5xx (`provider_error` with statusCode >= 500) and
|
|
121
|
-
* `network_error` whose message looks like a transient connection
|
|
122
|
-
*
|
|
123
|
-
*
|
|
122
|
+
* `network_error` whose message looks like a transient connection drop.
|
|
123
|
+
* NOT retryable: 4xx, `rate_limited` (429), `timeout`, `parse_error`, and
|
|
124
|
+
* context-overflow-classified errors.
|
|
125
|
+
*
|
|
126
|
+
* The connection-drop heuristic covers the substrings emitted across runtimes
|
|
127
|
+
* for a mid-flight socket close:
|
|
128
|
+
* - `ECONNRESET` / `EPIPE` — Node/libuv socket reset codes
|
|
129
|
+
* - `fetch failed` — undici's generic wrapper message
|
|
130
|
+
* - `socket connection was closed` — Bun's message for a dropped connection
|
|
131
|
+
* (e.g. "The socket connection was closed unexpectedly.")
|
|
132
|
+
* - `terminated` / `other side closed` — undici's phrasings for the same
|
|
133
|
+
*
|
|
134
|
+
* These all describe a transient transport failure where a second attempt can
|
|
135
|
+
* legitimately succeed, which is exactly the case a single bounded retry is
|
|
136
|
+
* meant to absorb. Before this list was widened, Bun's "socket connection was
|
|
137
|
+
* closed unexpectedly" fell through unretried and surfaced as a recurring
|
|
138
|
+
* failure in the improve/reflect and capability-probe flows.
|
|
124
139
|
*/
|
|
125
140
|
function isRetryable(err) {
|
|
126
141
|
if (looksLikeContextOverflow(err.message))
|
|
@@ -130,7 +145,12 @@ function isRetryable(err) {
|
|
|
130
145
|
}
|
|
131
146
|
if (err.code === "network_error") {
|
|
132
147
|
const lower = err.message.toLowerCase();
|
|
133
|
-
return lower.includes("econnreset") ||
|
|
148
|
+
return (lower.includes("econnreset") ||
|
|
149
|
+
lower.includes("epipe") ||
|
|
150
|
+
lower.includes("fetch failed") ||
|
|
151
|
+
lower.includes("socket connection was closed") ||
|
|
152
|
+
lower.includes("terminated") ||
|
|
153
|
+
lower.includes("other side closed"));
|
|
134
154
|
}
|
|
135
155
|
return false;
|
|
136
156
|
}
|
|
@@ -179,6 +199,10 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
179
199
|
const responseFormat = options?.responseSchema && config.supportsJsonSchema
|
|
180
200
|
? { response_format: { type: "json_schema", json_schema: { schema: options.responseSchema, strict: true } } }
|
|
181
201
|
: {};
|
|
202
|
+
// Wall-clock start for per-call usage telemetry (#576). Captured here so the
|
|
203
|
+
// emitted duration covers the full request/response/parse cycle of a single
|
|
204
|
+
// attempt, not the retry-wrapping `chatCompletion`.
|
|
205
|
+
const requestStartedAt = Date.now();
|
|
182
206
|
let response;
|
|
183
207
|
try {
|
|
184
208
|
response = await fetchWithTimeout(config.endpoint, {
|
|
@@ -241,6 +265,16 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs) {
|
|
|
241
265
|
catch {
|
|
242
266
|
throw new LlmCallError(`LLM response was not valid JSON ${config.endpoint}: ${redactErrorBody(rawOkBody)}`, "parse_error", response.status);
|
|
243
267
|
}
|
|
268
|
+
// Per-call usage telemetry (#576). Best-effort and fully isolated: a missing
|
|
269
|
+
// or garbled usage block still records duration + model, and a throwing sink
|
|
270
|
+
// can never fail the call (emitLlmUsage swallows its own errors). The stage
|
|
271
|
+
// is supplied ambiently by emitLlmUsage; no `stage` param is threaded here.
|
|
272
|
+
emitLlmUsage({
|
|
273
|
+
model: typeof json.model === "string" && json.model ? json.model : config.model,
|
|
274
|
+
durationMs: Date.now() - requestStartedAt,
|
|
275
|
+
finishReason: typeof json.choices?.[0]?.finish_reason === "string" ? json.choices[0].finish_reason : undefined,
|
|
276
|
+
...extractUsageTokens(json.usage),
|
|
277
|
+
});
|
|
244
278
|
const content = (json.choices?.[0]?.message?.content ?? "").trim();
|
|
245
279
|
const reasoning = (json.choices?.[0]?.message?.reasoning_content ?? "").trim();
|
|
246
280
|
return content || reasoning;
|
package/dist/llm/embedder.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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 { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache.js";
|
|
5
|
-
import { isTransformersAvailable, LocalEmbedder } from "./embedders/local.js";
|
|
5
|
+
import { DEFAULT_LOCAL_MODEL, isTransformersAvailable, LocalEmbedder } from "./embedders/local.js";
|
|
6
6
|
import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote.js";
|
|
7
7
|
// ── Re-exports (public API) ─────────────────────────────────────────────────
|
|
8
8
|
export { clearEmbeddingCache } from "./embedders/cache.js";
|
|
@@ -52,7 +52,8 @@ export async function embed(text, embeddingConfig, signal) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Generate embeddings for multiple texts in batch.
|
|
54
54
|
* Uses the OpenAI-compatible batch API for remote endpoints (batches of 100).
|
|
55
|
-
*
|
|
55
|
+
* Uses the LocalEmbedder.embedBatch path for the local transformer pipeline,
|
|
56
|
+
* which processes texts in chunks of 32 for genuine batched inference.
|
|
56
57
|
*/
|
|
57
58
|
export async function embedBatch(texts, embeddingConfig, signal) {
|
|
58
59
|
if (texts.length === 0)
|
|
@@ -60,8 +61,13 @@ export async function embedBatch(texts, embeddingConfig, signal) {
|
|
|
60
61
|
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
|
|
61
62
|
return new RemoteEmbedder(embeddingConfig).embedBatch(texts, signal);
|
|
62
63
|
}
|
|
63
|
-
// Local transformer:
|
|
64
|
+
// Local transformer: use the batched path (chunks of 32 via LocalEmbedder).
|
|
65
|
+
// When a localModel override is set we cannot share the singleton (which uses
|
|
66
|
+
// the default model), so fall back to per-text embedWithModel in that case.
|
|
64
67
|
const localModel = embeddingConfig?.localModel;
|
|
68
|
+
if (!localModel) {
|
|
69
|
+
return getLocalEmbedder().embedBatch(texts, signal);
|
|
70
|
+
}
|
|
65
71
|
const results = [];
|
|
66
72
|
for (const text of texts) {
|
|
67
73
|
if (signal?.aborted) {
|
|
@@ -77,6 +83,24 @@ export async function embedBatch(texts, embeddingConfig, signal) {
|
|
|
77
83
|
// facade and its `@huggingface/transformers` import chain. Re-export
|
|
78
84
|
// preserves the existing public API.
|
|
79
85
|
export { cosineSimilarity } from "./embedders/types.js";
|
|
86
|
+
// ── Model ID resolution ─────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Derive a stable string identifier for the embedding model in use.
|
|
89
|
+
* This is the `model_id` stored in `body_embeddings` (and used for the
|
|
90
|
+
* drop-all-on-mismatch purge when the model changes).
|
|
91
|
+
*
|
|
92
|
+
* Rules:
|
|
93
|
+
* - Remote endpoint: use `config.model` (the API-level model name).
|
|
94
|
+
* - Local transformers: use `config.localModel ?? DEFAULT_LOCAL_MODEL`.
|
|
95
|
+
* - No config: use `DEFAULT_LOCAL_MODEL` (the shared singleton model).
|
|
96
|
+
*/
|
|
97
|
+
export function resolveEmbeddingModelId(embeddingConfig) {
|
|
98
|
+
if (!embeddingConfig)
|
|
99
|
+
return DEFAULT_LOCAL_MODEL;
|
|
100
|
+
if (hasRemoteEndpoint(embeddingConfig))
|
|
101
|
+
return embeddingConfig.model ?? "remote";
|
|
102
|
+
return embeddingConfig.localModel ?? DEFAULT_LOCAL_MODEL;
|
|
103
|
+
}
|
|
80
104
|
// ── Availability check ──────────────────────────────────────────────────────
|
|
81
105
|
/**
|
|
82
106
|
* Check whether embedding is available with a detailed reason on failure.
|
|
@@ -19,8 +19,24 @@ import { getDirname, resolveModule } from "../../runtime.js";
|
|
|
19
19
|
* `all-MiniLM-L6-v2` at the same 384-dimension footprint.
|
|
20
20
|
*/
|
|
21
21
|
export const DEFAULT_LOCAL_MODEL = "Xenova/bge-small-en-v1.5";
|
|
22
|
+
/** Type-guard: true when the value looks like a batch Tensor (has .dims). */
|
|
23
|
+
function isBatchTensor(v) {
|
|
24
|
+
return (v !== null &&
|
|
25
|
+
typeof v === "object" &&
|
|
26
|
+
"data" in v &&
|
|
27
|
+
"dims" in v &&
|
|
28
|
+
Array.isArray(v.dims) &&
|
|
29
|
+
v.dims.length >= 2);
|
|
30
|
+
}
|
|
22
31
|
const LOCAL_EMBEDDER_DTYPE = "fp32";
|
|
23
32
|
const LOCAL_EMBEDDER_FALLBACK_DTYPE = "auto";
|
|
33
|
+
/**
|
|
34
|
+
* Maximum texts per batch for the local transformers pipeline. The pipeline
|
|
35
|
+
* can run genuine batched inference over a string array; 32 is a safe default
|
|
36
|
+
* that fits well inside most model context budgets while providing 10–50×
|
|
37
|
+
* throughput improvement over one-at-a-time calls on the cold minority.
|
|
38
|
+
*/
|
|
39
|
+
const LOCAL_BATCH_SIZE = 32;
|
|
24
40
|
/**
|
|
25
41
|
* Return the local model name that will be used for embedding.
|
|
26
42
|
* When `overrideModel` is provided it takes precedence; otherwise
|
|
@@ -77,15 +93,63 @@ export class LocalEmbedder {
|
|
|
77
93
|
}
|
|
78
94
|
return this.embedWithModel(text, this.defaultModel);
|
|
79
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Embed a batch of texts. Processes in chunks of `LOCAL_BATCH_SIZE` (32) so
|
|
98
|
+
* the transformers pipeline can run genuine batched inference rather than one
|
|
99
|
+
* call per text. Falls back to one-at-a-time if the pipeline does not support
|
|
100
|
+
* array input (older versions of @huggingface/transformers). Each chunk is
|
|
101
|
+
* checked against the AbortSignal between calls.
|
|
102
|
+
*/
|
|
80
103
|
async embedBatch(texts, signal) {
|
|
81
104
|
if (texts.length === 0)
|
|
82
105
|
return [];
|
|
106
|
+
if (signal?.aborted) {
|
|
107
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
|
|
108
|
+
}
|
|
109
|
+
const pipeline = await this.getPipeline(this.defaultModel);
|
|
83
110
|
const results = [];
|
|
84
|
-
for (
|
|
111
|
+
for (let i = 0; i < texts.length; i += LOCAL_BATCH_SIZE) {
|
|
85
112
|
if (signal?.aborted) {
|
|
86
113
|
throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
|
|
87
114
|
}
|
|
88
|
-
|
|
115
|
+
const chunk = texts.slice(i, i + LOCAL_BATCH_SIZE);
|
|
116
|
+
try {
|
|
117
|
+
// @huggingface/transformers feature-extraction pipeline accepts a
|
|
118
|
+
// string[] and returns a batch Tensor (NOT an Array<{data}>).
|
|
119
|
+
// The Tensor has .data (flat Float32Array, length = batch * dim) and
|
|
120
|
+
// .dims = [batch, dim]. Slice .data into per-row vectors using .dims.
|
|
121
|
+
const batchResult = await pipeline(chunk, {
|
|
122
|
+
pooling: "mean",
|
|
123
|
+
normalize: true,
|
|
124
|
+
});
|
|
125
|
+
if (isBatchTensor(batchResult)) {
|
|
126
|
+
const dim = batchResult.dims[1];
|
|
127
|
+
for (let row = 0; row < chunk.length; row++) {
|
|
128
|
+
results.push(Array.from(batchResult.data.subarray(row * dim, (row + 1) * dim)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (Array.isArray(batchResult)) {
|
|
132
|
+
// Older versions of @huggingface/transformers returned Array<{data}>.
|
|
133
|
+
for (const r of batchResult) {
|
|
134
|
+
results.push(Array.from(r.data));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Single-text result returned for a chunk — should not happen for
|
|
139
|
+
// string[] input, but handle defensively.
|
|
140
|
+
throw new Error("unexpected pipeline return shape for batch input");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Fallback: process one-at-a-time (older pipeline versions or mismatched
|
|
145
|
+
// return type). Fail-open per text: a single failure aborts the chunk.
|
|
146
|
+
for (const text of chunk) {
|
|
147
|
+
if (signal?.aborted) {
|
|
148
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
|
|
149
|
+
}
|
|
150
|
+
results.push(await this.embedWithModel(text, this.defaultModel));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
154
|
return results;
|
|
91
155
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* the connection via `resolveIndexPassLLM("graph", config)` and pass it
|
|
21
21
|
* straight through.
|
|
22
22
|
*/
|
|
23
|
+
import systemPromptTemplate from "../assets/prompts/graph-extract-system.md" with { type: "text" };
|
|
23
24
|
import userPromptTemplate from "../assets/prompts/graph-extract-user-prompt.md" with { type: "text" };
|
|
24
25
|
import { toErrorMessage } from "../core/common.js";
|
|
25
26
|
import { warn, warnVerbose } from "../core/warn.js";
|
|
@@ -41,7 +42,7 @@ const NON_ARRAY_BATCH_DISABLE_THRESHOLD = 2;
|
|
|
41
42
|
const MAX_ENTITIES_PER_ASSET = 32;
|
|
42
43
|
/** Hard cap on relations returned per asset. */
|
|
43
44
|
const MAX_RELATIONS_PER_ASSET = 32;
|
|
44
|
-
const SYSTEM_PROMPT =
|
|
45
|
+
const SYSTEM_PROMPT = systemPromptTemplate;
|
|
45
46
|
const USER_PROMPT_PREFIX = userPromptTemplate
|
|
46
47
|
.replace("{{MAX_ENTITIES}}", String(MAX_ENTITIES_PER_ASSET))
|
|
47
48
|
.replace("{{MAX_RELATIONS}}", String(MAX_RELATIONS_PER_ASSET));
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -18,20 +18,16 @@
|
|
|
18
18
|
* the connection via `resolveIndexPassLLM("memory", config)` and pass it
|
|
19
19
|
* straight through.
|
|
20
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" };
|
|
21
23
|
import { toErrorMessage } from "../core/common.js";
|
|
22
24
|
import { warn } from "../core/warn.js";
|
|
23
25
|
import { chatCompletion, LlmCallError, parseEmbeddedJsonResponse } from "./client.js";
|
|
24
26
|
import { tryLlmFeature } from "./feature-gate.js";
|
|
25
27
|
/** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
|
|
26
28
|
const MAX_BODY_CHARS = 4000;
|
|
27
|
-
const SYSTEM_PROMPT =
|
|
28
|
-
|
|
29
|
-
const USER_PROMPT_PREFIX = `Compress the memory below into one derived memory. Output ONLY JSON:
|
|
30
|
-
{"title":"short title string","description":"one sentence summary string","tags":["tag1","tag2"],"searchHints":["search phrase 1","search phrase 2"],"content":"2-3 sentence compressed body preserving key facts verbatim"}
|
|
31
|
-
Rules: be specific, no vague generalizations, preserve key facts (names/versions/paths/config keys verbatim), merge related points, 3-8 tags, 3-6 searchHints. The content field must be a plain string with 2-3 sentences.
|
|
32
|
-
|
|
33
|
-
Memory:
|
|
34
|
-
`;
|
|
29
|
+
const SYSTEM_PROMPT = memoryInferSystemPrompt;
|
|
30
|
+
const USER_PROMPT_PREFIX = memoryInferUserPrompt;
|
|
35
31
|
/**
|
|
36
32
|
* Strict JSON Schema for the derived-memory payload. Sent to providers that
|
|
37
33
|
* opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
|
|
@@ -1,9 +1,17 @@
|
|
|
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-driven metadata enhancement for stash entries.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `llm.ts` so the higher-level workflow (prompting the LLM to
|
|
8
|
+
* improve descriptions/tags/searchHints) lives separately from the low-level
|
|
9
|
+
* transport client in `client.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import metadataEnhanceSystemPrompt from "../assets/prompts/metadata-enhance-system.md" with { type: "text" };
|
|
4
12
|
import { chatCompletion, parseJsonResponse } from "./client.js";
|
|
5
13
|
import { tryLlmFeature } from "./feature-gate.js";
|
|
6
|
-
const SYSTEM_PROMPT =
|
|
14
|
+
const SYSTEM_PROMPT = metadataEnhanceSystemPrompt;
|
|
7
15
|
/**
|
|
8
16
|
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
9
17
|
* generate searchHints, and suggest tags.
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* Bridge per-call LLM usage telemetry (#576) to the events stream.
|
|
6
|
+
*
|
|
7
|
+
* `usage-telemetry.ts` stays dependency-free of the events/db layer so the
|
|
8
|
+
* low-level `client.ts` never imports persistence. This module is the wiring:
|
|
9
|
+
* it installs a {@link LlmUsageSink} that persists each {@link LlmUsageRecord}
|
|
10
|
+
* as one `llm_usage` event.
|
|
11
|
+
*
|
|
12
|
+
* Why reuse the events table (vs a dedicated table): volume is low (~100
|
|
13
|
+
* calls/day), the records are append-only and time-windowed exactly like every
|
|
14
|
+
* other event, and `akm health` already aggregates per-window event reads — a
|
|
15
|
+
* separate table would duplicate retention (`purgeOldEvents`), reads, and
|
|
16
|
+
* migration surface for no benefit. See the commit message for #576.
|
|
17
|
+
*
|
|
18
|
+
* Every record is written through `appendEvent`, which is itself best-effort
|
|
19
|
+
* (a write failure logs once and never throws). Combined with the sink-error
|
|
20
|
+
* swallowing in `emitLlmUsage`, telemetry can never break a real run.
|
|
21
|
+
*/
|
|
22
|
+
import { appendEvent } from "../core/events.js";
|
|
23
|
+
import { clearLlmUsageSink, hasLlmUsageSink, setLlmUsageSink } from "./usage-telemetry.js";
|
|
24
|
+
/** Event type for persisted per-call LLM usage telemetry. */
|
|
25
|
+
export const LLM_USAGE_EVENT = "llm_usage";
|
|
26
|
+
/**
|
|
27
|
+
* Project a usage record into event metadata, dropping `undefined` token
|
|
28
|
+
* fields so an absent-usage call records only `{stage, model, durationMs}`.
|
|
29
|
+
*/
|
|
30
|
+
function toEventMetadata(record) {
|
|
31
|
+
const metadata = { durationMs: record.durationMs };
|
|
32
|
+
if (record.stage !== undefined)
|
|
33
|
+
metadata.stage = record.stage;
|
|
34
|
+
if (record.model !== undefined)
|
|
35
|
+
metadata.model = record.model;
|
|
36
|
+
if (record.finishReason !== undefined)
|
|
37
|
+
metadata.finishReason = record.finishReason;
|
|
38
|
+
if (record.promptTokens !== undefined)
|
|
39
|
+
metadata.promptTokens = record.promptTokens;
|
|
40
|
+
if (record.completionTokens !== undefined)
|
|
41
|
+
metadata.completionTokens = record.completionTokens;
|
|
42
|
+
if (record.totalTokens !== undefined)
|
|
43
|
+
metadata.totalTokens = record.totalTokens;
|
|
44
|
+
if (record.reasoningTokens !== undefined)
|
|
45
|
+
metadata.reasoningTokens = record.reasoningTokens;
|
|
46
|
+
return metadata;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Install a usage sink that persists each LLM call as an `llm_usage` event via
|
|
50
|
+
* `appendEvent`. Returns a disposer that clears the sink — call it in a
|
|
51
|
+
* `finally` block so per-run wiring does not leak across runs (and so the
|
|
52
|
+
* test-isolation harness sees a clean sink between tests).
|
|
53
|
+
*
|
|
54
|
+
* `ctx` should carry the same long-lived `state.db` handle the caller already
|
|
55
|
+
* opened for its other events; when omitted, `appendEvent` falls back to its
|
|
56
|
+
* default open-insert-close path.
|
|
57
|
+
*/
|
|
58
|
+
export function installLlmUsagePersistence(ctx) {
|
|
59
|
+
setLlmUsageSink((record) => {
|
|
60
|
+
appendEvent({ eventType: LLM_USAGE_EVENT, metadata: toEventMetadata(record) }, ctx);
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
clearLlmUsageSink();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Like {@link installLlmUsagePersistence}, but a no-op when a sink is already
|
|
68
|
+
* installed — used by standalone entry points (`akm consolidate`, `akm drain`)
|
|
69
|
+
* that may also run as a sub-step of `akm improve`. When invoked inside an
|
|
70
|
+
* enclosing run the existing per-run sink keeps ownership; the returned
|
|
71
|
+
* disposer then does nothing, so the enclosing run's `finally` still clears it.
|
|
72
|
+
*/
|
|
73
|
+
export function installLlmUsagePersistenceIfAbsent(ctx) {
|
|
74
|
+
if (hasLlmUsageSink())
|
|
75
|
+
return () => { };
|
|
76
|
+
return installLlmUsagePersistence(ctx);
|
|
77
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
* Per-call LLM usage telemetry (#576).
|
|
6
|
+
*
|
|
7
|
+
* `chatCompletion` captures usage + model + finish_reason + wall-time for
|
|
8
|
+
* EVERY OpenAI-compatible call and emits one {@link LlmUsageRecord} through a
|
|
9
|
+
* module-level sink. The sink indirection keeps `client.ts` free of any
|
|
10
|
+
* dependency on the events/db layer: the application wires the sink to
|
|
11
|
+
* persistence at startup / per improve run, and tests can inspect records in
|
|
12
|
+
* memory.
|
|
13
|
+
*
|
|
14
|
+
* The pipeline *stage* that made the call is ambient, not threaded through
|
|
15
|
+
* call sites. A param-threading prototype was deliberately discarded in 0.8.5
|
|
16
|
+
* (every call site would have to forward a `stage` argument it does not care
|
|
17
|
+
* about). Instead callers wrap a well-delimited phase once with
|
|
18
|
+
* {@link withLlmStage}; any `chatCompletion` invoked inside that async region —
|
|
19
|
+
* however deeply nested — is attributed to that stage via `AsyncLocalStorage`.
|
|
20
|
+
*
|
|
21
|
+
* EVERYTHING here is best-effort. Telemetry must NEVER break a real LLM call:
|
|
22
|
+
* a sink that throws, an unset stage, or a malformed usage block all degrade
|
|
23
|
+
* silently. `emitLlmUsage` swallows sink errors; `currentLlmStage` returns
|
|
24
|
+
* `undefined` outside any `withLlmStage` scope.
|
|
25
|
+
*/
|
|
26
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
+
const stageStorage = new AsyncLocalStorage();
|
|
28
|
+
let usageSink;
|
|
29
|
+
/**
|
|
30
|
+
* Run `fn` with `stage` as the ambient LLM stage. Any `chatCompletion` call
|
|
31
|
+
* made synchronously or asynchronously within `fn` (including through awaited
|
|
32
|
+
* helpers and nested `withLlmStage` calls — the innermost wins) is attributed
|
|
33
|
+
* to `stage`. Returns whatever `fn` returns; never alters control flow.
|
|
34
|
+
*/
|
|
35
|
+
export function withLlmStage(stage, fn) {
|
|
36
|
+
return stageStorage.run(stage, fn);
|
|
37
|
+
}
|
|
38
|
+
/** The ambient LLM stage for the current async context, or `undefined` outside any {@link withLlmStage} scope. */
|
|
39
|
+
export function currentLlmStage() {
|
|
40
|
+
return stageStorage.getStore();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Install the process-wide usage sink. Replaces any previously installed sink.
|
|
44
|
+
* The application wires this to persistence; tests install an in-memory
|
|
45
|
+
* collector. Pair with {@link clearLlmUsageSink} in a `finally` block.
|
|
46
|
+
*/
|
|
47
|
+
export function setLlmUsageSink(sink) {
|
|
48
|
+
usageSink = sink;
|
|
49
|
+
}
|
|
50
|
+
/** Remove the installed sink so subsequent calls emit nowhere. Idempotent. */
|
|
51
|
+
export function clearLlmUsageSink() {
|
|
52
|
+
usageSink = undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a usage sink is currently installed. Standalone entry points use
|
|
56
|
+
* this to avoid clobbering a sink an enclosing run (e.g. `akm improve`) already
|
|
57
|
+
* installed: they install their own only when none is active.
|
|
58
|
+
*/
|
|
59
|
+
export function hasLlmUsageSink() {
|
|
60
|
+
return usageSink !== undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Emit one usage record to the installed sink, stamping the ambient stage.
|
|
64
|
+
* Best-effort: no sink is a no-op, and a sink that throws is swallowed so
|
|
65
|
+
* telemetry can never fail the LLM call that produced it.
|
|
66
|
+
*/
|
|
67
|
+
export function emitLlmUsage(record) {
|
|
68
|
+
const sink = usageSink;
|
|
69
|
+
if (!sink)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
sink({ ...record, stage: record.stage ?? currentLlmStage() });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Telemetry must never break a real run.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function asFiniteNonNegative(value) {
|
|
79
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Project a provider `usage` block into the token fields of an
|
|
83
|
+
* {@link LlmUsageRecord}. Missing or garbled values are omitted (not zeroed)
|
|
84
|
+
* so a best-effort record still distinguishes "0 tokens" from "unknown".
|
|
85
|
+
*/
|
|
86
|
+
export function extractUsageTokens(usage) {
|
|
87
|
+
if (!usage || typeof usage !== "object")
|
|
88
|
+
return {};
|
|
89
|
+
const out = {};
|
|
90
|
+
const prompt = asFiniteNonNegative(usage.prompt_tokens);
|
|
91
|
+
const completion = asFiniteNonNegative(usage.completion_tokens);
|
|
92
|
+
const total = asFiniteNonNegative(usage.total_tokens);
|
|
93
|
+
const reasoning = asFiniteNonNegative(usage.completion_tokens_details?.reasoning_tokens);
|
|
94
|
+
if (prompt !== undefined)
|
|
95
|
+
out.promptTokens = prompt;
|
|
96
|
+
if (completion !== undefined)
|
|
97
|
+
out.completionTokens = completion;
|
|
98
|
+
if (total !== undefined)
|
|
99
|
+
out.totalTokens = total;
|
|
100
|
+
if (reasoning !== undefined)
|
|
101
|
+
out.reasoningTokens = reasoning;
|
|
102
|
+
return out;
|
|
103
|
+
}
|
package/dist/output/context.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Initialized from `cli.ts` before `runMain`.
|
|
13
13
|
*/
|
|
14
14
|
import { UsageError } from "../core/errors.js";
|
|
15
|
-
export const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl", "md"];
|
|
15
|
+
export const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl", "md", "html"];
|
|
16
16
|
export const DETAIL_LEVELS = ["brief", "normal", "full"];
|
|
17
17
|
export const SHAPE_MODES = ["human", "agent", "summary"];
|
|
18
18
|
export function parseOutputFormat(value) {
|
|
@@ -80,7 +80,8 @@ export function resolveOutputMode(argv, defaults = {}) {
|
|
|
80
80
|
// use `--shape`. Unknown `--detail` values fall through to the default.
|
|
81
81
|
const detail = parseDetailLevel(rawDetail) ?? defaults?.detail ?? "brief";
|
|
82
82
|
const shape = parseShapeMode(rawShape) ?? "human";
|
|
83
|
-
|
|
83
|
+
const outputPath = parseFlagValue(argv, "--output");
|
|
84
|
+
return { format, detail, shape, forAgent: shape === "agent", ...(outputPath ? { outputPath } : {}) };
|
|
84
85
|
}
|
|
85
86
|
let _mode;
|
|
86
87
|
/**
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
* `--format html` rendering primitives (#582).
|
|
6
|
+
*
|
|
7
|
+
* Templates live in `src/assets/templates/html/` (mirrored to
|
|
8
|
+
* `dist/assets/templates/html/` by `scripts/copy-assets.ts`). A command with a
|
|
9
|
+
* bespoke template ships `<command>.html`; every other command falls back to
|
|
10
|
+
* `default.html`, which renders the command's JSON envelope in a `<pre>`
|
|
11
|
+
* block. Substitution is plain `%%TOKEN%%` string replacement — no template
|
|
12
|
+
* engine, by design.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { getDirname } from "../runtime.js";
|
|
17
|
+
const TEMPLATES_DIR = path.join(getDirname(import.meta.url), "../assets/templates/html");
|
|
18
|
+
/** Template used by every command without a bespoke `<command>.html`. */
|
|
19
|
+
export const DEFAULT_TEMPLATE = "default";
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the on-disk template path for a command. `<command>.html` when the
|
|
22
|
+
* command ships a bespoke template (today: `health`), otherwise
|
|
23
|
+
* `default.html`. Command names are sanitized to a bare basename so a hostile
|
|
24
|
+
* command string can never escape the templates directory.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveTemplatePath(command) {
|
|
27
|
+
const name = path.basename(command.trim());
|
|
28
|
+
const candidate = path.join(TEMPLATES_DIR, `${name}.html`);
|
|
29
|
+
if (name !== DEFAULT_TEMPLATE && fs.existsSync(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
return path.join(TEMPLATES_DIR, `${DEFAULT_TEMPLATE}.html`);
|
|
32
|
+
}
|
|
33
|
+
/** Matches a `%%TOKEN%%` placeholder (uppercase + underscore key). */
|
|
34
|
+
const TOKEN_RE = /%%[A-Z_]+%%/g;
|
|
35
|
+
/**
|
|
36
|
+
* Read a template and substitute every `%%TOKEN%%` in `replacements` in a
|
|
37
|
+
* single pass. Substitution is order-independent: a value that happens to
|
|
38
|
+
* contain another token's literal text is never re-processed (the pass scans
|
|
39
|
+
* the original template, not the growing output). Unknown tokens in the
|
|
40
|
+
* template are left in place (the health template is verified token-complete by
|
|
41
|
+
* tests); replacement keys missing from the template are silently ignored,
|
|
42
|
+
* matching the skill renderer's behaviour.
|
|
43
|
+
*/
|
|
44
|
+
export function renderHtml(templatePath, replacements) {
|
|
45
|
+
const html = fs.readFileSync(templatePath, "utf8");
|
|
46
|
+
return html.replace(TOKEN_RE, (token) => (token in replacements ? replacements[token] : token));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimal HTML entity escaping for text interpolated into templates. Escapes
|
|
50
|
+
* the single quote as well as the double quote so escaped values are safe in
|
|
51
|
+
* both `"…"` and `'…'` attribute contexts, not only the double-quoted
|
|
52
|
+
* attributes the bundled templates use today.
|
|
53
|
+
*/
|
|
54
|
+
export function escapeHtml(value) {
|
|
55
|
+
return value
|
|
56
|
+
.replaceAll("&", "&")
|
|
57
|
+
.replaceAll("<", "<")
|
|
58
|
+
.replaceAll(">", ">")
|
|
59
|
+
.replaceAll('"', """)
|
|
60
|
+
.replaceAll("'", "'");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Deliver a rendered document: write to `outputPath` when set (`--output`),
|
|
64
|
+
* otherwise print to stdout.
|
|
65
|
+
*/
|
|
66
|
+
export function deliverRendered(content, outputPath) {
|
|
67
|
+
if (outputPath) {
|
|
68
|
+
fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
|
|
69
|
+
fs.writeFileSync(outputPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(content);
|
|
73
|
+
}
|