@botbotgo/agent-harness 0.0.102 → 0.0.103

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.
@@ -0,0 +1,59 @@
1
+ import { isTerminalRunState, toPublicApprovalRecord } from "./helpers.js";
2
+ export async function getThreadRecord(input, threadId) {
3
+ const [threadSummary, meta, messages, runs] = await Promise.all([
4
+ input.getSession(threadId),
5
+ input.persistence.getThreadMeta(threadId),
6
+ input.persistence.listThreadMessages(threadId, 200),
7
+ input.persistence.listThreadRuns(threadId),
8
+ ]);
9
+ if (!threadSummary || !meta) {
10
+ return null;
11
+ }
12
+ const latestRunId = threadSummary.latestRunId;
13
+ const latestApprovals = await input.persistence.getRunApprovals(threadId, latestRunId);
14
+ const pendingApproval = latestApprovals
15
+ .filter((approval) => approval.status === "pending")
16
+ .sort((left, right) => right.requestedAt.localeCompare(left.requestedAt))[0];
17
+ return {
18
+ threadId,
19
+ entryAgentId: meta.entryAgentId,
20
+ currentState: threadSummary.status,
21
+ latestRunId,
22
+ createdAt: meta.createdAt,
23
+ updatedAt: threadSummary.updatedAt,
24
+ messages,
25
+ runs,
26
+ pendingDecision: pendingApproval
27
+ ? {
28
+ approvalId: pendingApproval.approvalId,
29
+ pendingActionId: pendingApproval.pendingActionId,
30
+ toolName: pendingApproval.toolName,
31
+ allowedDecisions: pendingApproval.allowedDecisions,
32
+ requestedAt: pendingApproval.requestedAt,
33
+ }
34
+ : undefined,
35
+ };
36
+ }
37
+ export async function listPublicApprovals(input, filter) {
38
+ const approvals = await input.persistence.listApprovals(filter);
39
+ return approvals.map((approval) => toPublicApprovalRecord(approval));
40
+ }
41
+ export async function getPublicApproval(input, approvalId) {
42
+ const approval = await input.persistence.getApproval(approvalId);
43
+ return approval ? toPublicApprovalRecord(approval) : null;
44
+ }
45
+ export async function deleteThreadRecord(input, threadId) {
46
+ const thread = await input.getThread(threadId);
47
+ if (!thread) {
48
+ return false;
49
+ }
50
+ const activeRun = thread.runs.find((run) => !isTerminalRunState(run.state));
51
+ if (activeRun) {
52
+ throw new Error(`Cannot delete thread ${threadId} while run ${activeRun.runId} is ${activeRun.state}`);
53
+ }
54
+ const deleted = await input.deleteThread(threadId);
55
+ if (deleted) {
56
+ await input.deleteThreadCheckpoints(threadId);
57
+ }
58
+ return deleted;
59
+ }
@@ -14,7 +14,7 @@ import { RuntimeRecordMaintenanceLoop, discoverRuntimeRecordMaintenanceTargets,
14
14
  import { HealthMonitor } from "./harness/system/health-monitor.js";
15
15
  import { readHealthMonitorConfig } from "./harness/system/health-monitor.js";
16
16
  import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
17
- import { buildPersistedRunRequest, isTerminalRunState, normalizeInvocationEnvelope, normalizeRunPriority, resolveRunListeners, toPublicApprovalRecord, } from "./harness/run/helpers.js";
17
+ import { buildPersistedRunRequest, normalizeInvocationEnvelope, normalizeRunPriority, resolveRunListeners, } from "./harness/run/helpers.js";
18
18
  import { emitHarnessEvent, emitRunCreatedEvent, emitSyntheticFallbackEvent, requestApprovalAndEmitEvent, setRunStateAndEmitEvent, } from "./harness/events/events.js";
19
19
  import { appendAssistantMessage as appendLifecycleAssistantMessage, finalizeCancelledRun as finalizeLifecycleCancelledRun, finalizeContinuedRun as finalizeLifecycleContinuedRun, } from "./harness/run/run-lifecycle.js";
20
20
  import { dispatchRunListeners as dispatchStreamingRunListeners, } from "./harness/events/streaming.js";
@@ -29,8 +29,9 @@ import { getBindingAdapterKind, getBindingPrimaryTools, getBindingStoreConfig }
29
29
  import { isRuntimeEntryBinding } from "./support/runtime-entry.js";
30
30
  import { describeWorkspaceInventory, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
31
31
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
32
- import { recoverQueuedStartupRun, recoverResumingStartupRun, recoverRunningStartupRun, } from "./harness/run/recovery.js";
32
+ import { initializeHarnessRuntime, isStaleRunningRun as isHarnessStaleRunningRun, reclaimExpiredClaimedRuns as reclaimHarnessExpiredClaimedRuns, recoverStartupRuns as recoverHarnessStartupRuns, } from "./harness/run/startup-runtime.js";
33
33
  import { streamHarnessRun } from "./harness/run/stream-run.js";
34
+ import { deleteThreadRecord, getPublicApproval, getThreadRecord, listPublicApprovals, } from "./harness/run/thread-records.js";
34
35
  export class AgentHarnessRuntime {
35
36
  workspace;
36
37
  runtimeAdapterOptions;
@@ -159,11 +160,13 @@ export class AgentHarnessRuntime {
159
160
  this.healthMonitor?.recordLlmFailure(Date.now() - startedAt);
160
161
  }
161
162
  async initialize() {
162
- await this.persistence.initialize();
163
- await this.checkpointMaintenance?.start();
164
- await this.runtimeRecordMaintenance?.start();
165
- await this.healthMonitor?.start();
166
- await this.recoverStartupRuns();
163
+ await initializeHarnessRuntime({
164
+ persistence: this.persistence,
165
+ checkpointMaintenance: this.checkpointMaintenance,
166
+ runtimeRecordMaintenance: this.runtimeRecordMaintenance,
167
+ healthMonitor: this.healthMonitor,
168
+ recoverStartupRuns: () => this.recoverStartupRuns(),
169
+ });
167
170
  }
168
171
  subscribe(listener) {
169
172
  return this.eventBus.subscribe(listener);
@@ -204,47 +207,20 @@ export class AgentHarnessRuntime {
204
207
  return this.persistence.getSession(threadId);
205
208
  }
206
209
  async getThread(threadId) {
207
- const [threadSummary, meta, messages, runs] = await Promise.all([
208
- this.getSession(threadId),
209
- this.persistence.getThreadMeta(threadId),
210
- this.persistence.listThreadMessages(threadId, 200),
211
- this.persistence.listThreadRuns(threadId),
212
- ]);
213
- if (!threadSummary || !meta) {
214
- return null;
215
- }
216
- const latestRunId = threadSummary.latestRunId;
217
- const latestApprovals = await this.persistence.getRunApprovals(threadId, latestRunId);
218
- const pendingApproval = latestApprovals
219
- .filter((approval) => approval.status === "pending")
220
- .sort((left, right) => right.requestedAt.localeCompare(left.requestedAt))[0];
221
- return {
222
- threadId,
223
- entryAgentId: meta.entryAgentId,
224
- currentState: threadSummary.status,
225
- latestRunId,
226
- createdAt: meta.createdAt,
227
- updatedAt: threadSummary.updatedAt,
228
- messages,
229
- runs,
230
- pendingDecision: pendingApproval
231
- ? {
232
- approvalId: pendingApproval.approvalId,
233
- pendingActionId: pendingApproval.pendingActionId,
234
- toolName: pendingApproval.toolName,
235
- allowedDecisions: pendingApproval.allowedDecisions,
236
- requestedAt: pendingApproval.requestedAt,
237
- }
238
- : undefined,
239
- };
210
+ return getThreadRecord({
211
+ persistence: this.persistence,
212
+ getSession: (currentThreadId) => this.getSession(currentThreadId),
213
+ }, threadId);
240
214
  }
241
215
  async listApprovals(filter) {
242
- const approvals = await this.persistence.listApprovals(filter);
243
- return approvals.map((approval) => toPublicApprovalRecord(approval));
216
+ return listPublicApprovals({
217
+ persistence: this.persistence,
218
+ }, filter);
244
219
  }
245
220
  async getApproval(approvalId) {
246
- const approval = await this.persistence.getApproval(approvalId);
247
- return approval ? toPublicApprovalRecord(approval) : null;
221
+ return getPublicApproval({
222
+ persistence: this.persistence,
223
+ }, approvalId);
248
224
  }
249
225
  listAgentSkills(agentId, options = {}) {
250
226
  return listWorkspaceAgentSkills(this.workspace, agentId, {
@@ -277,19 +253,11 @@ export class AgentHarnessRuntime {
277
253
  }
278
254
  }
279
255
  async deleteThread(threadId) {
280
- const thread = await this.getThread(threadId);
281
- if (!thread) {
282
- return false;
283
- }
284
- const activeRun = thread.runs.find((run) => !isTerminalRunState(run.state));
285
- if (activeRun) {
286
- throw new Error(`Cannot delete thread ${threadId} while run ${activeRun.runId} is ${activeRun.state}`);
287
- }
288
- const deleted = await this.persistence.deleteThread(threadId);
289
- if (deleted) {
290
- await this.deleteThreadCheckpoints(threadId);
291
- }
292
- return deleted;
256
+ return deleteThreadRecord({
257
+ getThread: (currentThreadId) => this.getThread(currentThreadId),
258
+ deleteThread: (currentThreadId) => this.persistence.deleteThread(currentThreadId),
259
+ deleteThreadCheckpoints: (currentThreadId) => this.deleteThreadCheckpoints(currentThreadId),
260
+ }, threadId);
293
261
  }
294
262
  async createToolMcpServer(options) {
295
263
  const tools = this.resolveAgentTools(options.agentId).map(({ compiledTool, resolvedTool }) => ({
@@ -771,63 +739,26 @@ export class AgentHarnessRuntime {
771
739
  }, options);
772
740
  }
773
741
  async recoverStartupRuns() {
774
- if (!this.recoveryConfig.enabled) {
775
- return;
776
- }
777
- await this.reclaimExpiredClaimedRuns();
778
- const threads = await this.persistence.listSessions();
779
- const recoveryContext = this.createStartupRecoveryContext();
780
- for (const thread of threads) {
781
- const handled = await recoverQueuedStartupRun(recoveryContext, thread) ||
782
- await recoverRunningStartupRun(recoveryContext, thread) ||
783
- await recoverResumingStartupRun(recoveryContext, thread);
784
- if (handled) {
785
- continue;
786
- }
787
- }
742
+ await recoverHarnessStartupRuns({
743
+ recoveryConfig: this.recoveryConfig,
744
+ persistence: this.persistence,
745
+ createStartupRecoveryContext: () => this.createStartupRecoveryContext(),
746
+ reclaimExpiredClaimedRuns: (nowIso) => this.reclaimExpiredClaimedRuns(nowIso),
747
+ });
788
748
  }
789
749
  async reclaimExpiredClaimedRuns(nowIso = new Date().toISOString()) {
790
- const expiredClaims = await this.persistence.listExpiredClaimedRuns(nowIso);
791
- for (const claim of expiredClaims) {
792
- const thread = await this.persistence.getSession(claim.threadId);
793
- if (!thread) {
794
- await this.persistence.releaseRunClaim(claim.runId);
795
- continue;
796
- }
797
- const lifecycle = await this.persistence.getRunLifecycle(claim.threadId, claim.runId);
798
- if (lifecycle.state === "claimed") {
799
- await this.persistence.enqueueRun({
800
- threadId: claim.threadId,
801
- runId: claim.runId,
802
- priority: claim.priority,
803
- queueKey: claim.queueKey,
804
- availableAt: nowIso,
805
- });
806
- await this.setRunStateAndEmit(claim.threadId, claim.runId, 99, "queued", {
807
- previousState: "claimed",
808
- });
809
- await this.emit(claim.threadId, claim.runId, 100, "run.queued", {
810
- queuePosition: 0,
811
- activeRunCount: this.activeRunSlots,
812
- maxConcurrentRuns: this.concurrencyConfig.maxConcurrentRuns,
813
- recoveredOnStartup: true,
814
- reclaimReason: "expired-lease",
815
- });
816
- continue;
817
- }
818
- await this.persistence.releaseRunClaim(claim.runId);
819
- }
750
+ await reclaimHarnessExpiredClaimedRuns({
751
+ persistence: this.persistence,
752
+ setRunStateAndEmit: (threadId, runId, sequence, state, options) => this.setRunStateAndEmit(threadId, runId, sequence, state, options),
753
+ emit: (threadId, runId, sequence, eventType, payload) => this.emit(threadId, runId, sequence, eventType, payload),
754
+ concurrencyConfig: this.concurrencyConfig,
755
+ getActiveRunSlots: () => this.activeRunSlots,
756
+ }, nowIso);
820
757
  }
821
758
  async isStaleRunningRun(thread, nowMs = Date.now()) {
822
- const control = await this.persistence.getRunControl(thread.latestRunId);
823
- const heartbeatAt = control?.heartbeatAt;
824
- if (!heartbeatAt) {
825
- return true;
826
- }
827
- const heartbeatAtMs = Date.parse(heartbeatAt);
828
- if (!Number.isFinite(heartbeatAtMs)) {
829
- return true;
830
- }
831
- return nowMs - heartbeatAtMs >= this.concurrencyConfig.heartbeatTimeoutMs;
759
+ return isHarnessStaleRunningRun({
760
+ persistence: this.persistence,
761
+ concurrencyConfig: this.concurrencyConfig,
762
+ }, thread, nowMs);
832
763
  }
833
764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.102",
3
+ "version": "0.0.103",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",