@botbotgo/agent-harness 0.0.101 → 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.
Files changed (40) hide show
  1. package/dist/package-version.d.ts +1 -1
  2. package/dist/package-version.js +1 -1
  3. package/dist/persistence/sqlite-run-context-store.d.ts +22 -0
  4. package/dist/persistence/sqlite-run-context-store.js +64 -0
  5. package/dist/persistence/sqlite-run-queue-store.d.ts +41 -0
  6. package/dist/persistence/sqlite-run-queue-store.js +120 -0
  7. package/dist/persistence/sqlite-store.d.ts +2 -2
  8. package/dist/persistence/sqlite-store.js +31 -117
  9. package/dist/resource/mcp-tool-support.d.ts +21 -0
  10. package/dist/resource/mcp-tool-support.js +173 -0
  11. package/dist/resource/resource-impl.d.ts +1 -18
  12. package/dist/resource/resource-impl.js +3 -166
  13. package/dist/runtime/adapter/invoke-runtime.d.ts +22 -0
  14. package/dist/runtime/adapter/invoke-runtime.js +18 -0
  15. package/dist/runtime/adapter/middleware-assembly.d.ts +75 -0
  16. package/dist/runtime/adapter/middleware-assembly.js +175 -0
  17. package/dist/runtime/adapter/runtime-shell.d.ts +27 -0
  18. package/dist/runtime/adapter/runtime-shell.js +168 -0
  19. package/dist/runtime/adapter/stream-runtime.d.ts +46 -0
  20. package/dist/runtime/adapter/stream-runtime.js +93 -0
  21. package/dist/runtime/adapter/tool-resolution.d.ts +14 -0
  22. package/dist/runtime/adapter/tool-resolution.js +57 -0
  23. package/dist/runtime/agent-runtime-adapter.d.ts +1 -6
  24. package/dist/runtime/agent-runtime-adapter.js +140 -495
  25. package/dist/runtime/harness/run/run-operations.d.ts +50 -0
  26. package/dist/runtime/harness/run/run-operations.js +113 -0
  27. package/dist/runtime/harness/run/run-slot-acquisition.d.ts +64 -0
  28. package/dist/runtime/harness/run/run-slot-acquisition.js +157 -0
  29. package/dist/runtime/harness/run/startup-runtime.d.ts +37 -0
  30. package/dist/runtime/harness/run/startup-runtime.js +68 -0
  31. package/dist/runtime/harness/run/stream-run.d.ts +53 -0
  32. package/dist/runtime/harness/run/stream-run.js +304 -0
  33. package/dist/runtime/harness/run/thread-records.d.ts +21 -0
  34. package/dist/runtime/harness/run/thread-records.js +59 -0
  35. package/dist/runtime/harness.js +121 -639
  36. package/dist/workspace/object-loader.d.ts +1 -8
  37. package/dist/workspace/object-loader.js +3 -197
  38. package/dist/workspace/yaml-object-reader.d.ts +15 -0
  39. package/dist/workspace/yaml-object-reader.js +202 -0
  40. package/package.json +1 -1
@@ -1,13 +1,12 @@
1
1
  import { AUTO_AGENT_ID } from "../contracts/types.js";
2
2
  import { SqlitePersistence } from "../persistence/sqlite-store.js";
3
3
  import { createPersistentId } from "../utils/id.js";
4
- import { AGENT_INTERRUPT_SENTINEL_PREFIX, AgentRuntimeAdapter, RuntimeOperationTimeoutError } from "./agent-runtime-adapter.js";
5
- import { normalizeUpstreamRuntimeEvent } from "./parsing/stream-event-parsing.js";
4
+ import { AgentRuntimeAdapter } from "./agent-runtime-adapter.js";
6
5
  import { createResourceBackendResolver, createResourceToolResolver } from "../resource/resource.js";
7
6
  import { EventBus } from "./harness/events/event-bus.js";
8
7
  import { PolicyEngine } from "./harness/system/policy-engine.js";
9
8
  import { getConcurrencyConfig, getRecoveryConfig, getRoutingDefaultAgentId, getRoutingRules, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
10
- import { createHarnessEvent, inferRoutingBindings, renderRuntimeFailure, renderToolFailure, } from "./support/harness-support.js";
9
+ import { createHarnessEvent, inferRoutingBindings, renderRuntimeFailure, } from "./support/harness-support.js";
11
10
  import { ThreadMemorySync } from "./harness/system/thread-memory-sync.js";
12
11
  import { FileBackedStore } from "./harness/system/store.js";
13
12
  import { CheckpointMaintenanceLoop, discoverCheckpointMaintenanceTargets, readCheckpointMaintenanceConfig, } from "./maintenance/checkpoint-maintenance.js";
@@ -15,12 +14,14 @@ import { RuntimeRecordMaintenanceLoop, discoverRuntimeRecordMaintenanceTargets,
15
14
  import { HealthMonitor } from "./harness/system/health-monitor.js";
16
15
  import { readHealthMonitorConfig } from "./harness/system/health-monitor.js";
17
16
  import { extractMessageText, normalizeMessageContent } from "../utils/message-content.js";
18
- import { buildPersistedRunRequest, isTerminalRunState, normalizeInvocationEnvelope, normalizeRunPriority, resolveRunListeners, toPublicApprovalRecord, } from "./harness/run/helpers.js";
17
+ import { buildPersistedRunRequest, normalizeInvocationEnvelope, normalizeRunPriority, resolveRunListeners, } from "./harness/run/helpers.js";
19
18
  import { emitHarnessEvent, emitRunCreatedEvent, emitSyntheticFallbackEvent, requestApprovalAndEmitEvent, setRunStateAndEmitEvent, } from "./harness/events/events.js";
20
19
  import { appendAssistantMessage as appendLifecycleAssistantMessage, finalizeCancelledRun as finalizeLifecycleCancelledRun, finalizeContinuedRun as finalizeLifecycleContinuedRun, } from "./harness/run/run-lifecycle.js";
21
- import { createContentBlocksItem as createStreamingContentBlocksItem, createToolResultKey as createStreamingToolResultKey, dispatchRunListeners as dispatchStreamingRunListeners, emitOutputDeltaAndCreateItem as emitStreamingOutputDeltaAndCreateItem, } from "./harness/events/streaming.js";
20
+ import { dispatchRunListeners as dispatchStreamingRunListeners, } from "./harness/events/streaming.js";
22
21
  import { buildResumePayload as buildHarnessResumePayload, resolveApprovalRecord as resolveHarnessApprovalRecord, } from "./harness/run/resume.js";
23
- import { dropPendingRunSlot, enqueuePendingRunSlot, shiftNextPendingRunSlot } from "./harness/run/run-queue.js";
22
+ import { cancelRunOperation, resumeRun } from "./harness/run/run-operations.js";
23
+ import { acquireRunSlot as acquireHarnessRunSlot } from "./harness/run/run-slot-acquisition.js";
24
+ import { dropPendingRunSlot, enqueuePendingRunSlot } from "./harness/run/run-queue.js";
24
25
  import { getDefaultHostAgentId, resolveSelectedAgentId } from "./harness/run/routing.js";
25
26
  import { resolveCheckpointer, resolveEmbeddingModel, resolveStore, resolveStoreFromConfig, resolveVectorStore, } from "./harness/run/resources.js";
26
27
  import { createToolMcpServerFromTools, serveToolsOverStdioFromHarness } from "../mcp.js";
@@ -28,7 +29,9 @@ import { getBindingAdapterKind, getBindingPrimaryTools, getBindingStoreConfig }
28
29
  import { isRuntimeEntryBinding } from "./support/runtime-entry.js";
29
30
  import { describeWorkspaceInventory, listAgentSkills as listWorkspaceAgentSkills, } from "./harness/system/inventory.js";
30
31
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
31
- 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
+ import { streamHarnessRun } from "./harness/run/stream-run.js";
34
+ import { deleteThreadRecord, getPublicApproval, getThreadRecord, listPublicApprovals, } from "./harness/run/thread-records.js";
32
35
  export class AgentHarnessRuntime {
33
36
  workspace;
34
37
  runtimeAdapterOptions;
@@ -157,11 +160,13 @@ export class AgentHarnessRuntime {
157
160
  this.healthMonitor?.recordLlmFailure(Date.now() - startedAt);
158
161
  }
159
162
  async initialize() {
160
- await this.persistence.initialize();
161
- await this.checkpointMaintenance?.start();
162
- await this.runtimeRecordMaintenance?.start();
163
- await this.healthMonitor?.start();
164
- 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
+ });
165
170
  }
166
171
  subscribe(listener) {
167
172
  return this.eventBus.subscribe(listener);
@@ -202,47 +207,20 @@ export class AgentHarnessRuntime {
202
207
  return this.persistence.getSession(threadId);
203
208
  }
204
209
  async getThread(threadId) {
205
- const [threadSummary, meta, messages, runs] = await Promise.all([
206
- this.getSession(threadId),
207
- this.persistence.getThreadMeta(threadId),
208
- this.persistence.listThreadMessages(threadId, 200),
209
- this.persistence.listThreadRuns(threadId),
210
- ]);
211
- if (!threadSummary || !meta) {
212
- return null;
213
- }
214
- const latestRunId = threadSummary.latestRunId;
215
- const latestApprovals = await this.persistence.getRunApprovals(threadId, latestRunId);
216
- const pendingApproval = latestApprovals
217
- .filter((approval) => approval.status === "pending")
218
- .sort((left, right) => right.requestedAt.localeCompare(left.requestedAt))[0];
219
- return {
220
- threadId,
221
- entryAgentId: meta.entryAgentId,
222
- currentState: threadSummary.status,
223
- latestRunId,
224
- createdAt: meta.createdAt,
225
- updatedAt: threadSummary.updatedAt,
226
- messages,
227
- runs,
228
- pendingDecision: pendingApproval
229
- ? {
230
- approvalId: pendingApproval.approvalId,
231
- pendingActionId: pendingApproval.pendingActionId,
232
- toolName: pendingApproval.toolName,
233
- allowedDecisions: pendingApproval.allowedDecisions,
234
- requestedAt: pendingApproval.requestedAt,
235
- }
236
- : undefined,
237
- };
210
+ return getThreadRecord({
211
+ persistence: this.persistence,
212
+ getSession: (currentThreadId) => this.getSession(currentThreadId),
213
+ }, threadId);
238
214
  }
239
215
  async listApprovals(filter) {
240
- const approvals = await this.persistence.listApprovals(filter);
241
- return approvals.map((approval) => toPublicApprovalRecord(approval));
216
+ return listPublicApprovals({
217
+ persistence: this.persistence,
218
+ }, filter);
242
219
  }
243
220
  async getApproval(approvalId) {
244
- const approval = await this.persistence.getApproval(approvalId);
245
- return approval ? toPublicApprovalRecord(approval) : null;
221
+ return getPublicApproval({
222
+ persistence: this.persistence,
223
+ }, approvalId);
246
224
  }
247
225
  listAgentSkills(agentId, options = {}) {
248
226
  return listWorkspaceAgentSkills(this.workspace, agentId, {
@@ -275,19 +253,11 @@ export class AgentHarnessRuntime {
275
253
  }
276
254
  }
277
255
  async deleteThread(threadId) {
278
- const thread = await this.getThread(threadId);
279
- if (!thread) {
280
- return false;
281
- }
282
- const activeRun = thread.runs.find((run) => !isTerminalRunState(run.state));
283
- if (activeRun) {
284
- throw new Error(`Cannot delete thread ${threadId} while run ${activeRun.runId} is ${activeRun.state}`);
285
- }
286
- const deleted = await this.persistence.deleteThread(threadId);
287
- if (deleted) {
288
- await this.deleteThreadCheckpoints(threadId);
289
- }
290
- 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);
291
261
  }
292
262
  async createToolMcpServer(options) {
293
263
  const tools = this.resolveAgentTools(options.agentId).map(({ compiledTool, resolvedTool }) => ({
@@ -582,159 +552,24 @@ export class AgentHarnessRuntime {
582
552
  };
583
553
  }
584
554
  async acquireRunSlot(threadId, runId, activeState = "running", priority = 0) {
585
- let stopHeartbeat = () => undefined;
586
- const beginLease = async (mode) => {
587
- if (!threadId || !runId) {
588
- return;
589
- }
590
- const claimedAt = new Date().toISOString();
591
- if (mode === "queue-claim") {
592
- await this.persistence.claimQueuedRun({
593
- threadId,
594
- runId,
595
- workerId: this.workerId,
596
- claimedAt,
597
- leaseExpiresAt: new Date(Date.now() + this.concurrencyConfig.leaseMs).toISOString(),
598
- });
599
- }
600
- else {
601
- await this.persistence.renewRunLease({
602
- runId,
603
- workerId: this.workerId,
604
- heartbeatAt: claimedAt,
605
- leaseExpiresAt: new Date(Date.now() + this.concurrencyConfig.leaseMs).toISOString(),
606
- });
607
- }
608
- if (this.concurrencyConfig.heartbeatIntervalMs <= 0) {
609
- return;
610
- }
611
- const timer = setInterval(() => {
612
- void this.persistence.renewRunLease({
613
- runId,
614
- workerId: this.workerId,
615
- heartbeatAt: new Date().toISOString(),
616
- leaseExpiresAt: new Date(Date.now() + this.concurrencyConfig.leaseMs).toISOString(),
617
- });
618
- }, this.concurrencyConfig.heartbeatIntervalMs);
619
- timer.unref?.();
620
- stopHeartbeat = () => {
621
- clearInterval(timer);
622
- };
623
- };
624
- const releaseLease = async () => {
625
- stopHeartbeat();
626
- if (runId) {
627
- await this.persistence.releaseRunClaim(runId);
628
- }
629
- };
630
- const maxConcurrentRuns = this.concurrencyConfig.maxConcurrentRuns;
631
- if (!maxConcurrentRuns) {
632
- await beginLease("direct-heartbeat");
633
- return async () => {
634
- await releaseLease();
635
- };
636
- }
637
- const canActivateImmediately = this.activeRunSlots < maxConcurrentRuns;
638
- const useDirectHeartbeatFastPath = canActivateImmediately && maxConcurrentRuns > 1;
639
- if (canActivateImmediately) {
640
- this.activeRunSlots += 1;
641
- if (threadId && runId && !useDirectHeartbeatFastPath) {
642
- await this.persistence.enqueueRun({ threadId, runId, priority });
643
- }
644
- await beginLease(useDirectHeartbeatFastPath ? "direct-heartbeat" : "queue-claim");
645
- let released = false;
646
- return async () => {
647
- if (released) {
648
- return;
649
- }
650
- released = true;
651
- await releaseLease();
652
- this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
653
- const next = shiftNextPendingRunSlot(this.pendingRunSlots);
654
- void next?.activate();
655
- };
656
- }
657
- const activateQueuedRun = async () => {
658
- const currentRun = runId ? await this.persistence.getRun(runId) : null;
659
- if (currentRun?.state === "cancelled") {
660
- return "abort";
661
- }
662
- this.activeRunSlots += 1;
663
- if (threadId && runId) {
664
- await this.emit(threadId, runId, 4, "run.dequeued", {
665
- queuePosition: 0,
666
- activeRunCount: this.activeRunSlots,
667
- maxConcurrentRuns,
668
- priority,
669
- });
670
- await this.setRunStateAndEmit(threadId, runId, 5, activeState, {
671
- previousState: "queued",
672
- });
673
- await beginLease("queue-claim");
674
- }
675
- return "activate";
676
- };
677
- if (threadId && runId) {
678
- await this.persistence.enqueueRun({ threadId, runId, priority });
679
- const slotAcquisition = new Promise((resolve, reject) => {
680
- const displacedEntries = this.enqueuePendingRunSlot({
681
- threadId,
682
- runId,
683
- priority,
684
- activate: async () => {
685
- try {
686
- resolve(await activateQueuedRun());
687
- }
688
- catch (error) {
689
- reject(error);
690
- }
691
- },
692
- abort: () => resolve("abort"),
693
- });
694
- void Promise.all(displacedEntries.map((candidate) => this.emit(candidate.threadId, candidate.runId, 3, "run.queued", {
695
- queuePosition: candidate.queuePosition,
696
- activeRunCount: this.activeRunSlots,
697
- maxConcurrentRuns,
698
- priority: candidate.priority,
699
- })));
700
- });
701
- const queuePosition = this.pendingRunSlots.findIndex((entry) => entry.runId === runId) + 1;
702
- await this.setRunStateAndEmit(threadId, runId, 2, "queued", {
703
- previousState: activeState,
704
- });
705
- await this.emit(threadId, runId, 3, "run.queued", {
706
- queuePosition,
707
- activeRunCount: this.activeRunSlots,
708
- maxConcurrentRuns,
709
- priority,
710
- });
711
- const slotAcquisitionResult = await slotAcquisition;
712
- if (slotAcquisitionResult === "abort") {
713
- return async () => undefined;
714
- }
715
- let released = false;
716
- return async () => {
717
- if (released) {
718
- return;
719
- }
720
- released = true;
721
- await releaseLease();
722
- this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
723
- const next = this.pendingRunSlots.shift();
724
- void next?.activate();
725
- };
726
- }
727
- let released = false;
728
- return async () => {
729
- if (released) {
730
- return;
731
- }
732
- released = true;
733
- await releaseLease();
734
- this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
735
- const next = shiftNextPendingRunSlot(this.pendingRunSlots);
736
- void next?.activate();
737
- };
555
+ return acquireHarnessRunSlot({
556
+ persistence: this.persistence,
557
+ workerId: this.workerId,
558
+ concurrencyConfig: this.concurrencyConfig,
559
+ pendingRunSlots: this.pendingRunSlots,
560
+ getActiveRunSlots: () => this.activeRunSlots,
561
+ setActiveRunSlots: (count) => {
562
+ this.activeRunSlots = count;
563
+ },
564
+ enqueuePendingRunSlot: (entry) => this.enqueuePendingRunSlot(entry),
565
+ emit: (currentThreadId, currentRunId, sequence, eventType, payload) => this.emit(currentThreadId, currentRunId, sequence, eventType, payload),
566
+ setRunStateAndEmit: (currentThreadId, currentRunId, sequence, state, stateOptions) => this.setRunStateAndEmit(currentThreadId, currentRunId, sequence, state, stateOptions),
567
+ }, {
568
+ threadId,
569
+ runId,
570
+ activeState,
571
+ priority,
572
+ });
738
573
  }
739
574
  dropPendingRunSlot(runId) {
740
575
  return dropPendingRunSlot(this.pendingRunSlots, runId);
@@ -801,8 +636,6 @@ export class AgentHarnessRuntime {
801
636
  }
802
637
  return;
803
638
  }
804
- let emitted = false;
805
- let streamActivityObserved = false;
806
639
  const { threadId, runId, isNewThread, runCreatedEventPromise, releaseRunSlotPromise, } = await this.prepareRunStart(options, invocation, (_binding, activeSelectedAgentId) => ({
807
640
  agentId: activeSelectedAgentId,
808
641
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
@@ -810,345 +643,55 @@ export class AgentHarnessRuntime {
810
643
  input: options.input,
811
644
  state: "running",
812
645
  }));
813
- const priorHistoryPromise = Promise.resolve(isNewThread ? [] : undefined).then((historyHint) => historyHint ?? this.loadPriorHistory(threadId, runId));
814
- yield { type: "event", event: await runCreatedEventPromise };
815
- let releaseRunSlot = async () => undefined;
816
- const emitOutputDelta = (content) => emitStreamingOutputDeltaAndCreateItem((currentThreadId, currentRunId, sequence, eventType, payload) => this.emit(currentThreadId, currentRunId, sequence, eventType, payload), threadId, runId, selectedAgentId, content);
817
- try {
818
- const [priorHistory, acquiredReleaseRunSlot] = await Promise.all([
819
- priorHistoryPromise,
820
- releaseRunSlotPromise,
821
- ]).then(([loadedPriorHistory, resolvedReleaseRunSlot]) => [loadedPriorHistory, resolvedReleaseRunSlot]);
822
- releaseRunSlot = acquiredReleaseRunSlot;
823
- let assistantOutput = "";
824
- const toolErrors = [];
825
- let lastToolResultKey = null;
826
- for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory, {
827
- context: invocation.context,
828
- state: invocation.state,
829
- files: invocation.files,
830
- runId,
831
- })) {
832
- if (chunk) {
833
- streamActivityObserved = true;
834
- const normalizedChunk = typeof chunk === "string"
835
- ? chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)
836
- ? { kind: "interrupt", content: chunk.slice(AGENT_INTERRUPT_SENTINEL_PREFIX.length) }
837
- : { kind: "content", content: chunk }
838
- : chunk;
839
- if (normalizedChunk.kind === "upstream-event") {
840
- yield {
841
- type: "upstream-event",
842
- threadId,
843
- runId,
844
- agentId: selectedAgentId,
845
- event: normalizedChunk.event.format === "langgraph-v2"
846
- ? normalizedChunk.event
847
- : normalizeUpstreamRuntimeEvent(normalizedChunk.event.raw ?? normalizedChunk.event),
848
- };
849
- continue;
850
- }
851
- if (normalizedChunk.kind === "interrupt") {
852
- const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
853
- const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 6, "waiting_for_approval", {
854
- previousState: "running",
855
- checkpointRef,
856
- });
857
- const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 7);
858
- yield {
859
- type: "event",
860
- event: waitingEvent,
861
- };
862
- yield {
863
- type: "event",
864
- event: approvalRequest.event,
865
- };
866
- yield {
867
- type: "result",
868
- result: {
869
- threadId,
870
- runId,
871
- agentId: selectedAgentId,
872
- state: "waiting_for_approval",
873
- output: assistantOutput,
874
- finalMessageText: assistantOutput,
875
- interruptContent: normalizedChunk.content,
876
- approvalId: approvalRequest.approval.approvalId,
877
- pendingActionId: approvalRequest.approval.pendingActionId,
878
- },
879
- };
880
- return;
881
- }
882
- if (normalizedChunk.kind === "reasoning") {
883
- await this.emit(threadId, runId, 3, "reasoning.delta", {
884
- content: normalizedChunk.content,
885
- });
886
- yield {
887
- type: "reasoning",
888
- threadId,
889
- runId,
890
- agentId: selectedAgentId,
891
- content: normalizedChunk.content,
892
- };
893
- continue;
894
- }
895
- if (normalizedChunk.kind === "step") {
896
- yield {
897
- type: "step",
898
- threadId,
899
- runId,
900
- agentId: selectedAgentId,
901
- content: normalizedChunk.content,
902
- };
903
- continue;
904
- }
905
- if (normalizedChunk.kind === "tool-result") {
906
- const toolResultKey = createStreamingToolResultKey(normalizedChunk.toolName, normalizedChunk.output, normalizedChunk.isError);
907
- if (toolResultKey === lastToolResultKey) {
908
- continue;
909
- }
910
- lastToolResultKey = toolResultKey;
911
- if (normalizedChunk.isError) {
912
- toolErrors.push(renderToolFailure(normalizedChunk.toolName, normalizedChunk.output));
913
- }
914
- yield {
915
- type: "tool-result",
916
- threadId,
917
- runId,
918
- agentId: selectedAgentId,
919
- toolName: normalizedChunk.toolName,
920
- output: normalizedChunk.output,
921
- isError: normalizedChunk.isError,
922
- };
923
- continue;
924
- }
925
- emitted = true;
926
- assistantOutput += normalizedChunk.content;
927
- yield await emitOutputDelta(normalizedChunk.content);
928
- }
929
- }
930
- if (!assistantOutput && toolErrors.length > 0) {
931
- assistantOutput = toolErrors.join("\n\n");
932
- emitted = true;
933
- yield await emitOutputDelta(assistantOutput);
934
- }
935
- if (!assistantOutput) {
936
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
937
- if (Array.isArray(actual.contentBlocks) && actual.contentBlocks.length > 0) {
938
- yield createStreamingContentBlocksItem(threadId, runId, selectedAgentId, actual.contentBlocks);
939
- }
940
- if (actual.output) {
941
- assistantOutput = actual.output;
942
- emitted = true;
943
- yield await emitOutputDelta(actual.output);
944
- }
945
- }
946
- await appendLifecycleAssistantMessage(this.persistence, threadId, runId, assistantOutput);
947
- yield {
948
- type: "result",
949
- result: {
950
- threadId,
951
- runId,
952
- agentId: selectedAgentId,
953
- state: "completed",
954
- output: assistantOutput,
955
- finalMessageText: assistantOutput,
956
- },
957
- };
958
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "completed", {
959
- previousState: "running",
960
- }) };
961
- return;
962
- }
963
- catch (error) {
964
- if (emitted || streamActivityObserved) {
965
- const runtimeFailure = renderRuntimeFailure(error);
966
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
967
- previousState: "running",
968
- error: error instanceof Error ? error.message : String(error),
969
- }) };
970
- yield {
971
- type: "content",
972
- threadId,
973
- runId,
974
- agentId: selectedAgentId,
975
- content: runtimeFailure,
976
- };
977
- yield {
978
- type: "result",
979
- result: {
980
- threadId,
981
- runId,
982
- agentId: selectedAgentId,
983
- state: "failed",
984
- output: runtimeFailure,
985
- finalMessageText: runtimeFailure,
986
- },
987
- };
988
- return;
989
- }
990
- if (error instanceof RuntimeOperationTimeoutError && error.stage === "invoke") {
991
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
992
- previousState: "running",
993
- error: error.message,
994
- }) };
995
- yield {
996
- type: "content",
997
- threadId,
998
- runId,
999
- agentId: selectedAgentId,
1000
- content: renderRuntimeFailure(error),
1001
- };
1002
- yield {
1003
- type: "result",
1004
- result: {
1005
- threadId,
1006
- runId,
1007
- agentId: selectedAgentId,
1008
- state: "failed",
1009
- output: renderRuntimeFailure(error),
1010
- finalMessageText: renderRuntimeFailure(error),
1011
- },
1012
- };
1013
- return;
1014
- }
1015
- try {
1016
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
1017
- await appendLifecycleAssistantMessage(this.persistence, threadId, runId, actual.output);
1018
- if (Array.isArray(actual.contentBlocks) && actual.contentBlocks.length > 0) {
1019
- yield createStreamingContentBlocksItem(threadId, runId, selectedAgentId, actual.contentBlocks);
1020
- }
1021
- if (actual.output) {
1022
- yield await emitOutputDelta(actual.output);
1023
- }
1024
- yield {
1025
- type: "result",
1026
- result: {
1027
- ...actual,
1028
- threadId,
1029
- runId,
1030
- agentId: selectedAgentId,
1031
- },
1032
- };
1033
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, actual.state, {
1034
- previousState: "running",
1035
- }) };
1036
- return;
1037
- }
1038
- catch (invokeError) {
1039
- await emitSyntheticFallbackEvent({
1040
- persistence: this.persistence,
1041
- publishEvent: (event) => this.eventBus.publish(event),
1042
- trackBackgroundTask: (task) => this.trackBackgroundTask(task),
1043
- backgroundEventTypes: AgentHarnessRuntime.BACKGROUND_EVENT_TYPES,
1044
- }, threadId, runId, selectedAgentId, invokeError);
1045
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
1046
- previousState: "running",
1047
- error: invokeError instanceof Error ? invokeError.message : String(invokeError),
1048
- }) };
1049
- yield {
1050
- type: "content",
1051
- threadId,
1052
- runId,
1053
- agentId: selectedAgentId,
1054
- content: renderRuntimeFailure(invokeError),
1055
- };
1056
- yield {
1057
- type: "result",
1058
- result: {
1059
- threadId,
1060
- runId,
1061
- agentId: selectedAgentId,
1062
- state: "failed",
1063
- output: renderRuntimeFailure(invokeError),
1064
- finalMessageText: renderRuntimeFailure(invokeError),
1065
- },
1066
- };
1067
- return;
1068
- }
1069
- }
1070
- finally {
1071
- await this.persistence.clearRunRequest(threadId, runId);
1072
- await releaseRunSlot();
1073
- }
646
+ yield* streamHarnessRun({
647
+ binding,
648
+ input: options.input,
649
+ invocation,
650
+ threadId,
651
+ runId,
652
+ selectedAgentId,
653
+ isNewThread,
654
+ runCreatedEventPromise,
655
+ releaseRunSlotPromise,
656
+ loadPriorHistory: (currentThreadId, currentRunId) => this.loadPriorHistory(currentThreadId, currentRunId),
657
+ stream: (activeBinding, input, currentThreadId, priorHistory, streamOptions) => this.runtimeAdapter.stream(activeBinding, input, currentThreadId, priorHistory, streamOptions),
658
+ invokeWithHistory: (activeBinding, input, currentThreadId, currentRunId) => this.invokeWithHistory(activeBinding, input, currentThreadId, currentRunId),
659
+ emit: (currentThreadId, currentRunId, sequence, eventType, payload) => this.emit(currentThreadId, currentRunId, sequence, eventType, payload),
660
+ setRunStateAndEmit: (currentThreadId, currentRunId, sequence, state, stateOptions) => this.setRunStateAndEmit(currentThreadId, currentRunId, sequence, state, stateOptions),
661
+ requestApprovalAndEmit: (currentThreadId, currentRunId, input, interruptContent, checkpointRef, sequence) => this.requestApprovalAndEmit(currentThreadId, currentRunId, input, interruptContent, checkpointRef, sequence),
662
+ appendAssistantMessage: (currentThreadId, currentRunId, content) => appendLifecycleAssistantMessage(this.persistence, currentThreadId, currentRunId, content),
663
+ clearRunRequest: (currentThreadId, currentRunId) => this.persistence.clearRunRequest(currentThreadId, currentRunId),
664
+ emitSyntheticFallback: (currentThreadId, currentRunId, currentSelectedAgentId, error) => emitSyntheticFallbackEvent({
665
+ persistence: this.persistence,
666
+ publishEvent: (event) => this.eventBus.publish(event),
667
+ trackBackgroundTask: (task) => this.trackBackgroundTask(task),
668
+ backgroundEventTypes: AgentHarnessRuntime.BACKGROUND_EVENT_TYPES,
669
+ }, currentThreadId, currentRunId, currentSelectedAgentId, error),
670
+ });
1074
671
  }
1075
672
  async resume(options) {
1076
- const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
1077
- const thread = options.threadId
1078
- ? await this.getSession(options.threadId)
1079
- : approvalById
1080
- ? await this.getSession(approvalById.threadId)
1081
- : null;
1082
- if (!thread) {
1083
- throw new Error("resume requires either threadId or approvalId");
1084
- }
1085
- const approval = approvalById ?? await resolveHarnessApprovalRecord(this.persistence, options, thread);
1086
- const threadId = approval.threadId;
1087
- const runId = approval.runId;
1088
- const binding = this.workspace.bindings.get(thread.agentId);
1089
- if (!binding) {
1090
- throw new Error(`Unknown agent ${thread.agentId}`);
1091
- }
1092
- const resumePayload = buildHarnessResumePayload(binding, approval, options);
1093
- const cancellation = await this.getRunCancellation(runId);
1094
- if (cancellation.requested) {
1095
- return this.finalizeCancelledRun(threadId, runId, thread.status, cancellation.reason);
1096
- }
1097
- await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
1098
- const releaseRunSlot = await this.acquireRunSlot(threadId, runId, "resuming", await this.resolvePersistedRunPriority(threadId, runId));
1099
- try {
1100
- await this.persistence.saveRecoveryIntent(threadId, runId, {
1101
- kind: "approval-decision",
1102
- savedAt: new Date().toISOString(),
1103
- checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
1104
- resumePayload,
1105
- attempts: 0,
1106
- });
1107
- await this.emit(threadId, runId, 5, "run.resumed", {
1108
- resumeKind: "cross-restart",
1109
- checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
1110
- state: "resuming",
1111
- approvalId: approval.approvalId,
1112
- pendingActionId: approval.pendingActionId,
1113
- });
1114
- await this.persistence.resolveApproval(threadId, runId, approval.approvalId, options.decision === "reject" ? "rejected" : options.decision === "edit" ? "edited" : "approved");
1115
- await this.emit(threadId, runId, 6, "approval.resolved", {
1116
- approvalId: approval.approvalId,
1117
- pendingActionId: approval.pendingActionId,
1118
- decision: options.decision ?? "approve",
1119
- toolName: approval.toolName,
1120
- });
1121
- const history = await this.persistence.listThreadMessages(threadId);
1122
- const priorHistory = history.filter((message) => message.runId !== runId);
1123
- const runInput = await this.loadRunInput(threadId, runId);
1124
- const startedAt = Date.now();
1125
- try {
1126
- const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumePayload, priorHistory);
1127
- this.recordLlmSuccess(startedAt);
1128
- const cancelledAfterInvoke = await this.getRunCancellation(runId);
1129
- if (cancelledAfterInvoke.requested) {
1130
- return this.finalizeCancelledRun(threadId, runId, "resuming", cancelledAfterInvoke.reason);
1131
- }
1132
- await this.persistence.clearRecoveryIntent(threadId, runId);
1133
- const finalized = await this.finalizeContinuedRun(binding, threadId, runId, runInput, actual, {
1134
- previousState: "resuming",
1135
- stateSequence: 7,
1136
- approvalSequence: 8,
1137
- });
1138
- return {
1139
- ...finalized,
1140
- approvalId: finalized.approvalId ?? approval.approvalId,
1141
- pendingActionId: finalized.pendingActionId ?? approval.pendingActionId,
1142
- };
1143
- }
1144
- catch (error) {
1145
- this.recordLlmFailure(startedAt);
1146
- throw error;
1147
- }
1148
- }
1149
- finally {
1150
- await releaseRunSlot();
1151
- }
673
+ return resumeRun({
674
+ getApprovalById: (approvalId) => this.persistence.getApproval(approvalId),
675
+ getSession: (threadId) => this.getSession(threadId),
676
+ resolveApprovalRecord: (resumeOptions, thread) => resolveHarnessApprovalRecord(this.persistence, resumeOptions, thread),
677
+ getBinding: (agentId) => this.workspace.bindings.get(agentId),
678
+ buildResumePayload: (binding, approval, resumeOptions) => buildHarnessResumePayload(binding, approval, resumeOptions),
679
+ getRunCancellation: (runId) => this.getRunCancellation(runId),
680
+ finalizeCancelledRun: (threadId, runId, previousState, reason) => this.finalizeCancelledRun(threadId, runId, previousState, reason),
681
+ setRunState: (threadId, runId, state, checkpointRef) => this.persistence.setRunState(threadId, runId, state, checkpointRef),
682
+ acquireRunSlot: (threadId, runId, activeState, priority) => this.acquireRunSlot(threadId, runId, activeState, priority),
683
+ resolvePersistedRunPriority: (threadId, runId) => this.resolvePersistedRunPriority(threadId, runId),
684
+ saveRecoveryIntent: (threadId, runId, payload) => this.persistence.saveRecoveryIntent(threadId, runId, payload),
685
+ emit: (threadId, runId, sequence, eventType, payload) => this.emit(threadId, runId, sequence, eventType, payload),
686
+ resolveApproval: (threadId, runId, approvalId, resolution) => this.persistence.resolveApproval(threadId, runId, approvalId, resolution),
687
+ listThreadMessages: (threadId) => this.persistence.listThreadMessages(threadId),
688
+ loadRunInput: (threadId, runId) => this.loadRunInput(threadId, runId),
689
+ invoke: (binding, input, threadId, runId, resumePayload, priorHistory) => this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory),
690
+ recordLlmSuccess: (startedAt) => this.recordLlmSuccess(startedAt),
691
+ recordLlmFailure: (startedAt) => this.recordLlmFailure(startedAt),
692
+ clearRecoveryIntent: (threadId, runId) => this.persistence.clearRecoveryIntent(threadId, runId),
693
+ finalizeContinuedRun: (binding, threadId, runId, input, actual, operationOptions) => this.finalizeContinuedRun(binding, threadId, runId, input, actual, operationOptions),
694
+ }, options);
1152
695
  }
1153
696
  async restartConversation(options) {
1154
697
  const thread = await this.getSession(options.threadId);
@@ -1187,96 +730,35 @@ export class AgentHarnessRuntime {
1187
730
  await this.close();
1188
731
  }
1189
732
  async cancelRun(options) {
1190
- const run = await this.persistence.getRun(options.runId);
1191
- if (!run) {
1192
- throw new Error(`Unknown run ${options.runId}`);
1193
- }
1194
- if (isTerminalRunState(run.state)) {
1195
- return {
1196
- threadId: run.threadId,
1197
- runId: run.runId,
1198
- agentId: run.agentId,
1199
- state: run.state,
1200
- output: run.state,
1201
- };
1202
- }
1203
- await this.persistence.requestRunCancel(run.runId, options.reason);
1204
- if (run.state === "queued" || run.state === "waiting_for_approval" || run.state === "claimed") {
1205
- if (run.state === "queued") {
1206
- this.dropPendingRunSlot(run.runId);
1207
- }
1208
- return this.finalizeCancelledRun(run.threadId, run.runId, run.state, options.reason);
1209
- }
1210
- await this.setRunStateAndEmit(run.threadId, run.runId, 103, "cancelling", {
1211
- previousState: run.state,
1212
- ...(options.reason ? { error: options.reason } : {}),
1213
- });
1214
- return {
1215
- threadId: run.threadId,
1216
- runId: run.runId,
1217
- agentId: run.agentId,
1218
- state: "cancelling",
1219
- output: options.reason ? `cancelling: ${options.reason}` : "cancelling",
1220
- };
733
+ return cancelRunOperation({
734
+ getRun: (runId) => this.persistence.getRun(runId),
735
+ requestRunCancel: (runId, reason) => this.persistence.requestRunCancel(runId, reason),
736
+ dropPendingRunSlot: (runId) => this.dropPendingRunSlot(runId),
737
+ finalizeCancelledRun: (threadId, runId, previousState, reason) => this.finalizeCancelledRun(threadId, runId, previousState, reason),
738
+ setRunStateAndEmit: (threadId, runId, sequence, state, stateOptions) => this.setRunStateAndEmit(threadId, runId, sequence, state, stateOptions),
739
+ }, options);
1221
740
  }
1222
741
  async recoverStartupRuns() {
1223
- if (!this.recoveryConfig.enabled) {
1224
- return;
1225
- }
1226
- await this.reclaimExpiredClaimedRuns();
1227
- const threads = await this.persistence.listSessions();
1228
- const recoveryContext = this.createStartupRecoveryContext();
1229
- for (const thread of threads) {
1230
- const handled = await recoverQueuedStartupRun(recoveryContext, thread) ||
1231
- await recoverRunningStartupRun(recoveryContext, thread) ||
1232
- await recoverResumingStartupRun(recoveryContext, thread);
1233
- if (handled) {
1234
- continue;
1235
- }
1236
- }
742
+ await recoverHarnessStartupRuns({
743
+ recoveryConfig: this.recoveryConfig,
744
+ persistence: this.persistence,
745
+ createStartupRecoveryContext: () => this.createStartupRecoveryContext(),
746
+ reclaimExpiredClaimedRuns: (nowIso) => this.reclaimExpiredClaimedRuns(nowIso),
747
+ });
1237
748
  }
1238
749
  async reclaimExpiredClaimedRuns(nowIso = new Date().toISOString()) {
1239
- const expiredClaims = await this.persistence.listExpiredClaimedRuns(nowIso);
1240
- for (const claim of expiredClaims) {
1241
- const thread = await this.persistence.getSession(claim.threadId);
1242
- if (!thread) {
1243
- await this.persistence.releaseRunClaim(claim.runId);
1244
- continue;
1245
- }
1246
- const lifecycle = await this.persistence.getRunLifecycle(claim.threadId, claim.runId);
1247
- if (lifecycle.state === "claimed") {
1248
- await this.persistence.enqueueRun({
1249
- threadId: claim.threadId,
1250
- runId: claim.runId,
1251
- priority: claim.priority,
1252
- queueKey: claim.queueKey,
1253
- availableAt: nowIso,
1254
- });
1255
- await this.setRunStateAndEmit(claim.threadId, claim.runId, 99, "queued", {
1256
- previousState: "claimed",
1257
- });
1258
- await this.emit(claim.threadId, claim.runId, 100, "run.queued", {
1259
- queuePosition: 0,
1260
- activeRunCount: this.activeRunSlots,
1261
- maxConcurrentRuns: this.concurrencyConfig.maxConcurrentRuns,
1262
- recoveredOnStartup: true,
1263
- reclaimReason: "expired-lease",
1264
- });
1265
- continue;
1266
- }
1267
- await this.persistence.releaseRunClaim(claim.runId);
1268
- }
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);
1269
757
  }
1270
758
  async isStaleRunningRun(thread, nowMs = Date.now()) {
1271
- const control = await this.persistence.getRunControl(thread.latestRunId);
1272
- const heartbeatAt = control?.heartbeatAt;
1273
- if (!heartbeatAt) {
1274
- return true;
1275
- }
1276
- const heartbeatAtMs = Date.parse(heartbeatAt);
1277
- if (!Number.isFinite(heartbeatAtMs)) {
1278
- return true;
1279
- }
1280
- return nowMs - heartbeatAtMs >= this.concurrencyConfig.heartbeatTimeoutMs;
759
+ return isHarnessStaleRunningRun({
760
+ persistence: this.persistence,
761
+ concurrencyConfig: this.concurrencyConfig,
762
+ }, thread, nowMs);
1281
763
  }
1282
764
  }