@botbotgo/agent-harness 0.0.44 → 0.0.46

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.
@@ -213,6 +213,10 @@ export class AgentHarnessRuntime {
213
213
  resolvedTool: resolvedTools[index],
214
214
  }));
215
215
  }
216
+ supportsRunningReplay(binding) {
217
+ const tools = getBindingPrimaryTools(binding);
218
+ return tools.every((tool) => tool.retryable === true);
219
+ }
216
220
  async listThreads(filter) {
217
221
  const threadSummaries = await this.persistence.listSessions();
218
222
  if (!filter?.agentId) {
@@ -385,6 +389,72 @@ export class AgentHarnessRuntime {
385
389
  const priorHistory = await this.loadPriorHistory(threadId, runId);
386
390
  return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory, options);
387
391
  }
392
+ buildPersistedRunRequest(input, invocation) {
393
+ const envelope = invocation.invocation ?? {
394
+ ...(invocation.context ? { context: invocation.context } : {}),
395
+ ...(invocation.state ? { inputs: invocation.state } : {}),
396
+ ...(invocation.files ? { attachments: invocation.files } : {}),
397
+ };
398
+ return {
399
+ input: normalizeMessageContent(input),
400
+ invocation: envelope && Object.keys(envelope).length > 0
401
+ ? {
402
+ ...(envelope.context ? { context: envelope.context } : {}),
403
+ ...(envelope.inputs ? { inputs: envelope.inputs } : {}),
404
+ ...(envelope.attachments ? { attachments: envelope.attachments } : {}),
405
+ ...(envelope.capabilities ? { capabilities: envelope.capabilities } : {}),
406
+ }
407
+ : undefined,
408
+ savedAt: new Date().toISOString(),
409
+ };
410
+ }
411
+ async executeQueuedRun(binding, input, threadId, runId, agentId, options = {}) {
412
+ const previousState = options.previousState ?? "running";
413
+ if (previousState === "queued") {
414
+ await this.emit(threadId, runId, 101, "run.dequeued", {
415
+ queuePosition: 0,
416
+ activeRunCount: this.activeRunSlots,
417
+ maxConcurrentRuns: this.concurrencyConfig.maxConcurrentRuns,
418
+ recoveredOnStartup: true,
419
+ });
420
+ await this.setRunStateAndEmit(threadId, runId, 102, "running", {
421
+ previousState: "queued",
422
+ });
423
+ }
424
+ try {
425
+ const actual = await this.invokeWithHistory(binding, input, threadId, runId, undefined, {
426
+ context: options.context,
427
+ state: options.state,
428
+ files: options.files,
429
+ });
430
+ const finalized = await this.finalizeContinuedRun(threadId, runId, input, actual, {
431
+ previousState: previousState === "queued" ? "running" : previousState,
432
+ stateSequence: options.stateSequence ?? 103,
433
+ approvalSequence: options.approvalSequence ?? 104,
434
+ });
435
+ return {
436
+ ...finalized,
437
+ agentId,
438
+ };
439
+ }
440
+ catch (error) {
441
+ await this.emitSyntheticFallback(threadId, runId, agentId, error, 103);
442
+ await this.setRunStateAndEmit(threadId, runId, 104, "failed", {
443
+ previousState: previousState === "queued" ? "running" : previousState,
444
+ error: error instanceof Error ? error.message : String(error),
445
+ });
446
+ return {
447
+ threadId,
448
+ runId,
449
+ agentId,
450
+ state: "failed",
451
+ output: renderRuntimeFailure(error),
452
+ };
453
+ }
454
+ finally {
455
+ await this.persistence.clearRunRequest(threadId, runId);
456
+ }
457
+ }
388
458
  checkpointRefForState(threadId, runId, state) {
389
459
  return state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
390
460
  }
@@ -492,17 +562,56 @@ export class AgentHarnessRuntime {
492
562
  }
493
563
  await listener(value);
494
564
  }
495
- async acquireRunSlot() {
565
+ async acquireRunSlot(threadId, runId, activeState = "running") {
496
566
  const maxConcurrentRuns = this.concurrencyConfig.maxConcurrentRuns;
497
567
  if (!maxConcurrentRuns) {
498
568
  return () => undefined;
499
569
  }
500
- if (this.activeRunSlots >= maxConcurrentRuns) {
501
- await new Promise((resolve) => {
502
- this.pendingRunSlots.push(resolve);
570
+ if (this.activeRunSlots < maxConcurrentRuns) {
571
+ this.activeRunSlots += 1;
572
+ let released = false;
573
+ return () => {
574
+ if (released) {
575
+ return;
576
+ }
577
+ released = true;
578
+ this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
579
+ const next = this.pendingRunSlots.shift();
580
+ void next?.();
581
+ };
582
+ }
583
+ if (threadId && runId) {
584
+ const queuePosition = this.pendingRunSlots.length + 1;
585
+ await this.setRunStateAndEmit(threadId, runId, 2, "queued", {
586
+ previousState: activeState,
587
+ });
588
+ await this.emit(threadId, runId, 3, "run.queued", {
589
+ queuePosition,
590
+ activeRunCount: this.activeRunSlots,
591
+ maxConcurrentRuns,
503
592
  });
504
593
  }
505
- this.activeRunSlots += 1;
594
+ await new Promise((resolve, reject) => {
595
+ this.pendingRunSlots.push(async () => {
596
+ try {
597
+ this.activeRunSlots += 1;
598
+ if (threadId && runId) {
599
+ await this.emit(threadId, runId, 4, "run.dequeued", {
600
+ queuePosition: 0,
601
+ activeRunCount: this.activeRunSlots,
602
+ maxConcurrentRuns,
603
+ });
604
+ await this.setRunStateAndEmit(threadId, runId, 5, activeState, {
605
+ previousState: "queued",
606
+ });
607
+ }
608
+ resolve();
609
+ }
610
+ catch (error) {
611
+ reject(error);
612
+ }
613
+ });
614
+ });
506
615
  let released = false;
507
616
  return () => {
508
617
  if (released) {
@@ -511,7 +620,7 @@ export class AgentHarnessRuntime {
511
620
  released = true;
512
621
  this.activeRunSlots = Math.max(0, this.activeRunSlots - 1);
513
622
  const next = this.pendingRunSlots.shift();
514
- next?.();
623
+ void next?.();
515
624
  };
516
625
  }
517
626
  async dispatchRunListeners(stream, listeners) {
@@ -587,88 +696,68 @@ export class AgentHarnessRuntime {
587
696
  if (options.listeners) {
588
697
  return this.dispatchRunListeners(this.streamEvents(options), options.listeners);
589
698
  }
590
- const releaseRunSlot = await this.acquireRunSlot();
699
+ const invocation = this.normalizeInvocationEnvelope(options);
700
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
701
+ const binding = this.workspace.bindings.get(selectedAgentId);
702
+ if (!binding) {
703
+ throw new Error(`Unknown agent ${selectedAgentId}`);
704
+ }
705
+ const policyDecision = this.policyEngine.evaluate(binding);
706
+ if (!policyDecision.allowed) {
707
+ throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
708
+ }
709
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
710
+ await this.persistence.saveRunRequest(threadId, runId, this.buildPersistedRunRequest(options.input, invocation));
711
+ await this.emitRunCreated(threadId, runId, {
712
+ agentId: binding.agent.id,
713
+ requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
714
+ selectedAgentId,
715
+ executionMode: getBindingAdapterKind(binding),
716
+ });
717
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId);
591
718
  try {
592
- const invocation = this.normalizeInvocationEnvelope(options);
593
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
594
- const binding = this.workspace.bindings.get(selectedAgentId);
595
- if (!binding) {
596
- throw new Error(`Unknown agent ${selectedAgentId}`);
597
- }
598
- const policyDecision = this.policyEngine.evaluate(binding);
599
- if (!policyDecision.allowed) {
600
- throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
601
- }
602
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
603
- await this.emitRunCreated(threadId, runId, {
604
- agentId: binding.agent.id,
605
- requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
606
- selectedAgentId,
607
- executionMode: getBindingAdapterKind(binding),
719
+ return await this.executeQueuedRun(binding, options.input, threadId, runId, selectedAgentId, {
720
+ context: invocation.context,
721
+ state: invocation.state,
722
+ files: invocation.files,
723
+ previousState: "running",
724
+ stateSequence: 6,
725
+ approvalSequence: 7,
608
726
  });
609
- try {
610
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId, undefined, {
611
- context: invocation.context,
612
- state: invocation.state,
613
- files: invocation.files,
614
- });
615
- const finalized = await this.finalizeContinuedRun(threadId, runId, options.input, actual, {
616
- previousState: null,
617
- stateSequence: 3,
618
- approvalSequence: 4,
619
- });
620
- return {
621
- ...finalized,
622
- agentId: selectedAgentId,
623
- };
624
- }
625
- catch (error) {
626
- await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
627
- await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
628
- previousState: null,
629
- error: error instanceof Error ? error.message : String(error),
630
- });
631
- return {
632
- threadId,
633
- runId,
634
- agentId: selectedAgentId,
635
- state: "failed",
636
- output: renderRuntimeFailure(error),
637
- };
638
- }
639
727
  }
640
728
  finally {
641
729
  releaseRunSlot();
642
730
  }
643
731
  }
644
732
  async *streamEvents(options) {
645
- const releaseRunSlot = await this.acquireRunSlot();
646
- try {
647
- const invocation = this.normalizeInvocationEnvelope(options);
648
- const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
649
- const binding = this.workspace.bindings.get(selectedAgentId);
650
- if (!binding) {
651
- const result = await this.run(options);
652
- for (const line of result.output.split("\n")) {
653
- yield {
654
- type: "content",
655
- threadId: result.threadId,
656
- runId: result.runId,
657
- agentId: result.agentId ?? selectedAgentId,
658
- content: `${line}\n`,
659
- };
660
- }
661
- return;
733
+ const invocation = this.normalizeInvocationEnvelope(options);
734
+ const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
735
+ const binding = this.workspace.bindings.get(selectedAgentId);
736
+ if (!binding) {
737
+ const result = await this.run(options);
738
+ for (const line of result.output.split("\n")) {
739
+ yield {
740
+ type: "content",
741
+ threadId: result.threadId,
742
+ runId: result.runId,
743
+ agentId: result.agentId ?? selectedAgentId,
744
+ content: `${line}\n`,
745
+ };
662
746
  }
663
- let emitted = false;
664
- const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
665
- yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
666
- agentId: selectedAgentId,
667
- requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
668
- selectedAgentId,
669
- input: options.input,
670
- state: "running",
671
- }) };
747
+ return;
748
+ }
749
+ let emitted = false;
750
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
751
+ await this.persistence.saveRunRequest(threadId, runId, this.buildPersistedRunRequest(options.input, invocation));
752
+ yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
753
+ agentId: selectedAgentId,
754
+ requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
755
+ selectedAgentId,
756
+ input: options.input,
757
+ state: "running",
758
+ }) };
759
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId);
760
+ try {
672
761
  try {
673
762
  const priorHistory = await this.loadPriorHistory(threadId, runId);
674
763
  let assistantOutput = "";
@@ -686,11 +775,11 @@ export class AgentHarnessRuntime {
686
775
  : chunk;
687
776
  if (normalizedChunk.kind === "interrupt") {
688
777
  const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
689
- const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
690
- previousState: null,
778
+ const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 6, "waiting_for_approval", {
779
+ previousState: "running",
691
780
  checkpointRef,
692
781
  });
693
- const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
782
+ const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 7);
694
783
  yield {
695
784
  type: "event",
696
785
  event: waitingEvent,
@@ -783,22 +872,22 @@ export class AgentHarnessRuntime {
783
872
  finalMessageText: assistantOutput,
784
873
  },
785
874
  };
786
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
787
- previousState: null,
875
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "completed", {
876
+ previousState: "running",
788
877
  }) };
789
878
  return;
790
879
  }
791
880
  catch (error) {
792
881
  if (emitted) {
793
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
794
- previousState: null,
882
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
883
+ previousState: "running",
795
884
  error: error instanceof Error ? error.message : String(error),
796
885
  }) };
797
886
  return;
798
887
  }
799
888
  if (error instanceof RuntimeOperationTimeoutError && error.stage === "invoke") {
800
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
801
- previousState: null,
889
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
890
+ previousState: "running",
802
891
  error: error.message,
803
892
  }) };
804
893
  yield {
@@ -836,15 +925,15 @@ export class AgentHarnessRuntime {
836
925
  agentId: selectedAgentId,
837
926
  },
838
927
  };
839
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
840
- previousState: null,
928
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, actual.state, {
929
+ previousState: "running",
841
930
  }) };
842
931
  return;
843
932
  }
844
933
  catch (invokeError) {
845
934
  await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
846
- yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
847
- previousState: null,
935
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 6, "failed", {
936
+ previousState: "running",
848
937
  error: invokeError instanceof Error ? invokeError.message : String(invokeError),
849
938
  }) };
850
939
  yield {
@@ -870,29 +959,30 @@ export class AgentHarnessRuntime {
870
959
  }
871
960
  }
872
961
  finally {
962
+ await this.persistence.clearRunRequest(threadId, runId);
873
963
  releaseRunSlot();
874
964
  }
875
965
  }
876
966
  async resume(options) {
877
- const releaseRunSlot = await this.acquireRunSlot();
967
+ const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
968
+ const thread = options.threadId
969
+ ? await this.getSession(options.threadId)
970
+ : approvalById
971
+ ? await this.getSession(approvalById.threadId)
972
+ : null;
973
+ if (!thread) {
974
+ throw new Error("resume requires either threadId or approvalId");
975
+ }
976
+ const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
977
+ const threadId = approval.threadId;
978
+ const runId = approval.runId;
979
+ const binding = this.workspace.bindings.get(thread.agentId);
980
+ if (!binding) {
981
+ throw new Error(`Unknown agent ${thread.agentId}`);
982
+ }
983
+ await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
984
+ const releaseRunSlot = await this.acquireRunSlot(threadId, runId, "resuming");
878
985
  try {
879
- const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
880
- const thread = options.threadId
881
- ? await this.getSession(options.threadId)
882
- : approvalById
883
- ? await this.getSession(approvalById.threadId)
884
- : null;
885
- if (!thread) {
886
- throw new Error("resume requires either threadId or approvalId");
887
- }
888
- const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
889
- const threadId = approval.threadId;
890
- const runId = approval.runId;
891
- const binding = this.workspace.bindings.get(thread.agentId);
892
- if (!binding) {
893
- throw new Error(`Unknown agent ${thread.agentId}`);
894
- }
895
- await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
896
986
  await this.persistence.saveRecoveryIntent(threadId, runId, {
897
987
  kind: "approval-decision",
898
988
  savedAt: new Date().toISOString(),
@@ -978,12 +1068,72 @@ export class AgentHarnessRuntime {
978
1068
  await this.close();
979
1069
  }
980
1070
  async recoverStartupRuns() {
981
- if (!this.recoveryConfig.enabled || !this.recoveryConfig.resumeResumingRunsOnStartup) {
1071
+ if (!this.recoveryConfig.enabled) {
982
1072
  return;
983
1073
  }
984
1074
  const threads = await this.persistence.listSessions();
985
1075
  for (const thread of threads) {
986
- if (thread.status !== "resuming") {
1076
+ if (thread.status === "queued") {
1077
+ const runMeta = await this.persistence.getRunMeta(thread.threadId, thread.latestRunId);
1078
+ const binding = this.workspace.bindings.get(runMeta.agentId);
1079
+ if (!binding) {
1080
+ continue;
1081
+ }
1082
+ const request = await this.persistence.getRunRequest(thread.threadId, thread.latestRunId);
1083
+ if (!request) {
1084
+ await this.setRunStateAndEmit(thread.threadId, thread.latestRunId, 100, "failed", {
1085
+ previousState: "queued",
1086
+ error: "missing persisted run request for queued run recovery",
1087
+ });
1088
+ continue;
1089
+ }
1090
+ const releaseRunSlot = await this.acquireRunSlot();
1091
+ try {
1092
+ await this.executeQueuedRun(binding, request.input, thread.threadId, thread.latestRunId, runMeta.agentId, {
1093
+ context: request.invocation?.context,
1094
+ state: request.invocation?.inputs,
1095
+ files: request.invocation?.attachments,
1096
+ previousState: "queued",
1097
+ stateSequence: 103,
1098
+ approvalSequence: 104,
1099
+ });
1100
+ }
1101
+ finally {
1102
+ releaseRunSlot();
1103
+ }
1104
+ continue;
1105
+ }
1106
+ if (thread.status === "running") {
1107
+ const runMeta = await this.persistence.getRunMeta(thread.threadId, thread.latestRunId);
1108
+ const binding = this.workspace.bindings.get(runMeta.agentId);
1109
+ if (!binding || !this.supportsRunningReplay(binding)) {
1110
+ continue;
1111
+ }
1112
+ const request = await this.persistence.getRunRequest(thread.threadId, thread.latestRunId);
1113
+ if (!request) {
1114
+ continue;
1115
+ }
1116
+ const releaseRunSlot = await this.acquireRunSlot();
1117
+ try {
1118
+ await this.emit(thread.threadId, thread.latestRunId, 100, "run.resumed", {
1119
+ resumeKind: "startup-running-recovery",
1120
+ state: "running",
1121
+ });
1122
+ await this.executeQueuedRun(binding, request.input, thread.threadId, thread.latestRunId, runMeta.agentId, {
1123
+ context: request.invocation?.context,
1124
+ state: request.invocation?.inputs,
1125
+ files: request.invocation?.attachments,
1126
+ previousState: "running",
1127
+ stateSequence: 103,
1128
+ approvalSequence: 104,
1129
+ });
1130
+ }
1131
+ finally {
1132
+ releaseRunSlot();
1133
+ }
1134
+ continue;
1135
+ }
1136
+ if (thread.status !== "resuming" || !this.recoveryConfig.resumeResumingRunsOnStartup) {
987
1137
  continue;
988
1138
  }
989
1139
  const binding = this.workspace.bindings.get(thread.agentId);
@@ -44,6 +44,8 @@ function renderOpenApprovalsMarkdown(approvals) {
44
44
  }
45
45
  const THREAD_MEMORY_EVENT_TYPES = new Set([
46
46
  "run.state.changed",
47
+ "run.queued",
48
+ "run.dequeued",
47
49
  "approval.resolved",
48
50
  "approval.requested",
49
51
  ]);
@@ -9,6 +9,7 @@ export type LoadedToolModule = {
9
9
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
10
10
  schema: SchemaLike;
11
11
  description: string;
12
+ retryable?: boolean;
12
13
  };
13
14
  export declare function isSupportedToolModulePath(filePath: string): boolean;
14
15
  export declare function discoverAnnotatedFunctionNames(sourceText: string): string[];
@@ -76,6 +76,7 @@ function loadToolObjectDefinition(imported, exportName) {
76
76
  invoke: definition.invoke,
77
77
  schema: normalizeToolSchema(definition.schema),
78
78
  description: definition.description.trim(),
79
+ retryable: definition.retryable === true,
79
80
  };
80
81
  }
81
82
  export function isSupportedToolModulePath(filePath) {
@@ -113,6 +114,11 @@ export function discoverToolModuleDefinitions(sourceText, imported) {
113
114
  invoke: invoke,
114
115
  schema,
115
116
  description: readToolDescription(imported, implementationName, schema),
117
+ retryable: typeof imported[`${implementationName}Retryable`] === "boolean"
118
+ ? imported[`${implementationName}Retryable`] === true
119
+ : typeof imported.retryable === "boolean"
120
+ ? imported.retryable === true
121
+ : undefined,
116
122
  });
117
123
  }
118
124
  return discovered;
@@ -139,5 +145,10 @@ export function loadToolModuleDefinition(imported, implementationName) {
139
145
  invoke: invoke,
140
146
  schema,
141
147
  description: readToolDescription(imported, implementationName, schema),
148
+ retryable: typeof imported[`${implementationName}Retryable`] === "boolean"
149
+ ? imported[`${implementationName}Retryable`] === true
150
+ : typeof imported.retryable === "boolean"
151
+ ? imported.retryable === true
152
+ : undefined,
142
153
  };
143
154
  }
package/dist/tools.d.ts CHANGED
@@ -5,6 +5,7 @@ export type ToolDefinitionObject = {
5
5
  name?: string;
6
6
  description: string;
7
7
  schema: SchemaInput;
8
+ retryable?: boolean;
8
9
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
9
10
  [TOOL_DEFINITION_MARKER]: true;
10
11
  };
@@ -15,6 +16,7 @@ export declare function tool(definition: {
15
16
  name?: string;
16
17
  description: string;
17
18
  schema: SchemaInput;
19
+ retryable?: boolean;
18
20
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
19
21
  }): ToolDefinitionObject;
20
22
  export {};
@@ -61,7 +61,7 @@ function toArray(value) {
61
61
  function asObject(value) {
62
62
  return typeof value === "object" && value ? value : undefined;
63
63
  }
64
- function normalizeNamedResourceSpec(document, kind) {
64
+ function normalizeCatalogSpec(document, options = {}) {
65
65
  const typed = asObject(document);
66
66
  const spec = typed?.spec;
67
67
  if (!Array.isArray(spec)) {
@@ -75,12 +75,15 @@ function normalizeNamedResourceSpec(document, kind) {
75
75
  : typeof item.name === "string" && item.name.trim()
76
76
  ? item.name
77
77
  : undefined;
78
- if (!id) {
78
+ const itemKind = typeof item.kind === "string" && item.kind.trim()
79
+ ? item.kind
80
+ : options.defaultKind;
81
+ if (!id || !itemKind) {
79
82
  return null;
80
83
  }
81
84
  return {
82
85
  ...item,
83
- kind,
86
+ kind: itemKind,
84
87
  id,
85
88
  };
86
89
  });
@@ -305,6 +308,21 @@ async function objectItemsFromDocument(document, sourcePath) {
305
308
  if (typeof document !== "object" || !document) {
306
309
  return [];
307
310
  }
311
+ const catalogKind = typeof document.kind === "string"
312
+ ? String(document.kind)
313
+ : undefined;
314
+ const catalogItems = catalogKind === "Models"
315
+ ? normalizeCatalogSpec(document, { defaultKind: "Model" })
316
+ : catalogKind === "Stores"
317
+ ? normalizeCatalogSpec(document)
318
+ : catalogKind === "Tools"
319
+ ? normalizeCatalogSpec(document, { defaultKind: "Tool" })
320
+ : catalogKind === "McpServers"
321
+ ? normalizeCatalogSpec(document)
322
+ : [];
323
+ if (catalogItems.length > 0) {
324
+ return catalogItems;
325
+ }
308
326
  const items = document.items;
309
327
  if (Array.isArray(items)) {
310
328
  const records = [];
@@ -497,15 +515,7 @@ async function readNamedModelItems(root) {
497
515
  }
498
516
  const parsedDocuments = parseAllDocuments(await readYamlOrJson(filePath));
499
517
  for (const parsedDocument of parsedDocuments) {
500
- const document = parsedDocument.toJSON();
501
- const catalogItems = normalizeNamedResourceSpec(document, "model");
502
- if (catalogItems.length > 0) {
503
- for (const item of catalogItems) {
504
- records.push({ item: normalizeYamlItem(item), sourcePath: filePath });
505
- }
506
- continue;
507
- }
508
- for (const item of await objectItemsFromDocument(document, filePath)) {
518
+ for (const item of await objectItemsFromDocument(parsedDocument.toJSON(), filePath)) {
509
519
  const normalized = normalizeYamlItem(item);
510
520
  if (normalized.kind === "model" && typeof normalized.id === "string" && normalized.id.trim()) {
511
521
  records.push({ item: normalized, sourcePath: filePath });
@@ -556,6 +566,7 @@ export async function readToolModuleItems(root) {
556
566
  name: definition.implementationName,
557
567
  description: definition.description,
558
568
  implementationName: definition.implementationName,
569
+ ...(definition.retryable !== undefined ? { retryable: definition.retryable } : {}),
559
570
  },
560
571
  sourcePath: filePath,
561
572
  });
@@ -254,6 +254,7 @@ export function parseToolObject(object) {
254
254
  : undefined,
255
255
  bundleRefs,
256
256
  hitl: parseHitlPolicy(value.hitl),
257
+ retryable: value.retryable === true,
257
258
  sourcePath: object.sourcePath,
258
259
  };
259
260
  }
@@ -22,7 +22,7 @@ export type RecoveryConfig = {
22
22
  maxRecoveryAttempts: number;
23
23
  };
24
24
  export type ConcurrencyConfig = {
25
- maxConcurrentRuns?: number;
25
+ maxConcurrentRuns: number;
26
26
  };
27
27
  export declare function getWorkspaceObject(refs: Map<string, WorkspaceObject | ParsedAgentObject>, ref: string | undefined): WorkspaceObject | undefined;
28
28
  export declare function getRuntimeDefaults(refs: Map<string, WorkspaceObject | ParsedAgentObject>): Record<string, unknown> | undefined;
@@ -62,7 +62,7 @@ export function getConcurrencyConfig(refs) {
62
62
  Number.isFinite(concurrency.maxConcurrentRuns) &&
63
63
  concurrency.maxConcurrentRuns > 0
64
64
  ? Math.floor(concurrency.maxConcurrentRuns)
65
- : undefined;
65
+ : 3;
66
66
  return { maxConcurrentRuns };
67
67
  }
68
68
  export function getRoutingSystemPrompt(refs) {
@@ -105,6 +105,7 @@ export async function hydrateResourceAndExternalTools(tools, toolSourceRefs, wor
105
105
  backendOperation: existing?.backendOperation ?? resourceTool.backendOperation,
106
106
  bundleRefs: existing?.bundleRefs ?? [],
107
107
  hitl: existing?.hitl ?? resourceTool.hitl,
108
+ retryable: existing?.retryable ?? resourceTool.retryable,
108
109
  sourcePath: existing?.sourcePath ?? resourceTool.toolPath,
109
110
  });
110
111
  }