@kal-elsam/kairo-runtime 0.1.5 → 0.2.1

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,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
+ }
@@ -0,0 +1,77 @@
1
+ import { BACKEND_IDS } from "./types.js";
2
+ import { createOllamaBackend } from "./backends/ollama.js";
3
+ import { createOpenRouterBackend } from "./backends/openrouter.js";
4
+ import { createCustomHttpBackend } from "./backends/custom-http.js";
5
+ import { CAPABILITY_STATES } from "../capability-states.js";
6
+
7
+ export function createDefaultBackends({
8
+ env = process.env,
9
+ fetchImpl = globalThis.fetch,
10
+ customProviders = []
11
+ } = {}) {
12
+ const backends = [
13
+ createOllamaBackend({ env, fetchImpl }),
14
+ createOpenRouterBackend({ env, fetchImpl })
15
+ ];
16
+
17
+ for (const provider of customProviders) {
18
+ backends.push(createCustomHttpBackend({
19
+ ...provider,
20
+ env,
21
+ fetchImpl
22
+ }));
23
+ }
24
+
25
+ return backends;
26
+ }
27
+
28
+ export async function inspectIntelligenceBackends({
29
+ env = process.env,
30
+ fetchImpl = globalThis.fetch,
31
+ customProviders = [],
32
+ backends = null
33
+ } = {}) {
34
+ const resolved = backends ?? createDefaultBackends({ env, fetchImpl, customProviders });
35
+ return Promise.all(resolved.map(async (backend) => {
36
+ const detection = await backend.detect();
37
+ const models = detection.detected || detection.available
38
+ ? await backend.listModels()
39
+ : [];
40
+ const capabilities = await backend.capabilities();
41
+
42
+ return {
43
+ ...detection,
44
+ models,
45
+ capabilities
46
+ };
47
+ }));
48
+ }
49
+
50
+ export function summarizeIntelligenceBackends(inspections) {
51
+ const byState = {};
52
+ for (const state of Object.values(CAPABILITY_STATES)) {
53
+ byState[state] = 0;
54
+ }
55
+
56
+ for (const entry of inspections) {
57
+ if (byState[entry.state] != null) {
58
+ byState[entry.state] += 1;
59
+ }
60
+ }
61
+
62
+ return {
63
+ total: inspections.length,
64
+ available: inspections.filter((entry) => entry.available).length,
65
+ localAvailable: inspections.some(
66
+ (entry) => entry.id === BACKEND_IDS.OLLAMA && entry.available
67
+ ),
68
+ cloudAuthenticated: inspections.some(
69
+ (entry) => entry.id === BACKEND_IDS.OPENROUTER && entry.hasApiKey
70
+ ),
71
+ byState
72
+ };
73
+ }
74
+
75
+ export function resolveBackendById(backends, backendId) {
76
+ return backends.find((backend) => backend.id === backendId) ?? null;
77
+ }
@@ -0,0 +1,191 @@
1
+ import {
2
+ BACKEND_IDS,
3
+ OPENROUTER_FREE_MODEL,
4
+ PRIVACY_CLASSES,
5
+ ROUTING_MODES,
6
+ createRoutingDecision
7
+ } from "./types.js";
8
+ import { estimateTokens } from "./context-compiler.js";
9
+
10
+ const TASK_WEIGHTS = {
11
+ architecture: "heavy",
12
+ security: "heavy",
13
+ review: "heavy",
14
+ diagnose: "light",
15
+ explain: "light",
16
+ scaffold: "light",
17
+ test: "light",
18
+ default: "light"
19
+ };
20
+
21
+ export function classifyTaskWeight(task = "") {
22
+ const text = String(task).toLowerCase();
23
+ if (/architect|adr|design system|security|threat/.test(text)) return "heavy";
24
+ if (/review|refactor complex|debug complex/.test(text)) return "heavy";
25
+ if (/test|scaffold|explain|status|diagnose|lint|format/.test(text)) return "light";
26
+ return TASK_WEIGHTS.default;
27
+ }
28
+
29
+ /**
30
+ * Resolve which backend/model to use.
31
+ * Precedence: user override > Ollama local > OpenRouter free (consent) > diagnostics.
32
+ */
33
+ export function resolveRoutingDecision({
34
+ backends = [],
35
+ profile = {},
36
+ contextPack = null,
37
+ task = null,
38
+ cloudConsent = false,
39
+ tokenBudget = null
40
+ } = {}) {
41
+ const estimatedTokens = contextPack?.estimatedTokens
42
+ ?? estimateTokens(contextPack?.systemPrompt ?? "")
43
+ + estimateTokens(task ?? "");
44
+
45
+ const budget = tokenBudget ?? profile.tokenBudget ?? null;
46
+ if (budget != null && estimatedTokens > budget) {
47
+ return createRoutingDecision({
48
+ backendId: null,
49
+ model: null,
50
+ reason: `Estimated tokens (${estimatedTokens}) exceed budget (${budget}). Compact context or raise tokenBudget.`,
51
+ estimatedTokens,
52
+ privacyImpact: PRIVACY_CLASSES.UNKNOWN,
53
+ mode: ROUTING_MODES.DIAGNOSTICS,
54
+ requiresCloudConsent: false,
55
+ canInvoke: false
56
+ });
57
+ }
58
+
59
+ const override = resolveUserOverride(profile, backends);
60
+ if (override) {
61
+ return createRoutingDecision({
62
+ backendId: override.backend.id,
63
+ model: override.model,
64
+ reason: `User override: ${override.backend.id}/${override.model.modelId}`,
65
+ estimatedTokens,
66
+ privacyImpact: override.model.privacyClass,
67
+ mode: ROUTING_MODES.USER_OVERRIDE,
68
+ requiresCloudConsent: !override.model.local,
69
+ canInvoke: override.model.local || cloudConsent,
70
+ fallback: buildLocalFallback(backends)
71
+ });
72
+ }
73
+
74
+ const ollama = backends.find((entry) => entry.id === BACKEND_IDS.OLLAMA);
75
+ if (ollama?.available && Array.isArray(ollama.models) && ollama.models.length > 0) {
76
+ const model = selectLocalModel(ollama.models, task);
77
+ return createRoutingDecision({
78
+ backendId: BACKEND_IDS.OLLAMA,
79
+ model,
80
+ reason: `Local-first: Ollama model ${model.modelId}`,
81
+ estimatedTokens,
82
+ privacyImpact: PRIVACY_CLASSES.LOCAL,
83
+ mode: ROUTING_MODES.LOCAL,
84
+ requiresCloudConsent: false,
85
+ canInvoke: true,
86
+ fallback: buildCloudFallback(backends, cloudConsent)
87
+ });
88
+ }
89
+
90
+ const openrouter = backends.find((entry) => entry.id === BACKEND_IDS.OPENROUTER);
91
+ if (openrouter?.hasApiKey) {
92
+ const model = openrouter.models?.find((entry) => entry.modelId === OPENROUTER_FREE_MODEL)
93
+ ?? openrouter.models?.[0]
94
+ ?? {
95
+ provider: BACKEND_IDS.OPENROUTER,
96
+ modelId: OPENROUTER_FREE_MODEL,
97
+ local: false,
98
+ privacyClass: PRIVACY_CLASSES.CLOUD,
99
+ costClass: "free",
100
+ opaque: true
101
+ };
102
+
103
+ return createRoutingDecision({
104
+ backendId: BACKEND_IDS.OPENROUTER,
105
+ model,
106
+ reason: cloudConsent
107
+ ? `Cloud fallback approved: ${model.modelId}`
108
+ : `OpenRouter available (${model.modelId}) but cloud consent required before invoke`,
109
+ estimatedTokens,
110
+ privacyImpact: PRIVACY_CLASSES.CLOUD,
111
+ mode: ROUTING_MODES.CLOUD_CONSENT,
112
+ requiresCloudConsent: true,
113
+ canInvoke: cloudConsent,
114
+ fallback: null
115
+ });
116
+ }
117
+
118
+ return createRoutingDecision({
119
+ backendId: null,
120
+ model: null,
121
+ reason: "No intelligence backend available. Remaining in diagnostics/configuration mode.",
122
+ estimatedTokens,
123
+ privacyImpact: PRIVACY_CLASSES.UNKNOWN,
124
+ mode: ROUTING_MODES.DIAGNOSTICS,
125
+ requiresCloudConsent: false,
126
+ canInvoke: false
127
+ });
128
+ }
129
+
130
+ function resolveUserOverride(profile, backends) {
131
+ const preferredBackend = profile.preferredBackend ?? null;
132
+ const preferredModel = profile.preferredModel ?? null;
133
+ if (!preferredBackend && !preferredModel) return null;
134
+
135
+ if (preferredBackend) {
136
+ const backend = backends.find((entry) => entry.id === preferredBackend);
137
+ if (!backend || (!backend.available && !backend.hasApiKey && !backend.detected)) {
138
+ return null;
139
+ }
140
+ const model = (backend.models ?? []).find((entry) => entry.modelId === preferredModel)
141
+ ?? backend.models?.[0]
142
+ ?? (preferredModel
143
+ ? {
144
+ provider: preferredBackend,
145
+ modelId: preferredModel,
146
+ local: backend.id === BACKEND_IDS.OLLAMA,
147
+ privacyClass: backend.id === BACKEND_IDS.OLLAMA ? PRIVACY_CLASSES.LOCAL : PRIVACY_CLASSES.CLOUD,
148
+ costClass: "unknown",
149
+ opaque: true
150
+ }
151
+ : null);
152
+ if (!model) return null;
153
+ return { backend, model };
154
+ }
155
+
156
+ if (preferredModel) {
157
+ for (const backend of backends) {
158
+ const model = (backend.models ?? []).find((entry) => entry.modelId === preferredModel);
159
+ if (model) return { backend, model };
160
+ }
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ function selectLocalModel(models, task) {
167
+ const weight = classifyTaskWeight(task);
168
+ if (weight === "heavy" && models.length > 1) {
169
+ return [...models].sort((a, b) => String(b.modelId).localeCompare(String(a.modelId)))[0];
170
+ }
171
+ return models[0];
172
+ }
173
+
174
+ function buildLocalFallback(backends) {
175
+ const ollama = backends.find((entry) => entry.id === BACKEND_IDS.OLLAMA && entry.available);
176
+ if (!ollama?.models?.length) return null;
177
+ return {
178
+ backendId: BACKEND_IDS.OLLAMA,
179
+ modelId: ollama.models[0].modelId
180
+ };
181
+ }
182
+
183
+ function buildCloudFallback(backends, cloudConsent) {
184
+ const openrouter = backends.find((entry) => entry.id === BACKEND_IDS.OPENROUTER && entry.hasApiKey);
185
+ if (!openrouter) return null;
186
+ return {
187
+ backendId: BACKEND_IDS.OPENROUTER,
188
+ modelId: OPENROUTER_FREE_MODEL,
189
+ requiresConsent: !cloudConsent
190
+ };
191
+ }