plugin-agent-orchestrator 1.0.20 → 1.0.21

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 (98) hide show
  1. package/dist/client/hooks/useRunEventStream.d.ts +22 -0
  2. package/dist/client/index.d.ts +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/externalVersion.js +6 -6
  5. package/dist/server/collections/agent-execution-spans.js +24 -0
  6. package/dist/server/collections/agent-loop-runs.js +36 -0
  7. package/dist/server/collections/orchestrator-config.js +14 -0
  8. package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
  9. package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
  10. package/dist/server/plugin.js +47 -0
  11. package/dist/server/resources/agent-loop.js +33 -25
  12. package/dist/server/resources/tracing.js +5 -8
  13. package/dist/server/services/AgentHarness.d.ts +2 -0
  14. package/dist/server/services/AgentHarness.js +56 -90
  15. package/dist/server/services/AgentLoopController.d.ts +33 -20
  16. package/dist/server/services/AgentLoopController.js +164 -125
  17. package/dist/server/services/AgentLoopRepository.js +16 -34
  18. package/dist/server/services/AgentLoopService.d.ts +28 -18
  19. package/dist/server/services/AgentLoopService.js +7 -1
  20. package/dist/server/services/AgentPlannerService.js +5 -25
  21. package/dist/server/services/AgentRegistryService.d.ts +8 -0
  22. package/dist/server/services/AgentRegistryService.js +34 -24
  23. package/dist/server/services/CircuitBreaker.d.ts +40 -0
  24. package/dist/server/services/CircuitBreaker.js +120 -0
  25. package/dist/server/services/ContextAggregator.d.ts +45 -0
  26. package/dist/server/services/ContextAggregator.js +201 -0
  27. package/dist/server/services/ExecutionSpanService.js +2 -5
  28. package/dist/server/services/RunEventBus.d.ts +9 -0
  29. package/dist/server/services/RunEventBus.js +73 -0
  30. package/dist/server/services/TokenTracker.d.ts +62 -0
  31. package/dist/server/services/TokenTracker.js +173 -0
  32. package/dist/server/tools/agent-loop.d.ts +8 -8
  33. package/dist/server/tools/agent-loop.js +30 -63
  34. package/dist/server/tools/delegate-task.js +14 -72
  35. package/dist/server/tools/orchestrator-plan.d.ts +6 -6
  36. package/dist/server/tools/orchestrator-plan.js +10 -47
  37. package/dist/server/types.d.ts +47 -0
  38. package/dist/server/types.js +24 -0
  39. package/dist/server/utils/ctx-utils.d.ts +30 -0
  40. package/dist/server/utils/ctx-utils.js +152 -0
  41. package/dist/server/utils/logging.d.ts +6 -0
  42. package/dist/server/utils/logging.js +86 -0
  43. package/package.json +44 -44
  44. package/src/client/AgentRunsTab.tsx +764 -764
  45. package/src/client/HarnessProfilesTab.tsx +247 -247
  46. package/src/client/OrchestratorSettings.tsx +106 -106
  47. package/src/client/RulesTab.tsx +716 -716
  48. package/src/client/hooks/useRunEventStream.ts +76 -0
  49. package/src/client/index.tsx +2 -1
  50. package/src/client/plugin.tsx +27 -27
  51. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  52. package/src/client/skill-hub/index.tsx +51 -51
  53. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  54. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  55. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  56. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  57. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  58. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  59. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  60. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  61. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  62. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  63. package/src/server/__tests__/smoke.test.ts +120 -0
  64. package/src/server/collections/agent-execution-spans.ts +24 -0
  65. package/src/server/collections/agent-harness-profiles.ts +59 -59
  66. package/src/server/collections/agent-loop-events.ts +71 -71
  67. package/src/server/collections/agent-loop-runs.ts +38 -1
  68. package/src/server/collections/agent-loop-steps.ts +144 -144
  69. package/src/server/collections/orchestrator-config.ts +14 -0
  70. package/src/server/collections/skill-executions.ts +106 -106
  71. package/src/server/collections/skill-loop-configs.ts +65 -65
  72. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  73. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  74. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  75. package/src/server/plugin.ts +53 -0
  76. package/src/server/resources/agent-loop.ts +21 -12
  77. package/src/server/resources/tracing.ts +3 -7
  78. package/src/server/services/AgentHarness.ts +78 -116
  79. package/src/server/services/AgentLoopController.ts +197 -122
  80. package/src/server/services/AgentLoopRepository.ts +9 -25
  81. package/src/server/services/AgentLoopService.ts +13 -1
  82. package/src/server/services/AgentPlanValidator.ts +73 -73
  83. package/src/server/services/AgentPlannerService.ts +2 -25
  84. package/src/server/services/AgentRegistryService.ts +40 -31
  85. package/src/server/services/CircuitBreaker.ts +116 -0
  86. package/src/server/services/ContextAggregator.ts +239 -0
  87. package/src/server/services/ExecutionSpanService.ts +2 -4
  88. package/src/server/services/RunEventBus.ts +45 -0
  89. package/src/server/services/TokenTracker.ts +209 -0
  90. package/src/server/skill-hub/plugin.ts +898 -898
  91. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -460
  92. package/src/server/tools/agent-loop.ts +18 -57
  93. package/src/server/tools/delegate-task.ts +11 -93
  94. package/src/server/tools/orchestrator-plan.ts +26 -50
  95. package/src/server/tools/skill-execute.ts +160 -160
  96. package/src/server/types.ts +55 -0
  97. package/src/server/utils/ctx-utils.ts +118 -0
  98. package/src/server/utils/logging.ts +63 -0
@@ -4,7 +4,10 @@ import { AgentPlanValidator } from './AgentPlanValidator';
4
4
  import { AgentLoopRepository } from './AgentLoopRepository';
5
5
  import { AgentHarness } from './AgentHarness';
6
6
  import { AgentLoopPolicy, AgentLoopPlanStepInput, AgentLoopRunStatus, AgentLoopStepStatus } from './AgentLoopService';
7
+ import { TokenTracker } from './TokenTracker';
8
+ import { getCircuitBreaker } from './CircuitBreaker';
7
9
  import { createHash } from 'crypto';
10
+ import { asObject, asArray, trimText, normalizeStepType, normalizePlanKey } from '../utils/ctx-utils';
8
11
 
9
12
  const DEFAULT_POLICY: AgentLoopPolicy = {
10
13
  maxIterations: 20,
@@ -12,6 +15,11 @@ const DEFAULT_POLICY: AgentLoopPolicy = {
12
15
  allowReplan: true,
13
16
  requireVerification: true,
14
17
  stopOnApprovalRequired: true,
18
+ maxContextTokens: 4000,
19
+ contextSummaryStrategy: 'last_n',
20
+ includeToolResults: false,
21
+ includeStepOutputs: true,
22
+ maxConcurrency: 5,
15
23
  };
16
24
 
17
25
  const TERMINAL_RUN_STATUSES = new Set<AgentLoopRunStatus>(['succeeded', 'failed', 'rejected', 'canceled']);
@@ -34,55 +42,22 @@ function normalizePolicy(policy?: Partial<AgentLoopPolicy>): AgentLoopPolicy {
34
42
  next.allowReplan = next.allowReplan !== false;
35
43
  next.requireVerification = next.requireVerification !== false;
36
44
  next.stopOnApprovalRequired = next.stopOnApprovalRequired !== false;
45
+ next.maxContextTokens = next.maxContextTokens || DEFAULT_POLICY.maxContextTokens;
46
+ next.contextSummaryStrategy = next.contextSummaryStrategy || DEFAULT_POLICY.contextSummaryStrategy;
47
+ next.includeToolResults = next.includeToolResults !== false;
48
+ next.includeStepOutputs = next.includeStepOutputs !== false;
49
+ next.maxConcurrency = Math.max(1, Number(next.maxConcurrency || DEFAULT_POLICY.maxConcurrency));
37
50
  return next;
38
51
  }
39
52
 
40
- function asArray(value: any): any[] {
41
- return Array.isArray(value) ? value : [];
42
- }
43
-
44
- function asObject(value: any) {
45
- if (value && typeof value === 'object' && !Array.isArray(value)) return value;
46
- if (typeof value === 'string' && value.trim()) {
47
- try {
48
- const parsed = JSON.parse(value);
49
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
50
- } catch {
51
- return {};
52
- }
53
- }
54
- return {};
55
- }
56
-
57
- function trimText(value: any, max = 50000) {
58
- let text = '';
59
- if (typeof value === 'string') {
60
- text = value;
61
- } else if (value != null) {
62
- try {
63
- text = JSON.stringify(value);
64
- } catch {
65
- text = String(value);
66
- }
67
- }
68
- return text.length > max ? `${text.slice(0, max)}\n...[truncated]` : text;
69
- }
70
-
71
- function normalizeStepType(value: any) {
72
- return ['reasoning', 'skill', 'tool', 'sub_agent', 'verification'].includes(value) ? value : 'tool';
73
- }
74
-
75
- function normalizePlanKey(step: AgentLoopPlanStepInput, index: number) {
76
- return String(step.planKey || step.key || step.id || `step_${index + 1}`);
77
- }
78
-
79
53
  export class AgentLoopController {
80
54
  constructor(
81
55
  private readonly registryService: AgentRegistryService,
82
56
  private readonly plannerService: AgentPlannerService,
83
57
  private readonly validator: AgentPlanValidator,
84
58
  private readonly repository: AgentLoopRepository,
85
- private readonly harness: AgentHarness
59
+ private readonly harness: AgentHarness,
60
+ private readonly tokenTracker: TokenTracker | null = null,
86
61
  ) {}
87
62
 
88
63
  async createRun(options: {
@@ -165,7 +140,8 @@ export class AgentLoopController {
165
140
  goal: options.goal,
166
141
  userId: options.userId,
167
142
  metadata: options.metadata,
168
- planSource: options.planSource || (Array.isArray(options.plan) && options.plan.length ? 'provided' : 'template'),
143
+ planSource:
144
+ options.planSource || (Array.isArray(options.plan) && options.plan.length ? 'provided' : 'template'),
169
145
  plannerModel: options.plannerModel,
170
146
  harnessTag,
171
147
  harnessProfileId: harnessProfile?.id,
@@ -241,7 +217,7 @@ export class AgentLoopController {
241
217
  harnessTag?: string;
242
218
  harnessProfileId?: string | number;
243
219
  harnessSettings?: any;
244
- } = {}
220
+ } = {},
245
221
  ) {
246
222
  this.validator.validate(plan);
247
223
  const run = await this.repository.requireRun(runId);
@@ -298,7 +274,7 @@ export class AgentLoopController {
298
274
 
299
275
  async approvePlanAndExecute(
300
276
  runId: string | number,
301
- options: { userId?: string | number; ctx?: any; reason?: string } = {}
277
+ options: { userId?: string | number; ctx?: any; reason?: string } = {},
302
278
  ) {
303
279
  const run = await this.repository.requireRun(runId);
304
280
  if (TERMINAL_RUN_STATUSES.has(run.status)) {
@@ -409,7 +385,7 @@ export class AgentLoopController {
409
385
  mode?: 'append' | 'replace_pending';
410
386
  reason?: string;
411
387
  markRunning?: boolean;
412
- } = {}
388
+ } = {},
413
389
  ) {
414
390
  if (!Array.isArray(plan) || plan.length === 0) {
415
391
  throw new Error('Plan must include at least one step.');
@@ -485,7 +461,7 @@ export class AgentLoopController {
485
461
  async replan(
486
462
  runId: string | number,
487
463
  plan: AgentLoopPlanStepInput[],
488
- options: { reason?: string; mode?: 'append' | 'replace_pending'; userId?: string | number } = {}
464
+ options: { reason?: string; mode?: 'append' | 'replace_pending'; userId?: string | number } = {},
489
465
  ) {
490
466
  if (!Array.isArray(plan) || plan.length === 0) {
491
467
  throw new Error('Plan must include at least one step.');
@@ -519,7 +495,7 @@ export class AgentLoopController {
519
495
 
520
496
  async startStep(
521
497
  stepId: string | number,
522
- options: { userId?: string | number; agentExecutionSpanId?: string | number } = {}
498
+ options: { userId?: string | number; agentExecutionSpanId?: string | number } = {},
523
499
  ) {
524
500
  const step = await this.repository.requireStep(stepId);
525
501
  const run = await this.repository.requireRun(step.runId);
@@ -548,7 +524,6 @@ export class AgentLoopController {
548
524
 
549
525
  await this.repository.updateRun(run.id, {
550
526
  status: 'running',
551
- currentStepId: step.id,
552
527
  updatedAt: now(),
553
528
  });
554
529
 
@@ -589,7 +564,7 @@ export class AgentLoopController {
589
564
  skillExecutionId?: string | number;
590
565
  agentExecutionSpanId?: string | number;
591
566
  metadata?: any;
592
- } = {}
567
+ } = {},
593
568
  ) {
594
569
  const step = await this.repository.requireStep(stepId);
595
570
  const run = await this.repository.requireRun(step.runId);
@@ -600,9 +575,6 @@ export class AgentLoopController {
600
575
  if (step.status !== 'running') {
601
576
  throw new Error(`Step ${step.id} cannot complete from status "${step.status}".`);
602
577
  }
603
- if (!run.currentStepId || String(run.currentStepId) !== String(step.id)) {
604
- throw new Error(`Step ${step.id} is not the current running step for run ${run.id}.`);
605
- }
606
578
 
607
579
  await this.repository.updateStep(step.id, {
608
580
  status: 'succeeded',
@@ -617,7 +589,6 @@ export class AgentLoopController {
617
589
 
618
590
  await this.repository.updateRun(run.id, {
619
591
  status: 'running',
620
- currentStepId: null,
621
592
  updatedAt: now(),
622
593
  });
623
594
 
@@ -644,9 +615,6 @@ export class AgentLoopController {
644
615
  if (step.status !== 'running') {
645
616
  throw new Error(`Step ${step.id} cannot fail from status "${step.status}".`);
646
617
  }
647
- if (!run.currentStepId || String(run.currentStepId) !== String(step.id)) {
648
- throw new Error(`Step ${step.id} is not the current running step for run ${run.id}.`);
649
- }
650
618
 
651
619
  const policy = normalizePolicy(run.policy);
652
620
 
@@ -660,7 +628,6 @@ export class AgentLoopController {
660
628
 
661
629
  await this.repository.updateRun(run.id, {
662
630
  status: 'running',
663
- currentStepId: null,
664
631
  updatedAt: now(),
665
632
  });
666
633
 
@@ -680,7 +647,7 @@ export class AgentLoopController {
680
647
  return this.getRunSnapshot(run.id);
681
648
  }
682
649
 
683
- async skipStep(stepId: string | number, reason = 'Skipped', options: { userId?: string | number} = {}) {
650
+ async skipStep(stepId: string | number, reason = 'Skipped', options: { userId?: string | number } = {}) {
684
651
  const step = await this.repository.requireStep(stepId);
685
652
  const run = await this.repository.requireRun(step.runId);
686
653
  if (TERMINAL_RUN_STATUSES.has(run.status)) {
@@ -690,11 +657,6 @@ export class AgentLoopController {
690
657
  if (!['pending', 'running', 'failed'].includes(step.status)) {
691
658
  throw new Error(`Step ${step.id} cannot skip from status "${step.status}".`);
692
659
  }
693
- if (step.status === 'running') {
694
- if (!run.currentStepId || String(run.currentStepId) !== String(step.id)) {
695
- throw new Error(`Step ${step.id} is not the current running step for run ${run.id}.`);
696
- }
697
- }
698
660
 
699
661
  await this.repository.updateStep(step.id, {
700
662
  status: 'skipped',
@@ -719,7 +681,7 @@ export class AgentLoopController {
719
681
  async requestApproval(
720
682
  stepId: string | number,
721
683
  approval: any,
722
- options: { userId?: string | number; reason?: string } = {}
684
+ options: { userId?: string | number; reason?: string } = {},
723
685
  ) {
724
686
  const step = await this.repository.requireStep(stepId);
725
687
  const run = await this.repository.requireRun(step.runId);
@@ -730,11 +692,6 @@ export class AgentLoopController {
730
692
  if (!['pending', 'running'].includes(step.status)) {
731
693
  throw new Error(`Step ${step.id} cannot request approval for status "${step.status}".`);
732
694
  }
733
- if (step.status === 'running') {
734
- if (!run.currentStepId || String(run.currentStepId) !== String(step.id)) {
735
- throw new Error(`Step ${step.id} is not the current running step for run ${run.id}.`);
736
- }
737
- }
738
695
 
739
696
  await this.repository.updateStep(step.id, {
740
697
  status: 'waiting_user',
@@ -770,7 +727,7 @@ export class AgentLoopController {
770
727
  editedInput?: any;
771
728
  userId?: string | number;
772
729
  ctx?: any;
773
- }
730
+ },
774
731
  ) {
775
732
  const run = await this.repository.requireRun(runId);
776
733
  const stepId = options.stepId || run.currentStepId;
@@ -784,9 +741,6 @@ export class AgentLoopController {
784
741
  if (run.status !== 'waiting_user' || step.status !== 'waiting_user') {
785
742
  throw new Error('Run is not waiting for user approval.');
786
743
  }
787
- if (run.currentStepId && String(run.currentStepId) !== String(step.id)) {
788
- throw new Error(`Step ${step.id} is not the current waiting step for run ${run.id}.`);
789
- }
790
744
 
791
745
  if (options.approved) {
792
746
  const nextValues: any = {
@@ -853,12 +807,45 @@ export class AgentLoopController {
853
807
  throw new Error(`Step ${step.id} reached maxAttempts=${step.maxAttempts}.`);
854
808
  }
855
809
 
856
- await this.repository.updateStep(step.id, {
810
+ // ── Smart retry routing (Phase 8b) ─────────────────────────────────
811
+ // For sub_agent steps, check circuit breaker state on the target.
812
+ // If the circuit is open or has multiple failures, route to an alternative.
813
+ let routedTarget: string | undefined;
814
+ let routeReason: string | undefined;
815
+ if (step.type === 'sub_agent' && run.leaderUsername) {
816
+ const target = step.target || '';
817
+ if (target) {
818
+ const circuitBreaker = getCircuitBreaker();
819
+ const state = circuitBreaker.getState(target);
820
+ const attempt = Number(step.attempt || 0);
821
+
822
+ // Route away if: circuit is open, or 2+ failures and at least 1 retry already attempted
823
+ const shouldRoute = state?.state === 'open' || (state && state.failures >= 2 && attempt >= 1);
824
+
825
+ if (shouldRoute) {
826
+ const alternatives = await this.registryService.findAlternativeSubAgents(run.leaderUsername, target);
827
+ if (alternatives.length > 0) {
828
+ const chosen = alternatives[0]; // pick first alternative
829
+ routedTarget = chosen.username;
830
+ routeReason =
831
+ `Sub-agent "${target}" has ${state?.failures || 0} failure(s) (circuit: ${state?.state || 'closed'}). ` +
832
+ `Routing retry to alternative "${routedTarget}".`;
833
+ }
834
+ }
835
+ }
836
+ }
837
+
838
+ const updateValues: any = {
857
839
  status: 'pending',
858
840
  error: '',
859
841
  endedAt: null,
860
842
  updatedAt: now(),
861
- });
843
+ };
844
+ if (routedTarget) {
845
+ updateValues.target = routedTarget;
846
+ }
847
+
848
+ await this.repository.updateStep(step.id, updateValues);
862
849
 
863
850
  await this.repository.updateRun(run.id, {
864
851
  status: 'running',
@@ -869,9 +856,13 @@ export class AgentLoopController {
869
856
  runId: run.id,
870
857
  stepId: step.id,
871
858
  type: 'step_retry',
872
- title: `Retry queued: ${step.title || step.planKey}`,
859
+ title: routedTarget
860
+ ? `Retry routed: ${step.title || step.planKey} → ${routedTarget}`
861
+ : `Retry queued: ${step.title || step.planKey}`,
862
+ content: routeReason || '',
873
863
  status: 'pending',
874
864
  userId: options.userId,
865
+ payload: routedTarget ? { originalTarget: step.target, routedTarget, reason: routeReason } : undefined,
875
866
  });
876
867
 
877
868
  return this.getRunSnapshot(run.id);
@@ -885,7 +876,7 @@ export class AgentLoopController {
885
876
  summary?: string;
886
877
  evidence?: any;
887
878
  userId?: string | number;
888
- } = {}
879
+ } = {},
889
880
  ) {
890
881
  const run = await this.repository.requireRun(runId);
891
882
  if (TERMINAL_RUN_STATUSES.has(run.status)) {
@@ -929,6 +920,40 @@ export class AgentLoopController {
929
920
  return this.getRunSnapshot(run.id);
930
921
  }
931
922
 
923
+ async stepFeedback(
924
+ stepId: string | number,
925
+ feedback: { rating: 'positive' | 'negative'; comment?: string; category?: string },
926
+ options: { userId?: string | number } = {},
927
+ ) {
928
+ const step = await this.repository.requireStep(stepId);
929
+ const run = await this.repository.requireRun(step.runId);
930
+
931
+ if (step.status !== 'succeeded' && step.status !== 'failed') {
932
+ throw new Error(`Cannot provide feedback for step ${step.id} in status "${step.status}".`);
933
+ }
934
+
935
+ await this.repository.createEvent({
936
+ runId: run.id,
937
+ stepId: step.id,
938
+ type: 'step_feedback',
939
+ title: `Feedback: ${feedback.rating === 'positive' ? '👍' : '👎'} ${step.title || step.planKey}`,
940
+ content: feedback.comment || '',
941
+ status: step.status,
942
+ userId: options.userId,
943
+ payload: {
944
+ rating: feedback.rating,
945
+ comment: feedback.comment || '',
946
+ category: feedback.category || '',
947
+ stepType: step.type,
948
+ stepPlanKey: step.planKey,
949
+ stepTarget: step.target || '',
950
+ attempt: step.attempt || 0,
951
+ },
952
+ });
953
+
954
+ return this.getRunSnapshot(run.id);
955
+ }
956
+
932
957
  async cancelRun(runId: string | number, options: { userId?: string | number; reason?: string } = {}) {
933
958
  const run = await this.repository.requireRun(runId);
934
959
  if (TERMINAL_RUN_STATUSES.has(run.status)) {
@@ -982,49 +1007,96 @@ export class AgentLoopController {
982
1007
  1,
983
1008
  Math.min(
984
1009
  ORCHESTRATOR_CONTROLLER_MAX_STEPS,
985
- Number(harnessSettings.maxControllerSteps || ORCHESTRATOR_CONTROLLER_MAX_STEPS)
986
- )
1010
+ Number(harnessSettings.maxControllerSteps || ORCHESTRATOR_CONTROLLER_MAX_STEPS),
1011
+ ),
987
1012
  );
988
1013
 
989
1014
  const policy = normalizePolicy(snapshot.run.policy);
1015
+ const maxConcurrency = policy.maxConcurrency;
1016
+
1017
+ while (snapshot.nextSteps && snapshot.nextSteps.length > 0 && iterations < maxControllerSteps) {
1018
+ const batch = snapshot.nextSteps.slice(0, maxConcurrency);
1019
+ iterations += batch.length;
1020
+
1021
+ // Budget check before batch
1022
+ if (this.tokenTracker) {
1023
+ const budgetCheck = await this.tokenTracker.checkBudget(runId);
1024
+ if (!budgetCheck.allowed) {
1025
+ return this.finishRun(runId, budgetCheck.reason, {
1026
+ status: 'failed' as const,
1027
+ summary: budgetCheck.reason,
1028
+ userId: options.userId,
1029
+ });
1030
+ }
1031
+ }
990
1032
 
991
- while (snapshot.nextStep && iterations < maxControllerSteps) {
992
- iterations += 1;
993
- const nextStep = snapshot.nextStep;
994
- snapshot = await this.startStep(nextStep.id, { userId: options.userId });
995
- const runningStep = snapshot.steps.find((step: any) => String(step.id) === String(nextStep.id)) || nextStep;
1033
+ // Start all steps concurrently
1034
+ await Promise.all(batch.map((step: any) => this.startStep(step.id, { userId: options.userId })));
996
1035
 
997
- try {
998
- const output = await this.harness.executeStep(snapshot.run, runningStep, options);
999
- snapshot = await this.completeStep(runningStep.id, output, {
1000
- userId: options.userId,
1001
- metadata: { controller: 'agent-loop-service' },
1002
- });
1003
- } catch (error: any) {
1004
- if (error?.message === 'requires_approval') {
1005
- // Pause the execution and request approval
1006
- snapshot = await this.requestApproval(runningStep.id, {
1007
- prompt: `Execution of step "${runningStep.title}" requires permission.`,
1008
- }, {
1036
+ // Refresh snapshot to get updated step states
1037
+ snapshot = await this.getRunSnapshot(runId);
1038
+ const runningSteps = batch.map(
1039
+ (step: any) => snapshot.steps.find((s: any) => String(s.id) === String(step.id)) || step,
1040
+ );
1041
+
1042
+ // Execute all steps concurrently via harness
1043
+ const results = await Promise.allSettled(
1044
+ runningSteps.map((step: any) => this.harness.executeStep(snapshot.run, step, options)),
1045
+ );
1046
+
1047
+ // Process results — failures need individual backoff before retry
1048
+ for (let i = 0; i < results.length; i++) {
1049
+ const runningStep = runningSteps[i];
1050
+ const result = results[i];
1051
+
1052
+ if (result.status === 'fulfilled') {
1053
+ snapshot = await this.completeStep(runningStep.id, result.value, {
1009
1054
  userId: options.userId,
1010
- reason: 'Dynamic tool approval required by policy.',
1055
+ metadata: { controller: 'agent-loop-service' },
1011
1056
  });
1012
- break;
1057
+ } else {
1058
+ const error = result.reason;
1059
+ if (error?.message === 'requires_approval') {
1060
+ snapshot = await this.requestApproval(
1061
+ runningStep.id,
1062
+ { prompt: `Execution of step "${runningStep.title}" requires permission.` },
1063
+ { userId: options.userId, reason: 'Dynamic tool approval required by policy.' },
1064
+ );
1065
+ // Stop processing more results; remaining steps are left pending
1066
+ break;
1067
+ }
1068
+
1069
+ snapshot = await this.failStep(runningStep.id, error?.message || String(error), {
1070
+ userId: options.userId,
1071
+ metadata: { controller: 'agent-loop-service' },
1072
+ });
1073
+ const failedStep = snapshot.steps.find((s: any) => String(s.id) === String(runningStep.id));
1074
+ if (
1075
+ failedStep &&
1076
+ Number(failedStep.attempt || 0) < Number(failedStep.maxAttempts || policy.maxStepAttempts)
1077
+ ) {
1078
+ // Exponential backoff before retry
1079
+ const attempt = Number(failedStep.attempt || 1);
1080
+ const baseDelay = 1000;
1081
+ const maxDelay = 60000;
1082
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
1083
+ await new Promise((resolve) => setTimeout(resolve, delay));
1084
+ // Step remains pending for retry (failStep resets status back)
1085
+ }
1013
1086
  }
1087
+ }
1014
1088
 
1015
- snapshot = await this.failStep(runningStep.id, error?.message || String(error), {
1016
- userId: options.userId,
1017
- metadata: { controller: 'agent-loop-service' },
1018
- });
1019
- const failedStep = snapshot.steps.find((step: any) => String(step.id) === String(runningStep.id));
1020
- if (!failedStep || Number(failedStep.attempt || 0) >= Number(failedStep.maxAttempts || policy.maxStepAttempts)) {
1021
- break;
1022
- }
1089
+ // Re-evaluate the plan newly unblocked steps become eligible
1090
+ snapshot = await this.getRunSnapshot(runId);
1091
+
1092
+ // If we broke for approval, exit loop
1093
+ if (snapshot.run.status === 'waiting_user') {
1094
+ break;
1023
1095
  }
1024
1096
  }
1025
1097
 
1026
1098
  snapshot = await this.getRunSnapshot(runId);
1027
- if (iterations >= maxControllerSteps && snapshot.nextStep) {
1099
+ if (iterations >= maxControllerSteps && snapshot.nextSteps && snapshot.nextSteps.length > 0) {
1028
1100
  return this.finishRun(runId, `Agent loop stopped after ${maxControllerSteps} controller steps.`, {
1029
1101
  status: 'failed',
1030
1102
  summary: 'Controller iteration limit reached.',
@@ -1039,7 +1111,9 @@ export class AgentLoopController {
1039
1111
 
1040
1112
  const steps = snapshot.steps || [];
1041
1113
  const failed = steps.filter((step: any) => step.status === 'failed');
1042
- const unfinished = steps.filter((step: any) => !TERMINAL_STEP_STATUSES.has(step.status) && step.status !== 'failed');
1114
+ const unfinished = steps.filter(
1115
+ (step: any) => !TERMINAL_STEP_STATUSES.has(step.status) && step.status !== 'failed',
1116
+ );
1043
1117
  if (failed.length || unfinished.length) {
1044
1118
  return this.finishRun(
1045
1119
  runId,
@@ -1049,9 +1123,12 @@ export class AgentLoopController {
1049
1123
  {
1050
1124
  status: 'failed',
1051
1125
  summary: failed[0]?.error || 'No executable step is available.',
1052
- evidence: { failedStepIds: failed.map((step: any) => step.id), unfinishedStepIds: unfinished.map((step: any) => step.id) },
1126
+ evidence: {
1127
+ failedStepIds: failed.map((step: any) => step.id),
1128
+ unfinishedStepIds: unfinished.map((step: any) => step.id),
1129
+ },
1053
1130
  userId: options.userId,
1054
- }
1131
+ },
1055
1132
  );
1056
1133
  }
1057
1134
 
@@ -1074,11 +1151,12 @@ export class AgentLoopController {
1074
1151
  async getRunSnapshot(runId: string | number) {
1075
1152
  const run = await this.repository.requireRun(runId);
1076
1153
  const steps = await this.repository.getSteps(run.id);
1077
- const nextStep = this.pickNextStep(steps, run.policy);
1154
+ const nextSteps = this.pickNextSteps(steps, run.policy);
1155
+ const runningStepIds = steps.filter((s) => s.status === 'running').map((s) => s.id);
1078
1156
  return {
1079
- run,
1157
+ run: { ...run, runningStepIds },
1080
1158
  steps,
1081
- nextStep,
1159
+ nextSteps,
1082
1160
  };
1083
1161
  }
1084
1162
 
@@ -1095,7 +1173,7 @@ export class AgentLoopController {
1095
1173
  };
1096
1174
  }
1097
1175
 
1098
- pickNextStep(steps: any[], runPolicy?: any) {
1176
+ pickNextSteps(steps: any[], runPolicy?: any) {
1099
1177
  const byPlanKey = new Map(steps.map((step) => [String(step.planKey), step]));
1100
1178
  const policy = normalizePolicy(runPolicy);
1101
1179
 
@@ -1103,26 +1181,23 @@ export class AgentLoopController {
1103
1181
  .filter(
1104
1182
  (step) =>
1105
1183
  step.status === 'pending' ||
1106
- (step.status === 'failed' &&
1107
- Number(step.attempt || 0) < Number(step.maxAttempts || policy.maxStepAttempts))
1184
+ (step.status === 'failed' && Number(step.attempt || 0) < Number(step.maxAttempts || policy.maxStepAttempts)),
1108
1185
  )
1109
1186
  .sort((a, b) => Number(a.index || 0) - Number(b.index || 0));
1110
1187
 
1188
+ const ready: any[] = [];
1111
1189
  for (const step of candidates) {
1112
1190
  const dependencies = asArray(step.dependsOn).map(String);
1113
1191
  const allowSkipped =
1114
1192
  step.dependencyPolicy === 'allow_skipped' || step.metadata?.dependencyPolicy === 'allow_skipped';
1115
- const ready = dependencies.every((key) => {
1193
+ const allDepsReady = dependencies.every((key) => {
1116
1194
  const dependency = byPlanKey.get(key);
1117
1195
  return dependency?.status === 'succeeded' || (allowSkipped && dependency?.status === 'skipped');
1118
1196
  });
1119
- if (ready) {
1120
- return {
1121
- ...step,
1122
- retryable: step.status === 'failed',
1123
- };
1197
+ if (allDepsReady) {
1198
+ ready.push(step);
1124
1199
  }
1125
1200
  }
1126
- return null;
1201
+ return ready;
1127
1202
  }
1128
1203
  }
@@ -1,20 +1,5 @@
1
- function toPlain(record: any) {
2
- return record?.toJSON?.() || record;
3
- }
4
-
5
- function trimText(value: any, max = 50000) {
6
- let text = '';
7
- if (typeof value === 'string') {
8
- text = value;
9
- } else if (value != null) {
10
- try {
11
- text = JSON.stringify(value);
12
- } catch {
13
- text = String(value);
14
- }
15
- }
16
- return text.length > max ? `${text.slice(0, max)}\n...[truncated]` : text;
17
- }
1
+ import { toPlain, trimText } from '../utils/ctx-utils';
2
+ import { getRunEventBus } from './RunEventBus';
18
3
 
19
4
  export class AgentLoopRepository {
20
5
  constructor(private readonly plugin: any) {}
@@ -99,7 +84,10 @@ export class AgentLoopRepository {
99
84
  createdAt: new Date(),
100
85
  },
101
86
  });
102
- return toPlain(record);
87
+ const event = toPlain(record);
88
+ // Push event to SSE subscribers
89
+ getRunEventBus().emit(values.runId, event);
90
+ return event;
103
91
  }
104
92
 
105
93
  async getEvents(runId: string | number) {
@@ -172,13 +160,9 @@ export class AgentLoopRepository {
172
160
  {
173
161
  where: {
174
162
  id: runId,
175
- [Op.or]: [
176
- { lockedBy: null },
177
- { lockedUntil: { [Op.lt]: now.toISOString() } },
178
- { lockedBy: lockName }
179
- ]
180
- }
181
- }
163
+ [Op.or]: [{ lockedBy: null }, { lockedUntil: { [Op.lt]: now.toISOString() } }, { lockedBy: lockName }],
164
+ },
165
+ },
182
166
  );
183
167
 
184
168
  const affectedCount = Array.isArray(result) ? result[0] : Number(result || 0);
@@ -4,6 +4,7 @@ import { AgentPlanValidator } from './AgentPlanValidator';
4
4
  import { AgentLoopRepository } from './AgentLoopRepository';
5
5
  import { AgentHarness } from './AgentHarness';
6
6
  import { AgentLoopController } from './AgentLoopController';
7
+ import { TokenTracker } from './TokenTracker';
7
8
 
8
9
  export type AgentLoopRunStatus =
9
10
  | 'planning'
@@ -26,6 +27,11 @@ export type AgentLoopPolicy = {
26
27
  allowReplan: boolean;
27
28
  requireVerification: boolean;
28
29
  stopOnApprovalRequired: boolean;
30
+ maxContextTokens?: number;
31
+ contextSummaryStrategy?: 'last_n' | 'all';
32
+ includeToolResults?: boolean;
33
+ includeStepOutputs?: boolean;
34
+ maxConcurrency?: number;
29
35
  };
30
36
 
31
37
  export type AgentLoopPlanStepInput = {
@@ -58,12 +64,14 @@ export class AgentLoopService {
58
64
  this.validator = new AgentPlanValidator();
59
65
  this.repository = new AgentLoopRepository(plugin);
60
66
  this.harness = new AgentHarness(plugin, this.registryService);
67
+ const tokenTracker = new TokenTracker(plugin);
61
68
  this.controller = new AgentLoopController(
62
69
  this.registryService,
63
70
  this.plannerService,
64
71
  this.validator,
65
72
  this.repository,
66
- this.harness
73
+ this.harness,
74
+ tokenTracker,
67
75
  );
68
76
  }
69
77
 
@@ -135,6 +143,10 @@ export class AgentLoopService {
135
143
  return this.controller.retryStep(stepId, options);
136
144
  }
137
145
 
146
+ async stepFeedback(stepId: any, feedback: any, options: any = {}) {
147
+ return this.controller.stepFeedback(stepId, feedback, options);
148
+ }
149
+
138
150
  async finishRun(runId: any, finalAnswer: any, options: any = {}) {
139
151
  return this.controller.finishRun(runId, finalAnswer, options);
140
152
  }