@botbotgo/agent-harness 0.0.100 → 0.0.102
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/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/persistence/sqlite-run-context-store.d.ts +22 -0
- package/dist/persistence/sqlite-run-context-store.js +64 -0
- package/dist/persistence/sqlite-run-queue-store.d.ts +41 -0
- package/dist/persistence/sqlite-run-queue-store.js +120 -0
- package/dist/persistence/sqlite-store.d.ts +2 -2
- package/dist/persistence/sqlite-store.js +31 -117
- package/dist/resource/mcp-tool-support.d.ts +21 -0
- package/dist/resource/mcp-tool-support.js +173 -0
- package/dist/resource/resource-impl.d.ts +1 -18
- package/dist/resource/resource-impl.js +79 -240
- package/dist/runtime/adapter/invoke-runtime.d.ts +22 -0
- package/dist/runtime/adapter/invoke-runtime.js +18 -0
- package/dist/runtime/adapter/stream-runtime.d.ts +46 -0
- package/dist/runtime/adapter/stream-runtime.js +93 -0
- package/dist/runtime/agent-runtime-adapter.d.ts +1 -12
- package/dist/runtime/agent-runtime-adapter.js +122 -312
- package/dist/runtime/harness/run/recovery.d.ts +42 -0
- package/dist/runtime/harness/run/recovery.js +139 -0
- package/dist/runtime/harness/run/run-operations.d.ts +50 -0
- package/dist/runtime/harness/run/run-operations.js +113 -0
- package/dist/runtime/harness/run/run-slot-acquisition.d.ts +64 -0
- package/dist/runtime/harness/run/run-slot-acquisition.js +157 -0
- package/dist/runtime/harness/run/stream-run.d.ts +53 -0
- package/dist/runtime/harness/run/stream-run.js +304 -0
- package/dist/runtime/harness.d.ts +2 -17
- package/dist/runtime/harness.js +157 -773
- package/dist/runtime/support/runtime-factories.js +2 -2
- package/dist/workspace/object-loader.d.ts +1 -8
- package/dist/workspace/object-loader.js +43 -275
- package/dist/workspace/yaml-object-reader.d.ts +15 -0
- package/dist/workspace/yaml-object-reader.js +202 -0
- package/package.json +1 -1
- package/dist/runtime/checkpoint-maintenance.d.ts +0 -1
- package/dist/runtime/checkpoint-maintenance.js +0 -1
- package/dist/runtime/file-checkpoint-saver.d.ts +0 -1
- package/dist/runtime/file-checkpoint-saver.js +0 -1
- package/dist/runtime/sqlite-maintained-checkpoint-saver.d.ts +0 -1
- package/dist/runtime/sqlite-maintained-checkpoint-saver.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.101";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.101";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InValue } from "@libsql/client";
|
|
2
|
+
import type { TranscriptMessage } from "../contracts/types.js";
|
|
3
|
+
import type { PersistedRunRequest, RecoveryIntent } from "./types.js";
|
|
4
|
+
type SqlRow = Record<string, unknown>;
|
|
5
|
+
type SqliteRunContextStoreDb = {
|
|
6
|
+
execute(sql: string, args?: InValue[]): Promise<void>;
|
|
7
|
+
selectOne(sql: string, args?: InValue[]): Promise<SqlRow | null>;
|
|
8
|
+
selectAll(sql: string, args?: InValue[]): Promise<SqlRow[]>;
|
|
9
|
+
};
|
|
10
|
+
export declare class SqliteRunContextStore {
|
|
11
|
+
private readonly db;
|
|
12
|
+
constructor(db: SqliteRunContextStoreDb);
|
|
13
|
+
saveRunRequest(threadId: string, runId: string, request: PersistedRunRequest): Promise<void>;
|
|
14
|
+
getRunRequest(threadId: string, runId: string): Promise<PersistedRunRequest | null>;
|
|
15
|
+
clearRunRequest(threadId: string, runId: string): Promise<void>;
|
|
16
|
+
appendThreadMessage(threadId: string, message: TranscriptMessage): Promise<void>;
|
|
17
|
+
listThreadMessages(threadId: string, limit?: number): Promise<TranscriptMessage[]>;
|
|
18
|
+
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
19
|
+
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
20
|
+
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
3
|
+
}
|
|
4
|
+
function parseJson(value) {
|
|
5
|
+
return JSON.parse(asString(value));
|
|
6
|
+
}
|
|
7
|
+
function nowIso() {
|
|
8
|
+
return new Date(Date.now()).toISOString();
|
|
9
|
+
}
|
|
10
|
+
export class SqliteRunContextStore {
|
|
11
|
+
db;
|
|
12
|
+
constructor(db) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
}
|
|
15
|
+
async saveRunRequest(threadId, runId, request) {
|
|
16
|
+
await this.db.execute(`INSERT OR REPLACE INTO run_requests
|
|
17
|
+
(run_id, thread_id, request_json, saved_at)
|
|
18
|
+
VALUES (?, ?, ?, ?)`, [runId, threadId, JSON.stringify(request), request.savedAt]);
|
|
19
|
+
}
|
|
20
|
+
async getRunRequest(threadId, runId) {
|
|
21
|
+
const row = await this.db.selectOne("SELECT request_json FROM run_requests WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
22
|
+
return row ? parseJson(row.request_json) : null;
|
|
23
|
+
}
|
|
24
|
+
async clearRunRequest(threadId, runId) {
|
|
25
|
+
await this.db.execute("DELETE FROM run_requests WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
26
|
+
}
|
|
27
|
+
async appendThreadMessage(threadId, message) {
|
|
28
|
+
await this.db.execute(`INSERT INTO thread_messages
|
|
29
|
+
(thread_id, role, content_json, run_id, created_at)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?)`, [threadId, message.role, JSON.stringify(message.content), message.runId, message.createdAt]);
|
|
31
|
+
}
|
|
32
|
+
async listThreadMessages(threadId, limit = 12) {
|
|
33
|
+
const rows = await this.db.selectAll(`SELECT role, content_json, run_id, created_at
|
|
34
|
+
FROM (
|
|
35
|
+
SELECT role, content_json, run_id, created_at, id
|
|
36
|
+
FROM thread_messages
|
|
37
|
+
WHERE thread_id = ?
|
|
38
|
+
ORDER BY created_at DESC, id DESC
|
|
39
|
+
LIMIT ?
|
|
40
|
+
) recent
|
|
41
|
+
ORDER BY created_at ASC, id ASC`, [threadId, limit]);
|
|
42
|
+
return rows.map((row) => ({
|
|
43
|
+
role: asString(row.role),
|
|
44
|
+
content: parseJson(row.content_json),
|
|
45
|
+
runId: asString(row.run_id),
|
|
46
|
+
createdAt: asString(row.created_at),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
async saveRecoveryIntent(threadId, runId, intent) {
|
|
50
|
+
const savedAt = typeof intent.savedAt === "string"
|
|
51
|
+
? (intent.savedAt)
|
|
52
|
+
: nowIso();
|
|
53
|
+
await this.db.execute(`INSERT OR REPLACE INTO recovery_intents
|
|
54
|
+
(run_id, thread_id, intent_json, saved_at)
|
|
55
|
+
VALUES (?, ?, ?, ?)`, [runId, threadId, JSON.stringify(intent), savedAt]);
|
|
56
|
+
}
|
|
57
|
+
async getRecoveryIntent(threadId, runId) {
|
|
58
|
+
const row = await this.db.selectOne("SELECT intent_json FROM recovery_intents WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
59
|
+
return row ? parseJson(row.intent_json) : null;
|
|
60
|
+
}
|
|
61
|
+
async clearRecoveryIntent(threadId, runId) {
|
|
62
|
+
await this.db.execute("DELETE FROM recovery_intents WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InValue } from "@libsql/client";
|
|
2
|
+
import type { PersistedRunControlRecord, PersistedRunQueueRecord } from "./types.js";
|
|
3
|
+
type SqlRow = Record<string, unknown>;
|
|
4
|
+
type SqliteRunQueueStoreDb = {
|
|
5
|
+
execute(sql: string, args?: InValue[]): Promise<void>;
|
|
6
|
+
selectOne(sql: string, args?: InValue[]): Promise<SqlRow | null>;
|
|
7
|
+
selectAll(sql: string, args?: InValue[]): Promise<SqlRow[]>;
|
|
8
|
+
};
|
|
9
|
+
export declare class SqliteRunQueueStore {
|
|
10
|
+
private readonly db;
|
|
11
|
+
constructor(db: SqliteRunQueueStoreDb);
|
|
12
|
+
private mapQueuedRun;
|
|
13
|
+
private mapRunControl;
|
|
14
|
+
enqueueRun(input: {
|
|
15
|
+
threadId: string;
|
|
16
|
+
runId: string;
|
|
17
|
+
priority?: number;
|
|
18
|
+
queueKey?: string | null;
|
|
19
|
+
availableAt?: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
|
|
22
|
+
claimQueuedRun(input: {
|
|
23
|
+
threadId: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
workerId: string;
|
|
26
|
+
claimedAt?: string;
|
|
27
|
+
leaseExpiresAt: string;
|
|
28
|
+
}): Promise<PersistedRunQueueRecord>;
|
|
29
|
+
renewRunLease(input: {
|
|
30
|
+
runId: string;
|
|
31
|
+
workerId: string;
|
|
32
|
+
heartbeatAt?: string;
|
|
33
|
+
leaseExpiresAt: string;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
releaseRunClaim(runId: string): Promise<void>;
|
|
36
|
+
listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
|
|
37
|
+
getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
|
|
38
|
+
requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
|
|
39
|
+
clearRunCancel(runId: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
3
|
+
}
|
|
4
|
+
function asNullableString(value) {
|
|
5
|
+
return value == null ? null : asString(value);
|
|
6
|
+
}
|
|
7
|
+
function asBoolean(value) {
|
|
8
|
+
return value === true || value === 1 || value === "1";
|
|
9
|
+
}
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date(Date.now()).toISOString();
|
|
12
|
+
}
|
|
13
|
+
export class SqliteRunQueueStore {
|
|
14
|
+
db;
|
|
15
|
+
constructor(db) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
}
|
|
18
|
+
mapQueuedRun(row) {
|
|
19
|
+
return {
|
|
20
|
+
runId: asString(row.run_id),
|
|
21
|
+
threadId: asString(row.thread_id),
|
|
22
|
+
priority: Number(row.priority ?? 0),
|
|
23
|
+
queueKey: asNullableString(row.queue_key),
|
|
24
|
+
enqueuedAt: asString(row.enqueued_at),
|
|
25
|
+
availableAt: asString(row.available_at),
|
|
26
|
+
claimedBy: asNullableString(row.claimed_by),
|
|
27
|
+
claimedAt: asNullableString(row.claimed_at),
|
|
28
|
+
leaseExpiresAt: asNullableString(row.lease_expires_at),
|
|
29
|
+
attemptCount: Number(row.attempt_count ?? 0),
|
|
30
|
+
lastError: asNullableString(row.last_error),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
mapRunControl(row) {
|
|
34
|
+
return {
|
|
35
|
+
runId: asString(row.run_id),
|
|
36
|
+
cancelRequested: asBoolean(row.cancel_requested),
|
|
37
|
+
cancelReason: asNullableString(row.cancel_reason),
|
|
38
|
+
cancelRequestedAt: asNullableString(row.cancel_requested_at),
|
|
39
|
+
heartbeatAt: asNullableString(row.heartbeat_at),
|
|
40
|
+
workerId: asNullableString(row.worker_id),
|
|
41
|
+
workerStartedAt: asNullableString(row.worker_started_at),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async enqueueRun(input) {
|
|
45
|
+
const enqueuedAt = nowIso();
|
|
46
|
+
await this.db.execute(`INSERT OR REPLACE INTO run_queue
|
|
47
|
+
(run_id, thread_id, priority, queue_key, enqueued_at, available_at, claimed_by, claimed_at, lease_expires_at, attempt_count, last_error)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, COALESCE((SELECT attempt_count FROM run_queue WHERE run_id = ?), 0), NULL)`, [
|
|
49
|
+
input.runId,
|
|
50
|
+
input.threadId,
|
|
51
|
+
input.priority ?? 0,
|
|
52
|
+
input.queueKey ?? null,
|
|
53
|
+
enqueuedAt,
|
|
54
|
+
input.availableAt ?? enqueuedAt,
|
|
55
|
+
input.runId,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
async getQueuedRun(runId) {
|
|
59
|
+
const row = await this.db.selectOne("SELECT * FROM run_queue WHERE run_id = ?", [runId]);
|
|
60
|
+
return row ? this.mapQueuedRun(row) : null;
|
|
61
|
+
}
|
|
62
|
+
async claimQueuedRun(input) {
|
|
63
|
+
const claimedAt = input.claimedAt ?? nowIso();
|
|
64
|
+
await this.db.execute(`UPDATE run_queue
|
|
65
|
+
SET claimed_by = ?, claimed_at = ?, lease_expires_at = ?, attempt_count = attempt_count + 1
|
|
66
|
+
WHERE run_id = ? AND thread_id = ?`, [input.workerId, claimedAt, input.leaseExpiresAt, input.runId, input.threadId]);
|
|
67
|
+
await this.db.execute(`UPDATE run_control
|
|
68
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
69
|
+
WHERE run_id = ?`, [claimedAt, input.workerId, claimedAt, input.runId]);
|
|
70
|
+
const claimed = await this.getQueuedRun(input.runId);
|
|
71
|
+
if (!claimed) {
|
|
72
|
+
throw new Error(`Missing queued run ${input.runId}`);
|
|
73
|
+
}
|
|
74
|
+
return claimed;
|
|
75
|
+
}
|
|
76
|
+
async renewRunLease(input) {
|
|
77
|
+
const heartbeatAt = input.heartbeatAt ?? nowIso();
|
|
78
|
+
await this.db.execute(`UPDATE run_queue
|
|
79
|
+
SET lease_expires_at = ?, claimed_by = COALESCE(claimed_by, ?), claimed_at = COALESCE(claimed_at, ?)
|
|
80
|
+
WHERE run_id = ?`, [input.leaseExpiresAt, input.workerId, heartbeatAt, input.runId]);
|
|
81
|
+
await this.db.execute(`UPDATE run_control
|
|
82
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
83
|
+
WHERE run_id = ?`, [heartbeatAt, input.workerId, heartbeatAt, input.runId]);
|
|
84
|
+
}
|
|
85
|
+
async releaseRunClaim(runId) {
|
|
86
|
+
await this.db.execute("DELETE FROM run_queue WHERE run_id = ?", [runId]);
|
|
87
|
+
await this.db.execute(`UPDATE run_control
|
|
88
|
+
SET heartbeat_at = NULL, worker_id = NULL, worker_started_at = NULL
|
|
89
|
+
WHERE run_id = ?`, [runId]);
|
|
90
|
+
}
|
|
91
|
+
async listExpiredClaimedRuns(cutoffIso) {
|
|
92
|
+
const rows = await this.db.selectAll(`SELECT *
|
|
93
|
+
FROM run_queue
|
|
94
|
+
WHERE claimed_by IS NOT NULL
|
|
95
|
+
AND lease_expires_at IS NOT NULL
|
|
96
|
+
AND lease_expires_at <= ?
|
|
97
|
+
ORDER BY lease_expires_at ASC, run_id ASC`, [cutoffIso]);
|
|
98
|
+
return rows.map((row) => this.mapQueuedRun(row));
|
|
99
|
+
}
|
|
100
|
+
async getRunControl(runId) {
|
|
101
|
+
const row = await this.db.selectOne("SELECT * FROM run_control WHERE run_id = ?", [runId]);
|
|
102
|
+
return row ? this.mapRunControl(row) : null;
|
|
103
|
+
}
|
|
104
|
+
async requestRunCancel(runId, reason) {
|
|
105
|
+
const requestedAt = nowIso();
|
|
106
|
+
await this.db.execute(`UPDATE run_control
|
|
107
|
+
SET cancel_requested = 1, cancel_reason = ?, cancel_requested_at = ?
|
|
108
|
+
WHERE run_id = ?`, [reason ?? null, requestedAt, runId]);
|
|
109
|
+
const updated = await this.getRunControl(runId);
|
|
110
|
+
if (!updated) {
|
|
111
|
+
throw new Error(`Missing run control for ${runId}`);
|
|
112
|
+
}
|
|
113
|
+
return updated;
|
|
114
|
+
}
|
|
115
|
+
async clearRunCancel(runId) {
|
|
116
|
+
await this.db.execute(`UPDATE run_control
|
|
117
|
+
SET cancel_requested = 0, cancel_reason = NULL, cancel_requested_at = NULL
|
|
118
|
+
WHERE run_id = ?`, [runId]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -4,6 +4,8 @@ export declare function listProtectedCheckpointThreadIds(dbPath: string): Promis
|
|
|
4
4
|
export declare class SqlitePersistence implements RuntimePersistence {
|
|
5
5
|
private readonly runRoot;
|
|
6
6
|
private readonly dbPath;
|
|
7
|
+
private readonly runContextStore;
|
|
8
|
+
private readonly runQueueStore;
|
|
7
9
|
private client;
|
|
8
10
|
private initialized;
|
|
9
11
|
private initialization;
|
|
@@ -77,8 +79,6 @@ export declare class SqlitePersistence implements RuntimePersistence {
|
|
|
77
79
|
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
78
80
|
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
79
81
|
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
80
|
-
private mapQueuedRun;
|
|
81
|
-
private mapRunControl;
|
|
82
82
|
enqueueRun(input: {
|
|
83
83
|
threadId: string;
|
|
84
84
|
runId: string;
|
|
@@ -2,6 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { mkdir, rm } from "node:fs/promises";
|
|
3
3
|
import { createClient } from "@libsql/client";
|
|
4
4
|
import { fileExists, readJson, writeJson } from "../utils/fs.js";
|
|
5
|
+
import { SqliteRunContextStore } from "./sqlite-run-context-store.js";
|
|
6
|
+
import { SqliteRunQueueStore } from "./sqlite-run-queue-store.js";
|
|
5
7
|
const RUNTIME_SQLITE_SCHEMA_VERSION = 2;
|
|
6
8
|
const RUNTIME_SQLITE_SCHEMA_FAMILY = "agent-harness-runtime";
|
|
7
9
|
function asRow(value) {
|
|
@@ -53,12 +55,24 @@ export async function listProtectedCheckpointThreadIds(dbPath) {
|
|
|
53
55
|
export class SqlitePersistence {
|
|
54
56
|
runRoot;
|
|
55
57
|
dbPath;
|
|
58
|
+
runContextStore;
|
|
59
|
+
runQueueStore;
|
|
56
60
|
client = null;
|
|
57
61
|
initialized = false;
|
|
58
62
|
initialization = null;
|
|
59
63
|
constructor(runRoot, dbFile = "runtime.sqlite") {
|
|
60
64
|
this.runRoot = runRoot;
|
|
61
65
|
this.dbPath = path.join(runRoot, dbFile);
|
|
66
|
+
this.runContextStore = new SqliteRunContextStore({
|
|
67
|
+
execute: (sql, args) => this.execute(sql, args),
|
|
68
|
+
selectOne: (sql, args) => this.selectOne(sql, args),
|
|
69
|
+
selectAll: (sql, args) => this.selectAll(sql, args),
|
|
70
|
+
});
|
|
71
|
+
this.runQueueStore = new SqliteRunQueueStore({
|
|
72
|
+
execute: (sql, args) => this.execute(sql, args),
|
|
73
|
+
selectOne: (sql, args) => this.selectOne(sql, args),
|
|
74
|
+
selectAll: (sql, args) => this.selectAll(sql, args),
|
|
75
|
+
});
|
|
62
76
|
}
|
|
63
77
|
async getClient() {
|
|
64
78
|
if (!this.client) {
|
|
@@ -597,16 +611,13 @@ export class SqlitePersistence {
|
|
|
597
611
|
return true;
|
|
598
612
|
}
|
|
599
613
|
async saveRunRequest(threadId, runId, request) {
|
|
600
|
-
await this.
|
|
601
|
-
(run_id, thread_id, request_json, saved_at)
|
|
602
|
-
VALUES (?, ?, ?, ?)`, [runId, threadId, JSON.stringify(request), request.savedAt]);
|
|
614
|
+
await this.runContextStore.saveRunRequest(threadId, runId, request);
|
|
603
615
|
}
|
|
604
616
|
async getRunRequest(threadId, runId) {
|
|
605
|
-
|
|
606
|
-
return row ? parseJson(row.request_json) : null;
|
|
617
|
+
return this.runContextStore.getRunRequest(threadId, runId);
|
|
607
618
|
}
|
|
608
619
|
async clearRunRequest(threadId, runId) {
|
|
609
|
-
await this.
|
|
620
|
+
await this.runContextStore.clearRunRequest(threadId, runId);
|
|
610
621
|
}
|
|
611
622
|
async createApproval(record) {
|
|
612
623
|
await this.execute(`INSERT OR REPLACE INTO approvals
|
|
@@ -666,143 +677,46 @@ export class SqlitePersistence {
|
|
|
666
677
|
};
|
|
667
678
|
}
|
|
668
679
|
async appendThreadMessage(threadId, message) {
|
|
669
|
-
await this.
|
|
670
|
-
(thread_id, role, content_json, run_id, created_at)
|
|
671
|
-
VALUES (?, ?, ?, ?, ?)`, [threadId, message.role, JSON.stringify(message.content), message.runId, message.createdAt]);
|
|
680
|
+
await this.runContextStore.appendThreadMessage(threadId, message);
|
|
672
681
|
}
|
|
673
682
|
async listThreadMessages(threadId, limit = 12) {
|
|
674
|
-
|
|
675
|
-
FROM (
|
|
676
|
-
SELECT role, content_json, run_id, created_at, id
|
|
677
|
-
FROM thread_messages
|
|
678
|
-
WHERE thread_id = ?
|
|
679
|
-
ORDER BY created_at DESC, id DESC
|
|
680
|
-
LIMIT ?
|
|
681
|
-
) recent
|
|
682
|
-
ORDER BY created_at ASC, id ASC`, [threadId, limit]);
|
|
683
|
-
return rows.map((row) => ({
|
|
684
|
-
role: asString(row.role),
|
|
685
|
-
content: parseJson(row.content_json),
|
|
686
|
-
runId: asString(row.run_id),
|
|
687
|
-
createdAt: asString(row.created_at),
|
|
688
|
-
}));
|
|
683
|
+
return this.runContextStore.listThreadMessages(threadId, limit);
|
|
689
684
|
}
|
|
690
685
|
async saveRecoveryIntent(threadId, runId, intent) {
|
|
691
|
-
|
|
692
|
-
? (intent.savedAt)
|
|
693
|
-
: nowIso();
|
|
694
|
-
await this.execute(`INSERT OR REPLACE INTO recovery_intents
|
|
695
|
-
(run_id, thread_id, intent_json, saved_at)
|
|
696
|
-
VALUES (?, ?, ?, ?)`, [runId, threadId, JSON.stringify(intent), savedAt]);
|
|
686
|
+
await this.runContextStore.saveRecoveryIntent(threadId, runId, intent);
|
|
697
687
|
}
|
|
698
688
|
async getRecoveryIntent(threadId, runId) {
|
|
699
|
-
|
|
700
|
-
return row ? parseJson(row.intent_json) : null;
|
|
689
|
+
return this.runContextStore.getRecoveryIntent(threadId, runId);
|
|
701
690
|
}
|
|
702
691
|
async clearRecoveryIntent(threadId, runId) {
|
|
703
|
-
await this.
|
|
704
|
-
}
|
|
705
|
-
mapQueuedRun(row) {
|
|
706
|
-
return {
|
|
707
|
-
runId: asString(row.run_id),
|
|
708
|
-
threadId: asString(row.thread_id),
|
|
709
|
-
priority: Number(row.priority ?? 0),
|
|
710
|
-
queueKey: asNullableString(row.queue_key),
|
|
711
|
-
enqueuedAt: asString(row.enqueued_at),
|
|
712
|
-
availableAt: asString(row.available_at),
|
|
713
|
-
claimedBy: asNullableString(row.claimed_by),
|
|
714
|
-
claimedAt: asNullableString(row.claimed_at),
|
|
715
|
-
leaseExpiresAt: asNullableString(row.lease_expires_at),
|
|
716
|
-
attemptCount: Number(row.attempt_count ?? 0),
|
|
717
|
-
lastError: asNullableString(row.last_error),
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
mapRunControl(row) {
|
|
721
|
-
return {
|
|
722
|
-
runId: asString(row.run_id),
|
|
723
|
-
cancelRequested: asBoolean(row.cancel_requested),
|
|
724
|
-
cancelReason: asNullableString(row.cancel_reason),
|
|
725
|
-
cancelRequestedAt: asNullableString(row.cancel_requested_at),
|
|
726
|
-
heartbeatAt: asNullableString(row.heartbeat_at),
|
|
727
|
-
workerId: asNullableString(row.worker_id),
|
|
728
|
-
workerStartedAt: asNullableString(row.worker_started_at),
|
|
729
|
-
};
|
|
692
|
+
await this.runContextStore.clearRecoveryIntent(threadId, runId);
|
|
730
693
|
}
|
|
731
694
|
async enqueueRun(input) {
|
|
732
|
-
|
|
733
|
-
await this.execute(`INSERT OR REPLACE INTO run_queue
|
|
734
|
-
(run_id, thread_id, priority, queue_key, enqueued_at, available_at, claimed_by, claimed_at, lease_expires_at, attempt_count, last_error)
|
|
735
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, COALESCE((SELECT attempt_count FROM run_queue WHERE run_id = ?), 0), NULL)`, [
|
|
736
|
-
input.runId,
|
|
737
|
-
input.threadId,
|
|
738
|
-
input.priority ?? 0,
|
|
739
|
-
input.queueKey ?? null,
|
|
740
|
-
enqueuedAt,
|
|
741
|
-
input.availableAt ?? enqueuedAt,
|
|
742
|
-
input.runId,
|
|
743
|
-
]);
|
|
695
|
+
await this.runQueueStore.enqueueRun(input);
|
|
744
696
|
}
|
|
745
697
|
async getQueuedRun(runId) {
|
|
746
|
-
|
|
747
|
-
return row ? this.mapQueuedRun(row) : null;
|
|
698
|
+
return this.runQueueStore.getQueuedRun(runId);
|
|
748
699
|
}
|
|
749
700
|
async claimQueuedRun(input) {
|
|
750
|
-
|
|
751
|
-
await this.execute(`UPDATE run_queue
|
|
752
|
-
SET claimed_by = ?, claimed_at = ?, lease_expires_at = ?, attempt_count = attempt_count + 1
|
|
753
|
-
WHERE run_id = ? AND thread_id = ?`, [input.workerId, claimedAt, input.leaseExpiresAt, input.runId, input.threadId]);
|
|
754
|
-
await this.execute(`UPDATE run_control
|
|
755
|
-
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
756
|
-
WHERE run_id = ?`, [claimedAt, input.workerId, claimedAt, input.runId]);
|
|
757
|
-
const claimed = await this.getQueuedRun(input.runId);
|
|
758
|
-
if (!claimed) {
|
|
759
|
-
throw new Error(`Missing queued run ${input.runId}`);
|
|
760
|
-
}
|
|
761
|
-
return claimed;
|
|
701
|
+
return this.runQueueStore.claimQueuedRun(input);
|
|
762
702
|
}
|
|
763
703
|
async renewRunLease(input) {
|
|
764
|
-
|
|
765
|
-
await this.execute(`UPDATE run_queue
|
|
766
|
-
SET lease_expires_at = ?, claimed_by = COALESCE(claimed_by, ?), claimed_at = COALESCE(claimed_at, ?)
|
|
767
|
-
WHERE run_id = ?`, [input.leaseExpiresAt, input.workerId, heartbeatAt, input.runId]);
|
|
768
|
-
await this.execute(`UPDATE run_control
|
|
769
|
-
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
770
|
-
WHERE run_id = ?`, [heartbeatAt, input.workerId, heartbeatAt, input.runId]);
|
|
704
|
+
await this.runQueueStore.renewRunLease(input);
|
|
771
705
|
}
|
|
772
706
|
async releaseRunClaim(runId) {
|
|
773
|
-
await this.
|
|
774
|
-
await this.execute(`UPDATE run_control
|
|
775
|
-
SET heartbeat_at = NULL, worker_id = NULL, worker_started_at = NULL
|
|
776
|
-
WHERE run_id = ?`, [runId]);
|
|
707
|
+
await this.runQueueStore.releaseRunClaim(runId);
|
|
777
708
|
}
|
|
778
709
|
async listExpiredClaimedRuns(cutoffIso) {
|
|
779
|
-
|
|
780
|
-
FROM run_queue
|
|
781
|
-
WHERE claimed_by IS NOT NULL
|
|
782
|
-
AND lease_expires_at IS NOT NULL
|
|
783
|
-
AND lease_expires_at <= ?
|
|
784
|
-
ORDER BY lease_expires_at ASC, run_id ASC`, [cutoffIso]);
|
|
785
|
-
return rows.map((row) => this.mapQueuedRun(row));
|
|
710
|
+
return this.runQueueStore.listExpiredClaimedRuns(cutoffIso);
|
|
786
711
|
}
|
|
787
712
|
async getRunControl(runId) {
|
|
788
|
-
|
|
789
|
-
return row ? this.mapRunControl(row) : null;
|
|
713
|
+
return this.runQueueStore.getRunControl(runId);
|
|
790
714
|
}
|
|
791
715
|
async requestRunCancel(runId, reason) {
|
|
792
|
-
|
|
793
|
-
await this.execute(`UPDATE run_control
|
|
794
|
-
SET cancel_requested = 1, cancel_reason = ?, cancel_requested_at = ?
|
|
795
|
-
WHERE run_id = ?`, [reason ?? null, now, runId]);
|
|
796
|
-
const updated = await this.getRunControl(runId);
|
|
797
|
-
if (!updated) {
|
|
798
|
-
throw new Error(`Missing run control for ${runId}`);
|
|
799
|
-
}
|
|
800
|
-
return updated;
|
|
716
|
+
return this.runQueueStore.requestRunCancel(runId, reason);
|
|
801
717
|
}
|
|
802
718
|
async clearRunCancel(runId) {
|
|
803
|
-
await this.
|
|
804
|
-
SET cancel_requested = 0, cancel_reason = NULL, cancel_requested_at = NULL
|
|
805
|
-
WHERE run_id = ?`, [runId]);
|
|
719
|
+
await this.runQueueStore.clearRunCancel(runId);
|
|
806
720
|
}
|
|
807
721
|
async readArtifact(threadId, runId, artifactPath) {
|
|
808
722
|
const filePath = path.join(this.runDir(threadId, runId), artifactPath);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { RuntimeAdapterOptions, WorkspaceBundle } from "../contracts/types.js";
|
|
3
|
+
export type McpServerConfig = {
|
|
4
|
+
transport?: "stdio" | "http" | "sse" | "websocket";
|
|
5
|
+
command?: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export type McpToolDescriptor = {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
inputSchema?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
export declare function readMcpServerConfig(workspace: WorkspaceBundle, tool: WorkspaceBundle["tools"] extends Map<any, infer T> ? T : never): McpServerConfig | null;
|
|
19
|
+
export declare function getOrCreateMcpClient(config: McpServerConfig): Promise<Client>;
|
|
20
|
+
export declare function listRemoteMcpTools(config: McpServerConfig): Promise<McpToolDescriptor[]>;
|
|
21
|
+
export declare function createMcpToolResolver(workspace: WorkspaceBundle): NonNullable<RuntimeAdapterOptions["toolResolver"]>;
|