@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,447 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { databasePath, ensureStateDir } from "../filesystem/platform-paths.js";
|
|
3
|
+
export class SqliteSessionStore {
|
|
4
|
+
db;
|
|
5
|
+
/** Wraps an already-initialized database; callers use open() to guarantee setup. */
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
/** Opens the database, creates current schema objects, and recovers interrupted tasks. */
|
|
10
|
+
static async open(path) {
|
|
11
|
+
if (!path)
|
|
12
|
+
await ensureStateDir();
|
|
13
|
+
const db = new Database(path || databasePath());
|
|
14
|
+
db.exec(`PRAGMA journal_mode=WAL;
|
|
15
|
+
CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
|
|
16
|
+
INSERT INTO schema_version(version) SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM schema_version);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, workspace TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, tool_call_id TEXT, tool_name TEXT, created_at INTEGER NOT NULL, FOREIGN KEY(session_id) REFERENCES sessions(id));
|
|
19
|
+
CREATE TABLE IF NOT EXISTS tool_events (id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, call_id TEXT NOT NULL, name TEXT NOT NULL, args_json TEXT NOT NULL, approved INTEGER, output TEXT, created_at INTEGER NOT NULL);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, prompt TEXT NOT NULL, mode TEXT NOT NULL DEFAULT 'implementation', status TEXT NOT NULL, plan_json TEXT, changed_files_json TEXT NOT NULL DEFAULT '[]', approved_write_paths_json TEXT NOT NULL DEFAULT '[]', verification_command TEXT, verification_output TEXT, verification_ok INTEGER, verification_exit_code INTEGER, verification_discovered INTEGER, verification_selection_json TEXT, summary TEXT, error TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(session_id) REFERENCES sessions(id));
|
|
21
|
+
CREATE INDEX IF NOT EXISTS tasks_session_updated ON tasks(session_id, updated_at DESC);
|
|
22
|
+
CREATE TABLE IF NOT EXISTS context_checkpoints (id TEXT PRIMARY KEY, session_id TEXT NOT NULL, task_id TEXT, summary TEXT NOT NULL, through_message_id INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(session_id) REFERENCES sessions(id));
|
|
23
|
+
CREATE INDEX IF NOT EXISTS checkpoints_session_created ON context_checkpoints(session_id, created_at DESC);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS repository_profiles (session_id TEXT PRIMARY KEY, profile_json TEXT NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY(session_id) REFERENCES sessions(id));`);
|
|
25
|
+
db.exec(`CREATE TABLE IF NOT EXISTS repair_attempts (id TEXT PRIMARY KEY, task_id TEXT NOT NULL, command TEXT NOT NULL, evidence_json TEXT NOT NULL, selected_files_json TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(task_id) REFERENCES tasks(id));
|
|
26
|
+
CREATE INDEX IF NOT EXISTS repair_attempts_task_created ON repair_attempts(task_id, created_at DESC);`);
|
|
27
|
+
const columns = db.prepare("SELECT name FROM pragma_table_info('tasks')").all();
|
|
28
|
+
if (!columns.some((column) => column.name === "verification_ok"))
|
|
29
|
+
db.exec("ALTER TABLE tasks ADD COLUMN verification_ok INTEGER");
|
|
30
|
+
if (!columns.some((column) => column.name === "verification_exit_code"))
|
|
31
|
+
db.exec("ALTER TABLE tasks ADD COLUMN verification_exit_code INTEGER");
|
|
32
|
+
if (!columns.some((column) => column.name === "verification_discovered"))
|
|
33
|
+
db.exec("ALTER TABLE tasks ADD COLUMN verification_discovered INTEGER");
|
|
34
|
+
if (!columns.some((column) => column.name === "verification_selection_json"))
|
|
35
|
+
db.exec("ALTER TABLE tasks ADD COLUMN verification_selection_json TEXT");
|
|
36
|
+
if (!columns.some((column) => column.name === "mode"))
|
|
37
|
+
db.exec("ALTER TABLE tasks ADD COLUMN mode TEXT NOT NULL DEFAULT 'implementation'");
|
|
38
|
+
if (!columns.some((column) => column.name === "plan_json"))
|
|
39
|
+
db.exec("ALTER TABLE tasks ADD COLUMN plan_json TEXT");
|
|
40
|
+
if (!columns.some((column) => column.name === "approved_write_paths_json"))
|
|
41
|
+
db.exec("ALTER TABLE tasks ADD COLUMN approved_write_paths_json TEXT NOT NULL DEFAULT '[]'");
|
|
42
|
+
const store = new SqliteSessionStore(db);
|
|
43
|
+
db.exec("CREATE TABLE IF NOT EXISTS task_events (id INTEGER PRIMARY KEY, task_id TEXT NOT NULL, event_json TEXT NOT NULL); CREATE INDEX IF NOT EXISTS task_events_task ON task_events(task_id, id)");
|
|
44
|
+
db.exec(`CREATE TABLE IF NOT EXISTS evaluation_runs (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
suite TEXT NOT NULL,
|
|
47
|
+
provider TEXT NOT NULL DEFAULT 'gemini',
|
|
48
|
+
model TEXT NOT NULL,
|
|
49
|
+
source_revision TEXT NOT NULL,
|
|
50
|
+
trial_count INTEGER NOT NULL,
|
|
51
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
passed_count INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
started_at INTEGER NOT NULL,
|
|
54
|
+
completed_at INTEGER
|
|
55
|
+
);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS evaluation_runs_started ON evaluation_runs(started_at DESC);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS evaluation_attempts (
|
|
58
|
+
id INTEGER PRIMARY KEY,
|
|
59
|
+
run_id TEXT NOT NULL,
|
|
60
|
+
scenario_id TEXT NOT NULL,
|
|
61
|
+
trial INTEGER NOT NULL,
|
|
62
|
+
passed INTEGER NOT NULL,
|
|
63
|
+
task_status TEXT NOT NULL,
|
|
64
|
+
verified INTEGER NOT NULL,
|
|
65
|
+
expectation_passed INTEGER NOT NULL,
|
|
66
|
+
failure_category TEXT,
|
|
67
|
+
metrics_json TEXT NOT NULL,
|
|
68
|
+
duration_ms INTEGER NOT NULL,
|
|
69
|
+
created_at INTEGER NOT NULL,
|
|
70
|
+
FOREIGN KEY(run_id) REFERENCES evaluation_runs(id)
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS evaluation_attempts_run ON evaluation_attempts(run_id, id);`);
|
|
73
|
+
const evaluationColumns = db
|
|
74
|
+
.prepare("SELECT name FROM pragma_table_info('evaluation_runs')")
|
|
75
|
+
.all();
|
|
76
|
+
if (!evaluationColumns.some((column) => column.name === "provider"))
|
|
77
|
+
db.exec("ALTER TABLE evaluation_runs ADD COLUMN provider TEXT NOT NULL DEFAULT 'gemini'");
|
|
78
|
+
db.exec("CREATE TABLE IF NOT EXISTS evaluation_baselines (suite TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES evaluation_runs(id))");
|
|
79
|
+
store.recoverInterruptedTasks();
|
|
80
|
+
return store;
|
|
81
|
+
}
|
|
82
|
+
/** Closes the SQLite handle after the CLI session exits. */
|
|
83
|
+
close() {
|
|
84
|
+
this.db.close();
|
|
85
|
+
}
|
|
86
|
+
/** Atomically replaces the local self baseline after validating the selected run. */
|
|
87
|
+
setEvaluationBaseline(runId) {
|
|
88
|
+
const run = this.evaluationRun(runId);
|
|
89
|
+
if (!run)
|
|
90
|
+
throw new Error(`Evaluation run not found: ${runId}`);
|
|
91
|
+
if (run.suite !== "self" || run.completedAt === undefined || run.trialCount < 3)
|
|
92
|
+
throw new Error("Baseline must be a completed self-evaluation run with at least three trials.");
|
|
93
|
+
this.db
|
|
94
|
+
.prepare("INSERT INTO evaluation_baselines(suite, run_id) VALUES ('self', ?) ON CONFLICT(suite) DO UPDATE SET run_id=excluded.run_id")
|
|
95
|
+
.run(runId);
|
|
96
|
+
return run;
|
|
97
|
+
}
|
|
98
|
+
/** Resolves the baseline pointer without duplicating evaluation metadata. */
|
|
99
|
+
evaluationBaseline() {
|
|
100
|
+
const row = this.db
|
|
101
|
+
.prepare("SELECT run_id FROM evaluation_baselines WHERE suite='self'")
|
|
102
|
+
.get();
|
|
103
|
+
return row ? this.evaluationRun(row.run_id) : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** Starts a metadata-only real-model evaluation run. */
|
|
106
|
+
createEvaluationRun(input) {
|
|
107
|
+
const run = {
|
|
108
|
+
...input,
|
|
109
|
+
id: `eval-${input.startedAt.toString(36)}-${crypto.randomUUID().slice(0, 8)}`,
|
|
110
|
+
attemptCount: 0,
|
|
111
|
+
passedCount: 0,
|
|
112
|
+
};
|
|
113
|
+
this.db
|
|
114
|
+
.prepare("INSERT INTO evaluation_runs(id, suite, provider, model, source_revision, trial_count, attempt_count, passed_count, started_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
115
|
+
.run(run.id, run.suite, run.provider, run.model, run.sourceRevision, run.trialCount, 0, 0, run.startedAt, null);
|
|
116
|
+
return run;
|
|
117
|
+
}
|
|
118
|
+
/** Saves counters and a sanitized outcome without raw prompts, source, or tool output. */
|
|
119
|
+
saveEvaluationAttempt(attempt) {
|
|
120
|
+
this.db
|
|
121
|
+
.prepare("INSERT INTO evaluation_attempts(run_id, scenario_id, trial, passed, task_status, verified, expectation_passed, failure_category, metrics_json, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
122
|
+
.run(attempt.runId, attempt.scenarioId, attempt.trial, Number(attempt.passed), attempt.taskStatus, Number(attempt.verified), Number(attempt.expectationPassed), attempt.failureCategory ?? null, JSON.stringify(attempt.metrics), attempt.durationMs, attempt.createdAt);
|
|
123
|
+
}
|
|
124
|
+
/** Computes aggregate counts from attempts so historical reports cannot drift. */
|
|
125
|
+
completeEvaluationRun(id) {
|
|
126
|
+
const aggregate = this.db
|
|
127
|
+
.prepare("SELECT count(*) AS attempts, coalesce(sum(passed), 0) AS passed FROM evaluation_attempts WHERE run_id=?")
|
|
128
|
+
.get(id);
|
|
129
|
+
this.db
|
|
130
|
+
.prepare("UPDATE evaluation_runs SET attempt_count=?, passed_count=?, completed_at=? WHERE id=?")
|
|
131
|
+
.run(aggregate.attempts, aggregate.passed, Date.now(), id);
|
|
132
|
+
const run = this.evaluationRun(id);
|
|
133
|
+
if (!run)
|
|
134
|
+
throw new Error(`Evaluation run not found: ${id}`);
|
|
135
|
+
return run;
|
|
136
|
+
}
|
|
137
|
+
/** Lists recent evaluation runs, newest first. */
|
|
138
|
+
evaluationRuns(limit = 20) {
|
|
139
|
+
return this.db
|
|
140
|
+
.prepare("SELECT * FROM evaluation_runs ORDER BY started_at DESC LIMIT ?")
|
|
141
|
+
.all(limit).map((row) => this.toEvaluationRun(row));
|
|
142
|
+
}
|
|
143
|
+
/** Reads one saved evaluation run. */
|
|
144
|
+
evaluationRun(id) {
|
|
145
|
+
const row = this.db.prepare("SELECT * FROM evaluation_runs WHERE id=?").get(id);
|
|
146
|
+
return row ? this.toEvaluationRun(row) : undefined;
|
|
147
|
+
}
|
|
148
|
+
/** Reads attempts in stable execution order. */
|
|
149
|
+
evaluationAttempts(runId) {
|
|
150
|
+
return this.db
|
|
151
|
+
.prepare("SELECT * FROM evaluation_attempts WHERE run_id=? ORDER BY id")
|
|
152
|
+
.all(runId).map((row) => ({
|
|
153
|
+
runId: String(row.run_id),
|
|
154
|
+
scenarioId: String(row.scenario_id),
|
|
155
|
+
trial: Number(row.trial),
|
|
156
|
+
passed: Boolean(row.passed),
|
|
157
|
+
taskStatus: row.task_status,
|
|
158
|
+
verified: Boolean(row.verified),
|
|
159
|
+
expectationPassed: Boolean(row.expectation_passed),
|
|
160
|
+
failureCategory: row.failure_category,
|
|
161
|
+
metrics: JSON.parse(String(row.metrics_json)),
|
|
162
|
+
durationMs: Number(row.duration_ms),
|
|
163
|
+
createdAt: Number(row.created_at),
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
/** Stores bounded operation metadata separately from model context and raw tool history. */
|
|
167
|
+
recordTaskEvent(event) {
|
|
168
|
+
this.db
|
|
169
|
+
.prepare("INSERT INTO task_events(task_id, event_json) VALUES (?, ?)")
|
|
170
|
+
.run(event.taskId, JSON.stringify(event));
|
|
171
|
+
}
|
|
172
|
+
/** Uses insertion ids rather than timestamps to preserve ordering within the same millisecond. */
|
|
173
|
+
taskEvents(taskId) {
|
|
174
|
+
return this.db
|
|
175
|
+
.prepare("SELECT id, event_json FROM task_events WHERE task_id=? ORDER BY id")
|
|
176
|
+
.all(taskId).map((row) => ({ ...JSON.parse(row.event_json), id: row.id }));
|
|
177
|
+
}
|
|
178
|
+
/** Creates a durable session associated with one resolved workspace. */
|
|
179
|
+
create(workspace) {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const id = `${now.toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
182
|
+
this.db.prepare("INSERT INTO sessions VALUES (?, ?, ?, ?)").run(id, workspace, now, now);
|
|
183
|
+
return { id, workspace, createdAt: now, updatedAt: now };
|
|
184
|
+
}
|
|
185
|
+
/** Loads one session by id, if it still exists. */
|
|
186
|
+
get(id) {
|
|
187
|
+
const row = this.db
|
|
188
|
+
.prepare("SELECT id, workspace, created_at, updated_at FROM sessions WHERE id = ?")
|
|
189
|
+
.get(id);
|
|
190
|
+
return (row && {
|
|
191
|
+
id: String(row.id),
|
|
192
|
+
workspace: String(row.workspace),
|
|
193
|
+
createdAt: Number(row.created_at),
|
|
194
|
+
updatedAt: Number(row.updated_at),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Lists sessions from most recently active to oldest. */
|
|
198
|
+
list() {
|
|
199
|
+
return this.db
|
|
200
|
+
.prepare("SELECT id, workspace, created_at, updated_at FROM sessions ORDER BY updated_at DESC")
|
|
201
|
+
.all().map((r) => ({
|
|
202
|
+
id: String(r.id),
|
|
203
|
+
workspace: String(r.workspace),
|
|
204
|
+
createdAt: Number(r.created_at),
|
|
205
|
+
updatedAt: Number(r.updated_at),
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
/** Loads all messages required to reconstruct a full conversation. */
|
|
209
|
+
messages(sessionId) {
|
|
210
|
+
return this.db
|
|
211
|
+
.prepare("SELECT role, content, tool_call_id, tool_name, created_at FROM messages WHERE session_id=? ORDER BY id")
|
|
212
|
+
.all(sessionId).map((r) => ({
|
|
213
|
+
role: r.role,
|
|
214
|
+
content: String(r.content),
|
|
215
|
+
toolCallId: r.tool_call_id ? String(r.tool_call_id) : undefined,
|
|
216
|
+
toolName: r.tool_name ? String(r.tool_name) : undefined,
|
|
217
|
+
createdAt: Number(r.created_at),
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
/** Loads only the newest messages for bounded model context. */
|
|
221
|
+
recentMessages(sessionId, limit) {
|
|
222
|
+
return this.db
|
|
223
|
+
.prepare("SELECT role, content, tool_call_id, tool_name, created_at FROM (SELECT * FROM messages WHERE session_id=? ORDER BY id DESC LIMIT ?) ORDER BY id")
|
|
224
|
+
.all(sessionId, limit).map((r) => ({
|
|
225
|
+
role: r.role,
|
|
226
|
+
content: String(r.content),
|
|
227
|
+
toolCallId: r.tool_call_id ? String(r.tool_call_id) : undefined,
|
|
228
|
+
toolName: r.tool_name ? String(r.tool_name) : undefined,
|
|
229
|
+
createdAt: Number(r.created_at),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
/** Appends one durable message and refreshes the owning session timestamp. */
|
|
233
|
+
addMessage(sessionId, message) {
|
|
234
|
+
this.db
|
|
235
|
+
.prepare("INSERT INTO messages(session_id, role, content, tool_call_id, tool_name, created_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
236
|
+
.run(sessionId, message.role, message.content, message.toolCallId ?? null, message.toolName ?? null, message.createdAt);
|
|
237
|
+
this.db.prepare("UPDATE sessions SET updated_at=? WHERE id=?").run(Date.now(), sessionId);
|
|
238
|
+
}
|
|
239
|
+
/** Records the requested action, approval decision, and visible tool output. */
|
|
240
|
+
recordTool(sessionId, id, name, args, approved, output) {
|
|
241
|
+
this.db
|
|
242
|
+
.prepare("INSERT INTO tool_events(session_id, call_id, name, args_json, approved, output, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
243
|
+
.run(sessionId, id, name, JSON.stringify(args), approved === null ? null : Number(approved), output, Date.now());
|
|
244
|
+
}
|
|
245
|
+
/** Creates a new task in the initial planning state. */
|
|
246
|
+
startTask(sessionId, prompt, mode = "implementation") {
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const id = `task-${now.toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
249
|
+
this.db
|
|
250
|
+
.prepare("INSERT INTO tasks(id, session_id, prompt, mode, status, changed_files_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
251
|
+
.run(id, sessionId, prompt, mode, "planning", "[]", now, now);
|
|
252
|
+
this.recordTaskEvent({ taskId: id, kind: "status", outcome: "planning", createdAt: now });
|
|
253
|
+
return this.task(id);
|
|
254
|
+
}
|
|
255
|
+
/** Loads one task by id and maps database columns to domain names. */
|
|
256
|
+
task(id) {
|
|
257
|
+
return this.toTask(this.db.prepare("SELECT * FROM tasks WHERE id=?").get(id));
|
|
258
|
+
}
|
|
259
|
+
/** Finds the newest task belonging to a session. */
|
|
260
|
+
latestTask(sessionId) {
|
|
261
|
+
return this.toTask(this.db
|
|
262
|
+
.prepare("SELECT * FROM tasks WHERE session_id=? ORDER BY updated_at DESC, rowid DESC LIMIT 1")
|
|
263
|
+
.get(sessionId));
|
|
264
|
+
}
|
|
265
|
+
/** Finds the most recently saved planning artifact in a session. */
|
|
266
|
+
latestPlan(sessionId) {
|
|
267
|
+
return this.toTask(this.db
|
|
268
|
+
.prepare("SELECT * FROM tasks WHERE session_id=? AND mode='planning' ORDER BY updated_at DESC LIMIT 1")
|
|
269
|
+
.get(sessionId));
|
|
270
|
+
}
|
|
271
|
+
/** Merges a partial task update and writes the complete task state atomically. */
|
|
272
|
+
updateTask(id, patch) {
|
|
273
|
+
const task = this.task(id);
|
|
274
|
+
if (!task)
|
|
275
|
+
throw new Error(`Task not found: ${id}`);
|
|
276
|
+
const next = { ...task, ...patch, updatedAt: Date.now() };
|
|
277
|
+
this.db
|
|
278
|
+
.prepare("UPDATE tasks SET mode=?, status=?, plan_json=?, changed_files_json=?, approved_write_paths_json=?, verification_command=?, verification_output=?, verification_ok=?, verification_exit_code=?, verification_discovered=?, verification_selection_json=?, summary=?, error=?, updated_at=? WHERE id=?")
|
|
279
|
+
.run(next.mode, next.status, next.plan ? JSON.stringify(next.plan) : null, JSON.stringify(next.changedFiles), JSON.stringify(next.approvedWritePaths), next.verificationCommand ?? null, next.verificationOutput ?? null, next.verificationPassed === undefined ? null : Number(next.verificationPassed), next.verificationExitCode ?? null, next.verificationDiscovered === undefined ? null : Number(next.verificationDiscovered), next.verificationSelection ? JSON.stringify(next.verificationSelection) : null, next.summary ?? null, next.error ?? null, next.updatedAt, id);
|
|
280
|
+
if (next.status !== task.status)
|
|
281
|
+
this.recordTaskEvent({
|
|
282
|
+
taskId: id,
|
|
283
|
+
kind: "status",
|
|
284
|
+
outcome: next.status,
|
|
285
|
+
createdAt: next.updatedAt,
|
|
286
|
+
});
|
|
287
|
+
return next;
|
|
288
|
+
}
|
|
289
|
+
/** Persists a summary that replaces older conversation detail in future context. */
|
|
290
|
+
saveCheckpoint(sessionId, taskId, summary, throughMessageId) {
|
|
291
|
+
const checkpoint = {
|
|
292
|
+
id: `checkpoint-${crypto.randomUUID()}`,
|
|
293
|
+
sessionId,
|
|
294
|
+
taskId,
|
|
295
|
+
summary,
|
|
296
|
+
throughMessageId,
|
|
297
|
+
createdAt: Date.now(),
|
|
298
|
+
};
|
|
299
|
+
this.db
|
|
300
|
+
.prepare("INSERT INTO context_checkpoints VALUES (?, ?, ?, ?, ?, ?)")
|
|
301
|
+
.run(checkpoint.id, checkpoint.sessionId, checkpoint.taskId ?? null, checkpoint.summary, checkpoint.throughMessageId, checkpoint.createdAt);
|
|
302
|
+
return checkpoint;
|
|
303
|
+
}
|
|
304
|
+
/** Retrieves the most recent compaction checkpoint for a session. */
|
|
305
|
+
latestCheckpoint(sessionId) {
|
|
306
|
+
const row = this.db
|
|
307
|
+
.prepare("SELECT * FROM context_checkpoints WHERE session_id=? ORDER BY created_at DESC LIMIT 1")
|
|
308
|
+
.get(sessionId);
|
|
309
|
+
return (row && {
|
|
310
|
+
id: String(row.id),
|
|
311
|
+
sessionId: String(row.session_id),
|
|
312
|
+
taskId: row.task_id ? String(row.task_id) : undefined,
|
|
313
|
+
summary: String(row.summary),
|
|
314
|
+
throughMessageId: Number(row.through_message_id),
|
|
315
|
+
createdAt: Number(row.created_at),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/** Counts messages to decide when automatic compaction is needed. */
|
|
319
|
+
messageCount(sessionId) {
|
|
320
|
+
return Number(this.db
|
|
321
|
+
.prepare("SELECT count(*) AS count FROM messages WHERE session_id=?")
|
|
322
|
+
.get(sessionId).count);
|
|
323
|
+
}
|
|
324
|
+
/** Returns the newest message id used to mark checkpoint coverage. */
|
|
325
|
+
lastMessageId(sessionId) {
|
|
326
|
+
return Number(this.db
|
|
327
|
+
.prepare("SELECT coalesce(max(id), 0) AS id FROM messages WHERE session_id=?")
|
|
328
|
+
.get(sessionId).id);
|
|
329
|
+
}
|
|
330
|
+
/** Upserts the session's bounded, derived repository snapshot. */
|
|
331
|
+
saveRepositorySnapshot(sessionId, snapshot) {
|
|
332
|
+
this.db
|
|
333
|
+
.prepare("INSERT INTO repository_profiles(session_id, profile_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET profile_json=excluded.profile_json, updated_at=excluded.updated_at")
|
|
334
|
+
.run(sessionId, JSON.stringify(snapshot), Date.now());
|
|
335
|
+
}
|
|
336
|
+
/** Reads current snapshots and normalizes legacy profiles as stale snapshots. */
|
|
337
|
+
repositorySnapshot(sessionId) {
|
|
338
|
+
const row = this.db
|
|
339
|
+
.prepare("SELECT profile_json FROM repository_profiles WHERE session_id=?")
|
|
340
|
+
.get(sessionId);
|
|
341
|
+
if (!row)
|
|
342
|
+
return undefined;
|
|
343
|
+
const value = JSON.parse(row.profile_json);
|
|
344
|
+
if (value.schemaVersion === 1 && value.fingerprint && Array.isArray(value.entries))
|
|
345
|
+
return value;
|
|
346
|
+
return {
|
|
347
|
+
...value,
|
|
348
|
+
schemaVersion: 1,
|
|
349
|
+
fingerprint: { value: "legacy-stale", kind: "filesystem" },
|
|
350
|
+
entries: value.entries ?? [],
|
|
351
|
+
ecosystems: value.ecosystems ?? [],
|
|
352
|
+
changedPaths: value.changedPaths ?? [],
|
|
353
|
+
instructionFiles: value.instructionFiles ?? [],
|
|
354
|
+
documentationFiles: value.documentationFiles ?? [],
|
|
355
|
+
manifestFiles: value.manifestFiles ?? [],
|
|
356
|
+
ciFiles: value.ciFiles ?? [],
|
|
357
|
+
buildFiles: value.buildFiles ?? [],
|
|
358
|
+
truncated: value.truncated ?? false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Persists one verification failure that started an agent repair cycle. */
|
|
362
|
+
recordRepairAttempt(attempt) {
|
|
363
|
+
this.recordTaskEvent({ taskId: attempt.taskId, kind: "repair", createdAt: attempt.createdAt });
|
|
364
|
+
this.db
|
|
365
|
+
.prepare("INSERT INTO repair_attempts(id, task_id, command, evidence_json, selected_files_json, created_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
366
|
+
.run(attempt.id, attempt.taskId, attempt.command, JSON.stringify(attempt.evidence), JSON.stringify(attempt.selectedFiles), attempt.createdAt);
|
|
367
|
+
}
|
|
368
|
+
/** Returns repair attempts in the order they happened for context and evaluation. */
|
|
369
|
+
repairAttempts(taskId) {
|
|
370
|
+
return this.db
|
|
371
|
+
.prepare("SELECT * FROM repair_attempts WHERE task_id=? ORDER BY created_at")
|
|
372
|
+
.all(taskId).map((row) => ({
|
|
373
|
+
id: String(row.id),
|
|
374
|
+
taskId: String(row.task_id),
|
|
375
|
+
command: String(row.command),
|
|
376
|
+
evidence: JSON.parse(String(row.evidence_json)),
|
|
377
|
+
selectedFiles: JSON.parse(String(row.selected_files_json)),
|
|
378
|
+
createdAt: Number(row.created_at),
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
/** Marks tasks left active by a process exit so the user can explicitly resume them. */
|
|
382
|
+
recoverInterruptedTasks() {
|
|
383
|
+
const active = this.db
|
|
384
|
+
.prepare("SELECT id FROM tasks WHERE status IN ('planning', 'acting', 'verifying')")
|
|
385
|
+
.all();
|
|
386
|
+
for (const task of active)
|
|
387
|
+
this.recordTaskEvent({
|
|
388
|
+
taskId: task.id,
|
|
389
|
+
kind: "status",
|
|
390
|
+
outcome: "interrupted",
|
|
391
|
+
createdAt: Date.now(),
|
|
392
|
+
});
|
|
393
|
+
this.db
|
|
394
|
+
.prepare("UPDATE tasks SET status='interrupted', updated_at=? WHERE status IN ('planning', 'acting', 'verifying')")
|
|
395
|
+
.run(Date.now());
|
|
396
|
+
}
|
|
397
|
+
/** Converts a raw SQLite row into the application's Task object. */
|
|
398
|
+
toTask(row) {
|
|
399
|
+
if (!row)
|
|
400
|
+
return undefined;
|
|
401
|
+
return {
|
|
402
|
+
id: String(row.id),
|
|
403
|
+
sessionId: String(row.session_id),
|
|
404
|
+
prompt: String(row.prompt),
|
|
405
|
+
mode: (row.mode ? String(row.mode) : "implementation"),
|
|
406
|
+
status: row.status,
|
|
407
|
+
plan: row.plan_json ? JSON.parse(String(row.plan_json)) : undefined,
|
|
408
|
+
changedFiles: JSON.parse(String(row.changed_files_json)),
|
|
409
|
+
approvedWritePaths: row.approved_write_paths_json
|
|
410
|
+
? JSON.parse(String(row.approved_write_paths_json))
|
|
411
|
+
: [],
|
|
412
|
+
verificationCommand: row.verification_command ? String(row.verification_command) : undefined,
|
|
413
|
+
verificationOutput: row.verification_output ? String(row.verification_output) : undefined,
|
|
414
|
+
verificationPassed: row.verification_ok === null || row.verification_ok === undefined
|
|
415
|
+
? undefined
|
|
416
|
+
: Boolean(row.verification_ok),
|
|
417
|
+
verificationExitCode: row.verification_exit_code === null || row.verification_exit_code === undefined
|
|
418
|
+
? undefined
|
|
419
|
+
: Number(row.verification_exit_code),
|
|
420
|
+
verificationDiscovered: row.verification_discovered === null || row.verification_discovered === undefined
|
|
421
|
+
? undefined
|
|
422
|
+
: Boolean(row.verification_discovered),
|
|
423
|
+
verificationSelection: row.verification_selection_json
|
|
424
|
+
? JSON.parse(String(row.verification_selection_json))
|
|
425
|
+
: undefined,
|
|
426
|
+
summary: row.summary ? String(row.summary) : undefined,
|
|
427
|
+
error: row.error ? String(row.error) : undefined,
|
|
428
|
+
createdAt: Number(row.created_at),
|
|
429
|
+
updatedAt: Number(row.updated_at),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/** Converts one evaluation run row while keeping persistence column names private. */
|
|
433
|
+
toEvaluationRun(row) {
|
|
434
|
+
return {
|
|
435
|
+
id: String(row.id),
|
|
436
|
+
suite: row.suite,
|
|
437
|
+
provider: (row.provider ? String(row.provider) : "gemini"),
|
|
438
|
+
model: String(row.model),
|
|
439
|
+
sourceRevision: String(row.source_revision),
|
|
440
|
+
trialCount: Number(row.trial_count),
|
|
441
|
+
attemptCount: Number(row.attempt_count),
|
|
442
|
+
passedCount: Number(row.passed_count),
|
|
443
|
+
startedAt: Number(row.started_at),
|
|
444
|
+
completedAt: row.completed_at === null ? undefined : Number(row.completed_at),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export declare class GeminiProvider implements ModelProvider {
|
|
5
|
+
private readonly model;
|
|
6
|
+
private readonly tools;
|
|
7
|
+
private readonly client;
|
|
8
|
+
/** Configures the Gemini client with the selected model and Kairo tool schema. */
|
|
9
|
+
constructor(apiKey: string, model: string, tools: ToolDefinition[]);
|
|
10
|
+
/** Streams Gemini text and normalizes function calls into the provider-neutral model turn. */
|
|
11
|
+
stream(messages: Message[], onText: (chunk: string) => void, onProgress?: (event: ProviderProgress) => void, systemInstruction?: string, toolsEnabled?: boolean): Promise<ModelTurn>;
|
|
12
|
+
/** Performs one stream; received calls prevent automatic replay even before execution. */
|
|
13
|
+
private streamOnce;
|
|
14
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import { recoverProvider } from "./provider-recovery.js";
|
|
3
|
+
import { modelSystemInstruction } from "../../application/model-system-instruction.js";
|
|
4
|
+
export class GeminiProvider {
|
|
5
|
+
model;
|
|
6
|
+
tools;
|
|
7
|
+
client;
|
|
8
|
+
/** Configures the Gemini client with the selected model and Kairo tool schema. */
|
|
9
|
+
constructor(apiKey, model, tools) {
|
|
10
|
+
this.model = model;
|
|
11
|
+
this.tools = tools;
|
|
12
|
+
// This SDK version uses a single fetch when retryOptions is absent.
|
|
13
|
+
// Enabling its retry wrapper discards HTTP status and structured retry hints.
|
|
14
|
+
this.client = new GoogleGenAI({ apiKey });
|
|
15
|
+
}
|
|
16
|
+
/** Streams Gemini text and normalizes function calls into the provider-neutral model turn. */
|
|
17
|
+
async stream(messages, onText, onProgress, systemInstruction = modelSystemInstruction, toolsEnabled = true) {
|
|
18
|
+
return recoverProvider((markContent) => this.streamOnce(messages, (text) => {
|
|
19
|
+
markContent();
|
|
20
|
+
onText(text);
|
|
21
|
+
}, markContent, systemInstruction, toolsEnabled), onProgress, undefined, "Gemini");
|
|
22
|
+
}
|
|
23
|
+
/** Performs one stream; received calls prevent automatic replay even before execution. */
|
|
24
|
+
async streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled) {
|
|
25
|
+
const contents = messages.map((message) => {
|
|
26
|
+
if (message.role === "tool")
|
|
27
|
+
return {
|
|
28
|
+
role: "user",
|
|
29
|
+
parts: [
|
|
30
|
+
{ functionResponse: { name: message.toolName, response: { result: message.content } } },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
if (message.role === "model" && message.toolCallId && message.toolName)
|
|
34
|
+
return {
|
|
35
|
+
role: "model",
|
|
36
|
+
parts: [
|
|
37
|
+
{
|
|
38
|
+
functionCall: {
|
|
39
|
+
id: message.toolCallId,
|
|
40
|
+
name: message.toolName,
|
|
41
|
+
args: JSON.parse(message.content),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
role: message.role === "model" ? "model" : "user",
|
|
48
|
+
parts: [{ text: message.content }],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const stream = await this.client.models.generateContentStream({
|
|
52
|
+
model: this.model,
|
|
53
|
+
contents: contents,
|
|
54
|
+
config: {
|
|
55
|
+
systemInstruction,
|
|
56
|
+
...(toolsEnabled
|
|
57
|
+
? {
|
|
58
|
+
tools: [
|
|
59
|
+
{
|
|
60
|
+
functionDeclarations: this.tools.map(({ name, description, parameters }) => ({
|
|
61
|
+
name,
|
|
62
|
+
description,
|
|
63
|
+
parameters,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
let text = "";
|
|
72
|
+
const calls = [];
|
|
73
|
+
for await (const chunk of stream) {
|
|
74
|
+
const chunkText = chunk.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("") ?? "";
|
|
75
|
+
if (chunkText) {
|
|
76
|
+
text += chunkText;
|
|
77
|
+
onText(chunkText);
|
|
78
|
+
}
|
|
79
|
+
for (const call of chunk.functionCalls ?? []) {
|
|
80
|
+
markContent();
|
|
81
|
+
calls.push({
|
|
82
|
+
id: call.id || crypto.randomUUID(),
|
|
83
|
+
name: String(call.name),
|
|
84
|
+
args: (call.args || {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { text, toolCalls: calls };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export declare class GroqProvider implements ModelProvider {
|
|
5
|
+
private readonly model;
|
|
6
|
+
private readonly tools;
|
|
7
|
+
private readonly client;
|
|
8
|
+
/** Configures Groq without hidden SDK retries so Kairo owns recovery reporting. */
|
|
9
|
+
constructor(apiKey: string, model: string, tools: ToolDefinition[]);
|
|
10
|
+
/** Streams Groq text and reconstructs incremental OpenAI-style tool calls. */
|
|
11
|
+
stream(messages: Message[], onText: (chunk: string) => void, onProgress?: (event: ProviderProgress) => void, systemInstruction?: string, toolsEnabled?: boolean): Promise<ModelTurn>;
|
|
12
|
+
/** Performs one request and rejects malformed tool arguments before execution. */
|
|
13
|
+
private streamOnce;
|
|
14
|
+
/** Converts Kairo's durable message representation to Groq chat messages. */
|
|
15
|
+
private message;
|
|
16
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Groq from "groq-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
|
+
export class GroqProvider {
|
|
6
|
+
model;
|
|
7
|
+
tools;
|
|
8
|
+
client;
|
|
9
|
+
/** Configures Groq without hidden SDK retries so Kairo owns recovery reporting. */
|
|
10
|
+
constructor(apiKey, model, tools) {
|
|
11
|
+
this.model = model;
|
|
12
|
+
this.tools = tools;
|
|
13
|
+
this.client = new Groq({ apiKey, maxRetries: 0 });
|
|
14
|
+
}
|
|
15
|
+
/** Streams Groq text and reconstructs incremental OpenAI-style tool calls. */
|
|
16
|
+
async stream(messages, onText, onProgress, systemInstruction = modelSystemInstruction, toolsEnabled = true) {
|
|
17
|
+
return recoverProvider((markContent) => this.streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled), onProgress, undefined, "Groq");
|
|
18
|
+
}
|
|
19
|
+
/** Performs one request and rejects malformed tool arguments before execution. */
|
|
20
|
+
async streamOnce(messages, onText, markContent, systemInstruction, toolsEnabled) {
|
|
21
|
+
const stream = await this.client.chat.completions.create({
|
|
22
|
+
model: this.model,
|
|
23
|
+
stream: true,
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: "system", content: systemInstruction },
|
|
26
|
+
...messages.map((message) => this.message(message)),
|
|
27
|
+
],
|
|
28
|
+
...(toolsEnabled
|
|
29
|
+
? {
|
|
30
|
+
tools: this.tools.map(({ name, description, parameters }) => ({
|
|
31
|
+
type: "function",
|
|
32
|
+
function: { name, description, parameters },
|
|
33
|
+
})),
|
|
34
|
+
tool_choice: "auto",
|
|
35
|
+
parallel_tool_calls: false,
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
});
|
|
39
|
+
let text = "";
|
|
40
|
+
const pending = new Map();
|
|
41
|
+
for await (const chunk of stream) {
|
|
42
|
+
const delta = chunk.choices[0]?.delta;
|
|
43
|
+
if (delta?.content) {
|
|
44
|
+
markContent();
|
|
45
|
+
text += delta.content;
|
|
46
|
+
onText(delta.content);
|
|
47
|
+
}
|
|
48
|
+
for (const call of delta?.tool_calls ?? []) {
|
|
49
|
+
markContent();
|
|
50
|
+
const current = pending.get(call.index) ?? { name: "", arguments: "" };
|
|
51
|
+
if (call.id)
|
|
52
|
+
current.id = call.id;
|
|
53
|
+
if (call.function?.name)
|
|
54
|
+
current.name += call.function.name;
|
|
55
|
+
if (call.function?.arguments)
|
|
56
|
+
current.arguments += call.function.arguments;
|
|
57
|
+
pending.set(call.index, current);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const toolCalls = [...pending.entries()]
|
|
61
|
+
.sort(([left], [right]) => left - right)
|
|
62
|
+
.map(([, call]) => {
|
|
63
|
+
try {
|
|
64
|
+
return {
|
|
65
|
+
id: call.id || crypto.randomUUID(),
|
|
66
|
+
name: call.name,
|
|
67
|
+
args: JSON.parse(call.arguments || "{}"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new ProviderError("request", false, undefined, "Groq");
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return { text, toolCalls };
|
|
75
|
+
}
|
|
76
|
+
/** Converts Kairo's durable message representation to Groq chat messages. */
|
|
77
|
+
message(message) {
|
|
78
|
+
if (message.role === "tool")
|
|
79
|
+
return {
|
|
80
|
+
role: "tool",
|
|
81
|
+
tool_call_id: message.toolCallId,
|
|
82
|
+
content: message.content,
|
|
83
|
+
};
|
|
84
|
+
if (message.role === "model" && message.toolCallId && message.toolName)
|
|
85
|
+
return {
|
|
86
|
+
role: "assistant",
|
|
87
|
+
content: null,
|
|
88
|
+
tool_calls: [
|
|
89
|
+
{
|
|
90
|
+
id: message.toolCallId,
|
|
91
|
+
type: "function",
|
|
92
|
+
function: { name: message.toolName, arguments: message.content },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
role: message.role === "model" ? "assistant" : "user",
|
|
98
|
+
content: message.content,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|