@botbotgo/agent-harness 0.0.40 → 0.0.42

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.
@@ -5,7 +5,7 @@ import { AGENT_INTERRUPT_SENTINEL_PREFIX, AgentRuntimeAdapter, RuntimeOperationT
5
5
  import { createResourceBackendResolver, createResourceToolResolver } from "../resource/resource.js";
6
6
  import { EventBus } from "./event-bus.js";
7
7
  import { PolicyEngine } from "./policy-engine.js";
8
- import { getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
8
+ import { getConcurrencyConfig, getRecoveryConfig, getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
9
9
  import { createHarnessEvent, createPendingApproval, heuristicRoute, inferRoutingBindings, renderRuntimeFailure, renderToolFailure, } from "./support/harness-support.js";
10
10
  import { createCheckpointerForConfig, createStoreForConfig } from "./support/runtime-factories.js";
11
11
  import { resolveCompiledEmbeddingModel, resolveCompiledEmbeddingModelRef } from "./support/embedding-models.js";
@@ -35,6 +35,10 @@ export class AgentHarnessRuntime {
35
35
  unregisterThreadMemorySync;
36
36
  resolvedRuntimeAdapterOptions;
37
37
  checkpointMaintenance;
38
+ recoveryConfig;
39
+ concurrencyConfig;
40
+ activeRunSlots = 0;
41
+ pendingRunSlots = [];
38
42
  listHostBindings() {
39
43
  return inferRoutingBindings(this.workspace).hostBindings;
40
44
  }
@@ -137,6 +141,10 @@ export class AgentHarnessRuntime {
137
141
  return existing;
138
142
  }
139
143
  const resolvedConfig = binding.harnessRuntime.checkpointer ?? { kind: "FileCheckpointer", path: "checkpoints.json" };
144
+ if (typeof resolvedConfig === "boolean") {
145
+ this.checkpointers.set(key, resolvedConfig);
146
+ return resolvedConfig;
147
+ }
140
148
  const saver = createCheckpointerForConfig(resolvedConfig, binding.harnessRuntime.runRoot);
141
149
  this.checkpointers.set(key, saver);
142
150
  return saver;
@@ -157,10 +165,13 @@ export class AgentHarnessRuntime {
157
165
  this.checkpointMaintenance = checkpointMaintenanceConfig
158
166
  ? new CheckpointMaintenanceLoop(discoverCheckpointMaintenanceTargets(workspace), checkpointMaintenanceConfig)
159
167
  : null;
168
+ this.recoveryConfig = getRecoveryConfig(workspace.refs);
169
+ this.concurrencyConfig = getConcurrencyConfig(workspace.refs);
160
170
  }
161
171
  async initialize() {
162
172
  await this.persistence.initialize();
163
173
  await this.checkpointMaintenance?.start();
174
+ await this.recoverStartupRuns();
164
175
  }
165
176
  subscribe(listener) {
166
177
  return this.eventBus.subscribe(listener);
@@ -338,6 +349,11 @@ export class AgentHarnessRuntime {
338
349
  const history = await this.persistence.listThreadMessages(threadId);
339
350
  return history.filter((message) => message.runId !== runId);
340
351
  }
352
+ async loadRunInput(threadId, runId) {
353
+ const history = await this.persistence.listThreadMessages(threadId, 100);
354
+ const userTurn = history.find((message) => message.runId === runId && message.role === "user");
355
+ return userTurn?.content ?? "";
356
+ }
341
357
  async appendAssistantMessage(threadId, runId, content) {
342
358
  if (!content) {
343
359
  return;
@@ -349,9 +365,31 @@ export class AgentHarnessRuntime {
349
365
  createdAt: new Date().toISOString(),
350
366
  });
351
367
  }
352
- async invokeWithHistory(binding, input, threadId, runId, resumePayload) {
368
+ async invokeWithHistory(binding, input, threadId, runId, resumePayload, options = {}) {
353
369
  const priorHistory = await this.loadPriorHistory(threadId, runId);
354
- return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory);
370
+ return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory, options);
371
+ }
372
+ checkpointRefForState(threadId, runId, state) {
373
+ return state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
374
+ }
375
+ async finalizeContinuedRun(threadId, runId, input, actual, options) {
376
+ let approval;
377
+ await this.appendAssistantMessage(threadId, runId, actual.output);
378
+ const checkpointRef = this.checkpointRefForState(threadId, runId, actual.state);
379
+ await this.setRunStateAndEmit(threadId, runId, options.stateSequence, actual.state, {
380
+ previousState: options.previousState,
381
+ checkpointRef,
382
+ });
383
+ if (actual.state === "waiting_for_approval" && options.approvalSequence) {
384
+ approval = (await this.requestApprovalAndEmit(threadId, runId, input, actual.interruptContent, checkpointRef, options.approvalSequence)).approval;
385
+ }
386
+ return {
387
+ ...actual,
388
+ threadId,
389
+ runId,
390
+ approvalId: approval?.approvalId ?? actual.approvalId,
391
+ pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
392
+ };
355
393
  }
356
394
  async emitOutputDeltaAndCreateItem(threadId, runId, agentId, content) {
357
395
  await this.emit(threadId, runId, 3, "output.delta", {
@@ -438,6 +476,28 @@ export class AgentHarnessRuntime {
438
476
  }
439
477
  await listener(value);
440
478
  }
479
+ async acquireRunSlot() {
480
+ const maxConcurrentRuns = this.concurrencyConfig.maxConcurrentRuns;
481
+ if (!maxConcurrentRuns) {
482
+ return () => undefined;
483
+ }
484
+ if (this.activeRunSlots >= maxConcurrentRuns) {
485
+ await new Promise((resolve) => {
486
+ this.pendingRunSlots.push(resolve);
487
+ });
488
+ }
489
+ this.activeRunSlots += 1;
490
+ let released = false;
491
+ return () => {
492
+ if (released) {
493
+ return;
494
+ }
495
+ released = true;
496
+ this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
497
+ const next = this.pendingRunSlots.shift();
498
+ next?.();
499
+ };
500
+ }
441
501
  async dispatchRunListeners(stream, listeners) {
442
502
  let latestEvent;
443
503
  let output = "";
@@ -499,275 +559,304 @@ export class AgentHarnessRuntime {
499
559
  if (options.listeners) {
500
560
  return this.dispatchRunListeners(this.streamEvents(options), options.listeners);
501
561
  }
502
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
503
- const binding = this.workspace.bindings.get(selectedAgentId);
504
- if (!binding) {
505
- throw new Error(`Unknown agent ${selectedAgentId}`);
506
- }
507
- const policyDecision = this.policyEngine.evaluate(binding);
508
- if (!policyDecision.allowed) {
509
- throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
510
- }
511
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
512
- await this.emitRunCreated(threadId, runId, {
513
- agentId: binding.agent.id,
514
- requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
515
- selectedAgentId,
516
- executionMode: binding.agent.executionMode,
517
- });
562
+ const releaseRunSlot = await this.acquireRunSlot();
518
563
  try {
519
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
520
- let approval;
521
- await this.appendAssistantMessage(threadId, runId, actual.output);
522
- const checkpointRef = actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
523
- await this.setRunStateAndEmit(threadId, runId, 3, actual.state, {
524
- previousState: null,
525
- checkpointRef,
526
- });
527
- if (actual.state === "waiting_for_approval") {
528
- approval = (await this.requestApprovalAndEmit(threadId, runId, options.input, actual.interruptContent, checkpointRef, 4)).approval;
564
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
565
+ const binding = this.workspace.bindings.get(selectedAgentId);
566
+ if (!binding) {
567
+ throw new Error(`Unknown agent ${selectedAgentId}`);
529
568
  }
530
- return {
531
- ...actual,
532
- threadId,
533
- runId,
534
- agentId: selectedAgentId,
535
- approvalId: approval?.approvalId ?? actual.approvalId,
536
- pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
537
- };
538
- }
539
- catch (error) {
540
- await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
541
- await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
542
- previousState: null,
543
- error: error instanceof Error ? error.message : String(error),
544
- });
545
- return {
546
- threadId,
547
- runId,
548
- agentId: selectedAgentId,
549
- state: "failed",
550
- output: renderRuntimeFailure(error),
551
- };
552
- }
553
- }
554
- async *streamEvents(options) {
555
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
556
- const binding = this.workspace.bindings.get(selectedAgentId);
557
- if (!binding) {
558
- const result = await this.run(options);
559
- for (const line of result.output.split("\n")) {
560
- yield {
561
- type: "content",
562
- threadId: result.threadId,
563
- runId: result.runId,
564
- agentId: result.agentId ?? selectedAgentId,
565
- content: `${line}\n`,
566
- };
569
+ const policyDecision = this.policyEngine.evaluate(binding);
570
+ if (!policyDecision.allowed) {
571
+ throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
567
572
  }
568
- return;
569
- }
570
- let emitted = false;
571
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
572
- yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
573
- agentId: selectedAgentId,
573
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
574
+ await this.emitRunCreated(threadId, runId, {
575
+ agentId: binding.agent.id,
574
576
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
575
577
  selectedAgentId,
576
- input: options.input,
577
- state: "running",
578
- }) };
579
- try {
580
- const priorHistory = await this.loadPriorHistory(threadId, runId);
581
- let assistantOutput = "";
582
- const toolErrors = [];
583
- for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory)) {
584
- if (chunk) {
585
- const normalizedChunk = typeof chunk === "string"
586
- ? chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)
587
- ? { kind: "interrupt", content: chunk.slice(AGENT_INTERRUPT_SENTINEL_PREFIX.length) }
588
- : { kind: "content", content: chunk }
589
- : chunk;
590
- if (normalizedChunk.kind === "interrupt") {
591
- const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
592
- const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
593
- previousState: null,
594
- checkpointRef,
595
- });
596
- const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
597
- yield {
598
- type: "event",
599
- event: waitingEvent,
600
- };
601
- yield {
602
- type: "event",
603
- event: approvalRequest.event,
604
- };
605
- return;
606
- }
607
- if (normalizedChunk.kind === "reasoning") {
608
- await this.emit(threadId, runId, 3, "reasoning.delta", {
609
- content: normalizedChunk.content,
610
- });
611
- yield {
612
- type: "reasoning",
613
- threadId,
614
- runId,
615
- agentId: selectedAgentId,
616
- content: normalizedChunk.content,
617
- };
618
- continue;
619
- }
620
- if (normalizedChunk.kind === "step") {
621
- yield {
622
- type: "step",
623
- threadId,
624
- runId,
625
- agentId: selectedAgentId,
626
- content: normalizedChunk.content,
627
- };
628
- continue;
629
- }
630
- if (normalizedChunk.kind === "tool-result") {
631
- if (normalizedChunk.isError) {
632
- toolErrors.push(renderToolFailure(normalizedChunk.toolName, normalizedChunk.output));
633
- }
634
- yield {
635
- type: "tool-result",
636
- threadId,
637
- runId,
638
- agentId: selectedAgentId,
639
- toolName: normalizedChunk.toolName,
640
- output: normalizedChunk.output,
641
- isError: normalizedChunk.isError,
642
- };
643
- continue;
644
- }
645
- emitted = true;
646
- assistantOutput += normalizedChunk.content;
647
- yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, normalizedChunk.content);
648
- }
649
- }
650
- if (!assistantOutput && toolErrors.length > 0) {
651
- assistantOutput = toolErrors.join("\n\n");
652
- emitted = true;
653
- yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, assistantOutput);
654
- }
655
- if (!assistantOutput) {
656
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
657
- if (actual.output) {
658
- assistantOutput = actual.output;
659
- emitted = true;
660
- yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
661
- }
662
- }
663
- await this.appendAssistantMessage(threadId, runId, assistantOutput);
664
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
578
+ executionMode: binding.agent.executionMode,
579
+ });
580
+ try {
581
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId, undefined, {
582
+ context: options.context,
583
+ state: options.state,
584
+ files: options.files,
585
+ });
586
+ const finalized = await this.finalizeContinuedRun(threadId, runId, options.input, actual, {
665
587
  previousState: null,
666
- }) };
667
- return;
668
- }
669
- catch (error) {
670
- if (emitted) {
671
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
672
- previousState: null,
673
- error: error instanceof Error ? error.message : String(error),
674
- }) };
675
- return;
588
+ stateSequence: 3,
589
+ approvalSequence: 4,
590
+ });
591
+ return {
592
+ ...finalized,
593
+ agentId: selectedAgentId,
594
+ };
676
595
  }
677
- if (error instanceof RuntimeOperationTimeoutError && error.stage === "invoke") {
678
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
679
- previousState: null,
680
- error: error.message,
681
- }) };
682
- yield {
683
- type: "content",
596
+ catch (error) {
597
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
598
+ await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
599
+ previousState: null,
600
+ error: error instanceof Error ? error.message : String(error),
601
+ });
602
+ return {
684
603
  threadId,
685
604
  runId,
686
605
  agentId: selectedAgentId,
687
- content: renderRuntimeFailure(error),
606
+ state: "failed",
607
+ output: renderRuntimeFailure(error),
688
608
  };
609
+ }
610
+ }
611
+ finally {
612
+ releaseRunSlot();
613
+ }
614
+ }
615
+ async *streamEvents(options) {
616
+ const releaseRunSlot = await this.acquireRunSlot();
617
+ try {
618
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
619
+ const binding = this.workspace.bindings.get(selectedAgentId);
620
+ if (!binding) {
621
+ const result = await this.run(options);
622
+ for (const line of result.output.split("\n")) {
623
+ yield {
624
+ type: "content",
625
+ threadId: result.threadId,
626
+ runId: result.runId,
627
+ agentId: result.agentId ?? selectedAgentId,
628
+ content: `${line}\n`,
629
+ };
630
+ }
689
631
  return;
690
632
  }
633
+ let emitted = false;
634
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
635
+ yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
636
+ agentId: selectedAgentId,
637
+ requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
638
+ selectedAgentId,
639
+ input: options.input,
640
+ state: "running",
641
+ }) };
691
642
  try {
692
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
693
- await this.appendAssistantMessage(threadId, runId, actual.output);
694
- if (actual.output) {
695
- yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
643
+ const priorHistory = await this.loadPriorHistory(threadId, runId);
644
+ let assistantOutput = "";
645
+ const toolErrors = [];
646
+ for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory, {
647
+ context: options.context,
648
+ state: options.state,
649
+ files: options.files,
650
+ })) {
651
+ if (chunk) {
652
+ const normalizedChunk = typeof chunk === "string"
653
+ ? chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)
654
+ ? { kind: "interrupt", content: chunk.slice(AGENT_INTERRUPT_SENTINEL_PREFIX.length) }
655
+ : { kind: "content", content: chunk }
656
+ : chunk;
657
+ if (normalizedChunk.kind === "interrupt") {
658
+ const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
659
+ const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
660
+ previousState: null,
661
+ checkpointRef,
662
+ });
663
+ const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
664
+ yield {
665
+ type: "event",
666
+ event: waitingEvent,
667
+ };
668
+ yield {
669
+ type: "event",
670
+ event: approvalRequest.event,
671
+ };
672
+ return;
673
+ }
674
+ if (normalizedChunk.kind === "reasoning") {
675
+ await this.emit(threadId, runId, 3, "reasoning.delta", {
676
+ content: normalizedChunk.content,
677
+ });
678
+ yield {
679
+ type: "reasoning",
680
+ threadId,
681
+ runId,
682
+ agentId: selectedAgentId,
683
+ content: normalizedChunk.content,
684
+ };
685
+ continue;
686
+ }
687
+ if (normalizedChunk.kind === "step") {
688
+ yield {
689
+ type: "step",
690
+ threadId,
691
+ runId,
692
+ agentId: selectedAgentId,
693
+ content: normalizedChunk.content,
694
+ };
695
+ continue;
696
+ }
697
+ if (normalizedChunk.kind === "tool-result") {
698
+ if (normalizedChunk.isError) {
699
+ toolErrors.push(renderToolFailure(normalizedChunk.toolName, normalizedChunk.output));
700
+ }
701
+ yield {
702
+ type: "tool-result",
703
+ threadId,
704
+ runId,
705
+ agentId: selectedAgentId,
706
+ toolName: normalizedChunk.toolName,
707
+ output: normalizedChunk.output,
708
+ isError: normalizedChunk.isError,
709
+ };
710
+ continue;
711
+ }
712
+ emitted = true;
713
+ assistantOutput += normalizedChunk.content;
714
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, normalizedChunk.content);
715
+ }
696
716
  }
697
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
717
+ if (!assistantOutput && toolErrors.length > 0) {
718
+ assistantOutput = toolErrors.join("\n\n");
719
+ emitted = true;
720
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, assistantOutput);
721
+ }
722
+ if (!assistantOutput) {
723
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
724
+ if (actual.output) {
725
+ assistantOutput = actual.output;
726
+ emitted = true;
727
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
728
+ }
729
+ }
730
+ await this.appendAssistantMessage(threadId, runId, assistantOutput);
731
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
698
732
  previousState: null,
699
733
  }) };
700
734
  return;
701
735
  }
702
- catch (invokeError) {
703
- await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
704
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
705
- previousState: null,
706
- error: invokeError instanceof Error ? invokeError.message : String(invokeError),
707
- }) };
708
- yield {
709
- type: "content",
710
- threadId,
711
- runId,
712
- agentId: selectedAgentId,
713
- content: renderRuntimeFailure(invokeError),
714
- };
715
- return;
736
+ catch (error) {
737
+ if (emitted) {
738
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
739
+ previousState: null,
740
+ error: error instanceof Error ? error.message : String(error),
741
+ }) };
742
+ return;
743
+ }
744
+ if (error instanceof RuntimeOperationTimeoutError && error.stage === "invoke") {
745
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
746
+ previousState: null,
747
+ error: error.message,
748
+ }) };
749
+ yield {
750
+ type: "content",
751
+ threadId,
752
+ runId,
753
+ agentId: selectedAgentId,
754
+ content: renderRuntimeFailure(error),
755
+ };
756
+ return;
757
+ }
758
+ try {
759
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
760
+ await this.appendAssistantMessage(threadId, runId, actual.output);
761
+ if (actual.output) {
762
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
763
+ }
764
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
765
+ previousState: null,
766
+ }) };
767
+ return;
768
+ }
769
+ catch (invokeError) {
770
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
771
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
772
+ previousState: null,
773
+ error: invokeError instanceof Error ? invokeError.message : String(invokeError),
774
+ }) };
775
+ yield {
776
+ type: "content",
777
+ threadId,
778
+ runId,
779
+ agentId: selectedAgentId,
780
+ content: renderRuntimeFailure(invokeError),
781
+ };
782
+ return;
783
+ }
716
784
  }
717
785
  }
786
+ finally {
787
+ releaseRunSlot();
788
+ }
718
789
  }
719
790
  async resume(options) {
720
- const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
721
- const thread = options.threadId
722
- ? await this.getSession(options.threadId)
723
- : approvalById
724
- ? await this.getSession(approvalById.threadId)
725
- : null;
726
- if (!thread) {
727
- throw new Error("resume requires either threadId or approvalId");
791
+ const releaseRunSlot = await this.acquireRunSlot();
792
+ try {
793
+ const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
794
+ const thread = options.threadId
795
+ ? await this.getSession(options.threadId)
796
+ : approvalById
797
+ ? await this.getSession(approvalById.threadId)
798
+ : null;
799
+ if (!thread) {
800
+ throw new Error("resume requires either threadId or approvalId");
801
+ }
802
+ const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
803
+ const threadId = approval.threadId;
804
+ const runId = approval.runId;
805
+ const binding = this.workspace.bindings.get(thread.agentId);
806
+ if (!binding) {
807
+ throw new Error(`Unknown agent ${thread.agentId}`);
808
+ }
809
+ await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
810
+ await this.persistence.saveRecoveryIntent(threadId, runId, {
811
+ kind: "approval-decision",
812
+ savedAt: new Date().toISOString(),
813
+ checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
814
+ resumePayload: options.decision === "edit" && options.editedInput
815
+ ? { decision: "edit", editedInput: options.editedInput }
816
+ : (options.decision ?? "approve"),
817
+ attempts: 0,
818
+ });
819
+ await this.emit(threadId, runId, 5, "run.resumed", {
820
+ resumeKind: "cross-restart",
821
+ checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
822
+ state: "resuming",
823
+ approvalId: approval.approvalId,
824
+ pendingActionId: approval.pendingActionId,
825
+ });
826
+ await this.persistence.resolveApproval(threadId, runId, approval.approvalId, options.decision === "reject" ? "rejected" : options.decision === "edit" ? "edited" : "approved");
827
+ await this.emit(threadId, runId, 6, "approval.resolved", {
828
+ approvalId: approval.approvalId,
829
+ pendingActionId: approval.pendingActionId,
830
+ decision: options.decision ?? "approve",
831
+ toolName: approval.toolName,
832
+ });
833
+ const history = await this.persistence.listThreadMessages(threadId);
834
+ const priorHistory = history.filter((message) => message.runId !== runId);
835
+ const runInput = await this.loadRunInput(threadId, runId);
836
+ const resumeDecision = options.decision === "edit" && options.editedInput
837
+ ? { decision: "edit", editedInput: options.editedInput }
838
+ : (options.decision ?? "approve");
839
+ try {
840
+ const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
841
+ await this.persistence.clearRecoveryIntent(threadId, runId);
842
+ const finalized = await this.finalizeContinuedRun(threadId, runId, runInput, actual, {
843
+ previousState: "resuming",
844
+ stateSequence: 7,
845
+ approvalSequence: 8,
846
+ });
847
+ return {
848
+ ...finalized,
849
+ approvalId: finalized.approvalId ?? approval.approvalId,
850
+ pendingActionId: finalized.pendingActionId ?? approval.pendingActionId,
851
+ };
852
+ }
853
+ catch (error) {
854
+ throw error;
855
+ }
728
856
  }
729
- const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
730
- const threadId = approval.threadId;
731
- const runId = approval.runId;
732
- const binding = this.workspace.bindings.get(thread.agentId);
733
- if (!binding) {
734
- throw new Error(`Unknown agent ${thread.agentId}`);
857
+ finally {
858
+ releaseRunSlot();
735
859
  }
736
- await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
737
- await this.emit(threadId, runId, 5, "run.resumed", {
738
- resumeKind: "cross-restart",
739
- checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
740
- state: "resuming",
741
- approvalId: approval.approvalId,
742
- pendingActionId: approval.pendingActionId,
743
- });
744
- await this.persistence.resolveApproval(threadId, runId, approval.approvalId, options.decision === "reject" ? "rejected" : options.decision === "edit" ? "edited" : "approved");
745
- await this.emit(threadId, runId, 6, "approval.resolved", {
746
- approvalId: approval.approvalId,
747
- pendingActionId: approval.pendingActionId,
748
- decision: options.decision ?? "approve",
749
- toolName: approval.toolName,
750
- });
751
- const history = await this.persistence.listThreadMessages(threadId);
752
- const priorHistory = history.filter((message) => message.runId !== runId);
753
- const resumeDecision = options.decision === "edit" && options.editedInput
754
- ? { decision: "edit", editedInput: options.editedInput }
755
- : (options.decision ?? "approve");
756
- const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
757
- await this.appendAssistantMessage(threadId, runId, actual.output);
758
- await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
759
- await this.emit(threadId, runId, 7, "run.state.changed", {
760
- previousState: "resuming",
761
- state: actual.state,
762
- checkpointRef: actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null,
763
- });
764
- return {
765
- ...actual,
766
- threadId,
767
- runId,
768
- approvalId: approval.approvalId,
769
- pendingActionId: approval.pendingActionId,
770
- };
771
860
  }
772
861
  async restartConversation(options) {
773
862
  const thread = await this.getSession(options.threadId);
@@ -802,5 +891,62 @@ export class AgentHarnessRuntime {
802
891
  async stop() {
803
892
  await this.close();
804
893
  }
894
+ async recoverStartupRuns() {
895
+ if (!this.recoveryConfig.enabled || !this.recoveryConfig.resumeResumingRunsOnStartup) {
896
+ return;
897
+ }
898
+ const threads = await this.persistence.listSessions();
899
+ for (const thread of threads) {
900
+ if (thread.status !== "resuming") {
901
+ continue;
902
+ }
903
+ const binding = this.workspace.bindings.get(thread.agentId);
904
+ if (!binding) {
905
+ continue;
906
+ }
907
+ const recoveryIntent = await this.persistence.getRecoveryIntent(thread.threadId, thread.latestRunId);
908
+ if (!recoveryIntent || recoveryIntent.kind !== "approval-decision") {
909
+ continue;
910
+ }
911
+ if (recoveryIntent.attempts >= this.recoveryConfig.maxRecoveryAttempts) {
912
+ await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
913
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
914
+ continue;
915
+ }
916
+ await this.persistence.saveRecoveryIntent(thread.threadId, thread.latestRunId, {
917
+ ...recoveryIntent,
918
+ attempts: recoveryIntent.attempts + 1,
919
+ });
920
+ await this.emit(thread.threadId, thread.latestRunId, 100, "run.resumed", {
921
+ resumeKind: "startup-recovery",
922
+ checkpointRef: recoveryIntent.checkpointRef,
923
+ state: "resuming",
924
+ });
925
+ const history = await this.persistence.listThreadMessages(thread.threadId);
926
+ const priorHistory = history.filter((message) => message.runId !== thread.latestRunId);
927
+ const runInput = await this.loadRunInput(thread.threadId, thread.latestRunId);
928
+ try {
929
+ const actual = await this.runtimeAdapter.invoke(binding, "", thread.threadId, thread.latestRunId, recoveryIntent.resumePayload, priorHistory);
930
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
931
+ await this.finalizeContinuedRun(thread.threadId, thread.latestRunId, runInput, actual, {
932
+ previousState: "resuming",
933
+ stateSequence: 101,
934
+ approvalSequence: 102,
935
+ });
936
+ }
937
+ catch (error) {
938
+ if (recoveryIntent.attempts + 1 >= this.recoveryConfig.maxRecoveryAttempts) {
939
+ await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
940
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
941
+ await this.emit(thread.threadId, thread.latestRunId, 101, "run.state.changed", {
942
+ previousState: "resuming",
943
+ state: "failed",
944
+ checkpointRef: recoveryIntent.checkpointRef,
945
+ error: error instanceof Error ? error.message : String(error),
946
+ });
947
+ }
948
+ }
949
+ }
950
+ }
805
951
  }
806
952
  export { AgentHarnessRuntime as AgentHarness };