@botbotgo/agent-harness 0.0.80 → 0.0.81
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 +29 -1
- package/dist/persistence/file-store.js +164 -0
- package/dist/persistence/sqlite-store.d.ts +29 -1
- package/dist/persistence/sqlite-store.js +143 -1
- package/dist/persistence/types.d.ts +48 -0
- package/dist/runtime/agent-runtime-adapter.js +9 -1
- package/dist/runtime/harness.d.ts +9 -1
- package/dist/runtime/harness.js +263 -32
- 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
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalRecord, RunRecord, RunOptions, RunSummary, ResumeOptions, RuntimeHealthSnapshot, RuntimeAdapterOptions, ThreadSummary, ThreadRecord, WorkspaceLoadOptions } from "./contracts/types.js";
|
|
1
|
+
import type { ApprovalRecord, CancelOptions, RunRecord, RunOptions, RunSummary, ResumeOptions, RuntimeHealthSnapshot, RuntimeAdapterOptions, ThreadSummary, ThreadRecord, WorkspaceLoadOptions } from "./contracts/types.js";
|
|
2
2
|
import { AgentHarnessRuntime } from "./runtime/harness.js";
|
|
3
3
|
import type { InventoryAgentRecord, InventorySkillRecord } from "./runtime/inventory.js";
|
|
4
4
|
import type { RequirementAssessmentOptions } from "./runtime/skill-requirements.js";
|
|
@@ -30,6 +30,7 @@ export declare function describeInventory(runtime: AgentHarnessRuntime, options?
|
|
|
30
30
|
agents: InventoryAgentRecord[];
|
|
31
31
|
};
|
|
32
32
|
export declare function resolveApproval(runtime: AgentHarnessRuntime, options: ResumeOptions): Promise<import("./contracts/types.js").RunResult>;
|
|
33
|
+
export declare function cancelRun(runtime: AgentHarnessRuntime, options: CancelOptions): Promise<import("./contracts/types.js").RunResult>;
|
|
33
34
|
export declare function stop(runtime: AgentHarnessRuntime): Promise<void>;
|
|
34
35
|
export declare function createToolMcpServer(runtime: AgentHarnessRuntime, options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
|
|
35
36
|
export declare function serveToolsOverStdio(runtime: AgentHarnessRuntime, options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
|
package/dist/api.js
CHANGED
|
@@ -46,6 +46,9 @@ export function describeInventory(runtime, options) {
|
|
|
46
46
|
export async function resolveApproval(runtime, options) {
|
|
47
47
|
return runtime.resume(options);
|
|
48
48
|
}
|
|
49
|
+
export async function cancelRun(runtime, options) {
|
|
50
|
+
return runtime.cancelRun(options);
|
|
51
|
+
}
|
|
49
52
|
export async function stop(runtime) {
|
|
50
53
|
return runtime.stop();
|
|
51
54
|
}
|
|
@@ -4,7 +4,7 @@ export type RuntimeCapabilities = {
|
|
|
4
4
|
delegation?: boolean;
|
|
5
5
|
memory?: boolean;
|
|
6
6
|
};
|
|
7
|
-
export type RunState = "queued" | "running" | "waiting_for_approval" | "resuming" | "completed" | "failed";
|
|
7
|
+
export type RunState = "queued" | "claimed" | "running" | "waiting_for_approval" | "resuming" | "cancelling" | "cancelled" | "completed" | "failed";
|
|
8
8
|
export type ParsedAgentObject = {
|
|
9
9
|
id: string;
|
|
10
10
|
executionMode: ExecutionMode;
|
|
@@ -440,6 +440,10 @@ export type ResumeOptions = {
|
|
|
440
440
|
decision?: "approve" | "edit" | "reject";
|
|
441
441
|
editedInput?: Record<string, unknown>;
|
|
442
442
|
};
|
|
443
|
+
export type CancelOptions = {
|
|
444
|
+
runId: string;
|
|
445
|
+
reason?: string;
|
|
446
|
+
};
|
|
443
447
|
export type RestartConversationOptions = {
|
|
444
448
|
threadId: string;
|
|
445
449
|
mode: "restart-in-thread" | "restart-new-thread";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getHealth, getRun, getThread, listAgentSkills, listApprovals, listRuns, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getHealth, getRun, getThread, listAgentSkills, listApprovals, listRuns, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
2
|
export type { ToolMcpServerOptions } from "./mcp.js";
|
|
3
3
|
export { tool } from "./tools.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getHealth, getRun, getThread, listAgentSkills, listApprovals, listRuns, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createToolMcpServer, deleteThread, describeInventory, getApproval, getHealth, getRun, getThread, listAgentSkills, listApprovals, listRuns, listThreads, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
2
|
export { tool } from "./tools.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.80";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.80";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ArtifactListing, ArtifactRecord, DelegationRecord, HarnessEvent, InternalApprovalRecord, RunSummary, RunState, ThreadSummary, ThreadRunRecord, TranscriptMessage } from "../contracts/types.js";
|
|
2
|
-
import type { PersistedRunRequest, PersistenceLifecycle as Lifecycle, RuntimePersistence, RecoveryIntent, PersistenceRunMeta as RunMeta, PersistenceThreadMeta as ThreadMeta } from "./types.js";
|
|
2
|
+
import type { PersistedRunRequest, PersistedRunControlRecord, PersistedRunQueueRecord, PersistenceLifecycle as Lifecycle, RuntimePersistence, RecoveryIntent, PersistenceRunMeta as RunMeta, PersistenceThreadMeta as ThreadMeta } from "./types.js";
|
|
3
3
|
type RunIndexRecord = {
|
|
4
4
|
runId: string;
|
|
5
5
|
threadId: string;
|
|
@@ -14,6 +14,8 @@ export declare class FilePersistence implements RuntimePersistence {
|
|
|
14
14
|
private runIndexPath;
|
|
15
15
|
private approvalIndexPath;
|
|
16
16
|
private delegationIndexPath;
|
|
17
|
+
private runQueuePath;
|
|
18
|
+
private runControlPath;
|
|
17
19
|
initialize(): Promise<void>;
|
|
18
20
|
threadDir(threadId: string): string;
|
|
19
21
|
runDir(threadId: string, runId: string): string;
|
|
@@ -64,5 +66,31 @@ export declare class FilePersistence implements RuntimePersistence {
|
|
|
64
66
|
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
65
67
|
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
66
68
|
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
69
|
+
enqueueRun(input: {
|
|
70
|
+
threadId: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
priority?: number;
|
|
73
|
+
queueKey?: string | null;
|
|
74
|
+
availableAt?: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
|
|
77
|
+
claimQueuedRun(input: {
|
|
78
|
+
threadId: string;
|
|
79
|
+
runId: string;
|
|
80
|
+
workerId: string;
|
|
81
|
+
claimedAt?: string;
|
|
82
|
+
leaseExpiresAt: string;
|
|
83
|
+
}): Promise<PersistedRunQueueRecord>;
|
|
84
|
+
renewRunLease(input: {
|
|
85
|
+
runId: string;
|
|
86
|
+
workerId: string;
|
|
87
|
+
heartbeatAt?: string;
|
|
88
|
+
leaseExpiresAt: string;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
releaseRunClaim(runId: string): Promise<void>;
|
|
91
|
+
listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
|
|
92
|
+
getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
|
|
93
|
+
requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
|
|
94
|
+
clearRunCancel(runId: string): Promise<void>;
|
|
67
95
|
}
|
|
68
96
|
export {};
|
|
@@ -22,12 +22,20 @@ export class FilePersistence {
|
|
|
22
22
|
delegationIndexPath(delegationId) {
|
|
23
23
|
return path.join(this.runRoot, "indexes", "delegations", `${delegationId}.json`);
|
|
24
24
|
}
|
|
25
|
+
runQueuePath(runId) {
|
|
26
|
+
return path.join(this.runRoot, "indexes", "queue", `${runId}.json`);
|
|
27
|
+
}
|
|
28
|
+
runControlPath(runId) {
|
|
29
|
+
return path.join(this.runRoot, "indexes", "run-control", `${runId}.json`);
|
|
30
|
+
}
|
|
25
31
|
async initialize() {
|
|
26
32
|
await Promise.all([
|
|
27
33
|
"indexes/threads",
|
|
28
34
|
"indexes/runs",
|
|
29
35
|
"indexes/approvals",
|
|
30
36
|
"indexes/delegations",
|
|
37
|
+
"indexes/queue",
|
|
38
|
+
"indexes/run-control",
|
|
31
39
|
"threads",
|
|
32
40
|
].map((segment) => ensureDir(path.join(this.runRoot, segment))));
|
|
33
41
|
}
|
|
@@ -107,6 +115,15 @@ export class FilePersistence {
|
|
|
107
115
|
resumable: false,
|
|
108
116
|
updatedAt: input.createdAt,
|
|
109
117
|
}),
|
|
118
|
+
writeJson(this.runControlPath(input.runId), {
|
|
119
|
+
runId: input.runId,
|
|
120
|
+
cancelRequested: false,
|
|
121
|
+
cancelReason: null,
|
|
122
|
+
cancelRequestedAt: null,
|
|
123
|
+
heartbeatAt: null,
|
|
124
|
+
workerId: null,
|
|
125
|
+
workerStartedAt: null,
|
|
126
|
+
}),
|
|
110
127
|
]);
|
|
111
128
|
}
|
|
112
129
|
async setRunState(threadId, runId, state, checkpointRef) {
|
|
@@ -314,6 +331,9 @@ export class FilePersistence {
|
|
|
314
331
|
...approvals
|
|
315
332
|
.filter((record) => record.threadId === threadId)
|
|
316
333
|
.map((record) => rm(this.approvalIndexPath(record.approvalId), { force: true })),
|
|
334
|
+
...runIndexes
|
|
335
|
+
.filter((record) => record.threadId === threadId)
|
|
336
|
+
.flatMap((record) => [rm(this.runQueuePath(record.runId), { force: true }), rm(this.runControlPath(record.runId), { force: true })]),
|
|
317
337
|
...delegations
|
|
318
338
|
.filter((record) => record.threadId === threadId)
|
|
319
339
|
.map((record) => rm(this.delegationIndexPath(record.delegationId), { force: true })),
|
|
@@ -427,4 +447,148 @@ export class FilePersistence {
|
|
|
427
447
|
}
|
|
428
448
|
await rm(intentPath, { force: true });
|
|
429
449
|
}
|
|
450
|
+
async enqueueRun(input) {
|
|
451
|
+
const current = (await this.getQueuedRun(input.runId)) ?? {
|
|
452
|
+
runId: input.runId,
|
|
453
|
+
threadId: input.threadId,
|
|
454
|
+
priority: input.priority ?? 0,
|
|
455
|
+
queueKey: input.queueKey ?? null,
|
|
456
|
+
enqueuedAt: nowIso(),
|
|
457
|
+
availableAt: input.availableAt ?? nowIso(),
|
|
458
|
+
claimedBy: null,
|
|
459
|
+
claimedAt: null,
|
|
460
|
+
leaseExpiresAt: null,
|
|
461
|
+
attemptCount: 0,
|
|
462
|
+
lastError: null,
|
|
463
|
+
};
|
|
464
|
+
await writeJson(this.runQueuePath(input.runId), {
|
|
465
|
+
...current,
|
|
466
|
+
threadId: input.threadId,
|
|
467
|
+
priority: input.priority ?? current.priority,
|
|
468
|
+
queueKey: input.queueKey ?? current.queueKey,
|
|
469
|
+
availableAt: input.availableAt ?? current.availableAt,
|
|
470
|
+
claimedBy: null,
|
|
471
|
+
claimedAt: null,
|
|
472
|
+
leaseExpiresAt: null,
|
|
473
|
+
lastError: null,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async getQueuedRun(runId) {
|
|
477
|
+
const filePath = this.runQueuePath(runId);
|
|
478
|
+
if (!(await fileExists(filePath))) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
return readJson(filePath);
|
|
482
|
+
}
|
|
483
|
+
async claimQueuedRun(input) {
|
|
484
|
+
const current = await this.getQueuedRun(input.runId);
|
|
485
|
+
if (!current) {
|
|
486
|
+
throw new Error(`Missing queued run ${input.runId}`);
|
|
487
|
+
}
|
|
488
|
+
const claimedAt = input.claimedAt ?? nowIso();
|
|
489
|
+
const next = {
|
|
490
|
+
...current,
|
|
491
|
+
threadId: input.threadId,
|
|
492
|
+
claimedBy: input.workerId,
|
|
493
|
+
claimedAt,
|
|
494
|
+
leaseExpiresAt: input.leaseExpiresAt,
|
|
495
|
+
attemptCount: current.attemptCount + 1,
|
|
496
|
+
};
|
|
497
|
+
await writeJson(this.runQueuePath(input.runId), next);
|
|
498
|
+
await writeJson(this.runControlPath(input.runId), {
|
|
499
|
+
...(await this.getRunControl(input.runId) ?? {
|
|
500
|
+
runId: input.runId,
|
|
501
|
+
cancelRequested: false,
|
|
502
|
+
cancelReason: null,
|
|
503
|
+
cancelRequestedAt: null,
|
|
504
|
+
heartbeatAt: null,
|
|
505
|
+
workerId: null,
|
|
506
|
+
workerStartedAt: null,
|
|
507
|
+
}),
|
|
508
|
+
heartbeatAt: claimedAt,
|
|
509
|
+
workerId: input.workerId,
|
|
510
|
+
workerStartedAt: claimedAt,
|
|
511
|
+
});
|
|
512
|
+
return next;
|
|
513
|
+
}
|
|
514
|
+
async renewRunLease(input) {
|
|
515
|
+
const current = await this.getQueuedRun(input.runId);
|
|
516
|
+
if (current) {
|
|
517
|
+
await writeJson(this.runQueuePath(input.runId), {
|
|
518
|
+
...current,
|
|
519
|
+
claimedBy: input.workerId,
|
|
520
|
+
claimedAt: current.claimedAt ?? input.heartbeatAt ?? nowIso(),
|
|
521
|
+
leaseExpiresAt: input.leaseExpiresAt,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
await writeJson(this.runControlPath(input.runId), {
|
|
525
|
+
...(await this.getRunControl(input.runId) ?? {
|
|
526
|
+
runId: input.runId,
|
|
527
|
+
cancelRequested: false,
|
|
528
|
+
cancelReason: null,
|
|
529
|
+
cancelRequestedAt: null,
|
|
530
|
+
heartbeatAt: null,
|
|
531
|
+
workerId: null,
|
|
532
|
+
workerStartedAt: null,
|
|
533
|
+
}),
|
|
534
|
+
heartbeatAt: input.heartbeatAt ?? nowIso(),
|
|
535
|
+
workerId: input.workerId,
|
|
536
|
+
workerStartedAt: (await this.getRunControl(input.runId))?.workerStartedAt ?? input.heartbeatAt ?? nowIso(),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
async releaseRunClaim(runId) {
|
|
540
|
+
await rm(this.runQueuePath(runId), { force: true });
|
|
541
|
+
const current = await this.getRunControl(runId);
|
|
542
|
+
if (!current) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
await writeJson(this.runControlPath(runId), {
|
|
546
|
+
...current,
|
|
547
|
+
heartbeatAt: null,
|
|
548
|
+
workerId: null,
|
|
549
|
+
workerStartedAt: null,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async listExpiredClaimedRuns(cutoffIso) {
|
|
553
|
+
const queueDir = path.join(this.runRoot, "indexes", "queue");
|
|
554
|
+
if (!(await fileExists(queueDir))) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
const entries = (await readdir(queueDir)).sort();
|
|
558
|
+
const records = await Promise.all(entries.map((entry) => readJson(path.join(queueDir, entry))));
|
|
559
|
+
return records.filter((record) => record.claimedBy && record.leaseExpiresAt && record.leaseExpiresAt <= cutoffIso);
|
|
560
|
+
}
|
|
561
|
+
async getRunControl(runId) {
|
|
562
|
+
const filePath = this.runControlPath(runId);
|
|
563
|
+
if (!(await fileExists(filePath))) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
return readJson(filePath);
|
|
567
|
+
}
|
|
568
|
+
async requestRunCancel(runId, reason) {
|
|
569
|
+
const current = await this.getRunControl(runId);
|
|
570
|
+
if (!current) {
|
|
571
|
+
throw new Error(`Missing run control for ${runId}`);
|
|
572
|
+
}
|
|
573
|
+
const updated = {
|
|
574
|
+
...current,
|
|
575
|
+
cancelRequested: true,
|
|
576
|
+
cancelReason: reason ?? null,
|
|
577
|
+
cancelRequestedAt: nowIso(),
|
|
578
|
+
};
|
|
579
|
+
await writeJson(this.runControlPath(runId), updated);
|
|
580
|
+
return updated;
|
|
581
|
+
}
|
|
582
|
+
async clearRunCancel(runId) {
|
|
583
|
+
const current = await this.getRunControl(runId);
|
|
584
|
+
if (!current) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
await writeJson(this.runControlPath(runId), {
|
|
588
|
+
...current,
|
|
589
|
+
cancelRequested: false,
|
|
590
|
+
cancelReason: null,
|
|
591
|
+
cancelRequestedAt: null,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
430
594
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ArtifactListing, ArtifactRecord, HarnessEvent, InternalApprovalRecord, RunState, RunSummary, ThreadRunRecord, ThreadSummary, TranscriptMessage } from "../contracts/types.js";
|
|
2
|
-
import type { PersistedRunRequest, PersistenceLifecycle, PersistenceRunMeta, PersistenceThreadMeta, RecoveryIntent, RuntimePersistence } from "./types.js";
|
|
2
|
+
import type { PersistedRunRequest, PersistedRunControlRecord, PersistedRunQueueRecord, PersistenceLifecycle, PersistenceRunMeta, PersistenceThreadMeta, RecoveryIntent, RuntimePersistence } from "./types.js";
|
|
3
3
|
export declare function listProtectedCheckpointThreadIds(dbPath: string): Promise<Set<string>>;
|
|
4
4
|
export declare class SqlitePersistence implements RuntimePersistence {
|
|
5
5
|
private readonly runRoot;
|
|
@@ -64,5 +64,33 @@ export declare class SqlitePersistence implements RuntimePersistence {
|
|
|
64
64
|
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
65
65
|
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
66
66
|
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
67
|
+
private mapQueuedRun;
|
|
68
|
+
private mapRunControl;
|
|
69
|
+
enqueueRun(input: {
|
|
70
|
+
threadId: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
priority?: number;
|
|
73
|
+
queueKey?: string | null;
|
|
74
|
+
availableAt?: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
|
|
77
|
+
claimQueuedRun(input: {
|
|
78
|
+
threadId: string;
|
|
79
|
+
runId: string;
|
|
80
|
+
workerId: string;
|
|
81
|
+
claimedAt?: string;
|
|
82
|
+
leaseExpiresAt: string;
|
|
83
|
+
}): Promise<PersistedRunQueueRecord>;
|
|
84
|
+
renewRunLease(input: {
|
|
85
|
+
runId: string;
|
|
86
|
+
workerId: string;
|
|
87
|
+
heartbeatAt?: string;
|
|
88
|
+
leaseExpiresAt: string;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
releaseRunClaim(runId: string): Promise<void>;
|
|
91
|
+
listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
|
|
92
|
+
getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
|
|
93
|
+
requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
|
|
94
|
+
clearRunCancel(runId: string): Promise<void>;
|
|
67
95
|
readArtifact(threadId: string, runId: string, artifactPath: string): Promise<unknown>;
|
|
68
96
|
}
|
|
@@ -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;
|
|
@@ -187,6 +187,37 @@ export class SqlitePersistence {
|
|
|
187
187
|
FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
|
|
188
188
|
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
189
189
|
)
|
|
190
|
+
`);
|
|
191
|
+
await this.rawExecute(`
|
|
192
|
+
CREATE TABLE IF NOT EXISTS run_queue (
|
|
193
|
+
run_id TEXT PRIMARY KEY,
|
|
194
|
+
thread_id TEXT NOT NULL,
|
|
195
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
196
|
+
queue_key TEXT,
|
|
197
|
+
enqueued_at TEXT NOT NULL,
|
|
198
|
+
available_at TEXT NOT NULL,
|
|
199
|
+
claimed_by TEXT,
|
|
200
|
+
claimed_at TEXT,
|
|
201
|
+
lease_expires_at TEXT,
|
|
202
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
203
|
+
last_error TEXT,
|
|
204
|
+
FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
|
|
205
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
206
|
+
)
|
|
207
|
+
`);
|
|
208
|
+
await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_available_idx ON run_queue(available_at, priority DESC, enqueued_at ASC)");
|
|
209
|
+
await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_claim_idx ON run_queue(claimed_by, lease_expires_at)");
|
|
210
|
+
await this.rawExecute(`
|
|
211
|
+
CREATE TABLE IF NOT EXISTS run_control (
|
|
212
|
+
run_id TEXT PRIMARY KEY,
|
|
213
|
+
cancel_requested INTEGER NOT NULL DEFAULT 0,
|
|
214
|
+
cancel_reason TEXT,
|
|
215
|
+
cancel_requested_at TEXT,
|
|
216
|
+
heartbeat_at TEXT,
|
|
217
|
+
worker_id TEXT,
|
|
218
|
+
worker_started_at TEXT,
|
|
219
|
+
FOREIGN KEY (run_id) REFERENCES runs(run_id)
|
|
220
|
+
)
|
|
190
221
|
`);
|
|
191
222
|
await this.rawExecute(`
|
|
192
223
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
@@ -292,6 +323,10 @@ export class SqlitePersistence {
|
|
|
292
323
|
if (family !== RUNTIME_SQLITE_SCHEMA_FAMILY) {
|
|
293
324
|
throw new Error(`Unsupported runtime sqlite schema family ${JSON.stringify(family)} in ${this.dbPath}. Expected ${JSON.stringify(RUNTIME_SQLITE_SCHEMA_FAMILY)}.`);
|
|
294
325
|
}
|
|
326
|
+
if (version === "1") {
|
|
327
|
+
await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
295
330
|
if (version !== String(RUNTIME_SQLITE_SCHEMA_VERSION)) {
|
|
296
331
|
throw new Error(`Unsupported runtime sqlite schema version ${JSON.stringify(version)} in ${this.dbPath}. Expected ${RUNTIME_SQLITE_SCHEMA_VERSION}.`);
|
|
297
332
|
}
|
|
@@ -321,6 +356,9 @@ export class SqlitePersistence {
|
|
|
321
356
|
0,
|
|
322
357
|
null,
|
|
323
358
|
]);
|
|
359
|
+
await this.execute(`INSERT OR REPLACE INTO run_control
|
|
360
|
+
(run_id, cancel_requested, cancel_reason, cancel_requested_at, heartbeat_at, worker_id, worker_started_at)
|
|
361
|
+
VALUES (?, 0, NULL, NULL, NULL, NULL, NULL)`, [input.runId]);
|
|
324
362
|
}
|
|
325
363
|
async setRunState(threadId, runId, state, checkpointRef) {
|
|
326
364
|
const current = await this.getRunLifecycle(threadId, runId);
|
|
@@ -445,9 +483,11 @@ export class SqlitePersistence {
|
|
|
445
483
|
await this.execute("DELETE FROM artifacts WHERE thread_id = ?", [threadId]);
|
|
446
484
|
await this.execute("DELETE FROM approvals WHERE thread_id = ?", [threadId]);
|
|
447
485
|
await this.execute("DELETE FROM events WHERE thread_id = ?", [threadId]);
|
|
486
|
+
await this.execute("DELETE FROM run_queue WHERE thread_id = ?", [threadId]);
|
|
448
487
|
await this.execute("DELETE FROM run_requests WHERE thread_id = ?", [threadId]);
|
|
449
488
|
await this.execute("DELETE FROM recovery_intents WHERE thread_id = ?", [threadId]);
|
|
450
489
|
await this.execute("DELETE FROM thread_messages WHERE thread_id = ?", [threadId]);
|
|
490
|
+
await this.execute("DELETE FROM run_control WHERE run_id IN (SELECT run_id FROM runs WHERE thread_id = ?)", [threadId]);
|
|
451
491
|
await this.execute("DELETE FROM runs WHERE thread_id = ?", [threadId]);
|
|
452
492
|
await this.execute("DELETE FROM threads WHERE thread_id = ?", [threadId]);
|
|
453
493
|
await rm(this.threadDir(threadId), { recursive: true, force: true });
|
|
@@ -559,6 +599,108 @@ export class SqlitePersistence {
|
|
|
559
599
|
async clearRecoveryIntent(threadId, runId) {
|
|
560
600
|
await this.execute("DELETE FROM recovery_intents WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
|
|
561
601
|
}
|
|
602
|
+
mapQueuedRun(row) {
|
|
603
|
+
return {
|
|
604
|
+
runId: asString(row.run_id),
|
|
605
|
+
threadId: asString(row.thread_id),
|
|
606
|
+
priority: Number(row.priority ?? 0),
|
|
607
|
+
queueKey: asNullableString(row.queue_key),
|
|
608
|
+
enqueuedAt: asString(row.enqueued_at),
|
|
609
|
+
availableAt: asString(row.available_at),
|
|
610
|
+
claimedBy: asNullableString(row.claimed_by),
|
|
611
|
+
claimedAt: asNullableString(row.claimed_at),
|
|
612
|
+
leaseExpiresAt: asNullableString(row.lease_expires_at),
|
|
613
|
+
attemptCount: Number(row.attempt_count ?? 0),
|
|
614
|
+
lastError: asNullableString(row.last_error),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
mapRunControl(row) {
|
|
618
|
+
return {
|
|
619
|
+
runId: asString(row.run_id),
|
|
620
|
+
cancelRequested: asBoolean(row.cancel_requested),
|
|
621
|
+
cancelReason: asNullableString(row.cancel_reason),
|
|
622
|
+
cancelRequestedAt: asNullableString(row.cancel_requested_at),
|
|
623
|
+
heartbeatAt: asNullableString(row.heartbeat_at),
|
|
624
|
+
workerId: asNullableString(row.worker_id),
|
|
625
|
+
workerStartedAt: asNullableString(row.worker_started_at),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
async enqueueRun(input) {
|
|
629
|
+
const enqueuedAt = nowIso();
|
|
630
|
+
await this.execute(`INSERT OR REPLACE INTO run_queue
|
|
631
|
+
(run_id, thread_id, priority, queue_key, enqueued_at, available_at, claimed_by, claimed_at, lease_expires_at, attempt_count, last_error)
|
|
632
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, COALESCE((SELECT attempt_count FROM run_queue WHERE run_id = ?), 0), NULL)`, [
|
|
633
|
+
input.runId,
|
|
634
|
+
input.threadId,
|
|
635
|
+
input.priority ?? 0,
|
|
636
|
+
input.queueKey ?? null,
|
|
637
|
+
enqueuedAt,
|
|
638
|
+
input.availableAt ?? enqueuedAt,
|
|
639
|
+
input.runId,
|
|
640
|
+
]);
|
|
641
|
+
}
|
|
642
|
+
async getQueuedRun(runId) {
|
|
643
|
+
const row = await this.selectOne("SELECT * FROM run_queue WHERE run_id = ?", [runId]);
|
|
644
|
+
return row ? this.mapQueuedRun(row) : null;
|
|
645
|
+
}
|
|
646
|
+
async claimQueuedRun(input) {
|
|
647
|
+
const claimedAt = input.claimedAt ?? nowIso();
|
|
648
|
+
await this.execute(`UPDATE run_queue
|
|
649
|
+
SET claimed_by = ?, claimed_at = ?, lease_expires_at = ?, attempt_count = attempt_count + 1
|
|
650
|
+
WHERE run_id = ? AND thread_id = ?`, [input.workerId, claimedAt, input.leaseExpiresAt, input.runId, input.threadId]);
|
|
651
|
+
await this.execute(`UPDATE run_control
|
|
652
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
653
|
+
WHERE run_id = ?`, [claimedAt, input.workerId, claimedAt, input.runId]);
|
|
654
|
+
const claimed = await this.getQueuedRun(input.runId);
|
|
655
|
+
if (!claimed) {
|
|
656
|
+
throw new Error(`Missing queued run ${input.runId}`);
|
|
657
|
+
}
|
|
658
|
+
return claimed;
|
|
659
|
+
}
|
|
660
|
+
async renewRunLease(input) {
|
|
661
|
+
const heartbeatAt = input.heartbeatAt ?? nowIso();
|
|
662
|
+
await this.execute(`UPDATE run_queue
|
|
663
|
+
SET lease_expires_at = ?, claimed_by = COALESCE(claimed_by, ?), claimed_at = COALESCE(claimed_at, ?)
|
|
664
|
+
WHERE run_id = ?`, [input.leaseExpiresAt, input.workerId, heartbeatAt, input.runId]);
|
|
665
|
+
await this.execute(`UPDATE run_control
|
|
666
|
+
SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
|
|
667
|
+
WHERE run_id = ?`, [heartbeatAt, input.workerId, heartbeatAt, input.runId]);
|
|
668
|
+
}
|
|
669
|
+
async releaseRunClaim(runId) {
|
|
670
|
+
await this.execute("DELETE FROM run_queue WHERE run_id = ?", [runId]);
|
|
671
|
+
await this.execute(`UPDATE run_control
|
|
672
|
+
SET heartbeat_at = NULL, worker_id = NULL, worker_started_at = NULL
|
|
673
|
+
WHERE run_id = ?`, [runId]);
|
|
674
|
+
}
|
|
675
|
+
async listExpiredClaimedRuns(cutoffIso) {
|
|
676
|
+
const rows = await this.selectAll(`SELECT *
|
|
677
|
+
FROM run_queue
|
|
678
|
+
WHERE claimed_by IS NOT NULL
|
|
679
|
+
AND lease_expires_at IS NOT NULL
|
|
680
|
+
AND lease_expires_at <= ?
|
|
681
|
+
ORDER BY lease_expires_at ASC, run_id ASC`, [cutoffIso]);
|
|
682
|
+
return rows.map((row) => this.mapQueuedRun(row));
|
|
683
|
+
}
|
|
684
|
+
async getRunControl(runId) {
|
|
685
|
+
const row = await this.selectOne("SELECT * FROM run_control WHERE run_id = ?", [runId]);
|
|
686
|
+
return row ? this.mapRunControl(row) : null;
|
|
687
|
+
}
|
|
688
|
+
async requestRunCancel(runId, reason) {
|
|
689
|
+
const now = nowIso();
|
|
690
|
+
await this.execute(`UPDATE run_control
|
|
691
|
+
SET cancel_requested = 1, cancel_reason = ?, cancel_requested_at = ?
|
|
692
|
+
WHERE run_id = ?`, [reason ?? null, now, runId]);
|
|
693
|
+
const updated = await this.getRunControl(runId);
|
|
694
|
+
if (!updated) {
|
|
695
|
+
throw new Error(`Missing run control for ${runId}`);
|
|
696
|
+
}
|
|
697
|
+
return updated;
|
|
698
|
+
}
|
|
699
|
+
async clearRunCancel(runId) {
|
|
700
|
+
await this.execute(`UPDATE run_control
|
|
701
|
+
SET cancel_requested = 0, cancel_reason = NULL, cancel_requested_at = NULL
|
|
702
|
+
WHERE run_id = ?`, [runId]);
|
|
703
|
+
}
|
|
562
704
|
async readArtifact(threadId, runId, artifactPath) {
|
|
563
705
|
const filePath = path.join(this.runDir(threadId, runId), artifactPath);
|
|
564
706
|
if (!(await fileExists(filePath))) {
|
|
@@ -37,6 +37,28 @@ 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
|
+
};
|
|
40
62
|
export interface RuntimePersistence {
|
|
41
63
|
initialize(): Promise<void>;
|
|
42
64
|
createThread(input: {
|
|
@@ -80,4 +102,30 @@ export interface RuntimePersistence {
|
|
|
80
102
|
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
81
103
|
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
82
104
|
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
105
|
+
enqueueRun(input: {
|
|
106
|
+
threadId: string;
|
|
107
|
+
runId: string;
|
|
108
|
+
priority?: number;
|
|
109
|
+
queueKey?: string | null;
|
|
110
|
+
availableAt?: string;
|
|
111
|
+
}): Promise<void>;
|
|
112
|
+
getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
|
|
113
|
+
claimQueuedRun(input: {
|
|
114
|
+
threadId: string;
|
|
115
|
+
runId: string;
|
|
116
|
+
workerId: string;
|
|
117
|
+
claimedAt?: string;
|
|
118
|
+
leaseExpiresAt: string;
|
|
119
|
+
}): Promise<PersistedRunQueueRecord>;
|
|
120
|
+
renewRunLease(input: {
|
|
121
|
+
runId: string;
|
|
122
|
+
workerId: string;
|
|
123
|
+
heartbeatAt?: string;
|
|
124
|
+
leaseExpiresAt: string;
|
|
125
|
+
}): Promise<void>;
|
|
126
|
+
releaseRunClaim(runId: string): Promise<void>;
|
|
127
|
+
listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
|
|
128
|
+
getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
|
|
129
|
+
requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
|
|
130
|
+
clearRunCancel(runId: string): Promise<void>;
|
|
83
131
|
}
|
|
@@ -1377,7 +1377,15 @@ export class AgentRuntimeAdapter {
|
|
|
1377
1377
|
const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
|
|
1378
1378
|
const emptyAssistantMessageFailure = extractEmptyAssistantMessageFailure(result);
|
|
1379
1379
|
const toolFallback = extractToolFallbackContext(result);
|
|
1380
|
-
|
|
1380
|
+
let synthesizedOutput = "";
|
|
1381
|
+
try {
|
|
1382
|
+
synthesizedOutput = await this.synthesizeDeepAgentAnswer(binding, input, result);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
if (!(error instanceof RuntimeOperationTimeoutError) || !toolFallback) {
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1381
1389
|
if (!visibleOutput && !synthesizedOutput && !toolFallback && emptyAssistantMessageFailure) {
|
|
1382
1390
|
throw new Error(emptyAssistantMessageFailure);
|
|
1383
1391
|
}
|
|
@@ -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 };
|