@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.
- package/README.md +94 -82
- package/dist/config/workspace.yaml +14 -0
- package/dist/contracts/types.d.ts +17 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/persistence/file-store.d.ts +36 -0
- package/dist/persistence/file-store.js +32 -0
- package/dist/runtime/agent-runtime-adapter.d.ts +11 -3
- package/dist/runtime/agent-runtime-adapter.js +31 -48
- package/dist/runtime/checkpoint-maintenance.js +1 -1
- package/dist/runtime/declared-middleware.d.ts +8 -1
- package/dist/runtime/declared-middleware.js +57 -5
- package/dist/runtime/harness.d.ts +9 -0
- package/dist/runtime/harness.js +393 -247
- package/dist/runtime/support/runtime-factories.d.ts +1 -1
- package/dist/runtime/support/runtime-factories.js +3 -0
- package/dist/workspace/agent-binding-compiler.js +37 -8
- package/dist/workspace/object-loader.js +12 -2
- package/dist/workspace/support/workspace-ref-utils.d.ts +15 -0
- package/dist/workspace/support/workspace-ref-utils.js +40 -0
- package/dist/workspace/validate.js +11 -11
- package/package.json +2 -2
package/dist/runtime/harness.js
CHANGED
|
@@ -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
|
|
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
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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 (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
runId,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
721
|
-
|
|
722
|
-
? await this.
|
|
723
|
-
|
|
724
|
-
? await this.getSession(
|
|
725
|
-
:
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
730
|
-
|
|
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 };
|