@mrace07/kairo 0.1.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.
- package/README.md +141 -0
- package/dist/application/coding-agent.d.ts +85 -0
- package/dist/application/coding-agent.js +765 -0
- package/dist/application/context-manager.d.ts +22 -0
- package/dist/application/context-manager.js +174 -0
- package/dist/application/context-selector.d.ts +11 -0
- package/dist/application/context-selector.js +74 -0
- package/dist/application/evaluated-agent.d.ts +7 -0
- package/dist/application/evaluated-agent.js +16 -0
- package/dist/application/evaluation-comparison.d.ts +34 -0
- package/dist/application/evaluation-comparison.js +91 -0
- package/dist/application/evaluation-harness.d.ts +19 -0
- package/dist/application/evaluation-harness.js +217 -0
- package/dist/application/failure-analyzer.d.ts +5 -0
- package/dist/application/failure-analyzer.js +37 -0
- package/dist/application/interaction-routing.d.ts +9 -0
- package/dist/application/interaction-routing.js +20 -0
- package/dist/application/live-evaluation.d.ts +8 -0
- package/dist/application/live-evaluation.js +185 -0
- package/dist/application/model-routing.d.ts +12 -0
- package/dist/application/model-routing.js +40 -0
- package/dist/application/model-system-instruction.d.ts +4 -0
- package/dist/application/model-system-instruction.js +4 -0
- package/dist/application/self-evaluation.d.ts +26 -0
- package/dist/application/self-evaluation.js +394 -0
- package/dist/application/task-metrics.d.ts +31 -0
- package/dist/application/task-metrics.js +42 -0
- package/dist/application/verification-planner.d.ts +12 -0
- package/dist/application/verification-planner.js +97 -0
- package/dist/domain/models.d.ts +247 -0
- package/dist/domain/models.js +1 -0
- package/dist/domain/ports.d.ts +87 -0
- package/dist/domain/ports.js +1 -0
- package/dist/domain/provider-error.d.ts +18 -0
- package/dist/domain/provider-error.js +17 -0
- package/dist/infrastructure/configuration/config.d.ts +24 -0
- package/dist/infrastructure/configuration/config.js +79 -0
- package/dist/infrastructure/filesystem/platform-paths.d.ts +8 -0
- package/dist/infrastructure/filesystem/platform-paths.js +18 -0
- package/dist/infrastructure/persistence/sqlite-session-store.d.ts +82 -0
- package/dist/infrastructure/persistence/sqlite-session-store.js +447 -0
- package/dist/infrastructure/providers/gemini-provider.d.ts +14 -0
- package/dist/infrastructure/providers/gemini-provider.js +90 -0
- package/dist/infrastructure/providers/groq-provider.d.ts +16 -0
- package/dist/infrastructure/providers/groq-provider.js +101 -0
- package/dist/infrastructure/providers/jev-safety-advisor.d.ts +18 -0
- package/dist/infrastructure/providers/jev-safety-advisor.js +95 -0
- package/dist/infrastructure/providers/mistral-provider.d.ts +15 -0
- package/dist/infrastructure/providers/mistral-provider.js +137 -0
- package/dist/infrastructure/providers/openrouter-provider.d.ts +15 -0
- package/dist/infrastructure/providers/openrouter-provider.js +104 -0
- package/dist/infrastructure/providers/provider-recovery.d.ts +10 -0
- package/dist/infrastructure/providers/provider-recovery.js +108 -0
- package/dist/infrastructure/providers/provider-registry.d.ts +22 -0
- package/dist/infrastructure/providers/provider-registry.js +67 -0
- package/dist/infrastructure/repository/repository-awareness.d.ts +12 -0
- package/dist/infrastructure/repository/repository-awareness.js +25 -0
- package/dist/infrastructure/repository/repository-profiler.d.ts +35 -0
- package/dist/infrastructure/repository/repository-profiler.js +498 -0
- package/dist/infrastructure/security/macos-keychain-store.d.ts +17 -0
- package/dist/infrastructure/security/macos-keychain-store.js +73 -0
- package/dist/infrastructure/tools/workspace-tools.d.ts +30 -0
- package/dist/infrastructure/tools/workspace-tools.js +321 -0
- package/dist/interface/cli/evaluation-comparison-report.d.ts +6 -0
- package/dist/interface/cli/evaluation-comparison-report.js +46 -0
- package/dist/interface/cli/evaluation-report.d.ts +14 -0
- package/dist/interface/cli/evaluation-report.js +122 -0
- package/dist/interface/cli/index.d.ts +2 -0
- package/dist/interface/cli/index.js +238 -0
- package/dist/interface/cli/provider-setup.d.ts +16 -0
- package/dist/interface/cli/provider-setup.js +86 -0
- package/dist/interface/cli/repl.d.ts +7 -0
- package/dist/interface/cli/repl.js +19 -0
- package/dist/interface/cli/task-trace.d.ts +7 -0
- package/dist/interface/cli/task-trace.js +48 -0
- package/dist/interface/cli/tui.d.ts +147 -0
- package/dist/interface/cli/tui.js +910 -0
- package/package.json +61 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JevAssessment, JevDecision, JevRecovery, JevModelTier, JevInteractionIntent, JevRoute, JevSafetyAdvisor } from "../../domain/ports.js";
|
|
2
|
+
/** Calls TypeSafe Jev directly for one bounded, typed action-risk decision. */
|
|
3
|
+
export declare class JevDecisionProvider implements JevSafetyAdvisor {
|
|
4
|
+
private readonly apiKey;
|
|
5
|
+
private readonly fetcher;
|
|
6
|
+
constructor(apiKey: string, fetcher?: typeof fetch);
|
|
7
|
+
assess(state: string): Promise<JevAssessment>;
|
|
8
|
+
/** Separates chat from repository work before Kairo exposes workspace context or tools. */
|
|
9
|
+
intent(state: string): Promise<JevDecision<JevInteractionIntent>>;
|
|
10
|
+
route(state: string): Promise<JevDecision<JevRoute>>;
|
|
11
|
+
recover(state: string): Promise<JevDecision<JevRecovery>>;
|
|
12
|
+
/** Selects a bounded coding-model tier; Kairo applies the local allowlist and credentials. */
|
|
13
|
+
modelTier(state: string): Promise<JevDecision<JevModelTier>>;
|
|
14
|
+
private decide;
|
|
15
|
+
/** Uses a minimal typed request because TypeSafe does not publish a key-only probe. */
|
|
16
|
+
validate(): Promise<void>;
|
|
17
|
+
private error;
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ProviderError } from "../../domain/provider-error.js";
|
|
2
|
+
const endpoint = "https://api.typesafe.ai/v1/systemone";
|
|
3
|
+
const riskChoices = {
|
|
4
|
+
low: "A workspace-local, reversible action with limited impact, such as an ordinary source edit or focused test.",
|
|
5
|
+
medium: "An action that can affect multiple files, dependencies, build artifacts, or execute a command with meaningful workspace impact.",
|
|
6
|
+
high: "An action that can delete data, publish or transmit data, change credentials, alter broad configuration, or run a destructive command.",
|
|
7
|
+
};
|
|
8
|
+
/** Calls TypeSafe Jev directly for one bounded, typed action-risk decision. */
|
|
9
|
+
export class JevDecisionProvider {
|
|
10
|
+
apiKey;
|
|
11
|
+
fetcher;
|
|
12
|
+
constructor(apiKey, fetcher = fetch) {
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
this.fetcher = fetcher;
|
|
15
|
+
}
|
|
16
|
+
async assess(state) {
|
|
17
|
+
const decision = await this.decide("action_risk", state, riskChoices);
|
|
18
|
+
return { risk: decision.value, confidence: decision.confidence };
|
|
19
|
+
}
|
|
20
|
+
/** Separates chat from repository work before Kairo exposes workspace context or tools. */
|
|
21
|
+
async intent(state) {
|
|
22
|
+
return this.decide("interaction_intent", state, {
|
|
23
|
+
conversation: "A greeting, acknowledgement, or casual social message that needs a short direct reply and no model call.",
|
|
24
|
+
answer: "A general question or explanation that can be answered without repository context or tools.",
|
|
25
|
+
repository_task: "A request to inspect, explain using, change, test, run, or otherwise act on the current repository.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async route(state) {
|
|
29
|
+
return this.decide("task_route", state, {
|
|
30
|
+
build: "A bounded implementation request that can proceed directly with the coding agent.",
|
|
31
|
+
plan: "A broad, ambiguous, multi-file, risky, or architectural request that should first create a read-only plan.",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async recover(state) {
|
|
35
|
+
return this.decide("verification_recovery", state, {
|
|
36
|
+
repair: "The failure is specific enough for the coding model to attempt a focused repair.",
|
|
37
|
+
broaden: "The current check is too narrow; recommend the next broader discovered verification command.",
|
|
38
|
+
escalate: "The failure is ambiguous, risky, or lacks enough evidence; stop automatic repair and ask the user.",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Selects a bounded coding-model tier; Kairo applies the local allowlist and credentials. */
|
|
42
|
+
async modelTier(state) {
|
|
43
|
+
return this.decide("coding_model_tier", state, {
|
|
44
|
+
fast: "A small, focused, low-risk implementation or explanation where low latency matters most.",
|
|
45
|
+
balanced: "A normal feature or bug fix that benefits from reliable coding ability and moderate latency.",
|
|
46
|
+
strong: "A complex, multi-file, architectural, or difficult debugging task needing the strongest available coding model.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async decide(questionId, state, criteria) {
|
|
50
|
+
const response = await this.fetcher(endpoint, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: "jev-latest",
|
|
58
|
+
state: state.slice(0, 4_000),
|
|
59
|
+
questions: {
|
|
60
|
+
[questionId]: {
|
|
61
|
+
type: "choice",
|
|
62
|
+
instructions: "Classify only the operational risk of the proposed Kairo action. Do not authorize it.",
|
|
63
|
+
criteria,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw this.error(response.status);
|
|
70
|
+
const answer = (await response.json());
|
|
71
|
+
const choice = answer.answers?.[questionId]?.choice;
|
|
72
|
+
const confidence = answer.answers?.[questionId]?.confidence;
|
|
73
|
+
if (typeof choice !== "string" ||
|
|
74
|
+
!Object.hasOwn(criteria, choice) ||
|
|
75
|
+
typeof confidence !== "number" ||
|
|
76
|
+
!Number.isFinite(confidence) ||
|
|
77
|
+
confidence < 0 ||
|
|
78
|
+
confidence > 1)
|
|
79
|
+
throw new ProviderError("request", false, undefined, "Jev");
|
|
80
|
+
return { value: choice, confidence };
|
|
81
|
+
}
|
|
82
|
+
/** Uses a minimal typed request because TypeSafe does not publish a key-only probe. */
|
|
83
|
+
async validate() {
|
|
84
|
+
await this.assess("Kairo credential validation. Proposed action: no operation.");
|
|
85
|
+
}
|
|
86
|
+
error(status) {
|
|
87
|
+
if (status === 401 || status === 403)
|
|
88
|
+
return new ProviderError("authentication", false, undefined, "Jev");
|
|
89
|
+
if (status === 429)
|
|
90
|
+
return new ProviderError("quota", true, undefined, "Jev");
|
|
91
|
+
if (status >= 500)
|
|
92
|
+
return new ProviderError("service", true, undefined, "Jev");
|
|
93
|
+
return new ProviderError("request", false, undefined, "Jev");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ProviderProgress } from "../../domain/provider-error.js";
|
|
2
|
+
import type { Message, ModelTurn } from "../../domain/models.js";
|
|
3
|
+
import type { ModelProvider, ToolDefinition } from "../../domain/ports.js";
|
|
4
|
+
/** Adapts Mistral's OpenAI-compatible Chat Completions API to Kairo's provider contract. */
|
|
5
|
+
export declare class MistralProvider implements ModelProvider {
|
|
6
|
+
private readonly apiKey;
|
|
7
|
+
private readonly model;
|
|
8
|
+
private readonly tools;
|
|
9
|
+
private readonly fetcher;
|
|
10
|
+
constructor(apiKey: string, model: string, tools: ToolDefinition[], fetcher?: typeof fetch);
|
|
11
|
+
stream(messages: Message[], onText: (chunk: string) => void, onProgress?: (event: ProviderProgress) => void, systemInstruction?: string, toolsEnabled?: boolean): Promise<ModelTurn>;
|
|
12
|
+
private streamOnce;
|
|
13
|
+
private message;
|
|
14
|
+
private error;
|
|
15
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { modelSystemInstruction } from "../../application/model-system-instruction.js";
|
|
2
|
+
import { ProviderError } from "../../domain/provider-error.js";
|
|
3
|
+
import { recoverProvider } from "./provider-recovery.js";
|
|
4
|
+
/** Adapts Mistral's OpenAI-compatible Chat Completions API to Kairo's provider contract. */
|
|
5
|
+
export class MistralProvider {
|
|
6
|
+
apiKey;
|
|
7
|
+
model;
|
|
8
|
+
tools;
|
|
9
|
+
fetcher;
|
|
10
|
+
constructor(apiKey, model, tools, fetcher = fetch) {
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.model = model;
|
|
13
|
+
this.tools = tools;
|
|
14
|
+
this.fetcher = fetcher;
|
|
15
|
+
}
|
|
16
|
+
async stream(messages, onText, onProgress, systemInstruction = modelSystemInstruction, toolsEnabled = true) {
|
|
17
|
+
return recoverProvider((markContent) => this.streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled), onProgress, undefined, "Mistral");
|
|
18
|
+
}
|
|
19
|
+
async streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled) {
|
|
20
|
+
const response = await this.fetcher("https://api.mistral.ai/v1/chat/completions", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
model: this.model,
|
|
25
|
+
stream: true,
|
|
26
|
+
messages: [
|
|
27
|
+
{ role: "system", content: systemInstruction },
|
|
28
|
+
...messages.map((message) => this.message(message)),
|
|
29
|
+
],
|
|
30
|
+
...(toolsEnabled
|
|
31
|
+
? {
|
|
32
|
+
tools: this.tools.map(({ name, description, parameters }) => ({
|
|
33
|
+
type: "function",
|
|
34
|
+
function: { name, description, parameters },
|
|
35
|
+
})),
|
|
36
|
+
tool_choice: "auto",
|
|
37
|
+
parallel_tool_calls: false,
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw this.error(response.status);
|
|
44
|
+
if (!response.body)
|
|
45
|
+
throw new ProviderError("service", true, undefined, "Mistral");
|
|
46
|
+
const reader = response.body.getReader();
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
let buffer = "";
|
|
49
|
+
let text = "";
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
while (true) {
|
|
52
|
+
const next = await reader.read();
|
|
53
|
+
if (next.done)
|
|
54
|
+
break;
|
|
55
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
56
|
+
const frames = buffer.split("\n\n");
|
|
57
|
+
buffer = frames.pop() ?? "";
|
|
58
|
+
for (const frame of frames) {
|
|
59
|
+
const payload = frame
|
|
60
|
+
.split("\n")
|
|
61
|
+
.filter((line) => line.startsWith("data:"))
|
|
62
|
+
.map((line) => line.slice(5).trim())
|
|
63
|
+
.join("\n");
|
|
64
|
+
if (!payload || payload === "[DONE]")
|
|
65
|
+
continue;
|
|
66
|
+
let chunk;
|
|
67
|
+
try {
|
|
68
|
+
chunk = JSON.parse(payload);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new ProviderError("service", true, undefined, "Mistral");
|
|
72
|
+
}
|
|
73
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
74
|
+
if (delta?.content) {
|
|
75
|
+
markContent();
|
|
76
|
+
text += delta.content;
|
|
77
|
+
onText(delta.content);
|
|
78
|
+
}
|
|
79
|
+
for (const call of delta?.tool_calls ?? []) {
|
|
80
|
+
markContent();
|
|
81
|
+
const index = call.index ?? 0;
|
|
82
|
+
const current = pending.get(index) ?? { name: "", arguments: "" };
|
|
83
|
+
if (call.id)
|
|
84
|
+
current.id = call.id;
|
|
85
|
+
if (call.function?.name)
|
|
86
|
+
current.name += call.function.name;
|
|
87
|
+
if (call.function?.arguments)
|
|
88
|
+
current.arguments += call.function.arguments;
|
|
89
|
+
pending.set(index, current);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
text,
|
|
95
|
+
toolCalls: [...pending.entries()]
|
|
96
|
+
.sort(([left], [right]) => left - right)
|
|
97
|
+
.map(([, call]) => {
|
|
98
|
+
try {
|
|
99
|
+
return {
|
|
100
|
+
id: call.id || crypto.randomUUID(),
|
|
101
|
+
name: call.name,
|
|
102
|
+
args: JSON.parse(call.arguments || "{}"),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new ProviderError("request", false, undefined, "Mistral");
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
message(message) {
|
|
112
|
+
if (message.role === "tool")
|
|
113
|
+
return { role: "tool", tool_call_id: message.toolCallId, content: message.content };
|
|
114
|
+
if (message.role === "model" && message.toolCallId && message.toolName)
|
|
115
|
+
return {
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: null,
|
|
118
|
+
tool_calls: [
|
|
119
|
+
{
|
|
120
|
+
id: message.toolCallId,
|
|
121
|
+
type: "function",
|
|
122
|
+
function: { name: message.toolName, arguments: message.content },
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
return { role: message.role === "model" ? "assistant" : "user", content: message.content };
|
|
127
|
+
}
|
|
128
|
+
error(status) {
|
|
129
|
+
if (status === 401 || status === 403)
|
|
130
|
+
return new ProviderError("authentication", false, undefined, "Mistral");
|
|
131
|
+
if (status === 429)
|
|
132
|
+
return new ProviderError("quota", true, undefined, "Mistral");
|
|
133
|
+
if (status >= 500)
|
|
134
|
+
return new ProviderError("service", true, undefined, "Mistral");
|
|
135
|
+
return new ProviderError("request", false, undefined, "Mistral");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ProviderProgress } from "../../domain/provider-error.js";
|
|
2
|
+
import type { Message, ModelTurn } from "../../domain/models.js";
|
|
3
|
+
import type { ModelProvider, ToolDefinition } from "../../domain/ports.js";
|
|
4
|
+
/** Adapts OpenRouter's OpenAI-compatible chat API to Kairo's provider contract. */
|
|
5
|
+
export declare class OpenRouterProvider implements ModelProvider {
|
|
6
|
+
private readonly model;
|
|
7
|
+
private readonly tools;
|
|
8
|
+
private readonly client;
|
|
9
|
+
constructor(apiKey: string, model: string, tools: ToolDefinition[]);
|
|
10
|
+
stream(messages: Message[], onText: (chunk: string) => void, onProgress?: (event: ProviderProgress) => void, systemInstruction?: string, toolsEnabled?: boolean): Promise<ModelTurn>;
|
|
11
|
+
private streamOnce;
|
|
12
|
+
private message;
|
|
13
|
+
/** Free endpoints are rate-limited; do not consume their small allowance with automatic retries. */
|
|
14
|
+
private isFreeModel;
|
|
15
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { OpenRouter } from "@openrouter/sdk";
|
|
2
|
+
import { modelSystemInstruction } from "../../application/model-system-instruction.js";
|
|
3
|
+
import { ProviderError } from "../../domain/provider-error.js";
|
|
4
|
+
import { recoverProvider } from "./provider-recovery.js";
|
|
5
|
+
/** Adapts OpenRouter's OpenAI-compatible chat API to Kairo's provider contract. */
|
|
6
|
+
export class OpenRouterProvider {
|
|
7
|
+
model;
|
|
8
|
+
tools;
|
|
9
|
+
client;
|
|
10
|
+
constructor(apiKey, model, tools) {
|
|
11
|
+
this.model = model;
|
|
12
|
+
this.tools = tools;
|
|
13
|
+
this.client = new OpenRouter({
|
|
14
|
+
apiKey,
|
|
15
|
+
appTitle: "Kairo",
|
|
16
|
+
// Kairo owns retry behavior so transcript progress remains accurate.
|
|
17
|
+
retryConfig: { strategy: "none" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async stream(messages, onText, onProgress, systemInstruction = modelSystemInstruction, toolsEnabled = true) {
|
|
21
|
+
return recoverProvider((markContent) => this.streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled), onProgress, undefined, "OpenRouter", !this.isFreeModel());
|
|
22
|
+
}
|
|
23
|
+
async streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled) {
|
|
24
|
+
const stream = (await this.client.chat.send({
|
|
25
|
+
chatRequest: {
|
|
26
|
+
model: this.model,
|
|
27
|
+
stream: true,
|
|
28
|
+
messages: [
|
|
29
|
+
{ role: "system", content: systemInstruction },
|
|
30
|
+
...messages.map((message) => this.message(message)),
|
|
31
|
+
],
|
|
32
|
+
...(toolsEnabled
|
|
33
|
+
? {
|
|
34
|
+
tools: this.tools.map(({ name, description, parameters }) => ({
|
|
35
|
+
type: "function",
|
|
36
|
+
function: { name, description, parameters },
|
|
37
|
+
})),
|
|
38
|
+
toolChoice: "auto",
|
|
39
|
+
parallelToolCalls: false,
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
let text = "";
|
|
45
|
+
const pending = new Map();
|
|
46
|
+
for await (const chunk of stream) {
|
|
47
|
+
if (chunk.error)
|
|
48
|
+
throw { status: chunk.error.code };
|
|
49
|
+
const delta = chunk.choices[0]?.delta;
|
|
50
|
+
if (delta?.content) {
|
|
51
|
+
markContent();
|
|
52
|
+
text += delta.content;
|
|
53
|
+
onText(delta.content);
|
|
54
|
+
}
|
|
55
|
+
for (const call of delta?.toolCalls ?? []) {
|
|
56
|
+
markContent();
|
|
57
|
+
const current = pending.get(call.index) ?? { name: "", arguments: "" };
|
|
58
|
+
if (call.id)
|
|
59
|
+
current.id = call.id;
|
|
60
|
+
if (call.function?.name)
|
|
61
|
+
current.name += call.function.name;
|
|
62
|
+
if (call.function?.arguments)
|
|
63
|
+
current.arguments += call.function.arguments;
|
|
64
|
+
pending.set(call.index, current);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const toolCalls = [...pending.entries()]
|
|
68
|
+
.sort(([left], [right]) => left - right)
|
|
69
|
+
.map(([, call]) => {
|
|
70
|
+
try {
|
|
71
|
+
return {
|
|
72
|
+
id: call.id || crypto.randomUUID(),
|
|
73
|
+
name: call.name,
|
|
74
|
+
args: JSON.parse(call.arguments || "{}"),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new ProviderError("request", false, undefined, "OpenRouter");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return { text, toolCalls };
|
|
82
|
+
}
|
|
83
|
+
message(message) {
|
|
84
|
+
if (message.role === "tool")
|
|
85
|
+
return { role: "tool", toolCallId: message.toolCallId, content: message.content };
|
|
86
|
+
if (message.role === "model" && message.toolCallId && message.toolName)
|
|
87
|
+
return {
|
|
88
|
+
role: "assistant",
|
|
89
|
+
content: null,
|
|
90
|
+
toolCalls: [
|
|
91
|
+
{
|
|
92
|
+
id: message.toolCallId,
|
|
93
|
+
type: "function",
|
|
94
|
+
function: { name: message.toolName, arguments: message.content },
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
return { role: message.role === "model" ? "assistant" : "user", content: message.content };
|
|
99
|
+
}
|
|
100
|
+
/** Free endpoints are rate-limited; do not consume their small allowance with automatic retries. */
|
|
101
|
+
isFreeModel() {
|
|
102
|
+
return this.model === "openrouter/free" || this.model.endsWith(":free");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ProviderError, type ProviderProgress } from "../../domain/provider-error.js";
|
|
2
|
+
/** Reads bounded nested SDK/HTTP error metadata without persisting any raw values. */
|
|
3
|
+
export declare function normalizeProviderError(error: unknown, provider?: string): ProviderError;
|
|
4
|
+
export interface RecoveryClock {
|
|
5
|
+
now(): number;
|
|
6
|
+
sleep(ms: number): Promise<void>;
|
|
7
|
+
random(): number;
|
|
8
|
+
}
|
|
9
|
+
/** Retries only pre-content failures within three retries and 30 seconds of waiting. */
|
|
10
|
+
export declare function recoverProvider<T>(request: (markContent: () => void) => Promise<T>, progress?: (event: ProviderProgress) => void, timer?: RecoveryClock, provider?: string, retryQuota?: boolean): Promise<T>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ProviderError } from "../../domain/provider-error.js";
|
|
2
|
+
/** Reads bounded nested SDK/HTTP error metadata without persisting any raw values. */
|
|
3
|
+
export function normalizeProviderError(error, provider = "Model provider") {
|
|
4
|
+
if (error instanceof ProviderError)
|
|
5
|
+
return error;
|
|
6
|
+
const texts = [];
|
|
7
|
+
const codes = [];
|
|
8
|
+
let retryAfterMs;
|
|
9
|
+
const visit = (value, depth) => {
|
|
10
|
+
if (depth > 6 || !value)
|
|
11
|
+
return;
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
const text = value.slice(0, 16000);
|
|
14
|
+
texts.push(text);
|
|
15
|
+
try {
|
|
16
|
+
visit(JSON.parse(text), depth + 1);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* SDK messages may be plain text. */
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "object")
|
|
24
|
+
return;
|
|
25
|
+
const record = value;
|
|
26
|
+
for (const field of ["code", "status", "statusCode", "name"])
|
|
27
|
+
if (record[field] !== undefined)
|
|
28
|
+
codes.push(String(record[field]));
|
|
29
|
+
if (typeof record.retryDelay === "string") {
|
|
30
|
+
const match = /^(\d+(?:\.\d+)?)s$/.exec(record.retryDelay);
|
|
31
|
+
if (match)
|
|
32
|
+
retryAfterMs = Math.max(retryAfterMs ?? 0, Number(match[1]) * 1000);
|
|
33
|
+
}
|
|
34
|
+
const headers = record.headers;
|
|
35
|
+
const header = headers?.get?.("retry-after") ??
|
|
36
|
+
record.headers?.["retry-after"];
|
|
37
|
+
if (header) {
|
|
38
|
+
const delay = /^\d+(?:\.\d+)?$/.test(header)
|
|
39
|
+
? Number(header) * 1000
|
|
40
|
+
: Date.parse(header) - Date.now();
|
|
41
|
+
if (Number.isFinite(delay))
|
|
42
|
+
retryAfterMs = Math.max(retryAfterMs ?? 0, delay, 0);
|
|
43
|
+
}
|
|
44
|
+
for (const field of [
|
|
45
|
+
"message",
|
|
46
|
+
"error",
|
|
47
|
+
"cause",
|
|
48
|
+
"details",
|
|
49
|
+
"response",
|
|
50
|
+
"violations",
|
|
51
|
+
"quotaId",
|
|
52
|
+
])
|
|
53
|
+
visit(record[field], depth + 1);
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
for (const item of value.slice(0, 20))
|
|
56
|
+
visit(item, depth + 1);
|
|
57
|
+
};
|
|
58
|
+
visit(error, 0);
|
|
59
|
+
const text = texts.join(" ");
|
|
60
|
+
const code = codes.join(" ");
|
|
61
|
+
if (/\b(401|403|UNAUTHENTICATED|PERMISSION_DENIED)\b/.test(code))
|
|
62
|
+
return new ProviderError("authentication", false, undefined, provider, "Check the API key in /models.");
|
|
63
|
+
if (/\b(402|429|RESOURCE_EXHAUSTED)\b/.test(code)) {
|
|
64
|
+
const permanent = /per.?day|daily|billing|credit/i.test(text.replace(/check your plan and billing details/gi, ""));
|
|
65
|
+
return new ProviderError("quota", !permanent, retryAfterMs, provider, "Check the provider's credits, limits, or available model routes.");
|
|
66
|
+
}
|
|
67
|
+
if (/\b(500|502|503|504|UNAVAILABLE|INTERNAL)\b/.test(code))
|
|
68
|
+
return new ProviderError("service", true, retryAfterMs, provider);
|
|
69
|
+
if (/ECONNRESET|ETIMEDOUT|ECONNREFUSED|EAI_AGAIN|TimeoutError|AbortError/.test(code) ||
|
|
70
|
+
/fetch failed|network error|timed out/i.test(text))
|
|
71
|
+
return new ProviderError("network", true, retryAfterMs, provider);
|
|
72
|
+
return new ProviderError("request", false, undefined, provider, "Check that the selected model supports chat and tool calling.");
|
|
73
|
+
}
|
|
74
|
+
const clock = {
|
|
75
|
+
now: () => performance.now(),
|
|
76
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
77
|
+
random: Math.random,
|
|
78
|
+
};
|
|
79
|
+
/** Retries only pre-content failures within three retries and 30 seconds of waiting. */
|
|
80
|
+
export async function recoverProvider(request, progress = () => { }, timer = clock, provider = "Model provider", retryQuota = true) {
|
|
81
|
+
let retries = 0;
|
|
82
|
+
let waited = 0;
|
|
83
|
+
for (;;) {
|
|
84
|
+
let hasContent = false;
|
|
85
|
+
try {
|
|
86
|
+
return await request(() => {
|
|
87
|
+
hasContent = true;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (raw) {
|
|
91
|
+
let error = normalizeProviderError(raw, provider);
|
|
92
|
+
if (!retryQuota && error.category === "quota")
|
|
93
|
+
error = new ProviderError("quota", false, error.retryAfterMs, provider, "The provider quota was exhausted. Wait before trying again or use a provider with available capacity.");
|
|
94
|
+
const delay = error.retryAfterMs ?? 1000 * 2 ** retries + timer.random() * 250;
|
|
95
|
+
if (!error.retryable || hasContent || retries >= 3 || delay > 30000 - waited) {
|
|
96
|
+
progress({ kind: "exhausted", category: error.category, retry: retries, delayMs: 0 });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
retries += 1;
|
|
100
|
+
progress({ kind: "retry", category: error.category, retry: retries, delayMs: delay });
|
|
101
|
+
const started = timer.now();
|
|
102
|
+
await timer.sleep(delay);
|
|
103
|
+
const elapsed = Math.max(delay, timer.now() - started);
|
|
104
|
+
waited += elapsed;
|
|
105
|
+
progress({ kind: "retry_wait", category: error.category, retry: retries, delayMs: elapsed });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModelSelection, ProviderId } from "../../domain/models.js";
|
|
2
|
+
import type { ModelProvider, ToolDefinition } from "../../domain/ports.js";
|
|
3
|
+
export interface ProviderDescriptor {
|
|
4
|
+
id: ProviderId;
|
|
5
|
+
name: string;
|
|
6
|
+
environmentVariable: string;
|
|
7
|
+
models: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
tier: "fast" | "balanced" | "strong";
|
|
11
|
+
recommended?: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
create(apiKey: string, model: string, tools: ToolDefinition[]): ModelProvider;
|
|
14
|
+
validate(apiKey: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare const providerRegistry: readonly ProviderDescriptor[];
|
|
17
|
+
/** Narrows untrusted CLI/config text to a registered provider identifier. */
|
|
18
|
+
export declare function isProviderId(value: unknown): value is ProviderId;
|
|
19
|
+
/** Returns the registered behavior for one supported provider. */
|
|
20
|
+
export declare function providerById(id: ProviderId): ProviderDescriptor;
|
|
21
|
+
/** Creates a provider from a validated global selection. */
|
|
22
|
+
export declare function createProvider(selection: ModelSelection, apiKey: string, tools: ToolDefinition[]): ModelProvider;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { GeminiProvider } from "./gemini-provider.js";
|
|
2
|
+
import { GroqProvider } from "./groq-provider.js";
|
|
3
|
+
import { MistralProvider } from "./mistral-provider.js";
|
|
4
|
+
/** Converts a failed credential probe into a provider-safe authentication error. */
|
|
5
|
+
async function validateResponse(provider, response) {
|
|
6
|
+
if (!response.ok)
|
|
7
|
+
throw new Error(`${provider} credential validation failed (${response.status}).`);
|
|
8
|
+
}
|
|
9
|
+
export const providerRegistry = [
|
|
10
|
+
{
|
|
11
|
+
id: "gemini",
|
|
12
|
+
name: "Gemini",
|
|
13
|
+
environmentVariable: "GEMINI_API_KEY",
|
|
14
|
+
models: [
|
|
15
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "balanced", recommended: true },
|
|
16
|
+
],
|
|
17
|
+
create: (apiKey, model, tools) => new GeminiProvider(apiKey, model, tools),
|
|
18
|
+
validate: async (apiKey) => validateResponse("Gemini", await fetch(`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1&key=${encodeURIComponent(apiKey)}`)),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "groq",
|
|
22
|
+
name: "Groq",
|
|
23
|
+
environmentVariable: "GROQ_API_KEY",
|
|
24
|
+
models: [
|
|
25
|
+
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", tier: "strong", recommended: true },
|
|
26
|
+
{ id: "openai/gpt-oss-20b", label: "GPT-OSS 20B (faster)", tier: "fast" },
|
|
27
|
+
],
|
|
28
|
+
create: (apiKey, model, tools) => new GroqProvider(apiKey, model, tools),
|
|
29
|
+
validate: async (apiKey) => validateResponse("Groq", await fetch("https://api.groq.com/openai/v1/models", {
|
|
30
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
31
|
+
})),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "mistral",
|
|
35
|
+
name: "Mistral",
|
|
36
|
+
environmentVariable: "MISTRAL_API_KEY",
|
|
37
|
+
models: [
|
|
38
|
+
{
|
|
39
|
+
id: "mistral-small-latest",
|
|
40
|
+
label: "Mistral Small 4",
|
|
41
|
+
tier: "fast",
|
|
42
|
+
recommended: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "mistral-medium-latest",
|
|
46
|
+
label: "Mistral Medium 3.5",
|
|
47
|
+
tier: "strong",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
create: (apiKey, model, tools) => new MistralProvider(apiKey, model, tools),
|
|
51
|
+
validate: async (apiKey) => validateResponse("Mistral", await fetch("https://api.mistral.ai/v1/models", {
|
|
52
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
/** Narrows untrusted CLI/config text to a registered provider identifier. */
|
|
57
|
+
export function isProviderId(value) {
|
|
58
|
+
return providerRegistry.some((provider) => provider.id === value);
|
|
59
|
+
}
|
|
60
|
+
/** Returns the registered behavior for one supported provider. */
|
|
61
|
+
export function providerById(id) {
|
|
62
|
+
return providerRegistry.find((provider) => provider.id === id);
|
|
63
|
+
}
|
|
64
|
+
/** Creates a provider from a validated global selection. */
|
|
65
|
+
export function createProvider(selection, apiKey, tools) {
|
|
66
|
+
return providerById(selection.provider).create(apiKey, selection.model, tools);
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RepositoryFingerprint, RepositorySnapshot } from "../../domain/models.js";
|
|
2
|
+
import type { TaskStore } from "../../domain/ports.js";
|
|
3
|
+
import { RepositoryProfiler } from "./repository-profiler.js";
|
|
4
|
+
export declare class RepositoryAwareness {
|
|
5
|
+
private readonly store;
|
|
6
|
+
private readonly profiler;
|
|
7
|
+
constructor(store: TaskStore, profiler?: RepositoryProfiler);
|
|
8
|
+
/** Reuses a current snapshot or atomically replaces stale derived metadata. */
|
|
9
|
+
ensureFresh(sessionId: string, root: string): Promise<RepositorySnapshot>;
|
|
10
|
+
build(root: string): Promise<RepositorySnapshot>;
|
|
11
|
+
fingerprint(root: string): Promise<RepositoryFingerprint>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RepositoryProfiler } from "./repository-profiler.js";
|
|
2
|
+
export class RepositoryAwareness {
|
|
3
|
+
store;
|
|
4
|
+
profiler;
|
|
5
|
+
constructor(store, profiler = new RepositoryProfiler()) {
|
|
6
|
+
this.store = store;
|
|
7
|
+
this.profiler = profiler;
|
|
8
|
+
}
|
|
9
|
+
/** Reuses a current snapshot or atomically replaces stale derived metadata. */
|
|
10
|
+
async ensureFresh(sessionId, root) {
|
|
11
|
+
const saved = this.store.repositorySnapshot(sessionId);
|
|
12
|
+
const current = await this.fingerprint(root);
|
|
13
|
+
if (saved?.fingerprint.value === current.value)
|
|
14
|
+
return saved;
|
|
15
|
+
const snapshot = await this.build(root);
|
|
16
|
+
this.store.saveRepositorySnapshot(sessionId, snapshot);
|
|
17
|
+
return snapshot;
|
|
18
|
+
}
|
|
19
|
+
build(root) {
|
|
20
|
+
return this.profiler.profile(root);
|
|
21
|
+
}
|
|
22
|
+
fingerprint(root) {
|
|
23
|
+
return this.profiler.fingerprint(root);
|
|
24
|
+
}
|
|
25
|
+
}
|