@botbotgo/agent-harness 0.0.45 → 0.0.47

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.
@@ -7,6 +7,18 @@ export class FilePersistence {
7
7
  constructor(runRoot) {
8
8
  this.runRoot = runRoot;
9
9
  }
10
+ threadIndexPath(threadId) {
11
+ return path.join(this.runRoot, "indexes", "threads", `${threadId}.json`);
12
+ }
13
+ runIndexPath(runId) {
14
+ return path.join(this.runRoot, "indexes", "runs", `${runId}.json`);
15
+ }
16
+ approvalIndexPath(approvalId) {
17
+ return path.join(this.runRoot, "indexes", "approvals", `${approvalId}.json`);
18
+ }
19
+ delegationIndexPath(delegationId) {
20
+ return path.join(this.runRoot, "indexes", "delegations", `${delegationId}.json`);
21
+ }
10
22
  async initialize() {
11
23
  await Promise.all([
12
24
  "indexes/threads",
@@ -266,6 +278,48 @@ export class FilePersistence {
266
278
  async getRunLifecycle(threadId, runId) {
267
279
  return readJson(path.join(this.runDir(threadId, runId), "lifecycle.json"));
268
280
  }
281
+ async deleteThread(threadId) {
282
+ const threadDir = this.threadDir(threadId);
283
+ const threadIndexPath = this.threadIndexPath(threadId);
284
+ if (!(await fileExists(threadDir)) && !(await fileExists(threadIndexPath))) {
285
+ return false;
286
+ }
287
+ const [runIndexes, approvals, delegations] = await Promise.all([
288
+ this.listRunIndexes(),
289
+ this.listApprovals(),
290
+ this.listDelegations(),
291
+ ]);
292
+ await Promise.all([
293
+ ...runIndexes
294
+ .filter((record) => record.threadId === threadId)
295
+ .map((record) => rm(this.runIndexPath(record.runId), { force: true })),
296
+ ...approvals
297
+ .filter((record) => record.threadId === threadId)
298
+ .map((record) => rm(this.approvalIndexPath(record.approvalId), { force: true })),
299
+ ...delegations
300
+ .filter((record) => record.threadId === threadId)
301
+ .map((record) => rm(this.delegationIndexPath(record.delegationId), { force: true })),
302
+ rm(threadIndexPath, { force: true }),
303
+ rm(threadDir, { recursive: true, force: true }),
304
+ ]);
305
+ return true;
306
+ }
307
+ async saveRunRequest(threadId, runId, request) {
308
+ await writeJson(path.join(this.runDir(threadId, runId), "request.json"), request);
309
+ }
310
+ async getRunRequest(threadId, runId) {
311
+ const requestPath = path.join(this.runDir(threadId, runId), "request.json");
312
+ if (!(await fileExists(requestPath))) {
313
+ return null;
314
+ }
315
+ return readJson(requestPath);
316
+ }
317
+ async clearRunRequest(threadId, runId) {
318
+ const requestPath = path.join(this.runDir(threadId, runId), "request.json");
319
+ if (await fileExists(requestPath)) {
320
+ await rm(requestPath, { force: true });
321
+ }
322
+ }
269
323
  async listDelegations() {
270
324
  const delegationsDir = path.join(this.runRoot, "indexes", "delegations");
271
325
  if (!(await fileExists(delegationsDir))) {
@@ -6,6 +6,7 @@ export type ResourceToolInfo = {
6
6
  backendOperation: string;
7
7
  name: string;
8
8
  description: string;
9
+ retryable?: boolean;
9
10
  hitl?: {
10
11
  enabled: boolean;
11
12
  allow: Array<"approve" | "edit" | "reject">;
@@ -26,6 +26,7 @@ export declare class AgentHarnessRuntime {
26
26
  private readonly pendingRunSlots;
27
27
  private toPublicApprovalRecord;
28
28
  private normalizeInvocationEnvelope;
29
+ private isTerminalRunState;
29
30
  private listHostBindings;
30
31
  private defaultRunRoot;
31
32
  private heuristicRoute;
@@ -40,6 +41,7 @@ export declare class AgentHarnessRuntime {
40
41
  private getBinding;
41
42
  private listAgentTools;
42
43
  private resolveAgentTools;
44
+ private supportsRunningReplay;
43
45
  listThreads(filter?: {
44
46
  agentId?: string;
45
47
  }): Promise<ThreadSummary[]>;
@@ -51,6 +53,8 @@ export declare class AgentHarnessRuntime {
51
53
  runId?: string;
52
54
  }): Promise<ApprovalRecord[]>;
53
55
  getApproval(approvalId: string): Promise<ApprovalRecord | null>;
56
+ private deleteThreadCheckpoints;
57
+ deleteThread(threadId: string): Promise<boolean>;
54
58
  createToolMcpServer(options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
55
59
  serveToolsOverStdio(options: ToolMcpServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp.js").McpServer>;
56
60
  routeAgent(input: MessageContent, options?: {
@@ -62,6 +66,8 @@ export declare class AgentHarnessRuntime {
62
66
  private loadRunInput;
63
67
  private appendAssistantMessage;
64
68
  private invokeWithHistory;
69
+ private buildPersistedRunRequest;
70
+ private executeQueuedRun;
65
71
  private checkpointRefForState;
66
72
  private finalizeContinuedRun;
67
73
  private emitOutputDeltaAndCreateItem;
@@ -53,6 +53,9 @@ export class AgentHarnessRuntime {
53
53
  invocation,
54
54
  };
55
55
  }
56
+ isTerminalRunState(state) {
57
+ return state === "completed" || state === "failed";
58
+ }
56
59
  listHostBindings() {
57
60
  return inferRoutingBindings(this.workspace).hostBindings;
58
61
  }
@@ -213,6 +216,10 @@ export class AgentHarnessRuntime {
213
216
  resolvedTool: resolvedTools[index],
214
217
  }));
215
218
  }
219
+ supportsRunningReplay(binding) {
220
+ const tools = getBindingPrimaryTools(binding);
221
+ return tools.every((tool) => tool.retryable === true);
222
+ }
216
223
  async listThreads(filter) {
217
224
  const threadSummaries = await this.persistence.listSessions();
218
225
  if (!filter?.agentId) {
@@ -279,6 +286,39 @@ export class AgentHarnessRuntime {
279
286
  const approval = await this.persistence.getApproval(approvalId);
280
287
  return approval ? this.toPublicApprovalRecord(approval) : null;
281
288
  }
289
+ async deleteThreadCheckpoints(threadId) {
290
+ const resolver = this.resolvedRuntimeAdapterOptions.checkpointerResolver;
291
+ if (!resolver) {
292
+ return;
293
+ }
294
+ const seen = new Set();
295
+ for (const binding of this.workspace.bindings.values()) {
296
+ const saver = resolver(binding);
297
+ if (!saver || seen.has(saver)) {
298
+ continue;
299
+ }
300
+ seen.add(saver);
301
+ const maybeDeleteThread = saver.deleteThread;
302
+ if (typeof maybeDeleteThread === "function") {
303
+ await maybeDeleteThread.call(saver, threadId);
304
+ }
305
+ }
306
+ }
307
+ async deleteThread(threadId) {
308
+ const thread = await this.getThread(threadId);
309
+ if (!thread) {
310
+ return false;
311
+ }
312
+ const activeRun = thread.runs.find((run) => !this.isTerminalRunState(run.state));
313
+ if (activeRun) {
314
+ throw new Error(`Cannot delete thread ${threadId} while run ${activeRun.runId} is ${activeRun.state}`);
315
+ }
316
+ const deleted = await this.persistence.deleteThread(threadId);
317
+ if (deleted) {
318
+ await this.deleteThreadCheckpoints(threadId);
319
+ }
320
+ return deleted;
321
+ }
282
322
  async createToolMcpServer(options) {
283
323
  const tools = this.resolveAgentTools(options.agentId).map(({ compiledTool, resolvedTool }) => ({
284
324
  compiledTool,
@@ -385,6 +425,72 @@ export class AgentHarnessRuntime {
385
425
  const priorHistory = await this.loadPriorHistory(threadId, runId);
386
426
  return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory, options);
387
427
  }
428
+ buildPersistedRunRequest(input, invocation) {
429
+ const envelope = invocation.invocation ?? {
430
+ ...(invocation.context ? { context: invocation.context } : {}),
431
+ ...(invocation.state ? { inputs: invocation.state } : {}),
432
+ ...(invocation.files ? { attachments: invocation.files } : {}),
433
+ };
434
+ return {
435
+ input: normalizeMessageContent(input),
436
+ invocation: envelope && Object.keys(envelope).length > 0
437
+ ? {
438
+ ...(envelope.context ? { context: envelope.context } : {}),
439
+ ...(envelope.inputs ? { inputs: envelope.inputs } : {}),
440
+ ...(envelope.attachments ? { attachments: envelope.attachments } : {}),
441
+ ...(envelope.capabilities ? { capabilities: envelope.capabilities } : {}),
442
+ }
443
+ : undefined,
444
+ savedAt: new Date().toISOString(),
445
+ };
446
+ }
447
+ async executeQueuedRun(binding, input, threadId, runId, agentId, options = {}) {
448
+ const previousState = options.previousState ?? "running";
449
+ if (previousState === "queued") {
450
+ await this.emit(threadId, runId, 101, "run.dequeued", {
451
+ queuePosition: 0,
452
+ activeRunCount: this.activeRunSlots,
453
+ maxConcurrentRuns: this.concurrencyConfig.maxConcurrentRuns,
454
+ recoveredOnStartup: true,
455
+ });
456
+ await this.setRunStateAndEmit(threadId, runId, 102, "running", {
457
+ previousState: "queued",
458
+ });
459
+ }
460
+ try {
461
+ const actual = await this.invokeWithHistory(binding, input, threadId, runId, undefined, {
462
+ context: options.context,
463
+ state: options.state,
464
+ files: options.files,
465
+ });
466
+ const finalized = await this.finalizeContinuedRun(threadId, runId, input, actual, {
467
+ previousState: previousState === "queued" ? "running" : previousState,
468
+ stateSequence: options.stateSequence ?? 103,
469
+ approvalSequence: options.approvalSequence ?? 104,
470
+ });
471
+ return {
472
+ ...finalized,
473
+ agentId,
474
+ };
475
+ }
476
+ catch (error) {
477
+ await this.emitSyntheticFallback(threadId, runId, agentId, error, 103);
478
+ await this.setRunStateAndEmit(threadId, runId, 104, "failed", {
479
+ previousState: previousState === "queued" ? "running" : previousState,
480
+ error: error instanceof Error ? error.message : String(error),
481
+ });
482
+ return {
483
+ threadId,
484
+ runId,
485
+ agentId,
486
+ state: "failed",
487
+ output: renderRuntimeFailure(error),
488
+ };
489
+ }
490
+ finally {
491
+ await this.persistence.clearRunRequest(threadId, runId);
492
+ }
493
+ }
388
494
  checkpointRefForState(threadId, runId, state) {
389
495
  return state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
390
496
  }
@@ -492,17 +598,56 @@ export class AgentHarnessRuntime {
492
598
  }
493
599
  await listener(value);
494
600
  }
495
- async acquireRunSlot() {
601
+ async acquireRunSlot(threadId, runId, activeState = "running") {
496
602
  const maxConcurrentRuns = this.concurrencyConfig.maxConcurrentRuns;
497
603
  if (!maxConcurrentRuns) {
498
604
  return () => undefined;
499
605
  }
500
- if (this.activeRunSlots >= maxConcurrentRuns) {
501
- await new Promise((resolve) => {
502
- this.pendingRunSlots.push(resolve);
606
+ if (this.activeRunSlots < maxConcurrentRuns) {
607
+ this.activeRunSlots += 1;
608
+ let released = false;
609
+ return () => {
610
+ if (released) {
611
+ return;
612
+ }
613
+ released = true;
614
+ this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
615
+ const next = this.pendingRunSlots.shift();
616
+ void next?.();
617
+ };
618
+ }
619
+ if (threadId && runId) {
620
+ const queuePosition = this.pendingRunSlots.length + 1;
621
+ await this.setRunStateAndEmit(threadId, runId, 2, "queued", {
622
+ previousState: activeState,
623
+ });
624
+ await this.emit(threadId, runId, 3, "run.queued", {
625
+ queuePosition,
626
+ activeRunCount: this.activeRunSlots,
627
+ maxConcurrentRuns,
503
628
  });
504
629
  }
505
- this.activeRunSlots += 1;
630
+ await new Promise((resolve, reject) => {
631
+ this.pendingRunSlots.push(async () => {
632
+ try {
633
+ this.activeRunSlots += 1;
634
+ if (threadId && runId) {
635
+ await this.emit(threadId, runId, 4, "run.dequeued", {
636
+ queuePosition: 0,
637
+ activeRunCount: this.activeRunSlots,
638
+ maxConcurrentRuns,
639
+ });
640
+ await this.setRunStateAndEmit(threadId, runId, 5, activeState, {
641
+ previousState: "queued",
642
+ });
643
+ }
644
+ resolve();
645
+ }
646
+ catch (error) {
647
+ reject(error);
648
+ }
649
+ });
650
+ });
506
651
  let released = false;
507
652
  return () => {
508
653
  if (released) {
@@ -511,7 +656,7 @@ export class AgentHarnessRuntime {
511
656
  released = true;
512
657
  this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
513
658
  const next = this.pendingRunSlots.shift();
514
- next?.();
659
+ void next?.();
515
660
  };
516
661
  }
517
662
  async dispatchRunListeners(stream, listeners) {
@@ -587,88 +732,68 @@ export class AgentHarnessRuntime {
587
732
  if (options.listeners) {
588
733
  return this.dispatchRunListeners(this.streamEvents(options), options.listeners);
589
734
  }
590
- const releaseRunSlot = await this.acquireRunSlot();
735
+ const invocation = this.normalizeInvocationEnvelope(options);
736
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
737
+ const binding = this.workspace.bindings.get(selectedAgentId);
738
+ if (!binding) {
739
+ throw new Error(`Unknown agent ${selectedAgentId}`);
740
+ }
741
+ const policyDecision = this.policyEngine.evaluate(binding);
742
+ if (!policyDecision.allowed) {
743
+ throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
744
+ }
745
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
746
+ await this.persistence.saveRunRequest(threadId, runId, this.buildPersistedRunRequest(options.input, invocation));
747
+ await this.emitRunCreated(threadId, runId, {
748
+ agentId: binding.agent.id,
749
+ requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
750
+ selectedAgentId,
751
+ executionMode: getBindingAdapterKind(binding),
752
+ });
753
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId);
591
754
  try {
592
- const invocation = this.normalizeInvocationEnvelope(options);
593
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
594
- const binding = this.workspace.bindings.get(selectedAgentId);
595
- if (!binding) {
596
- throw new Error(`Unknown agent ${selectedAgentId}`);
597
- }
598
- const policyDecision = this.policyEngine.evaluate(binding);
599
- if (!policyDecision.allowed) {
600
- throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
601
- }
602
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
603
- await this.emitRunCreated(threadId, runId, {
604
- agentId: binding.agent.id,
605
- requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
606
- selectedAgentId,
607
- executionMode: getBindingAdapterKind(binding),
755
+ return await this.executeQueuedRun(binding, options.input, threadId, runId, selectedAgentId, {
756
+ context: invocation.context,
757
+ state: invocation.state,
758
+ files: invocation.files,
759
+ previousState: "running",
760
+ stateSequence: 6,
761
+ approvalSequence: 7,
608
762
  });
609
- try {
610
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId, undefined, {
611
- context: invocation.context,
612
- state: invocation.state,
613
- files: invocation.files,
614
- });
615
- const finalized = await this.finalizeContinuedRun(threadId, runId, options.input, actual, {
616
- previousState: null,
617
- stateSequence: 3,
618
- approvalSequence: 4,
619
- });
620
- return {
621
- ...finalized,
622
- agentId: selectedAgentId,
623
- };
624
- }
625
- catch (error) {
626
- await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
627
- await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
628
- previousState: null,
629
- error: error instanceof Error ? error.message : String(error),
630
- });
631
- return {
632
- threadId,
633
- runId,
634
- agentId: selectedAgentId,
635
- state: "failed",
636
- output: renderRuntimeFailure(error),
637
- };
638
- }
639
763
  }
640
764
  finally {
641
765
  releaseRunSlot();
642
766
  }
643
767
  }
644
768
  async *streamEvents(options) {
645
- const releaseRunSlot = await this.acquireRunSlot();
646
- try {
647
- const invocation = this.normalizeInvocationEnvelope(options);
648
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
649
- const binding = this.workspace.bindings.get(selectedAgentId);
650
- if (!binding) {
651
- const result = await this.run(options);
652
- for (const line of result.output.split("\n")) {
653
- yield {
654
- type: "content",
655
- threadId: result.threadId,
656
- runId: result.runId,
657
- agentId: result.agentId ?? selectedAgentId,
658
- content: `${line}\n`,
659
- };
660
- }
661
- return;
769
+ const invocation = this.normalizeInvocationEnvelope(options);
770
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
771
+ const binding = this.workspace.bindings.get(selectedAgentId);
772
+ if (!binding) {
773
+ const result = await this.run(options);
774
+ for (const line of result.output.split("\n")) {
775
+ yield {
776
+ type: "content",
777
+ threadId: result.threadId,
778
+ runId: result.runId,
779
+ agentId: result.agentId ?? selectedAgentId,
780
+ content: `${line}\n`,
781
+ };
662
782
  }
663
- let emitted = false;
664
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
665
- yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
666
- agentId: selectedAgentId,
667
- requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
668
- selectedAgentId,
669
- input: options.input,
670
- state: "running",
671
- }) };
783
+ return;
784
+ }
785
+ let emitted = false;
786
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
787
+ await this.persistence.saveRunRequest(threadId, runId, this.buildPersistedRunRequest(options.input, invocation));
788
+ yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
789
+ agentId: selectedAgentId,
790
+ requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
791
+ selectedAgentId,
792
+ input: options.input,
793
+ state: "running",
794
+ }) };
795
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId);
796
+ try {
672
797
  try {
673
798
  const priorHistory = await this.loadPriorHistory(threadId, runId);
674
799
  let assistantOutput = "";
@@ -686,11 +811,11 @@ export class AgentHarnessRuntime {
686
811
  : chunk;
687
812
  if (normalizedChunk.kind === "interrupt") {
688
813
  const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
689
- const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
690
- previousState: null,
814
+ const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 6, "waiting_for_approval", {
815
+ previousState: "running",
691
816
  checkpointRef,
692
817
  });
693
- const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
818
+ const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 7);
694
819
  yield {
695
820
  type: "event",
696
821
  event: waitingEvent,
@@ -783,22 +908,22 @@ export class AgentHarnessRuntime {
783
908
  finalMessageText: assistantOutput,
784
909
  },
785
910
  };
786
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
787
- previousState: null,
911
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "completed", {
912
+ previousState: "running",
788
913
  }) };
789
914
  return;
790
915
  }
791
916
  catch (error) {
792
917
  if (emitted) {
793
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
794
- previousState: null,
918
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
919
+ previousState: "running",
795
920
  error: error instanceof Error ? error.message : String(error),
796
921
  }) };
797
922
  return;
798
923
  }
799
924
  if (error instanceof RuntimeOperationTimeoutError && error.stage === "invoke") {
800
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
801
- previousState: null,
925
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
926
+ previousState: "running",
802
927
  error: error.message,
803
928
  }) };
804
929
  yield {
@@ -836,15 +961,15 @@ export class AgentHarnessRuntime {
836
961
  agentId: selectedAgentId,
837
962
  },
838
963
  };
839
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
840
- previousState: null,
964
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, actual.state, {
965
+ previousState: "running",
841
966
  }) };
842
967
  return;
843
968
  }
844
969
  catch (invokeError) {
845
970
  await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
846
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
847
- previousState: null,
971
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
972
+ previousState: "running",
848
973
  error: invokeError instanceof Error ? invokeError.message : String(invokeError),
849
974
  }) };
850
975
  yield {
@@ -870,29 +995,30 @@ export class AgentHarnessRuntime {
870
995
  }
871
996
  }
872
997
  finally {
998
+ await this.persistence.clearRunRequest(threadId, runId);
873
999
  releaseRunSlot();
874
1000
  }
875
1001
  }
876
1002
  async resume(options) {
877
- const releaseRunSlot = await this.acquireRunSlot();
1003
+ const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
1004
+ const thread = options.threadId
1005
+ ? await this.getSession(options.threadId)
1006
+ : approvalById
1007
+ ? await this.getSession(approvalById.threadId)
1008
+ : null;
1009
+ if (!thread) {
1010
+ throw new Error("resume requires either threadId or approvalId");
1011
+ }
1012
+ const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
1013
+ const threadId = approval.threadId;
1014
+ const runId = approval.runId;
1015
+ const binding = this.workspace.bindings.get(thread.agentId);
1016
+ if (!binding) {
1017
+ throw new Error(`Unknown agent ${thread.agentId}`);
1018
+ }
1019
+ await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
1020
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId, "resuming");
878
1021
  try {
879
- const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
880
- const thread = options.threadId
881
- ? await this.getSession(options.threadId)
882
- : approvalById
883
- ? await this.getSession(approvalById.threadId)
884
- : null;
885
- if (!thread) {
886
- throw new Error("resume requires either threadId or approvalId");
887
- }
888
- const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
889
- const threadId = approval.threadId;
890
- const runId = approval.runId;
891
- const binding = this.workspace.bindings.get(thread.agentId);
892
- if (!binding) {
893
- throw new Error(`Unknown agent ${thread.agentId}`);
894
- }
895
- await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
896
1022
  await this.persistence.saveRecoveryIntent(threadId, runId, {
897
1023
  kind: "approval-decision",
898
1024
  savedAt: new Date().toISOString(),
@@ -978,12 +1104,72 @@ export class AgentHarnessRuntime {
978
1104
  await this.close();
979
1105
  }
980
1106
  async recoverStartupRuns() {
981
- if (!this.recoveryConfig.enabled || !this.recoveryConfig.resumeResumingRunsOnStartup) {
1107
+ if (!this.recoveryConfig.enabled) {
982
1108
  return;
983
1109
  }
984
1110
  const threads = await this.persistence.listSessions();
985
1111
  for (const thread of threads) {
986
- if (thread.status !== "resuming") {
1112
+ if (thread.status === "queued") {
1113
+ const runMeta = await this.persistence.getRunMeta(thread.threadId, thread.latestRunId);
1114
+ const binding = this.workspace.bindings.get(runMeta.agentId);
1115
+ if (!binding) {
1116
+ continue;
1117
+ }
1118
+ const request = await this.persistence.getRunRequest(thread.threadId, thread.latestRunId);
1119
+ if (!request) {
1120
+ await this.setRunStateAndEmit(thread.threadId, thread.latestRunId, 100, "failed", {
1121
+ previousState: "queued",
1122
+ error: "missing persisted run request for queued run recovery",
1123
+ });
1124
+ continue;
1125
+ }
1126
+ const releaseRunSlot = await this.acquireRunSlot();
1127
+ try {
1128
+ await this.executeQueuedRun(binding, request.input, thread.threadId, thread.latestRunId, runMeta.agentId, {
1129
+ context: request.invocation?.context,
1130
+ state: request.invocation?.inputs,
1131
+ files: request.invocation?.attachments,
1132
+ previousState: "queued",
1133
+ stateSequence: 103,
1134
+ approvalSequence: 104,
1135
+ });
1136
+ }
1137
+ finally {
1138
+ releaseRunSlot();
1139
+ }
1140
+ continue;
1141
+ }
1142
+ if (thread.status === "running") {
1143
+ const runMeta = await this.persistence.getRunMeta(thread.threadId, thread.latestRunId);
1144
+ const binding = this.workspace.bindings.get(runMeta.agentId);
1145
+ if (!binding || !this.supportsRunningReplay(binding)) {
1146
+ continue;
1147
+ }
1148
+ const request = await this.persistence.getRunRequest(thread.threadId, thread.latestRunId);
1149
+ if (!request) {
1150
+ continue;
1151
+ }
1152
+ const releaseRunSlot = await this.acquireRunSlot();
1153
+ try {
1154
+ await this.emit(thread.threadId, thread.latestRunId, 100, "run.resumed", {
1155
+ resumeKind: "startup-running-recovery",
1156
+ state: "running",
1157
+ });
1158
+ await this.executeQueuedRun(binding, request.input, thread.threadId, thread.latestRunId, runMeta.agentId, {
1159
+ context: request.invocation?.context,
1160
+ state: request.invocation?.inputs,
1161
+ files: request.invocation?.attachments,
1162
+ previousState: "running",
1163
+ stateSequence: 103,
1164
+ approvalSequence: 104,
1165
+ });
1166
+ }
1167
+ finally {
1168
+ releaseRunSlot();
1169
+ }
1170
+ continue;
1171
+ }
1172
+ if (thread.status !== "resuming" || !this.recoveryConfig.resumeResumingRunsOnStartup) {
987
1173
  continue;
988
1174
  }
989
1175
  const binding = this.workspace.bindings.get(thread.agentId);
@@ -44,6 +44,8 @@ function renderOpenApprovalsMarkdown(approvals) {
44
44
  }
45
45
  const THREAD_MEMORY_EVENT_TYPES = new Set([
46
46
  "run.state.changed",
47
+ "run.queued",
48
+ "run.dequeued",
47
49
  "approval.resolved",
48
50
  "approval.requested",
49
51
  ]);
@@ -9,6 +9,7 @@ export type LoadedToolModule = {
9
9
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
10
10
  schema: SchemaLike;
11
11
  description: string;
12
+ retryable?: boolean;
12
13
  };
13
14
  export declare function isSupportedToolModulePath(filePath: string): boolean;
14
15
  export declare function discoverAnnotatedFunctionNames(sourceText: string): string[];