@kal-elsam/kairo-runtime 0.1.4 → 0.2.0

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.
@@ -0,0 +1,338 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile, realpath } from "node:fs/promises";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { detectProject } from "../../project-detection.js";
5
+
6
+ const PRIVATE_PATH_PATTERNS = [
7
+ /^\.env(\.|$)/i,
8
+ /(^|\/)\.env(\.|$)/i,
9
+ /(^|\/)secrets?\//i,
10
+ /(^|\/)credentials?\./i,
11
+ /\.pem$/i,
12
+ /\.key$/i,
13
+ /(^|\/)id_rsa/i,
14
+ /(^|\/)\.npmrc$/i,
15
+ /(^|\/)\.netrc$/i
16
+ ];
17
+
18
+ const STABLE_DOC_CANDIDATES = [
19
+ "AGENTS.md",
20
+ "docs/ai/harness.md",
21
+ "docs/ai/architecture.md",
22
+ "docs/ai/conventions.md",
23
+ "docs/ai/testing.md",
24
+ "docs/ai/spec-driven-development.md",
25
+ "docs/ai/test-driven-development.md",
26
+ "docs/ai/provider-routing.md",
27
+ "docs/ai/model-policy.md",
28
+ "docs/ai/context-budget.md"
29
+ ];
30
+
31
+ const DEFAULT_STABLE_BUDGET = 6000;
32
+ const DEFAULT_REQUEST_BUDGET = 4000;
33
+
34
+ export async function compileContextPack({
35
+ workspaceRoot,
36
+ task = null,
37
+ relevantPaths = [],
38
+ includePrivate = false,
39
+ stableBudgetTokens = DEFAULT_STABLE_BUDGET,
40
+ requestBudgetTokens = DEFAULT_REQUEST_BUDGET
41
+ } = {}) {
42
+ const root = resolve(workspaceRoot ?? process.cwd());
43
+ const workspaceRealRoot = await resolveWorkspaceRoot(root);
44
+ const project = await detectProject(root);
45
+ const evidence = [];
46
+
47
+ const agentsMd = await readEvidenceFile(
48
+ root,
49
+ workspaceRealRoot,
50
+ "AGENTS.md",
51
+ evidence,
52
+ 2000,
53
+ { includePrivate }
54
+ );
55
+ const stableDocs = await collectStableDocs(root, workspaceRealRoot, evidence, stableBudgetTokens, includePrivate);
56
+ const skills = await listSkillIds(root, evidence);
57
+ const graphify = await detectGraphify(root, evidence);
58
+ const engram = await detectEngramHints(root, evidence);
59
+
60
+ const requestFiles = [];
61
+ for (const filePath of relevantPaths) {
62
+ const rel = relative(root, resolve(root, filePath));
63
+ const content = await readEvidenceFile(
64
+ root,
65
+ workspaceRealRoot,
66
+ rel,
67
+ evidence,
68
+ requestBudgetTokens,
69
+ { includePrivate }
70
+ );
71
+ if (content) {
72
+ requestFiles.push({ path: rel, content: content.text, truncated: content.truncated });
73
+ }
74
+ }
75
+
76
+ const stable = {
77
+ project: {
78
+ name: project.name,
79
+ purpose: project.purpose,
80
+ stack: project.stack,
81
+ architecturePattern: project.architecturePattern,
82
+ packageManager: project.packageManager,
83
+ commands: project.commands,
84
+ detectedAdapters: project.detectedAdapters
85
+ },
86
+ agentsMd: agentsMd?.text ?? null,
87
+ docs: stableDocs,
88
+ skills,
89
+ sdd: Boolean(stableDocs.find((doc) => doc.path.includes("spec-driven"))),
90
+ tdd: Boolean(stableDocs.find((doc) => doc.path.includes("test-driven"))),
91
+ graphify,
92
+ engram
93
+ };
94
+
95
+ const perRequest = {
96
+ task,
97
+ files: requestFiles
98
+ };
99
+
100
+ const systemPrompt = buildSystemPrompt(stable, perRequest);
101
+ const estimatedTokens = estimateTokens(systemPrompt);
102
+
103
+ return {
104
+ version: 1,
105
+ workspaceRoot: root,
106
+ stable,
107
+ perRequest,
108
+ evidence,
109
+ systemPrompt,
110
+ estimatedTokens,
111
+ budgets: {
112
+ stableBudgetTokens,
113
+ requestBudgetTokens
114
+ },
115
+ privacy: {
116
+ includePrivate,
117
+ excludedPrivate: evidence
118
+ .filter((entry) => entry.kind === "excluded_private")
119
+ .map((entry) => entry.path)
120
+ }
121
+ };
122
+ }
123
+
124
+ export function isPrivatePath(relativePath) {
125
+ const normalized = relativePath.replace(/\\/g, "/");
126
+ return PRIVATE_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
127
+ }
128
+
129
+ export function estimateTokens(text) {
130
+ if (!text) return 0;
131
+ return Math.ceil(text.length / 4);
132
+ }
133
+
134
+ async function collectStableDocs(root, workspaceRealRoot, evidence, budgetTokens, includePrivate) {
135
+ const docs = [];
136
+ let used = 0;
137
+
138
+ for (const candidate of STABLE_DOC_CANDIDATES) {
139
+ if (candidate === "AGENTS.md") continue;
140
+ const remaining = Math.max(500, budgetTokens - used);
141
+ const content = await readEvidenceFile(
142
+ root,
143
+ workspaceRealRoot,
144
+ candidate,
145
+ evidence,
146
+ remaining,
147
+ { includePrivate }
148
+ );
149
+ if (!content) continue;
150
+ docs.push({ path: candidate, content: content.text, truncated: content.truncated });
151
+ used += estimateTokens(content.text);
152
+ if (used >= budgetTokens) break;
153
+ }
154
+
155
+ return docs;
156
+ }
157
+
158
+ async function listSkillIds(root, evidence) {
159
+ const skillRoots = [
160
+ join(root, "docs", "skills"),
161
+ join(root, ".cursor", "skills"),
162
+ join(root, ".codex", "skills"),
163
+ join(root, ".claude", "skills")
164
+ ];
165
+
166
+ const ids = new Set();
167
+ for (const skillRoot of skillRoots) {
168
+ if (!existsSync(skillRoot)) continue;
169
+ evidence.push({ kind: "skills_root", path: relative(root, skillRoot) });
170
+ try {
171
+ const entries = await readdir(skillRoot, { withFileTypes: true });
172
+ for (const entry of entries) {
173
+ if (entry.isDirectory()) ids.add(entry.name);
174
+ }
175
+ } catch {
176
+ // ignore unreadable skill roots
177
+ }
178
+ }
179
+
180
+ return [...ids].sort();
181
+ }
182
+
183
+ async function detectGraphify(root, evidence) {
184
+ const reportPath = join(root, "graphify-out", "GRAPH_REPORT.md");
185
+ const graphPath = join(root, "graphify-out", "graph.json");
186
+ const present = existsSync(reportPath) || existsSync(graphPath);
187
+ if (present) {
188
+ evidence.push({ kind: "graphify", path: "graphify-out" });
189
+ }
190
+ return { present, report: existsSync(reportPath), graph: existsSync(graphPath) };
191
+ }
192
+
193
+ async function detectEngramHints(root, evidence) {
194
+ const memoryDoc = join(root, "docs", "ai", "memory.md");
195
+ const present = existsSync(memoryDoc);
196
+ if (present) {
197
+ evidence.push({ kind: "engram_doc", path: "docs/ai/memory.md" });
198
+ }
199
+ return { documented: present };
200
+ }
201
+
202
+ async function readEvidenceFile(
203
+ root,
204
+ workspaceRealRoot,
205
+ relativePath,
206
+ evidence,
207
+ maxTokens = 2000,
208
+ { includePrivate = false } = {}
209
+ ) {
210
+ const requested = resolve(root, relativePath);
211
+ const requestedRelative = relative(root, requested) || ".";
212
+ if (!isPathInside(root, requested)) {
213
+ evidence.push({
214
+ kind: "rejected_outside_workspace",
215
+ path: requestedRelative,
216
+ reason: "Path is outside workspaceRoot"
217
+ });
218
+ return null;
219
+ }
220
+
221
+ if (!existsSync(requested)) return null;
222
+
223
+ let target;
224
+ try {
225
+ target = await realpath(requested);
226
+ } catch {
227
+ evidence.push({ kind: "unreadable", path: requestedRelative });
228
+ return null;
229
+ }
230
+
231
+ if (!isPathInside(workspaceRealRoot, target)) {
232
+ evidence.push({
233
+ kind: "rejected_outside_workspace",
234
+ path: requestedRelative,
235
+ reason: "Symlink target is outside workspaceRoot"
236
+ });
237
+ return null;
238
+ }
239
+
240
+ const targetRelative = relative(workspaceRealRoot, target) || ".";
241
+ if ((isPrivatePath(requestedRelative) || isPrivatePath(targetRelative)) && !includePrivate) {
242
+ evidence.push({
243
+ kind: "excluded_private",
244
+ path: requestedRelative,
245
+ reason: "Private path excluded without consent"
246
+ });
247
+ return null;
248
+ }
249
+
250
+ try {
251
+ const raw = await readFile(target, "utf8");
252
+ const maxChars = maxTokens * 4;
253
+ const truncated = raw.length > maxChars;
254
+ const text = truncated ? `${raw.slice(0, maxChars)}\n…[truncated]` : raw;
255
+ evidence.push({
256
+ kind: "file",
257
+ path: requestedRelative,
258
+ truncated,
259
+ chars: text.length
260
+ });
261
+ return { text, truncated };
262
+ } catch {
263
+ evidence.push({ kind: "unreadable", path: requestedRelative });
264
+ return null;
265
+ }
266
+ }
267
+
268
+ async function resolveWorkspaceRoot(root) {
269
+ try {
270
+ return await realpath(root);
271
+ } catch {
272
+ return root;
273
+ }
274
+ }
275
+
276
+ function isPathInside(root, candidate) {
277
+ const rel = relative(root, candidate);
278
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${requirePathSeparator()}`));
279
+ }
280
+
281
+ function requirePathSeparator() {
282
+ return process.platform === "win32" ? "\\" : "/";
283
+ }
284
+
285
+ function buildSystemPrompt(stable, perRequest) {
286
+ const lines = [
287
+ "You are assisting inside Kairo Runtime harness governance.",
288
+ "Prefer architecture before implementation, existing patterns, minimal diffs, tests/specs.",
289
+ "Do not invent speculative code, unnecessary dependencies, or duplicate abstractions.",
290
+ "Never request or echo secrets. Private files require explicit human consent.",
291
+ "",
292
+ "## Project",
293
+ `Name: ${stable.project.name}`,
294
+ `Purpose: ${stable.project.purpose}`,
295
+ `Stack: ${stable.project.stack}`,
296
+ `Architecture: ${stable.project.architecturePattern}`,
297
+ `Package manager: ${stable.project.packageManager}`,
298
+ `Commands: ${JSON.stringify(stable.project.commands)}`,
299
+ `Adapters: ${(stable.project.detectedAdapters ?? []).join(", ") || "none"}`,
300
+ ""
301
+ ];
302
+
303
+ if (stable.agentsMd) {
304
+ lines.push("## AGENTS.md", stable.agentsMd, "");
305
+ }
306
+
307
+ if (stable.docs.length > 0) {
308
+ lines.push("## Stable docs");
309
+ for (const doc of stable.docs) {
310
+ lines.push(`### ${doc.path}`, doc.content, "");
311
+ }
312
+ }
313
+
314
+ if (stable.skills.length > 0) {
315
+ lines.push(`## Skills available: ${stable.skills.join(", ")}`, "");
316
+ }
317
+
318
+ lines.push(
319
+ `## SDD: ${stable.sdd ? "documented" : "not detected"}`,
320
+ `## TDD: ${stable.tdd ? "documented" : "not detected"}`,
321
+ `## Graphify: ${stable.graphify.present ? "present" : "absent"}`,
322
+ `## Engram docs: ${stable.engram.documented ? "present" : "absent"}`,
323
+ ""
324
+ );
325
+
326
+ if (perRequest.task) {
327
+ lines.push("## Task", perRequest.task, "");
328
+ }
329
+
330
+ if (perRequest.files.length > 0) {
331
+ lines.push("## Relevant files");
332
+ for (const file of perRequest.files) {
333
+ lines.push(`### ${file.path}`, file.content, "");
334
+ }
335
+ }
336
+
337
+ return lines.join("\n");
338
+ }
@@ -0,0 +1,82 @@
1
+ import net from "node:net";
2
+
3
+ const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
4
+ const LOCAL_HOSTNAMES = new Set(["localhost", "localhost.localdomain"]);
5
+ const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
6
+
7
+ export function classifyCustomBaseUrl(baseUrl) {
8
+ let parsed;
9
+ try {
10
+ parsed = new URL(String(baseUrl));
11
+ } catch {
12
+ throw new Error("Custom provider baseUrl must be an absolute http(s) URL.");
13
+ }
14
+
15
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
16
+ throw new Error("Custom provider baseUrl must use http or https.");
17
+ }
18
+ if (parsed.username || parsed.password) {
19
+ throw new Error("Custom provider baseUrl must not contain embedded credentials.");
20
+ }
21
+ if (parsed.search || parsed.hash) {
22
+ throw new Error("Custom provider baseUrl must not contain a query string or fragment.");
23
+ }
24
+
25
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
26
+ if (!hostname) {
27
+ throw new Error("Custom provider baseUrl must include a host.");
28
+ }
29
+
30
+ const local = isLocalHostname(hostname);
31
+ if (!local && parsed.protocol !== "https:") {
32
+ throw new Error("Remote custom providers must use https; http is allowed only for local/private endpoints.");
33
+ }
34
+
35
+ return {
36
+ url: parsed,
37
+ normalizedBaseUrl: parsed.toString().replace(/\/+$/, ""),
38
+ hostname,
39
+ local,
40
+ credentialSafe: local || parsed.protocol === "https:"
41
+ };
42
+ }
43
+
44
+ export function isValidEnvironmentName(name) {
45
+ return typeof name === "string" && ENV_NAME_PATTERN.test(name);
46
+ }
47
+
48
+ function isLocalHostname(hostname) {
49
+ if (LOCAL_HOSTNAMES.has(hostname)) return true;
50
+
51
+ const version = net.isIP(hostname);
52
+ if (version === 4) return isPrivateIpv4(hostname);
53
+ if (version === 6) return isPrivateIpv6(hostname);
54
+ return false;
55
+ }
56
+
57
+ function isPrivateIpv4(hostname) {
58
+ const octets = hostname.split(".").map(Number);
59
+ if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
60
+ return false;
61
+ }
62
+
63
+ const [first, second] = octets;
64
+ return first === 10
65
+ || (first === 172 && second >= 16 && second <= 31)
66
+ || (first === 192 && second === 168)
67
+ || (first === 169 && second === 254)
68
+ || first === 127
69
+ || first === 0;
70
+ }
71
+
72
+ function isPrivateIpv6(hostname) {
73
+ const normalized = hostname.toLowerCase();
74
+ return normalized === "::1"
75
+ || normalized === "::"
76
+ || normalized.startsWith("fc")
77
+ || normalized.startsWith("fd")
78
+ || normalized.startsWith("fe8")
79
+ || normalized.startsWith("fe9")
80
+ || normalized.startsWith("fea")
81
+ || normalized.startsWith("feb");
82
+ }
@@ -0,0 +1,63 @@
1
+ const DEFAULT_TIMEOUT_MS = 5000;
2
+
3
+ export async function fetchJson(url, {
4
+ method = "GET",
5
+ headers = {},
6
+ body = null,
7
+ timeoutMs = DEFAULT_TIMEOUT_MS,
8
+ fetchImpl = globalThis.fetch
9
+ } = {}) {
10
+ if (typeof fetchImpl !== "function") {
11
+ return {
12
+ ok: false,
13
+ status: 0,
14
+ data: null,
15
+ error: "fetch is not available in this runtime"
16
+ };
17
+ }
18
+
19
+ const controller = new AbortController();
20
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
21
+
22
+ try {
23
+ const response = await fetchImpl(url, {
24
+ method,
25
+ headers,
26
+ body: body == null ? undefined : JSON.stringify(body),
27
+ signal: controller.signal
28
+ });
29
+
30
+ const text = await response.text();
31
+ let data = null;
32
+ if (text) {
33
+ try {
34
+ data = JSON.parse(text);
35
+ } catch {
36
+ data = { raw: text };
37
+ }
38
+ }
39
+
40
+ return {
41
+ ok: response.ok,
42
+ status: response.status,
43
+ data,
44
+ error: response.ok ? null : summarizeHttpError(response.status, data, text)
45
+ };
46
+ } catch (error) {
47
+ return {
48
+ ok: false,
49
+ status: 0,
50
+ data: null,
51
+ error: error?.name === "AbortError" ? "request timed out" : (error?.message ?? String(error))
52
+ };
53
+ } finally {
54
+ clearTimeout(timer);
55
+ }
56
+ }
57
+
58
+ function summarizeHttpError(status, data, text) {
59
+ if (data?.error?.message) return data.error.message;
60
+ if (typeof data?.error === "string") return data.error;
61
+ if (text) return text.slice(0, 200);
62
+ return `HTTP ${status}`;
63
+ }
@@ -0,0 +1,38 @@
1
+ export {
2
+ BACKEND_IDS,
3
+ COST_CLASSES,
4
+ PRIVACY_CLASSES,
5
+ ROUTING_MODES,
6
+ OPENROUTER_FREE_MODEL,
7
+ DEFAULT_OLLAMA_HOST,
8
+ createModelDescriptor,
9
+ createRoutingDecision,
10
+ createUsageTelemetry
11
+ } from "./types.js";
12
+
13
+ export { createOllamaBackend } from "./backends/ollama.js";
14
+ export { createOpenRouterBackend } from "./backends/openrouter.js";
15
+ export { createCustomHttpBackend } from "./backends/custom-http.js";
16
+
17
+ export {
18
+ createDefaultBackends,
19
+ inspectIntelligenceBackends,
20
+ summarizeIntelligenceBackends,
21
+ resolveBackendById
22
+ } from "./registry.js";
23
+
24
+ export {
25
+ compileContextPack,
26
+ isPrivatePath,
27
+ estimateTokens
28
+ } from "./context-compiler.js";
29
+
30
+ export {
31
+ resolveRoutingDecision,
32
+ classifyTaskWeight
33
+ } from "./router.js";
34
+
35
+ export {
36
+ runIntelligenceRequest,
37
+ explainRouting
38
+ } from "./orchestrate.js";
@@ -0,0 +1,189 @@
1
+ import { createDefaultBackends, inspectIntelligenceBackends, resolveBackendById } from "./registry.js";
2
+ import { compileContextPack } from "./context-compiler.js";
3
+ import { resolveRoutingDecision } from "./router.js";
4
+ import { createUsageTelemetry, PRIVACY_CLASSES } from "./types.js";
5
+
6
+ /**
7
+ * Orchestrates detect → context → route → privacy gate → invoke.
8
+ * Cloud transmission requires explicit consent. Credentials never touch disk.
9
+ */
10
+ export async function runIntelligenceRequest({
11
+ workspaceRoot,
12
+ profile = {},
13
+ task = null,
14
+ prompt = null,
15
+ relevantPaths = [],
16
+ includePrivate = false,
17
+ cloudConsent = false,
18
+ confirmed = false,
19
+ env = process.env,
20
+ fetchImpl = globalThis.fetch,
21
+ backends = null,
22
+ tokenBudget = null
23
+ } = {}) {
24
+ const customProviders = Array.isArray(profile.customProviders) ? profile.customProviders : [];
25
+ const backendInstances = backends ?? createDefaultBackends({ env, fetchImpl, customProviders });
26
+ const inspections = await inspectIntelligenceBackends({
27
+ env,
28
+ fetchImpl,
29
+ customProviders,
30
+ backends: backendInstances
31
+ });
32
+
33
+ const privateConfirmationRequired = includePrivate && !confirmed;
34
+ const contextPack = await compileContextPack({
35
+ workspaceRoot,
36
+ task: task ?? prompt,
37
+ relevantPaths,
38
+ includePrivate: includePrivate && confirmed,
39
+ stableBudgetTokens: profile.stableContextBudget ?? undefined,
40
+ requestBudgetTokens: profile.requestContextBudget ?? undefined
41
+ });
42
+
43
+ const routing = resolveRoutingDecision({
44
+ backends: inspections,
45
+ profile,
46
+ contextPack,
47
+ task: task ?? prompt,
48
+ cloudConsent: Boolean(cloudConsent),
49
+ tokenBudget: tokenBudget ?? profile.tokenBudget ?? null
50
+ });
51
+
52
+ const explanation = explainRouting(routing, contextPack, inspections);
53
+ const base = {
54
+ routing,
55
+ explanation,
56
+ contextPack: summarizeContextPack(contextPack),
57
+ backends: inspections
58
+ };
59
+
60
+ if (privateConfirmationRequired) {
61
+ return {
62
+ ...base,
63
+ ok: false,
64
+ mode: routing.mode,
65
+ diagnosticsOnly: true,
66
+ result: null,
67
+ telemetry: null,
68
+ error: "Including private context requires explicit confirmation (--include-private --yes / --confirm)."
69
+ };
70
+ }
71
+
72
+ if (!routing.canInvoke) {
73
+ return {
74
+ ...base,
75
+ ok: false,
76
+ mode: routing.mode,
77
+ diagnosticsOnly: true,
78
+ result: null,
79
+ telemetry: null,
80
+ error: routing.reason
81
+ };
82
+ }
83
+
84
+ if (routing.privacyImpact === PRIVACY_CLASSES.CLOUD) {
85
+ if (!cloudConsent) {
86
+ return {
87
+ ...base,
88
+ ok: false,
89
+ mode: routing.mode,
90
+ diagnosticsOnly: true,
91
+ result: null,
92
+ telemetry: null,
93
+ error: "Cloud transmission requires explicit consent."
94
+ };
95
+ }
96
+ if (!confirmed) {
97
+ return {
98
+ ...base,
99
+ ok: false,
100
+ mode: routing.mode,
101
+ diagnosticsOnly: true,
102
+ result: null,
103
+ telemetry: null,
104
+ error: "Confirm cloud context transmission before invoke (--yes / --confirm)."
105
+ };
106
+ }
107
+ }
108
+
109
+ const backend = resolveBackendById(backendInstances, routing.backendId);
110
+ if (!backend) {
111
+ return {
112
+ ...base,
113
+ ok: false,
114
+ mode: routing.mode,
115
+ diagnosticsOnly: true,
116
+ result: null,
117
+ telemetry: null,
118
+ error: `Backend ${routing.backendId} is not loaded.`
119
+ };
120
+ }
121
+
122
+ const result = await backend.invoke(contextPack, {
123
+ modelId: routing.model?.modelId,
124
+ prompt: prompt ?? task,
125
+ timeoutMs: profile.invokeTimeoutMs ?? undefined
126
+ });
127
+
128
+ const telemetry = createUsageTelemetry({
129
+ ...(result.usage ?? {}),
130
+ model: result.model ?? routing.model?.modelId,
131
+ backendId: routing.backendId,
132
+ fallbackUsed: Boolean(result.usage?.fallbackUsed)
133
+ });
134
+
135
+ return {
136
+ ...base,
137
+ ok: Boolean(result.ok),
138
+ mode: routing.mode,
139
+ diagnosticsOnly: false,
140
+ result,
141
+ telemetry,
142
+ error: result.ok ? null : (result.error ?? "Invoke failed.")
143
+ };
144
+ }
145
+
146
+ export function explainRouting(routing, contextPack, backends = []) {
147
+ const evidencePaths = (contextPack?.evidence ?? [])
148
+ .filter((entry) => entry.kind === "file")
149
+ .map((entry) => entry.path);
150
+
151
+ return {
152
+ selectedBackend: routing.backendId,
153
+ selectedModel: routing.model?.modelId ?? null,
154
+ reason: routing.reason,
155
+ mode: routing.mode,
156
+ estimatedTokens: routing.estimatedTokens,
157
+ privacyImpact: routing.privacyImpact,
158
+ requiresCloudConsent: routing.requiresCloudConsent,
159
+ canInvoke: routing.canInvoke,
160
+ evidenceUsed: evidencePaths,
161
+ excludedPrivate: contextPack?.privacy?.excludedPrivate ?? [],
162
+ availableBackends: backends.map((entry) => ({
163
+ id: entry.id,
164
+ state: entry.state,
165
+ available: entry.available,
166
+ modelCount: entry.models?.length ?? 0
167
+ })),
168
+ fallback: routing.fallback
169
+ };
170
+ }
171
+
172
+ function summarizeContextPack(contextPack) {
173
+ if (!contextPack) return null;
174
+ return {
175
+ workspaceRoot: contextPack.workspaceRoot,
176
+ estimatedTokens: contextPack.estimatedTokens,
177
+ project: contextPack.stable?.project ?? null,
178
+ evidenceCount: contextPack.evidence?.length ?? 0,
179
+ evidence: contextPack.evidence,
180
+ privacy: contextPack.privacy,
181
+ budgets: contextPack.budgets,
182
+ skills: contextPack.stable?.skills ?? [],
183
+ sdd: contextPack.stable?.sdd ?? false,
184
+ tdd: contextPack.stable?.tdd ?? false,
185
+ graphify: contextPack.stable?.graphify ?? null,
186
+ hasAgentsMd: Boolean(contextPack.stable?.agentsMd),
187
+ relevantFiles: (contextPack.perRequest?.files ?? []).map((file) => file.path)
188
+ };
189
+ }