plugin-agent-orchestrator 1.0.19 → 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.
- package/dist/client/hooks/useRunEventStream.d.ts +22 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -6
- package/dist/server/collections/agent-execution-spans.js +24 -0
- package/dist/server/collections/agent-loop-runs.js +36 -0
- package/dist/server/collections/orchestrator-config.js +14 -0
- package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
- package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
- package/dist/server/plugin.js +47 -0
- package/dist/server/resources/agent-loop.js +33 -25
- package/dist/server/resources/tracing.js +5 -8
- package/dist/server/services/AgentHarness.d.ts +2 -0
- package/dist/server/services/AgentHarness.js +56 -90
- package/dist/server/services/AgentLoopController.d.ts +33 -20
- package/dist/server/services/AgentLoopController.js +164 -125
- package/dist/server/services/AgentLoopRepository.js +16 -34
- package/dist/server/services/AgentLoopService.d.ts +28 -18
- package/dist/server/services/AgentLoopService.js +7 -1
- package/dist/server/services/AgentPlannerService.js +5 -25
- package/dist/server/services/AgentRegistryService.d.ts +8 -0
- package/dist/server/services/AgentRegistryService.js +34 -24
- package/dist/server/services/CircuitBreaker.d.ts +40 -0
- package/dist/server/services/CircuitBreaker.js +120 -0
- package/dist/server/services/ContextAggregator.d.ts +45 -0
- package/dist/server/services/ContextAggregator.js +201 -0
- package/dist/server/services/ExecutionSpanService.js +2 -5
- package/dist/server/services/RunEventBus.d.ts +9 -0
- package/dist/server/services/RunEventBus.js +73 -0
- package/dist/server/services/TokenTracker.d.ts +62 -0
- package/dist/server/services/TokenTracker.js +173 -0
- package/dist/server/skill-hub/plugin.js +6 -6
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +6 -6
- package/dist/server/tools/agent-loop.d.ts +8 -8
- package/dist/server/tools/agent-loop.js +30 -63
- package/dist/server/tools/delegate-task.js +14 -72
- package/dist/server/tools/orchestrator-plan.d.ts +6 -6
- package/dist/server/tools/orchestrator-plan.js +10 -47
- package/dist/server/types.d.ts +47 -0
- package/dist/server/types.js +24 -0
- package/dist/server/utils/ctx-utils.d.ts +30 -0
- package/dist/server/utils/ctx-utils.js +152 -0
- package/dist/server/utils/logging.d.ts +6 -0
- package/dist/server/utils/logging.js +86 -0
- package/package.json +44 -44
- package/src/client/AgentRunsTab.tsx +764 -764
- package/src/client/HarnessProfilesTab.tsx +247 -247
- package/src/client/OrchestratorSettings.tsx +106 -106
- package/src/client/RulesTab.tsx +716 -716
- package/src/client/hooks/useRunEventStream.ts +76 -0
- package/src/client/index.tsx +2 -1
- package/src/client/plugin.tsx +27 -27
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
- package/src/client/skill-hub/index.tsx +51 -51
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
- package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
- package/src/client/tools/PlanApprovalCard.tsx +175 -175
- package/src/client/tools/registerOrchestratorCards.ts +7 -7
- package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
- package/src/server/__tests__/circuit-breaker.test.ts +169 -0
- package/src/server/__tests__/context-aggregator.test.ts +222 -0
- package/src/server/__tests__/parallel-execution.test.ts +318 -0
- package/src/server/__tests__/smoke.test.ts +120 -0
- package/src/server/collections/agent-execution-spans.ts +24 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -59
- package/src/server/collections/agent-loop-events.ts +71 -71
- package/src/server/collections/agent-loop-runs.ts +38 -1
- package/src/server/collections/agent-loop-steps.ts +144 -144
- package/src/server/collections/orchestrator-config.ts +14 -0
- package/src/server/collections/skill-executions.ts +106 -106
- package/src/server/collections/skill-loop-configs.ts +65 -65
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
- package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
- package/src/server/plugin.ts +53 -0
- package/src/server/resources/agent-loop.ts +21 -12
- package/src/server/resources/tracing.ts +3 -7
- package/src/server/services/AgentHarness.ts +78 -116
- package/src/server/services/AgentLoopController.ts +197 -122
- package/src/server/services/AgentLoopRepository.ts +9 -25
- package/src/server/services/AgentLoopService.ts +13 -1
- package/src/server/services/AgentPlanValidator.ts +73 -73
- package/src/server/services/AgentPlannerService.ts +2 -25
- package/src/server/services/AgentRegistryService.ts +40 -31
- package/src/server/services/CircuitBreaker.ts +116 -0
- package/src/server/services/ContextAggregator.ts +239 -0
- package/src/server/services/ExecutionSpanService.ts +2 -4
- package/src/server/services/RunEventBus.ts +45 -0
- package/src/server/services/TokenTracker.ts +209 -0
- package/src/server/skill-hub/plugin.ts +898 -897
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -458
- package/src/server/tools/agent-loop.ts +18 -57
- package/src/server/tools/delegate-task.ts +11 -93
- package/src/server/tools/orchestrator-plan.ts +26 -50
- package/src/server/tools/skill-execute.ts +160 -160
- package/src/server/types.ts +55 -0
- package/src/server/utils/ctx-utils.ts +118 -0
- 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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1055
|
+
metadata: { controller: 'agent-loop-service' },
|
|
1011
1056
|
});
|
|
1012
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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.
|
|
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(
|
|
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: {
|
|
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
|
|
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
|
-
|
|
1159
|
+
nextSteps,
|
|
1082
1160
|
};
|
|
1083
1161
|
}
|
|
1084
1162
|
|
|
@@ -1095,7 +1173,7 @@ export class AgentLoopController {
|
|
|
1095
1173
|
};
|
|
1096
1174
|
}
|
|
1097
1175
|
|
|
1098
|
-
|
|
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
|
|
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 (
|
|
1120
|
-
|
|
1121
|
-
...step,
|
|
1122
|
-
retryable: step.status === 'failed',
|
|
1123
|
-
};
|
|
1197
|
+
if (allDepsReady) {
|
|
1198
|
+
ready.push(step);
|
|
1124
1199
|
}
|
|
1125
1200
|
}
|
|
1126
|
-
return
|
|
1201
|
+
return ready;
|
|
1127
1202
|
}
|
|
1128
1203
|
}
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
}
|