@adia-ai/a2ui-retrieval 0.6.4 → 0.6.6

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.
@@ -1,55 +1,44 @@
1
- /**
2
- * Chunk embedding retriever loads the build-time chunk embedding index
3
- * and scores a query against every chunk via cosine similarity.
4
- *
5
- * Sibling to `embedding-retriever.js` (pattern embeddings); same provider
6
- * conventions, same graceful-degradation contract.
7
- *
8
- * Index: packages/a2ui/corpus/chunk-embeddings.json (built by
9
- * scripts/build/embeddings-chunks.mjs). When missing or empty, the
10
- * retriever is effectively a no-op — callers see an empty score map and
11
- * should fall back to keyword-only ranking.
12
- *
13
- * Used by chunk-library.searchChunks() to blend semantic + keyword scores.
14
- */
15
-
16
- import { detectProvider, cosine, voyage, openai } from './embedding-provider.js';
17
-
18
- const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
19
-
1
+ import { detectProvider, cosine, voyage, openai } from "./embedding-provider.js";
2
+ const IS_NODE = typeof process !== "undefined" && !!process.versions?.node;
20
3
  let _index = null;
21
- let _indexByName = null; // Map<chunk-name, Float32Array>
4
+ let _indexByName = null;
22
5
  let _loadPromise = null;
23
6
  let _embedFn = null;
24
7
  let _available = null;
25
-
26
8
  async function _loadIndex() {
27
9
  if (_index !== null) return _index;
28
10
  if (_loadPromise) return _loadPromise;
29
11
  _loadPromise = (async () => {
30
12
  try {
31
13
  if (IS_NODE) {
32
- const fs = await import(/* @vite-ignore */ 'node:fs/promises');
33
- const path = await import(/* @vite-ignore */ 'node:path');
34
- const url = await import(/* @vite-ignore */ 'node:url');
35
- // Try the package-import path first (works under node_modules
36
- // install layout `@adia-ai/a2ui-corpus` exports
37
- // `./chunk-embeddings`). Fall back to the relative source-tree
38
- // path so a source-checkout monorepo without symlinked deps
39
- // still resolves.
14
+ const fs = await import(
15
+ /* @vite-ignore */
16
+ "node:fs/promises"
17
+ );
18
+ const path = await import(
19
+ /* @vite-ignore */
20
+ "node:path"
21
+ );
22
+ const url = await import(
23
+ /* @vite-ignore */
24
+ "node:url"
25
+ );
40
26
  let p = null;
41
27
  try {
42
- const { createRequire } = await import(/* @vite-ignore */ 'node:module');
43
- const require = createRequire(import.meta.url);
44
- p = require.resolve('@adia-ai/a2ui-corpus/chunk-embeddings');
28
+ const { createRequire } = await import(
29
+ /* @vite-ignore */
30
+ "node:module"
31
+ );
32
+ const require2 = createRequire(import.meta.url);
33
+ p = require2.resolve("@adia-ai/a2ui-corpus/chunk-embeddings");
45
34
  } catch {
46
35
  const here = path.dirname(url.fileURLToPath(import.meta.url));
47
- p = path.resolve(here, '../../corpus/chunk-embeddings.json');
36
+ p = path.resolve(here, "../../corpus/chunk-embeddings.json");
48
37
  }
49
- const raw = await fs.readFile(p, 'utf8');
38
+ const raw = await fs.readFile(p, "utf8");
50
39
  _index = JSON.parse(raw);
51
40
  } else {
52
- const url = new URL('../../corpus/chunk-embeddings.json', import.meta.url);
41
+ const url = new URL("../../corpus/chunk-embeddings.json", import.meta.url);
53
42
  const res = await fetch(url).catch(() => null);
54
43
  _index = res?.ok ? await res.json().catch(() => null) : null;
55
44
  }
@@ -57,7 +46,7 @@ async function _loadIndex() {
57
46
  _index = null;
58
47
  }
59
48
  if (_index?.chunks?.length) {
60
- _indexByName = new Map();
49
+ _indexByName = /* @__PURE__ */ new Map();
61
50
  for (const c of _index.chunks) {
62
51
  if (c?.name && Array.isArray(c.vector)) {
63
52
  _indexByName.set(c.name, Float32Array.from(c.vector));
@@ -68,41 +57,22 @@ async function _loadIndex() {
68
57
  })();
69
58
  return _loadPromise;
70
59
  }
71
-
72
60
  function _resolveEmbed(providerName, model) {
73
- // The index's recorded (provider, model) is the source of truth — query
74
- // embeddings MUST be generated by the same model the corpus was indexed
75
- // with. If they're different models (e.g. voyage-3-lite vs.
76
- // text-embedding-3-small), the cosine similarity is meaningless, and even
77
- // same-provider/different-model (e.g. text-embedding-3-small@1536 vs
78
- // text-embedding-3-large@3072) will emit different dim vectors which
79
- // cosine() short-circuits to 0 — silent retrieval failure.
80
- //
81
- // Fail loud: do NOT silently fall back to a different provider's auto-pick.
82
- // Caller (available()) returns false → keyword-only retrieval, which is
83
- // the right behavior, but with a console.warn so the cause is visible.
84
61
  let fn = null;
85
- if (providerName === 'voyage') fn = voyage({ model });
86
- else if (providerName === 'openai') fn = openai({ model });
62
+ if (providerName === "voyage") fn = voyage({ ...model !== void 0 ? { model } : {} });
63
+ else if (providerName === "openai") fn = openai({ ...model !== void 0 ? { model } : {} });
87
64
  else {
88
- // No provider recorded in index header (legacy index?). Fall back to
89
- // auto-detect — this preserves the old behavior for indexes that pre-date
90
- // the provider/model header convention.
91
65
  const auto = detectProvider();
92
- return auto?.embed || null;
66
+ return auto?.embed ?? null;
93
67
  }
94
- if (!fn && typeof console !== 'undefined') {
68
+ if (!fn && typeof console !== "undefined") {
95
69
  console.warn(
96
- `[chunk-embedding-retriever] index was built with provider=${providerName} model=${model}, ` +
97
- `but the corresponding API key is not set. Embeddings will be unavailable; falling back to ` +
98
- `keyword-only retrieval. Set the matching API key, or rebuild the index with the available ` +
99
- `provider via \`npm run build:embeddings:chunks\`.`
70
+ `[chunk-embedding-retriever] index was built with provider=${providerName} model=${model}, but the corresponding API key is not set. Embeddings will be unavailable; falling back to keyword-only retrieval. Set the matching API key, or rebuild the index with the available provider via \`npm run build:embeddings:chunks\`.`
100
71
  );
101
72
  }
102
73
  return fn;
103
74
  }
104
-
105
- export async function available() {
75
+ async function available() {
106
76
  if (_available !== null) return _available;
107
77
  const idx = await _loadIndex();
108
78
  if (!idx || !idx.chunks?.length) {
@@ -113,37 +83,35 @@ export async function available() {
113
83
  _available = !!_embedFn;
114
84
  return _available;
115
85
  }
116
-
117
- /**
118
- * Embed a query and return a Map<chunk-name → cosine-score>.
119
- * Returns an empty Map when unavailable (no index or no API key).
120
- */
121
- export async function scoreAll(query) {
122
- if (!query || typeof query !== 'string') return new Map();
123
- if (!(await available())) return new Map();
124
-
86
+ async function scoreAll(query) {
87
+ if (!query || typeof query !== "string") return /* @__PURE__ */ new Map();
88
+ if (!await available()) return /* @__PURE__ */ new Map();
125
89
  let qVec;
126
90
  try {
127
91
  const [v] = await _embedFn([query]);
128
92
  qVec = v;
129
93
  } catch (e) {
130
- if (typeof console !== 'undefined') console.warn('[chunk-embedding-retriever]', e.message);
131
- return new Map();
94
+ if (typeof console !== "undefined") console.warn("[chunk-embedding-retriever]", e.message);
95
+ return /* @__PURE__ */ new Map();
132
96
  }
133
-
134
- const out = new Map();
97
+ const out = /* @__PURE__ */ new Map();
98
+ if (!_indexByName) return out;
135
99
  for (const [name, vec] of _indexByName) {
136
100
  out.set(name, cosine(qVec, vec));
137
101
  }
138
102
  return out;
139
103
  }
140
-
141
- export async function size() {
104
+ async function size() {
142
105
  const idx = await _loadIndex();
143
- return idx?.chunks?.length || 0;
106
+ return idx?.chunks?.length ?? 0;
144
107
  }
145
-
146
- export async function providerInfo() {
108
+ async function providerInfo() {
147
109
  const idx = await _loadIndex();
148
110
  return idx ? { provider: idx.provider, model: idx.model, dims: idx.dims } : null;
149
111
  }
112
+ export {
113
+ available,
114
+ providerInfo,
115
+ scoreAll,
116
+ size
117
+ };
@@ -84,8 +84,8 @@ function _resolveEmbed(providerName: string | undefined, model: string | undefin
84
84
  // The index's recorded (provider, model) is the source of truth — query
85
85
  // embeddings MUST be generated by the same model the corpus was indexed with.
86
86
  let fn: EmbedFn | null = null;
87
- if (providerName === 'voyage') fn = voyage({ model });
88
- else if (providerName === 'openai') fn = openai({ model });
87
+ if (providerName === 'voyage') fn = voyage({ ...(model !== undefined ? { model } : {}) });
88
+ else if (providerName === 'openai') fn = openai({ ...(model !== undefined ? { model } : {}) });
89
89
  else {
90
90
  // No provider recorded in index header (legacy index?). Fall back to auto-detect.
91
91
  const auto = detectProvider();
@@ -1,108 +1,72 @@
1
- /**
2
- * Pluggable embedding provider. Two implementations:
3
- *
4
- * voyage() — Voyage AI, voyage-3-lite by default (1024 dims).
5
- * Env: VOYAGE_API_KEY. Anthropic-recommended; generous free tier.
6
- *
7
- * openai() — OpenAI text-embedding-3-small (1536 dims).
8
- * Env: OPENAI_API_KEY. Cheap ($0.02/1M tokens).
9
- *
10
- * detectProvider() picks Voyage if available, else OpenAI, else null.
11
- * A null provider means "embeddings unavailable" — callers should fall back
12
- * to keyword retrieval cleanly.
13
- *
14
- * Both implementations batch inputs to minimize round-trips. Every provider
15
- * returns a Promise<Float32Array[]> of the same length as the input.
16
- */
17
-
18
- const VOYAGE_URL = 'https://api.voyageai.com/v1/embeddings';
19
- const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
20
-
1
+ const VOYAGE_URL = "https://api.voyageai.com/v1/embeddings";
2
+ const OPENAI_URL = "https://api.openai.com/v1/embeddings";
21
3
  function getEnv(key) {
22
- if (typeof process !== 'undefined' && process.env?.[key]) return process.env[key];
4
+ if (typeof process !== "undefined" && process.env?.[key]) return process.env[key];
23
5
  try {
24
6
  const env = import.meta.env;
25
7
  if (env?.[`VITE_${key}`]) return env[`VITE_${key}`];
26
8
  if (env?.[key]) return env[key];
27
- } catch {}
28
- return '';
9
+ } catch {
10
+ }
11
+ return "";
29
12
  }
30
-
31
- /**
32
- * Voyage AI embedding provider.
33
- *
34
- * @param {object} [opts]
35
- * @param {string} [opts.apiKey] — defaults to env VOYAGE_API_KEY
36
- * @param {string} [opts.model] — voyage-3-lite | voyage-3 | voyage-code-3
37
- * @returns {(texts: string[]) => Promise<Float32Array[]>} embedder, or null if no key
38
- */
39
- export function voyage({ apiKey, model = 'voyage-3-lite' } = {}) {
40
- const key = apiKey || getEnv('VOYAGE_API_KEY');
13
+ function voyage({ apiKey, model = "voyage-3-lite" } = {}) {
14
+ const key = apiKey ?? getEnv("VOYAGE_API_KEY");
41
15
  if (!key) return null;
42
-
43
16
  return async function embed(texts) {
44
17
  if (!Array.isArray(texts) || texts.length === 0) return [];
45
18
  const res = await fetch(VOYAGE_URL, {
46
- method: 'POST',
19
+ method: "POST",
47
20
  headers: {
48
- 'content-type': 'application/json',
49
- 'authorization': `Bearer ${key}`,
21
+ "content-type": "application/json",
22
+ "authorization": `Bearer ${key}`
50
23
  },
51
- body: JSON.stringify({ input: texts, model, input_type: 'document' }),
24
+ body: JSON.stringify({ input: texts, model, input_type: "document" })
52
25
  });
53
26
  if (!res.ok) throw new Error(`Voyage ${res.status}: ${(await res.text()).slice(0, 200)}`);
54
27
  const data = await res.json();
55
- return data.data.map(d => new Float32Array(d.embedding));
28
+ return data.data.map((d) => new Float32Array(d.embedding));
56
29
  };
57
30
  }
58
-
59
- /**
60
- * OpenAI text-embedding-3-small provider.
61
- *
62
- * @param {object} [opts]
63
- * @param {string} [opts.apiKey] — defaults to env OPENAI_API_KEY
64
- * @param {string} [opts.model] — text-embedding-3-small | text-embedding-3-large
65
- * @returns {(texts: string[]) => Promise<Float32Array[]>} embedder, or null if no key
66
- */
67
- export function openai({ apiKey, model = 'text-embedding-3-small' } = {}) {
68
- const key = apiKey || getEnv('OPENAI_API_KEY');
31
+ function openai({ apiKey, model = "text-embedding-3-small" } = {}) {
32
+ const key = apiKey ?? getEnv("OPENAI_API_KEY");
69
33
  if (!key) return null;
70
-
71
34
  return async function embed(texts) {
72
35
  if (!Array.isArray(texts) || texts.length === 0) return [];
73
36
  const res = await fetch(OPENAI_URL, {
74
- method: 'POST',
37
+ method: "POST",
75
38
  headers: {
76
- 'content-type': 'application/json',
77
- 'authorization': `Bearer ${key}`,
39
+ "content-type": "application/json",
40
+ "authorization": `Bearer ${key}`
78
41
  },
79
- body: JSON.stringify({ input: texts, model }),
42
+ body: JSON.stringify({ input: texts, model })
80
43
  });
81
44
  if (!res.ok) throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 200)}`);
82
45
  const data = await res.json();
83
- return data.data.map(d => new Float32Array(d.embedding));
46
+ return data.data.map((d) => new Float32Array(d.embedding));
84
47
  };
85
48
  }
86
-
87
- /**
88
- * Auto-detect the best available provider. Voyage first (cheaper, dense vectors
89
- * at 1024 dims), then OpenAI, then null.
90
- */
91
- export function detectProvider() {
92
- const v = voyage(); if (v) return { name: 'voyage', model: 'voyage-3-lite', embed: v };
93
- const o = openai(); if (o) return { name: 'openai', model: 'text-embedding-3-small', embed: o };
49
+ function detectProvider() {
50
+ const v = voyage();
51
+ if (v) return { name: "voyage", model: "voyage-3-lite", embed: v };
52
+ const o = openai();
53
+ if (o) return { name: "openai", model: "text-embedding-3-small", embed: o };
94
54
  return null;
95
55
  }
96
-
97
- /** Cosine similarity between two Float32Arrays of the same length. */
98
- export function cosine(a, b) {
56
+ function cosine(a, b) {
99
57
  if (!a || !b || a.length !== b.length) return 0;
100
58
  let dot = 0, na = 0, nb = 0;
101
59
  for (let i = 0; i < a.length; i++) {
102
- dot += a[i] * b[i];
103
- na += a[i] * a[i];
104
- nb += b[i] * b[i];
60
+ dot += (a[i] ?? 0) * (b[i] ?? 0);
61
+ na += (a[i] ?? 0) * (a[i] ?? 0);
62
+ nb += (b[i] ?? 0) * (b[i] ?? 0);
105
63
  }
106
64
  if (na === 0 || nb === 0) return 0;
107
65
  return dot / (Math.sqrt(na) * Math.sqrt(nb));
108
66
  }
67
+ export {
68
+ cosine,
69
+ detectProvider,
70
+ openai,
71
+ voyage
72
+ };
@@ -1,10 +1,2 @@
1
- /**
2
- * @adia-ai/a2ui-retrieval/embedding — embedding-provider + chunk retriever surface.
3
- *
4
- * Since §65 (v0.4.7), the legacy pattern-embedding retriever was retired
5
- * along with its only consumer (concept-mapper.js, dead post-§64). The
6
- * canonical retrieval surface post-§40 is chunk-embedding-retriever.
7
- */
8
-
9
- export * from './embedding-provider.js';
10
- export * from './chunk-embedding-retriever.js';
1
+ export * from "./embedding-provider.js";
2
+ export * from "./chunk-embedding-retriever.js";
@@ -1,191 +1,107 @@
1
- /**
2
- * Dialog Recorder write every generation turn to disk for debugging,
3
- * regression analysis, and training-data bootstrapping.
4
- *
5
- * Output: logs/dialogs/<sessionId>/<NNN>-<turnId>.json
6
- * - <sessionId>: storeId / executionId of the multi-turn session, or
7
- * 'standalone-<isodate>' for one-shot generations.
8
- * - <NNN>: zero-padded turn index within the session (000, 001, …)
9
- * - <turnId>: short slug from the intent + a random suffix for uniqueness
10
- *
11
- * Each file contains a flat JSON object with everything the pipeline knew at
12
- * generation time: intent, mode, model, the analyzer's structured signals,
13
- * pattern matches, the FULL system prompt, the raw LLM response, parsed A2UI
14
- * messages, validation, drift, suggestions, and timing/token telemetry.
15
- *
16
- * Gated by ADIA_LOG_DIALOGS=1 — opt-in, zero overhead when disabled. Browser
17
- * environment is also a no-op (no fs); only Node-side generation paths log.
18
- *
19
- * Use cases:
20
- * - replay a single bad turn locally without paying the LLM cost again
21
- * - diff two turns visually after wiring the headless renderer
22
- * - bootstrap an eval set from real dogfood — every turn becomes a labeled example
23
- * - regression detection on prompt / corpus / catalog changes
24
- */
25
-
26
- const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
27
- const ENABLED = IS_NODE && (process.env.ADIA_LOG_DIALOGS === '1' || process.env.ADIA_LOG_DIALOGS === 'true');
28
-
1
+ const IS_NODE = typeof process !== "undefined" && !!process.versions?.node;
2
+ const ENABLED = IS_NODE && (process?.env?.["ADIA_LOG_DIALOGS"] === "1" || process?.env?.["ADIA_LOG_DIALOGS"] === "true");
29
3
  let _fs = null;
30
4
  let _path = null;
31
5
  let _url = null;
32
6
  let _logsRoot = null;
33
-
34
7
  async function _ensureModules() {
35
8
  if (_fs) return;
36
- _fs = await import(/* @vite-ignore */ 'node:fs/promises');
37
- _path = await import(/* @vite-ignore */ 'node:path');
38
- _url = await import(/* @vite-ignore */ 'node:url');
39
- // logs/ lives at the repo root: packages/a2ui/retrieval/feedback → up 4 → repo root
9
+ _fs = await import(
10
+ /* @vite-ignore */
11
+ "node:fs/promises"
12
+ );
13
+ _path = await import(
14
+ /* @vite-ignore */
15
+ "node:path"
16
+ );
17
+ _url = await import(
18
+ /* @vite-ignore */
19
+ "node:url"
20
+ );
40
21
  const __dirname = _path.dirname(_url.fileURLToPath(import.meta.url));
41
- _logsRoot = _path.resolve(__dirname, '..', '..', '..', '..', 'logs', 'dialogs');
22
+ _logsRoot = _path.resolve(__dirname, "..", "..", "..", "..", "logs", "dialogs");
42
23
  }
43
-
44
- // In-memory turn counter per session keeps the on-disk turn ordering correct
45
- // even when timestamps collide (sub-millisecond turns from automated probes).
46
- const _turnCounter = new Map();
47
- // Tracks which sessions we've already written `_session.json` for. Per-process
48
- // memory; if the process restarts mid-session, the file is rewritten, which is
49
- // fine — content is idempotent.
50
- const _sessionMetaWritten = new Set();
51
-
52
- /**
53
- * Record one generation turn. Safe to call unconditionally — when the env var
54
- * is unset, this is a no-op that returns immediately.
55
- *
56
- * @param {object} record
57
- * @param {string} record.sessionId — multi-turn session handle (storeId / executionId)
58
- * @param {string} record.intent — user's prompt this turn
59
- * @param {string} [record.mode] — instant | pro | thinking | stream
60
- * @param {string} [record.engine] — monolithic | zettel | <custom>
61
- * @param {string} [record.model] — LLM model id
62
- * @param {object} [record.analysis] — output of analyzePrompt() (concepts, steelman, ...)
63
- * @param {object} [record.currentCanvas] — { components } or { messages } provided by caller
64
- * @param {object[]} [record.patterns] — patterns retrieved by searchBlocks()
65
- * @param {string} [record.systemPrompt] — full system prompt sent to LLM
66
- * @param {string} [record.rawLLMResponse] — raw LLM text (pre-parse)
67
- * @param {object[]} [record.messages] — parsed A2UI messages (the result)
68
- * @param {object} [record.validation] — validateSchema() result
69
- * @param {object} [record.drift] — getDriftMetrics() result
70
- * @param {string[]} [record.suggestions] — follow-up suggestions
71
- * @param {object} [record.timing] — { totalMs, llmMs, ... }
72
- * @param {object} [record.tokens] — { input, output }
73
- * @param {object} [record.engineDebug] — engine-specific extras (strategy, composition, fragmentsUsed for zettel; raw stage reports, etc.)
74
- * @param {boolean} [record.isIteration] — derived from executionId / currentCanvas presence
75
- * @returns {Promise<string|null>} the path written, or null when logging is disabled
76
- */
77
- export async function recordTurn(record) {
24
+ const _turnCounter = /* @__PURE__ */ new Map();
25
+ const _sessionMetaWritten = /* @__PURE__ */ new Set();
26
+ async function recordTurn(record) {
78
27
  if (!ENABLED) return null;
79
-
80
28
  try {
81
29
  await _ensureModules();
82
-
83
- const sessionId = record.sessionId || `standalone-${new Date().toISOString().replace(/[:.]/g, '-')}`;
84
- const turnIdx = (_turnCounter.get(sessionId) ?? 0);
30
+ const sessionId = record.sessionId ?? `standalone-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
31
+ const turnIdx = _turnCounter.get(sessionId) ?? 0;
85
32
  _turnCounter.set(sessionId, turnIdx + 1);
86
-
87
- const slug = String(record.intent || 'turn')
88
- .toLowerCase()
89
- .replace(/[^a-z0-9]+/g, '-')
90
- .replace(/^-+|-+$/g, '')
91
- .slice(0, 32) || 'turn';
33
+ const slug = String(record.intent ?? "turn").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "turn";
92
34
  const rand = Math.random().toString(36).slice(2, 6);
93
- const fileName = `${String(turnIdx).padStart(3, '0')}-${slug}-${rand}.json`;
94
-
35
+ const fileName = `${String(turnIdx).padStart(3, "0")}-${slug}-${rand}.json`;
95
36
  const sessionDir = _path.join(_logsRoot, sessionId);
96
37
  await _fs.mkdir(sessionDir, { recursive: true });
97
-
98
- // Write per-session header on the first turn so a directory listing is
99
- // immediately legible — origin intent, model, mode, first-turn analysis,
100
- // start time. Future headless renderer reads this for its session list UI.
101
38
  if (!_sessionMetaWritten.has(sessionId) && turnIdx === 0) {
102
39
  _sessionMetaWritten.add(sessionId);
103
40
  const sessionMeta = {
104
41
  sessionId,
105
- startedAt: new Date().toISOString(),
106
- originIntent: record.intent || null,
107
- engine: record.engine || null,
108
- mode: record.mode || null,
109
- model: record.model || null,
110
- // The analyzer's enriched brief + concept tags from turn 0 — these
111
- // characterize the session's intent space, not just the latest turn.
42
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
43
+ originIntent: record.intent ?? null,
44
+ engine: record.engine ?? null,
45
+ mode: record.mode ?? null,
46
+ model: record.model ?? null,
112
47
  originAnalysis: record.analysis ? {
113
- steelman: record.analysis.steelman || null,
114
- concepts: record.analysis.concepts || [],
115
- impliedComponents: record.analysis.impliedComponents || [],
116
- styleHints: record.analysis.styleHints || [],
117
- } : null,
48
+ steelman: record.analysis.steelman ?? null,
49
+ concepts: record.analysis.concepts ?? [],
50
+ impliedComponents: record.analysis.impliedComponents ?? [],
51
+ styleHints: record.analysis.styleHints ?? []
52
+ } : null
118
53
  };
119
54
  await _fs.writeFile(
120
- _path.join(sessionDir, '_session.json'),
121
- JSON.stringify(sessionMeta, null, 2) + '\n',
55
+ _path.join(sessionDir, "_session.json"),
56
+ JSON.stringify(sessionMeta, null, 2) + "\n"
122
57
  );
123
58
  }
124
-
125
59
  const payload = {
126
60
  // ── Identity ────────────────────────────────────────────────
127
61
  sessionId,
128
62
  turnIndex: turnIdx,
129
- timestamp: new Date().toISOString(),
63
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
130
64
  isIteration: !!record.isIteration,
131
-
132
65
  // ── Request ─────────────────────────────────────────────────
133
66
  intent: record.intent,
134
- mode: record.mode || null,
135
- engine: record.engine || null,
136
- model: record.model || null,
137
- currentCanvas: record.currentCanvas || null,
138
-
67
+ mode: record.mode ?? null,
68
+ engine: record.engine ?? null,
69
+ model: record.model ?? null,
70
+ currentCanvas: record.currentCanvas ?? null,
139
71
  // ── Pipeline-side reasoning ─────────────────────────────────
140
- analysis: record.analysis || null,
141
- patterns: (record.patterns || []).slice(0, 10).map(p => ({
72
+ analysis: record.analysis ?? null,
73
+ patterns: (record.patterns ?? []).slice(0, 10).map((p) => ({
142
74
  name: p.name,
143
75
  score: p.score ?? p.confidence ?? null,
144
- keywords: p.keywords || null,
76
+ keywords: p.keywords ?? null
145
77
  })),
146
-
147
- // ── LLM I/O (the largest fields — keep last for jq usability) ─
148
- systemPrompt: record.systemPrompt || null,
149
- rawLLMResponse: record.rawLLMResponse || null,
150
-
78
+ // ── LLM I/O ─────────────────────────────────────────────────
79
+ systemPrompt: record.systemPrompt ?? null,
80
+ rawLLMResponse: record.rawLLMResponse ?? null,
151
81
  // ── Result ──────────────────────────────────────────────────
152
- messages: record.messages || [],
153
- validation: record.validation || null,
154
- drift: record.drift || null,
155
- suggestions: record.suggestions || [],
156
-
82
+ messages: record.messages ?? [],
83
+ validation: record.validation ?? null,
84
+ drift: record.drift ?? null,
85
+ suggestions: record.suggestions ?? [],
157
86
  // ── Telemetry ───────────────────────────────────────────────
158
- timing: record.timing || null,
159
- tokens: record.tokens || null,
160
- // Engine-specific extras (zettel: strategy/composition/fragmentsUsed,
161
- // monolithic: nothing yet — but reserved). Whatever the engine put on
162
- // result._debug above and beyond the standard fields lands here.
163
- engineDebug: record.engineDebug || null,
87
+ timing: record.timing ?? null,
88
+ tokens: record.tokens ?? null,
89
+ engineDebug: record.engineDebug ?? null
164
90
  };
165
-
166
91
  const filePath = _path.join(sessionDir, fileName);
167
- await _fs.writeFile(filePath, JSON.stringify(payload, null, 2) + '\n');
92
+ await _fs.writeFile(filePath, JSON.stringify(payload, null, 2) + "\n");
168
93
  return filePath;
169
94
  } catch (err) {
170
- // Logging must NEVER break a generation. Swallow + warn once per turn.
171
- console.warn('[dialog-recorder] failed to record turn:', err.message);
95
+ console.warn("[dialog-recorder] failed to record turn:", err.message);
172
96
  return null;
173
97
  }
174
98
  }
175
-
176
- /** True when logging is on. Useful for guarding expensive capture work.
177
- *
178
- * Also returns true when `A2UI_COMPOSE_TRACE` is set, since the compose-trace
179
- * flag in `compose/core/generator.js` consumes the same `_debug` payload —
180
- * if we don't force-enable here, traces would be empty when the dialog
181
- * recorder is off (the common case).
182
- */
183
- export function isRecording() {
184
- // ENABLED is already gated by IS_NODE at module init. The A2UI_COMPOSE_TRACE
185
- // bypass must guard `process` again — without it, browser callers
186
- // (compose/core/generator.js → generate-pro.js → isRecording()) throw
187
- // `ReferenceError: process is not defined` after a successful LLM round-trip.
99
+ function isRecording() {
188
100
  if (ENABLED) return true;
189
- const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
190
- return IS_NODE ? !!process.env.A2UI_COMPOSE_TRACE : false;
101
+ const IS_NODE_RT = typeof process !== "undefined" && process.versions?.node;
102
+ return IS_NODE_RT ? !!process?.env?.["A2UI_COMPOSE_TRACE"] : false;
191
103
  }
104
+ export {
105
+ isRecording,
106
+ recordTurn
107
+ };
@@ -19,19 +19,22 @@ const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
19
19
  const ENABLED = IS_NODE && (process?.env?.['ADIA_LOG_DIALOGS'] === '1' || process?.env?.['ADIA_LOG_DIALOGS'] === 'true');
20
20
 
21
21
  // Lazy-loaded Node modules (only when ENABLED)
22
- let _fs: typeof import('node:fs/promises') | null = null;
23
- let _path: typeof import('node:path') | null = null;
24
- let _url: typeof import('node:url') | null = null;
22
+ type _FsModule = { readFile(p: string, enc: string): Promise<string>; writeFile(p: string, d: string): Promise<void>; mkdir(p: string, opts?: { recursive?: boolean }): Promise<string | undefined> };
23
+ type _PathModule = { dirname(p: string): string; resolve(...args: string[]): string; join(...args: string[]): string };
24
+ type _UrlModule = { fileURLToPath(u: string): string };
25
+ let _fs: _FsModule | null = null;
26
+ let _path: _PathModule | null = null;
27
+ let _url: _UrlModule | null = null;
25
28
  let _logsRoot: string | null = null;
26
29
 
27
30
  async function _ensureModules(): Promise<void> {
28
31
  if (_fs) return;
29
- _fs = await import(/* @vite-ignore */ 'node:fs/promises');
30
- _path = await import(/* @vite-ignore */ 'node:path');
31
- _url = await import(/* @vite-ignore */ 'node:url');
32
+ _fs = await import(/* @vite-ignore */ 'node:fs/promises') as unknown as _FsModule;
33
+ _path = await import(/* @vite-ignore */ 'node:path') as unknown as _PathModule;
34
+ _url = await import(/* @vite-ignore */ 'node:url') as unknown as _UrlModule;
32
35
  // logs/ lives at the repo root: packages/a2ui/retrieval/feedback → up 4 → repo root
33
- const __dirname = _path.dirname(_url.fileURLToPath(import.meta.url));
34
- _logsRoot = _path.resolve(__dirname, '..', '..', '..', '..', 'logs', 'dialogs');
36
+ const __dirname = _path!.dirname(_url!.fileURLToPath(import.meta.url));
37
+ _logsRoot = _path!.resolve(__dirname, '..', '..', '..', '..', 'logs', 'dialogs');
35
38
  }
36
39
 
37
40
  // In-memory turn counter per session