@botbotgo/agent-harness 0.0.80 → 0.0.82
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/api.d.ts +2 -1
- package/dist/api.js +3 -0
- package/dist/contracts/types.d.ts +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/persistence/file-store.d.ts +32 -4
- package/dist/persistence/file-store.js +197 -6
- package/dist/persistence/sqlite-store.d.ts +32 -4
- package/dist/persistence/sqlite-store.js +174 -9
- package/dist/persistence/types.d.ts +64 -3
- package/dist/runtime/agent-runtime-adapter.d.ts +5 -0
- package/dist/runtime/agent-runtime-adapter.js +93 -19
- package/dist/runtime/harness.d.ts +9 -1
- package/dist/runtime/harness.js +267 -65
- package/dist/runtime/health-monitor.js +1 -1
- package/dist/runtime/runtime-record-maintenance.js +2 -0
- package/dist/workspace/object-loader.js +133 -7
- package/dist/workspace/support/workspace-ref-utils.d.ts +3 -0
- package/dist/workspace/support/workspace-ref-utils.js +30 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ 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
|
-
const RUNTIME_SQLITE_SCHEMA_VERSION =
|
|
5
|
+
const RUNTIME_SQLITE_SCHEMA_VERSION = 2;
|
|
6
6
|
const RUNTIME_SQLITE_SCHEMA_FAMILY = "agent-harness-runtime";
|
|
7
7
|
function asRow(value) {
|
|
8
8
|
return value;
|
|
@@ -25,6 +25,16 @@ function toSqliteUrl(filePath) {
|
|
|
25
25
|
function nowIso() {
|
|
26
26
|
return new Date(Date.now()).toISOString();
|
|
27
27
|
}
|
|
28
|
+
function buildWhereClause(filters) {
|
|
29
|
+
const active = filters.filter(([, value]) => value !== undefined);
|
|
30
|
+
if (active.length === 0) {
|
|
31
|
+
return { clause: "", args: [] };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
clause: ` WHERE ${active.map(([sql]) => sql).join(" AND ")}`,
|
|
35
|
+
args: active.map(([, value]) => value),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
28
38
|
async function selectProtectedThreadIds(dbPath) {
|
|
29
39
|
if (!(await fileExists(dbPath))) {
|
|
30
40
|
return [];
|
|
@@ -187,6 +197,37 @@ export class SqlitePersistence {
|
|
|
187
197
|
FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
|
|
188
198
|
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
189
199
|
)
|
|
200
|
+
`);
|
|
201
|
+
await this.rawExecute(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS run_queue (
|
|
203
|
+
run_id TEXT PRIMARY KEY,
|
|
204
|
+
thread_id TEXT NOT NULL,
|
|
205
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
206
|
+
queue_key TEXT,
|
|
207
|
+
enqueued_at TEXT NOT NULL,
|
|
208
|
+
available_at TEXT NOT NULL,
|
|
209
|
+
claimed_by TEXT,
|
|
210
|
+
claimed_at TEXT,
|
|
211
|
+
lease_expires_at TEXT,
|
|
212
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
213
|
+
last_error TEXT,
|
|
214
|
+
FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
|
|
215
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
216
|
+
)
|
|
217
|
+
`);
|
|
218
|
+
await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_available_idx ON run_queue(available_at, priority DESC, enqueued_at ASC)");
|
|
219
|
+
await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_claim_idx ON run_queue(claimed_by, lease_expires_at)");
|
|
220
|
+
await this.rawExecute(`
|
|
221
|
+
CREATE TABLE IF NOT EXISTS run_control (
|
|
222
|
+
run_id TEXT PRIMARY KEY,
|
|
223
|
+
cancel_requested INTEGER NOT NULL DEFAULT 0,
|
|
224
|
+
cancel_reason TEXT,
|
|
225
|
+
cancel_requested_at TEXT,
|
|
226
|
+
heartbeat_at TEXT,
|
|
227
|
+
worker_id TEXT,
|
|
228
|
+
worker_started_at TEXT,
|
|
229
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
230
|
+
)
|
|
190
231
|
`);
|
|
191
232
|
await this.rawExecute(`
|
|
192
233
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
@@ -292,6 +333,10 @@ export class SqlitePersistence {
|
|
|
292
333
|
if (family !== RUNTIME_SQLITE_SCHEMA_FAMILY) {
|
|
293
334
|
throw new Error(`Unsupported runtime sqlite schema family ${JSON.stringify(family)} in ${this.dbPath}. Expected ${JSON.stringify(RUNTIME_SQLITE_SCHEMA_FAMILY)}.`);
|
|
294
335
|
}
|
|
336
|
+
if (version === "1") {
|
|
337
|
+
await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
295
340
|
if (version !== String(RUNTIME_SQLITE_SCHEMA_VERSION)) {
|
|
296
341
|
throw new Error(`Unsupported runtime sqlite schema version ${JSON.stringify(version)} in ${this.dbPath}. Expected ${RUNTIME_SQLITE_SCHEMA_VERSION}.`);
|
|
297
342
|
}
|
|
@@ -321,6 +366,9 @@ export class SqlitePersistence {
|
|
|
321
366
|
0,
|
|
322
367
|
null,
|
|
323
368
|
]);
|
|
369
|
+
await this.execute(`INSERT OR REPLACE INTO run_control
|
|
370
|
+
(run_id, cancel_requested, cancel_reason, cancel_requested_at, heartbeat_at, worker_id, worker_started_at)
|
|
371
|
+
VALUES (?, 0, NULL, NULL, NULL, NULL, NULL)`, [input.runId]);
|
|
324
372
|
}
|
|
325
373
|
async setRunState(threadId, runId, state, checkpointRef) {
|
|
326
374
|
const current = await this.getRunLifecycle(threadId, runId);
|
|
@@ -339,16 +387,24 @@ export class SqlitePersistence {
|
|
|
339
387
|
(thread_id, run_id, sequence, event_json, created_at)
|
|
340
388
|
VALUES (?, ?, ?, ?, ?)`, [event.threadId, event.runId, event.sequence, JSON.stringify(event), event.timestamp]);
|
|
341
389
|
}
|
|
342
|
-
async listSessions() {
|
|
390
|
+
async listSessions(filter = {}) {
|
|
391
|
+
const { clause, args } = buildWhereClause([
|
|
392
|
+
["entry_agent_id = ?", filter.agentId],
|
|
393
|
+
]);
|
|
343
394
|
const rows = await this.selectAll(`SELECT thread_id, entry_agent_id, latest_run_id, created_at, updated_at, status
|
|
344
|
-
FROM threads
|
|
345
|
-
ORDER BY updated_at DESC
|
|
395
|
+
FROM threads${clause}
|
|
396
|
+
ORDER BY updated_at DESC`, args);
|
|
346
397
|
return rows.map((row) => this.mapThreadSummary(row));
|
|
347
398
|
}
|
|
348
|
-
async listRuns() {
|
|
399
|
+
async listRuns(filter = {}) {
|
|
400
|
+
const { clause, args } = buildWhereClause([
|
|
401
|
+
["agent_id = ?", filter.agentId],
|
|
402
|
+
["thread_id = ?", filter.threadId],
|
|
403
|
+
["state = ?", filter.state],
|
|
404
|
+
]);
|
|
349
405
|
const rows = await this.selectAll(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, checkpoint_ref, resumable
|
|
350
|
-
FROM runs
|
|
351
|
-
ORDER BY updated_at DESC
|
|
406
|
+
FROM runs${clause}
|
|
407
|
+
ORDER BY updated_at DESC`, args);
|
|
352
408
|
return rows.map((row) => this.mapRunSummary(row));
|
|
353
409
|
}
|
|
354
410
|
async getRun(runId) {
|
|
@@ -392,8 +448,13 @@ export class SqlitePersistence {
|
|
|
392
448
|
ORDER BY sequence ASC`, [threadId, runId]);
|
|
393
449
|
return rows.map((row) => parseJson(row.event_json));
|
|
394
450
|
}
|
|
395
|
-
async listApprovals() {
|
|
396
|
-
const
|
|
451
|
+
async listApprovals(filter = {}) {
|
|
452
|
+
const { clause, args } = buildWhereClause([
|
|
453
|
+
["status = ?", filter.status],
|
|
454
|
+
["thread_id = ?", filter.threadId],
|
|
455
|
+
["run_id = ?", filter.runId],
|
|
456
|
+
]);
|
|
457
|
+
const rows = await this.selectAll(`SELECT * FROM approvals${clause} ORDER BY requested_at ASC, approval_id ASC`, args);
|
|
397
458
|
return rows.map((row) => this.mapApproval(row));
|
|
398
459
|
}
|
|
399
460
|
async getApproval(approvalId) {
|
|
@@ -445,9 +506,11 @@ export class SqlitePersistence {
|
|
|
445
506
|
await this.execute("DELETE FROM artifacts WHERE thread_id = ?", [threadId]);
|
|
446
507
|
await this.execute("DELETE FROM approvals WHERE thread_id = ?", [threadId]);
|
|
447
508
|
await this.execute("DELETE FROM events WHERE thread_id = ?", [threadId]);
|
|
509
|
+
await this.execute("DELETE FROM run_queue WHERE thread_id = ?", [threadId]);
|
|
448
510
|
await this.execute("DELETE FROM run_requests WHERE thread_id = ?", [threadId]);
|
|
449
511
|
await this.execute("DELETE FROM recovery_intents WHERE thread_id = ?", [threadId]);
|
|
450
512
|
await this.execute("DELETE FROM thread_messages WHERE thread_id = ?", [threadId]);
|
|
513
|
+
await this.execute("DELETE FROM run_control WHERE run_id IN (SELECT run_id FROM runs WHERE thread_id = ?)", [threadId]);
|
|
451
514
|
await this.execute("DELETE FROM runs WHERE thread_id = ?", [threadId]);
|
|
452
515
|
await this.execute("DELETE FROM threads WHERE thread_id = ?", [threadId]);
|
|
453
516
|
await rm(this.threadDir(threadId), { recursive: true, force: true });
|
|
@@ -559,6 +622,108 @@ export class SqlitePersistence {
|
|
|
559
622
|
async clearRecoveryIntent(threadId, runId) {
|
|
560
623
|
await this.execute("DELETE FROM recovery_intents WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
561
624
|
}
|
|
625
|
+
mapQueuedRun(row) {
|
|
626
|
+
return {
|
|
627
|
+
runId: asString(row.run_id),
|
|
628
|
+
threadId: asString(row.thread_id),
|
|
629
|
+
priority: Number(row.priority ?? 0),
|
|
630
|
+
queueKey: asNullableString(row.queue_key),
|
|
631
|
+
enqueuedAt: asString(row.enqueued_at),
|
|
632
|
+
availableAt: asString(row.available_at),
|
|
633
|
+
claimedBy: asNullableString(row.claimed_by),
|
|
634
|
+
claimedAt: asNullableString(row.claimed_at),
|
|
635
|
+
leaseExpiresAt: asNullableString(row.lease_expires_at),
|
|
636
|
+
attemptCount: Number(row.attempt_count ?? 0),
|
|
637
|
+
lastError: asNullableString(row.last_error),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
mapRunControl(row) {
|
|
641
|
+
return {
|
|
642
|
+
runId: asString(row.run_id),
|
|
643
|
+
cancelRequested: asBoolean(row.cancel_requested),
|
|
644
|
+
cancelReason: asNullableString(row.cancel_reason),
|
|
645
|
+
cancelRequestedAt: asNullableString(row.cancel_requested_at),
|
|
646
|
+
heartbeatAt: asNullableString(row.heartbeat_at),
|
|
647
|
+
workerId: asNullableString(row.worker_id),
|
|
648
|
+
workerStartedAt: asNullableString(row.worker_started_at),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async enqueueRun(input) {
|
|
652
|
+
const enqueuedAt = nowIso();
|
|
653
|
+
await this.execute(`INSERT OR REPLACE INTO run_queue
|
|
654
|
+
(run_id, thread_id, priority, queue_key, enqueued_at, available_at, claimed_by, claimed_at, lease_expires_at, attempt_count, last_error)
|
|
655
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, COALESCE((SELECT attempt_count FROM run_queue WHERE run_id = ?), 0), NULL)`, [
|
|
656
|
+
input.runId,
|
|
657
|
+
input.threadId,
|
|
658
|
+
input.priority ?? 0,
|
|
659
|
+
input.queueKey ?? null,
|
|
660
|
+
enqueuedAt,
|
|
661
|
+
input.availableAt ?? enqueuedAt,
|
|
662
|
+
input.runId,
|
|
663
|
+
]);
|
|
664
|
+
}
|
|
665
|
+
async getQueuedRun(runId) {
|
|
666
|
+
const row = await this.selectOne("SELECT * FROM run_queue WHERE run_id = ?", [runId]);
|
|
667
|
+
return row ? this.mapQueuedRun(row) : null;
|
|
668
|
+
}
|
|
669
|
+
async claimQueuedRun(input) {
|
|
670
|
+
const claimedAt = input.claimedAt ?? nowIso();
|
|
671
|
+
await this.execute(`UPDATE run_queue
|
|
672
|
+
SET claimed_by = ?, claimed_at = ?, lease_expires_at = ?, attempt_count = attempt_count + 1
|
|
673
|
+
WHERE run_id = ? AND thread_id = ?`, [input.workerId, claimedAt, input.leaseExpiresAt, input.runId, input.threadId]);
|
|
674
|
+
await this.execute(`UPDATE run_control
|
|
675
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
676
|
+
WHERE run_id = ?`, [claimedAt, input.workerId, claimedAt, input.runId]);
|
|
677
|
+
const claimed = await this.getQueuedRun(input.runId);
|
|
678
|
+
if (!claimed) {
|
|
679
|
+
throw new Error(`Missing queued run ${input.runId}`);
|
|
680
|
+
}
|
|
681
|
+
return claimed;
|
|
682
|
+
}
|
|
683
|
+
async renewRunLease(input) {
|
|
684
|
+
const heartbeatAt = input.heartbeatAt ?? nowIso();
|
|
685
|
+
await this.execute(`UPDATE run_queue
|
|
686
|
+
SET lease_expires_at = ?, claimed_by = COALESCE(claimed_by, ?), claimed_at = COALESCE(claimed_at, ?)
|
|
687
|
+
WHERE run_id = ?`, [input.leaseExpiresAt, input.workerId, heartbeatAt, input.runId]);
|
|
688
|
+
await this.execute(`UPDATE run_control
|
|
689
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
690
|
+
WHERE run_id = ?`, [heartbeatAt, input.workerId, heartbeatAt, input.runId]);
|
|
691
|
+
}
|
|
692
|
+
async releaseRunClaim(runId) {
|
|
693
|
+
await this.execute("DELETE FROM run_queue WHERE run_id = ?", [runId]);
|
|
694
|
+
await this.execute(`UPDATE run_control
|
|
695
|
+
SET heartbeat_at = NULL, worker_id = NULL, worker_started_at = NULL
|
|
696
|
+
WHERE run_id = ?`, [runId]);
|
|
697
|
+
}
|
|
698
|
+
async listExpiredClaimedRuns(cutoffIso) {
|
|
699
|
+
const rows = await this.selectAll(`SELECT *
|
|
700
|
+
FROM run_queue
|
|
701
|
+
WHERE claimed_by IS NOT NULL
|
|
702
|
+
AND lease_expires_at IS NOT NULL
|
|
703
|
+
AND lease_expires_at <= ?
|
|
704
|
+
ORDER BY lease_expires_at ASC, run_id ASC`, [cutoffIso]);
|
|
705
|
+
return rows.map((row) => this.mapQueuedRun(row));
|
|
706
|
+
}
|
|
707
|
+
async getRunControl(runId) {
|
|
708
|
+
const row = await this.selectOne("SELECT * FROM run_control WHERE run_id = ?", [runId]);
|
|
709
|
+
return row ? this.mapRunControl(row) : null;
|
|
710
|
+
}
|
|
711
|
+
async requestRunCancel(runId, reason) {
|
|
712
|
+
const now = nowIso();
|
|
713
|
+
await this.execute(`UPDATE run_control
|
|
714
|
+
SET cancel_requested = 1, cancel_reason = ?, cancel_requested_at = ?
|
|
715
|
+
WHERE run_id = ?`, [reason ?? null, now, runId]);
|
|
716
|
+
const updated = await this.getRunControl(runId);
|
|
717
|
+
if (!updated) {
|
|
718
|
+
throw new Error(`Missing run control for ${runId}`);
|
|
719
|
+
}
|
|
720
|
+
return updated;
|
|
721
|
+
}
|
|
722
|
+
async clearRunCancel(runId) {
|
|
723
|
+
await this.execute(`UPDATE run_control
|
|
724
|
+
SET cancel_requested = 0, cancel_reason = NULL, cancel_requested_at = NULL
|
|
725
|
+
WHERE run_id = ?`, [runId]);
|
|
726
|
+
}
|
|
562
727
|
async readArtifact(threadId, runId, artifactPath) {
|
|
563
728
|
const filePath = path.join(this.runDir(threadId, runId), artifactPath);
|
|
564
729
|
if (!(await fileExists(filePath))) {
|
|
@@ -37,6 +37,41 @@ export type RecoveryIntent = {
|
|
|
37
37
|
resumePayload: unknown;
|
|
38
38
|
attempts: number;
|
|
39
39
|
};
|
|
40
|
+
export type PersistedRunQueueRecord = {
|
|
41
|
+
runId: string;
|
|
42
|
+
threadId: string;
|
|
43
|
+
priority: number;
|
|
44
|
+
queueKey: string | null;
|
|
45
|
+
enqueuedAt: string;
|
|
46
|
+
availableAt: string;
|
|
47
|
+
claimedBy: string | null;
|
|
48
|
+
claimedAt: string | null;
|
|
49
|
+
leaseExpiresAt: string | null;
|
|
50
|
+
attemptCount: number;
|
|
51
|
+
lastError: string | null;
|
|
52
|
+
};
|
|
53
|
+
export type PersistedRunControlRecord = {
|
|
54
|
+
runId: string;
|
|
55
|
+
cancelRequested: boolean;
|
|
56
|
+
cancelReason: string | null;
|
|
57
|
+
cancelRequestedAt: string | null;
|
|
58
|
+
heartbeatAt: string | null;
|
|
59
|
+
workerId: string | null;
|
|
60
|
+
workerStartedAt: string | null;
|
|
61
|
+
};
|
|
62
|
+
export type ThreadSummaryFilter = {
|
|
63
|
+
agentId?: string;
|
|
64
|
+
};
|
|
65
|
+
export type RunSummaryFilter = {
|
|
66
|
+
agentId?: string;
|
|
67
|
+
threadId?: string;
|
|
68
|
+
state?: RunSummary["state"];
|
|
69
|
+
};
|
|
70
|
+
export type ApprovalFilter = {
|
|
71
|
+
status?: InternalApprovalRecord["status"];
|
|
72
|
+
threadId?: string;
|
|
73
|
+
runId?: string;
|
|
74
|
+
};
|
|
40
75
|
export interface RuntimePersistence {
|
|
41
76
|
initialize(): Promise<void>;
|
|
42
77
|
createThread(input: {
|
|
@@ -56,13 +91,13 @@ export interface RuntimePersistence {
|
|
|
56
91
|
}): Promise<void>;
|
|
57
92
|
setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
|
|
58
93
|
appendEvent(event: HarnessEvent): Promise<void>;
|
|
59
|
-
listSessions(): Promise<ThreadSummary[]>;
|
|
60
|
-
listRuns(): Promise<RunSummary[]>;
|
|
94
|
+
listSessions(filter?: ThreadSummaryFilter): Promise<ThreadSummary[]>;
|
|
95
|
+
listRuns(filter?: RunSummaryFilter): Promise<RunSummary[]>;
|
|
61
96
|
getRun(runId: string): Promise<RunSummary | null>;
|
|
62
97
|
getSession(threadId: string): Promise<ThreadSummary | null>;
|
|
63
98
|
getThreadMeta(threadId: string): Promise<PersistenceThreadMeta | null>;
|
|
64
99
|
listThreadRuns(threadId: string): Promise<ThreadRunRecord[]>;
|
|
65
|
-
listApprovals(): Promise<InternalApprovalRecord[]>;
|
|
100
|
+
listApprovals(filter?: ApprovalFilter): Promise<InternalApprovalRecord[]>;
|
|
66
101
|
getApproval(approvalId: string): Promise<InternalApprovalRecord | null>;
|
|
67
102
|
getRunApprovals(threadId: string, runId: string): Promise<InternalApprovalRecord[]>;
|
|
68
103
|
getRunMeta(threadId: string, runId: string): Promise<PersistenceRunMeta>;
|
|
@@ -80,4 +115,30 @@ export interface RuntimePersistence {
|
|
|
80
115
|
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
81
116
|
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
82
117
|
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
118
|
+
enqueueRun(input: {
|
|
119
|
+
threadId: string;
|
|
120
|
+
runId: string;
|
|
121
|
+
priority?: number;
|
|
122
|
+
queueKey?: string | null;
|
|
123
|
+
availableAt?: string;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
|
|
126
|
+
claimQueuedRun(input: {
|
|
127
|
+
threadId: string;
|
|
128
|
+
runId: string;
|
|
129
|
+
workerId: string;
|
|
130
|
+
claimedAt?: string;
|
|
131
|
+
leaseExpiresAt: string;
|
|
132
|
+
}): Promise<PersistedRunQueueRecord>;
|
|
133
|
+
renewRunLease(input: {
|
|
134
|
+
runId: string;
|
|
135
|
+
workerId: string;
|
|
136
|
+
heartbeatAt?: string;
|
|
137
|
+
leaseExpiresAt: string;
|
|
138
|
+
}): Promise<void>;
|
|
139
|
+
releaseRunClaim(runId: string): Promise<void>;
|
|
140
|
+
listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
|
|
141
|
+
getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
|
|
142
|
+
requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
|
|
143
|
+
clearRunCancel(runId: string): Promise<void>;
|
|
83
144
|
}
|
|
@@ -21,7 +21,10 @@ export declare function materializeDeepAgentSkillSourcePaths(options: {
|
|
|
21
21
|
}): Promise<string[] | undefined>;
|
|
22
22
|
export declare class AgentRuntimeAdapter {
|
|
23
23
|
private readonly options;
|
|
24
|
+
private readonly modelCache;
|
|
25
|
+
private readonly runnableCache;
|
|
24
26
|
constructor(options?: RuntimeAdapterOptions);
|
|
27
|
+
private getModelCacheKey;
|
|
25
28
|
private resolveBindingTimeout;
|
|
26
29
|
private resolveStreamIdleTimeout;
|
|
27
30
|
private resolveProviderRetryPolicy;
|
|
@@ -48,11 +51,13 @@ export declare class AgentRuntimeAdapter {
|
|
|
48
51
|
private resolveBuiltinMiddlewareBackend;
|
|
49
52
|
private invokeBuiltinTaskTool;
|
|
50
53
|
private resolveBuiltinMiddlewareTools;
|
|
54
|
+
private canReplayToolCallsLocally;
|
|
51
55
|
private resolveLangChainAutomaticMiddleware;
|
|
52
56
|
private resolveMiddleware;
|
|
53
57
|
private resolveCheckpointer;
|
|
54
58
|
private buildRouteSystemPrompt;
|
|
55
59
|
private resolveSubagents;
|
|
60
|
+
private createRunnable;
|
|
56
61
|
create(binding: CompiledAgentBinding): Promise<RunnableLike>;
|
|
57
62
|
route(input: MessageContent, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
|
|
58
63
|
systemPrompt?: string;
|
|
@@ -471,9 +471,19 @@ function asStructuredExecutableTool(resolvedTool, modelFacingName, description)
|
|
|
471
471
|
}
|
|
472
472
|
export class AgentRuntimeAdapter {
|
|
473
473
|
options;
|
|
474
|
+
modelCache = new Map();
|
|
475
|
+
runnableCache = new WeakMap();
|
|
474
476
|
constructor(options = {}) {
|
|
475
477
|
this.options = options;
|
|
476
478
|
}
|
|
479
|
+
getModelCacheKey(model) {
|
|
480
|
+
return JSON.stringify({
|
|
481
|
+
id: model.id,
|
|
482
|
+
runtimeValue: model.runtimeValue,
|
|
483
|
+
init: model.init,
|
|
484
|
+
clientRef: model.clientRef,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
477
487
|
resolveBindingTimeout(binding) {
|
|
478
488
|
return resolveTimeoutMs(getBindingModelInit(binding)?.timeout);
|
|
479
489
|
}
|
|
@@ -686,25 +696,40 @@ export class AgentRuntimeAdapter {
|
|
|
686
696
|
return sanitizeVisibleText(extractVisibleOutput(synthesized));
|
|
687
697
|
}
|
|
688
698
|
async resolveModel(model) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return wrapResolvedModel(new ChatOllama({ model: model.model, ...model.init }));
|
|
694
|
-
}
|
|
695
|
-
if (model.provider === "openai-compatible") {
|
|
696
|
-
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
|
|
697
|
-
}
|
|
698
|
-
if (model.provider === "openai") {
|
|
699
|
-
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...model.init }));
|
|
699
|
+
const cacheKey = this.getModelCacheKey(model);
|
|
700
|
+
const cached = this.modelCache.get(cacheKey);
|
|
701
|
+
if (cached) {
|
|
702
|
+
return cached;
|
|
700
703
|
}
|
|
701
|
-
|
|
702
|
-
|
|
704
|
+
const pending = (async () => {
|
|
705
|
+
if (this.options.modelResolver) {
|
|
706
|
+
return wrapResolvedModel(await this.options.modelResolver(model.id));
|
|
707
|
+
}
|
|
708
|
+
if (model.provider === "ollama") {
|
|
709
|
+
return wrapResolvedModel(new ChatOllama({ model: model.model, ...model.init }));
|
|
710
|
+
}
|
|
711
|
+
if (model.provider === "openai-compatible") {
|
|
712
|
+
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
|
|
713
|
+
}
|
|
714
|
+
if (model.provider === "openai") {
|
|
715
|
+
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...model.init }));
|
|
716
|
+
}
|
|
717
|
+
if (model.provider === "anthropic") {
|
|
718
|
+
return wrapResolvedModel(new ChatAnthropic({ model: model.model, ...model.init }));
|
|
719
|
+
}
|
|
720
|
+
if (model.provider === "google" || model.provider === "google-genai" || model.provider === "gemini") {
|
|
721
|
+
return wrapResolvedModel(new ChatGoogle({ model: model.model, ...model.init }));
|
|
722
|
+
}
|
|
723
|
+
return wrapResolvedModel(await initChatModel(model.model, { modelProvider: model.provider, ...model.init }));
|
|
724
|
+
})();
|
|
725
|
+
this.modelCache.set(cacheKey, pending);
|
|
726
|
+
try {
|
|
727
|
+
return await pending;
|
|
703
728
|
}
|
|
704
|
-
|
|
705
|
-
|
|
729
|
+
catch (error) {
|
|
730
|
+
this.modelCache.delete(cacheKey);
|
|
731
|
+
throw error;
|
|
706
732
|
}
|
|
707
|
-
return wrapResolvedModel(await initChatModel(model.model, { modelProvider: model.provider, ...model.init }));
|
|
708
733
|
}
|
|
709
734
|
buildToolNameMapping(tools) {
|
|
710
735
|
return buildToolNameMapping(tools);
|
|
@@ -1065,6 +1090,27 @@ export class AgentRuntimeAdapter {
|
|
|
1065
1090
|
}
|
|
1066
1091
|
return tools;
|
|
1067
1092
|
}
|
|
1093
|
+
canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools) {
|
|
1094
|
+
if (toolCalls.length === 0) {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
if (isLangChainBinding(binding)) {
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
return toolCalls.every((toolCall) => {
|
|
1101
|
+
const resolvedToolName = resolveModelFacingToolName(toolCall.name, toolNameMapping, primaryTools);
|
|
1102
|
+
const executable = executableTools.get(toolCall.name) ?? executableTools.get(resolvedToolName);
|
|
1103
|
+
if (executable) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
const builtinExecutable = builtinExecutableTools.get(toolCall.name) ??
|
|
1107
|
+
builtinExecutableTools.get(resolvedToolName) ??
|
|
1108
|
+
createModelFacingToolNameLookupCandidates(toolCall.name)
|
|
1109
|
+
.map((candidate) => builtinExecutableTools.get(candidate))
|
|
1110
|
+
.find((candidate) => candidate !== undefined);
|
|
1111
|
+
return builtinExecutable !== undefined;
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1068
1114
|
async resolveLangChainAutomaticMiddleware(binding) {
|
|
1069
1115
|
const params = getBindingLangChainParams(binding);
|
|
1070
1116
|
if (!params) {
|
|
@@ -1170,7 +1216,7 @@ export class AgentRuntimeAdapter {
|
|
|
1170
1216
|
})),
|
|
1171
1217
|
})));
|
|
1172
1218
|
}
|
|
1173
|
-
async
|
|
1219
|
+
async createRunnable(binding) {
|
|
1174
1220
|
if (isLangChainBinding(binding)) {
|
|
1175
1221
|
const params = getBindingLangChainParams(binding);
|
|
1176
1222
|
const interruptOn = this.resolveInterruptOn(binding);
|
|
@@ -1226,6 +1272,21 @@ export class AgentRuntimeAdapter {
|
|
|
1226
1272
|
};
|
|
1227
1273
|
return createDeepAgent(deepAgentConfig);
|
|
1228
1274
|
}
|
|
1275
|
+
async create(binding) {
|
|
1276
|
+
const cached = this.runnableCache.get(binding);
|
|
1277
|
+
if (cached) {
|
|
1278
|
+
return cached;
|
|
1279
|
+
}
|
|
1280
|
+
const pending = this.createRunnable(binding);
|
|
1281
|
+
this.runnableCache.set(binding, pending);
|
|
1282
|
+
try {
|
|
1283
|
+
return await pending;
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
this.runnableCache.delete(binding);
|
|
1287
|
+
throw error;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1229
1290
|
async route(input, primaryBinding, secondaryBinding, options = {}) {
|
|
1230
1291
|
const routeModelConfig = getBindingPrimaryModel(primaryBinding) ??
|
|
1231
1292
|
getBindingPrimaryModel(secondaryBinding);
|
|
@@ -1323,12 +1384,17 @@ export class AgentRuntimeAdapter {
|
|
|
1323
1384
|
let activeRequest = request;
|
|
1324
1385
|
let currentMessages = Array.isArray(activeRequest.messages) ? [...activeRequest.messages] : [];
|
|
1325
1386
|
const maxToolIterations = 8;
|
|
1387
|
+
let pendingResult;
|
|
1326
1388
|
for (let iteration = 0; iteration < maxToolIterations; iteration += 1) {
|
|
1327
|
-
result = await callRuntimeWithToolParseRecovery(activeRequest);
|
|
1389
|
+
result = pendingResult ?? await callRuntimeWithToolParseRecovery(activeRequest);
|
|
1390
|
+
pendingResult = undefined;
|
|
1328
1391
|
const toolCalls = extractToolCallsFromResult(result);
|
|
1329
1392
|
if (toolCalls.length === 0) {
|
|
1330
1393
|
break;
|
|
1331
1394
|
}
|
|
1395
|
+
if (!this.canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools)) {
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1332
1398
|
if (iteration + 1 === maxToolIterations) {
|
|
1333
1399
|
throw new Error(`Tool-calling loop exceeded the maximum of ${maxToolIterations} iterations`);
|
|
1334
1400
|
}
|
|
@@ -1377,7 +1443,15 @@ export class AgentRuntimeAdapter {
|
|
|
1377
1443
|
const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
|
|
1378
1444
|
const emptyAssistantMessageFailure = extractEmptyAssistantMessageFailure(result);
|
|
1379
1445
|
const toolFallback = extractToolFallbackContext(result);
|
|
1380
|
-
|
|
1446
|
+
let synthesizedOutput = "";
|
|
1447
|
+
try {
|
|
1448
|
+
synthesizedOutput = await this.synthesizeDeepAgentAnswer(binding, input, result);
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
if (!(error instanceof RuntimeOperationTimeoutError) || !toolFallback) {
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1381
1455
|
if (!visibleOutput && !synthesizedOutput && !toolFallback && emptyAssistantMessageFailure) {
|
|
1382
1456
|
throw new Error(emptyAssistantMessageFailure);
|
|
1383
1457
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalRecord, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
1
|
+
import type { ApprovalRecord, CancelOptions, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
2
2
|
import { type ToolMcpServerOptions } from "../mcp.js";
|
|
3
3
|
import { type InventoryAgentRecord, type InventorySkillRecord } from "./inventory.js";
|
|
4
4
|
import type { RequirementAssessmentOptions } from "./skill-requirements.js";
|
|
@@ -27,6 +27,7 @@ export declare class AgentHarnessRuntime {
|
|
|
27
27
|
private readonly healthMonitor;
|
|
28
28
|
private readonly recoveryConfig;
|
|
29
29
|
private readonly concurrencyConfig;
|
|
30
|
+
private readonly workerId;
|
|
30
31
|
private activeRunSlots;
|
|
31
32
|
private readonly pendingRunSlots;
|
|
32
33
|
private runtimeEventSequence;
|
|
@@ -84,6 +85,9 @@ export declare class AgentHarnessRuntime {
|
|
|
84
85
|
private loadPriorHistory;
|
|
85
86
|
private loadRunInput;
|
|
86
87
|
private appendAssistantMessage;
|
|
88
|
+
private getRunCancellation;
|
|
89
|
+
private expirePendingApprovals;
|
|
90
|
+
private finalizeCancelledRun;
|
|
87
91
|
private invokeWithHistory;
|
|
88
92
|
private buildPersistedRunRequest;
|
|
89
93
|
private executeQueuedRun;
|
|
@@ -100,6 +104,7 @@ export declare class AgentHarnessRuntime {
|
|
|
100
104
|
private isDecisionRun;
|
|
101
105
|
private notifyListener;
|
|
102
106
|
private acquireRunSlot;
|
|
107
|
+
private dropPendingRunSlot;
|
|
103
108
|
private dispatchRunListeners;
|
|
104
109
|
run(options: RunOptions): Promise<RunResult>;
|
|
105
110
|
streamEvents(options: RunStartOptions): AsyncGenerator<HarnessStreamItem>;
|
|
@@ -110,6 +115,9 @@ export declare class AgentHarnessRuntime {
|
|
|
110
115
|
}>;
|
|
111
116
|
close(): Promise<void>;
|
|
112
117
|
stop(): Promise<void>;
|
|
118
|
+
cancelRun(options: CancelOptions): Promise<RunResult>;
|
|
113
119
|
private recoverStartupRuns;
|
|
120
|
+
private reclaimExpiredClaimedRuns;
|
|
121
|
+
private isStaleRunningRun;
|
|
114
122
|
}
|
|
115
123
|
export { AgentHarnessRuntime as AgentHarness };
|