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.
Files changed (100) 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/skill-hub/plugin.js +6 -6
  33. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +6 -6
  34. package/dist/server/tools/agent-loop.d.ts +8 -8
  35. package/dist/server/tools/agent-loop.js +30 -63
  36. package/dist/server/tools/delegate-task.js +14 -72
  37. package/dist/server/tools/orchestrator-plan.d.ts +6 -6
  38. package/dist/server/tools/orchestrator-plan.js +10 -47
  39. package/dist/server/types.d.ts +47 -0
  40. package/dist/server/types.js +24 -0
  41. package/dist/server/utils/ctx-utils.d.ts +30 -0
  42. package/dist/server/utils/ctx-utils.js +152 -0
  43. package/dist/server/utils/logging.d.ts +6 -0
  44. package/dist/server/utils/logging.js +86 -0
  45. package/package.json +44 -44
  46. package/src/client/AgentRunsTab.tsx +764 -764
  47. package/src/client/HarnessProfilesTab.tsx +247 -247
  48. package/src/client/OrchestratorSettings.tsx +106 -106
  49. package/src/client/RulesTab.tsx +716 -716
  50. package/src/client/hooks/useRunEventStream.ts +76 -0
  51. package/src/client/index.tsx +2 -1
  52. package/src/client/plugin.tsx +27 -27
  53. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  54. package/src/client/skill-hub/index.tsx +51 -51
  55. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  56. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  57. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  58. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  59. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  60. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  61. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  62. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  63. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  64. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  65. package/src/server/__tests__/smoke.test.ts +120 -0
  66. package/src/server/collections/agent-execution-spans.ts +24 -0
  67. package/src/server/collections/agent-harness-profiles.ts +59 -59
  68. package/src/server/collections/agent-loop-events.ts +71 -71
  69. package/src/server/collections/agent-loop-runs.ts +38 -1
  70. package/src/server/collections/agent-loop-steps.ts +144 -144
  71. package/src/server/collections/orchestrator-config.ts +14 -0
  72. package/src/server/collections/skill-executions.ts +106 -106
  73. package/src/server/collections/skill-loop-configs.ts +65 -65
  74. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  75. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  76. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  77. package/src/server/plugin.ts +53 -0
  78. package/src/server/resources/agent-loop.ts +21 -12
  79. package/src/server/resources/tracing.ts +3 -7
  80. package/src/server/services/AgentHarness.ts +78 -116
  81. package/src/server/services/AgentLoopController.ts +197 -122
  82. package/src/server/services/AgentLoopRepository.ts +9 -25
  83. package/src/server/services/AgentLoopService.ts +13 -1
  84. package/src/server/services/AgentPlanValidator.ts +73 -73
  85. package/src/server/services/AgentPlannerService.ts +2 -25
  86. package/src/server/services/AgentRegistryService.ts +40 -31
  87. package/src/server/services/CircuitBreaker.ts +116 -0
  88. package/src/server/services/ContextAggregator.ts +239 -0
  89. package/src/server/services/ExecutionSpanService.ts +2 -4
  90. package/src/server/services/RunEventBus.ts +45 -0
  91. package/src/server/services/TokenTracker.ts +209 -0
  92. package/src/server/skill-hub/plugin.ts +898 -897
  93. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -458
  94. package/src/server/tools/agent-loop.ts +18 -57
  95. package/src/server/tools/delegate-task.ts +11 -93
  96. package/src/server/tools/orchestrator-plan.ts +26 -50
  97. package/src/server/tools/skill-execute.ts +160 -160
  98. package/src/server/types.ts +55 -0
  99. package/src/server/utils/ctx-utils.ts +118 -0
  100. package/src/server/utils/logging.ts +63 -0
@@ -1,73 +1,73 @@
1
- import { AgentLoopPlanStepInput } from './AgentLoopService';
2
-
3
- function normalizeStepType(value: any) {
4
- return ['reasoning', 'skill', 'tool', 'sub_agent', 'verification'].includes(value) ? value : 'tool';
5
- }
6
-
7
- function normalizePlanKey(step: any, index: number) {
8
- return String(step.planKey || step.key || step.id || `step_${index + 1}`);
9
- }
10
-
11
- function asArray(value: any): any[] {
12
- return Array.isArray(value) ? value : [];
13
- }
14
-
15
- const ORCHESTRATOR_CONTROLLER_MAX_STEPS = 100;
16
-
17
- export class AgentPlanValidator {
18
- validate(plan: AgentLoopPlanStepInput[]) {
19
- if (!Array.isArray(plan) || plan.length === 0) {
20
- throw new Error('Plan must include at least one step.');
21
- }
22
- if (plan.length > ORCHESTRATOR_CONTROLLER_MAX_STEPS) {
23
- throw new Error(`Plan cannot exceed ${ORCHESTRATOR_CONTROLLER_MAX_STEPS} steps.`);
24
- }
25
-
26
- const keys = new Set<string>();
27
- const graph = new Map<string, string[]>();
28
- for (let i = 0; i < plan.length; i++) {
29
- const key = normalizePlanKey(plan[i], i).trim();
30
- if (!key) {
31
- throw new Error(`Plan step ${i + 1} has an empty planKey.`);
32
- }
33
- if (keys.has(key)) {
34
- throw new Error(`Duplicate planKey "${key}" in plan.`);
35
- }
36
- const type = normalizeStepType(plan[i].type);
37
- if (['tool', 'skill', 'sub_agent'].includes(type) && !plan[i].target) {
38
- throw new Error(`Step "${key}" of type "${type}" must include a target.`);
39
- }
40
- keys.add(key);
41
- graph.set(key, asArray(plan[i].dependsOn).map(String));
42
- }
43
-
44
- for (const [key, dependencies] of graph.entries()) {
45
- for (const dependency of dependencies) {
46
- if (!keys.has(dependency)) {
47
- throw new Error(`Step "${key}" depends on unknown step "${dependency}".`);
48
- }
49
- if (dependency === key) {
50
- throw new Error(`Step "${key}" cannot depend on itself.`);
51
- }
52
- }
53
- }
54
-
55
- const visiting = new Set<string>();
56
- const visited = new Set<string>();
57
- const visit = (key: string) => {
58
- if (visited.has(key)) return;
59
- if (visiting.has(key)) {
60
- throw new Error(`Plan has a dependency cycle at "${key}".`);
61
- }
62
- visiting.add(key);
63
- for (const dependency of graph.get(key) || []) {
64
- visit(dependency);
65
- }
66
- visiting.delete(key);
67
- visited.add(key);
68
- };
69
- for (const key of graph.keys()) {
70
- visit(key);
71
- }
72
- }
73
- }
1
+ import { AgentLoopPlanStepInput } from './AgentLoopService';
2
+
3
+ function normalizeStepType(value: any) {
4
+ return ['reasoning', 'skill', 'tool', 'sub_agent', 'verification'].includes(value) ? value : 'tool';
5
+ }
6
+
7
+ function normalizePlanKey(step: any, index: number) {
8
+ return String(step.planKey || step.key || step.id || `step_${index + 1}`);
9
+ }
10
+
11
+ function asArray(value: any): any[] {
12
+ return Array.isArray(value) ? value : [];
13
+ }
14
+
15
+ const ORCHESTRATOR_CONTROLLER_MAX_STEPS = 100;
16
+
17
+ export class AgentPlanValidator {
18
+ validate(plan: AgentLoopPlanStepInput[]) {
19
+ if (!Array.isArray(plan) || plan.length === 0) {
20
+ throw new Error('Plan must include at least one step.');
21
+ }
22
+ if (plan.length > ORCHESTRATOR_CONTROLLER_MAX_STEPS) {
23
+ throw new Error(`Plan cannot exceed ${ORCHESTRATOR_CONTROLLER_MAX_STEPS} steps.`);
24
+ }
25
+
26
+ const keys = new Set<string>();
27
+ const graph = new Map<string, string[]>();
28
+ for (let i = 0; i < plan.length; i++) {
29
+ const key = normalizePlanKey(plan[i], i).trim();
30
+ if (!key) {
31
+ throw new Error(`Plan step ${i + 1} has an empty planKey.`);
32
+ }
33
+ if (keys.has(key)) {
34
+ throw new Error(`Duplicate planKey "${key}" in plan.`);
35
+ }
36
+ const type = normalizeStepType(plan[i].type);
37
+ if (['tool', 'skill', 'sub_agent'].includes(type) && !plan[i].target) {
38
+ throw new Error(`Step "${key}" of type "${type}" must include a target.`);
39
+ }
40
+ keys.add(key);
41
+ graph.set(key, asArray(plan[i].dependsOn).map(String));
42
+ }
43
+
44
+ for (const [key, dependencies] of graph.entries()) {
45
+ for (const dependency of dependencies) {
46
+ if (!keys.has(dependency)) {
47
+ throw new Error(`Step "${key}" depends on unknown step "${dependency}".`);
48
+ }
49
+ if (dependency === key) {
50
+ throw new Error(`Step "${key}" cannot depend on itself.`);
51
+ }
52
+ }
53
+ }
54
+
55
+ const visiting = new Set<string>();
56
+ const visited = new Set<string>();
57
+ const visit = (key: string) => {
58
+ if (visited.has(key)) return;
59
+ if (visiting.has(key)) {
60
+ throw new Error(`Plan has a dependency cycle at "${key}".`);
61
+ }
62
+ visiting.add(key);
63
+ for (const dependency of graph.get(key) || []) {
64
+ visit(dependency);
65
+ }
66
+ visiting.delete(key);
67
+ visited.add(key);
68
+ };
69
+ for (const key of graph.keys()) {
70
+ visit(key);
71
+ }
72
+ }
73
+ }
@@ -1,35 +1,12 @@
1
1
  import { AgentLoopPlanStepInput } from './AgentLoopService';
2
2
 
3
- function normalizeStepType(value: any) {
4
- return ['reasoning', 'skill', 'tool', 'sub_agent', 'verification'].includes(value) ? value : 'tool';
5
- }
6
-
7
- function normalizePlanKey(step: any, index: number) {
8
- return String(step.planKey || step.key || step.id || `step_${index + 1}`);
9
- }
10
-
11
- function asArray(value: any): any[] {
12
- return Array.isArray(value) ? value : [];
13
- }
14
-
15
- function asObject(value: any) {
16
- if (value && typeof value === 'object' && !Array.isArray(value)) return value;
17
- if (typeof value === 'string' && value.trim()) {
18
- try {
19
- const parsed = JSON.parse(value);
20
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
21
- } catch {
22
- return {};
23
- }
24
- }
25
- return {};
26
- }
3
+ import { normalizeStepType, normalizePlanKey, asArray, asObject } from '../utils/ctx-utils';
27
4
 
28
5
  export class AgentPlannerService {
29
6
  buildPlan(
30
7
  goal: string,
31
8
  plan: AgentLoopPlanStepInput[] | undefined,
32
- options: { targetAgent?: string; harnessTag?: string; metadata?: any }
9
+ options: { targetAgent?: string; harnessTag?: string; metadata?: any },
33
10
  ): AgentLoopPlanStepInput[] {
34
11
  if (Array.isArray(plan) && plan.length > 0) {
35
12
  return plan.map((step, index) => ({
@@ -1,25 +1,4 @@
1
- function toPlain(record: any) {
2
- return record?.toJSON?.() || record;
3
- }
4
-
5
- function asObject(value: any) {
6
- if (value && typeof value === 'object' && !Array.isArray(value)) return value;
7
- if (typeof value === 'string' && value.trim()) {
8
- try {
9
- const parsed = JSON.parse(value);
10
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
11
- } catch {
12
- return {};
13
- }
14
- }
15
- return {};
16
- }
17
-
18
- function normalizeEmployeeUsername(raw: any) {
19
- if (!raw) return null;
20
- if (typeof raw === 'string') return raw;
21
- return raw.username || raw.aiEmployeeUsername || raw.name || null;
22
- }
1
+ import { toPlain, asObject, normalizeEmployeeUsername } from '../utils/ctx-utils';
23
2
 
24
3
  export class AgentRegistryService {
25
4
  constructor(private readonly plugin: any) {}
@@ -74,11 +53,7 @@ export class AgentRegistryService {
74
53
  }
75
54
  }
76
55
 
77
- async resolveModelSettings(
78
- subAgentUsername: string,
79
- leaderUsername?: string,
80
- dynamicValues?: any
81
- ) {
56
+ async resolveModelSettings(subAgentUsername: string, leaderUsername?: string, dynamicValues?: any) {
82
57
  const subAgent = await this.getAIEmployee(subAgentUsername);
83
58
  if (!subAgent) {
84
59
  throw new Error(`Sub-agent "${subAgentUsername}" was not found.`);
@@ -88,9 +63,7 @@ export class AgentRegistryService {
88
63
  return Boolean(val?.llmService && val?.model);
89
64
  };
90
65
 
91
- let modelSettings = hasModelSettings(dynamicValues)
92
- ? dynamicValues
93
- : undefined;
66
+ let modelSettings = hasModelSettings(dynamicValues) ? dynamicValues : undefined;
94
67
 
95
68
  if (!modelSettings) {
96
69
  if (hasModelSettings(subAgent.modelSettings)) {
@@ -108,8 +81,44 @@ export class AgentRegistryService {
108
81
  return modelSettings;
109
82
  }
110
83
 
84
+ /**
85
+ * Find alternative sub-agents for the same leader, excluding a specific one.
86
+ * Used by the smart retry feature to route around a failing sub-agent.
87
+ */
88
+ async findAlternativeSubAgents(
89
+ leaderUsername: string,
90
+ excludeSubAgentUsername: string,
91
+ ): Promise<{ username: string; label: string }[]> {
92
+ try {
93
+ const repo = this.db.getRepository('orchestratorConfig');
94
+ if (!repo) return [];
95
+ const configs = await repo.find({
96
+ filter: {
97
+ leaderUsername,
98
+ enabled: true,
99
+ subAgentUsername: { $ne: excludeSubAgentUsername },
100
+ },
101
+ });
102
+ if (!configs || configs.length === 0) return [];
103
+
104
+ // Enrich with AI employee display names
105
+ const result: { username: string; label: string }[] = [];
106
+ for (const config of configs) {
107
+ const plain = toPlain(config);
108
+ const employee = await this.getAIEmployee(plain.subAgentUsername);
109
+ result.push({
110
+ username: plain.subAgentUsername,
111
+ label: employee?.nickname || employee?.username || plain.subAgentUsername,
112
+ });
113
+ }
114
+ return result;
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
111
120
  async isRegisteredDelegationTool(toolName: string): Promise<boolean> {
112
- if (!toolName || (typeof toolName !== 'string')) return false;
121
+ if (!toolName || typeof toolName !== 'string') return false;
113
122
  if (!toolName.startsWith('delegate_') && !toolName.startsWith('dispatch_subagents_')) {
114
123
  return false;
115
124
  }
@@ -0,0 +1,116 @@
1
+ export interface CircuitState {
2
+ failures: number;
3
+ lastFailureTime: number;
4
+ state: 'closed' | 'open' | 'half-open';
5
+ }
6
+
7
+ /**
8
+ * CircuitBreakerRegistry — prevents cascading failures by tracking per-key
9
+ * failure rates and temporarily stopping calls when thresholds are exceeded.
10
+ *
11
+ * State machine: closed → open (after threshold failures) → half-open (after recoveryTimeout)
12
+ * half-open: allows 1 probe request; success → closed, failure → open.
13
+ */
14
+ export class CircuitBreakerRegistry {
15
+ private readonly circuits = new Map<string, CircuitState>();
16
+
17
+ readonly threshold: number;
18
+ readonly recoveryTimeout: number;
19
+ readonly halfOpenMaxRequests: number;
20
+ private readonly appLog: any;
21
+
22
+ constructor(options?: { threshold?: number; recoveryTimeout?: number; halfOpenMaxRequests?: number; appLog?: any }) {
23
+ this.threshold = options?.threshold ?? 3;
24
+ this.recoveryTimeout = options?.recoveryTimeout ?? 30000;
25
+ this.halfOpenMaxRequests = options?.halfOpenMaxRequests ?? 1;
26
+ this.appLog = options?.appLog;
27
+ }
28
+
29
+ private getOrCreate(key: string): CircuitState {
30
+ let state = this.circuits.get(key);
31
+ if (!state) {
32
+ state = { failures: 0, lastFailureTime: 0, state: 'closed' };
33
+ this.circuits.set(key, state);
34
+ }
35
+ return state;
36
+ }
37
+
38
+ recordSuccess(key: string): void {
39
+ const state = this.getOrCreate(key);
40
+ if (state.state === 'half-open') {
41
+ state.state = 'closed';
42
+ state.failures = 0;
43
+ this.appLog?.debug?.(`[CircuitBreaker] "${key}" recovered → closed`);
44
+ } else if (state.state === 'closed' && state.failures > 0) {
45
+ // Decrement failure count on success (graceful recovery)
46
+ state.failures = Math.max(0, state.failures - 1);
47
+ }
48
+ }
49
+
50
+ recordFailure(key: string): void {
51
+ const state = this.getOrCreate(key);
52
+ state.failures += 1;
53
+ state.lastFailureTime = Date.now();
54
+
55
+ if (state.state === 'half-open') {
56
+ state.state = 'open';
57
+ this.appLog?.warn?.(`[CircuitBreaker] "${key}" half-open probe failed → open`);
58
+ } else if (state.state === 'closed' && state.failures >= this.threshold) {
59
+ state.state = 'open';
60
+ this.appLog?.warn?.(`[CircuitBreaker] "${key}" opened after ${state.failures} failures`);
61
+ }
62
+ }
63
+
64
+ isAllowed(key: string): boolean {
65
+ const state = this.getOrCreate(key);
66
+
67
+ if (state.state === 'closed') return true;
68
+
69
+ if (state.state === 'open') {
70
+ const elapsed = Date.now() - state.lastFailureTime;
71
+ if (elapsed >= this.recoveryTimeout) {
72
+ state.state = 'half-open';
73
+ this.appLog?.debug?.(`[CircuitBreaker] "${key}" open timeout elapsed → half-open`);
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ // half-open: allow limited probe requests
80
+ return this.halfOpenMaxRequests > 0;
81
+ }
82
+
83
+ getState(key: string): CircuitState | null {
84
+ return this.circuits.get(key) ?? null;
85
+ }
86
+
87
+ /** Reset a circuit back to closed state. */
88
+ reset(key: string): void {
89
+ const state = this.circuits.get(key);
90
+ if (state) {
91
+ state.state = 'closed';
92
+ state.failures = 0;
93
+ state.lastFailureTime = 0;
94
+ }
95
+ }
96
+
97
+ /** Get all circuit state keys (for monitoring/diagnostics). */
98
+ getKeys(): string[] {
99
+ return Array.from(this.circuits.keys());
100
+ }
101
+ }
102
+
103
+ // Singleton shared across the plugin
104
+ let globalInstance: CircuitBreakerRegistry | null = null;
105
+
106
+ export function getCircuitBreaker(options?: {
107
+ threshold?: number;
108
+ recoveryTimeout?: number;
109
+ halfOpenMaxRequests?: number;
110
+ appLog?: any;
111
+ }): CircuitBreakerRegistry {
112
+ if (!globalInstance) {
113
+ globalInstance = new CircuitBreakerRegistry(options);
114
+ }
115
+ return globalInstance;
116
+ }
@@ -0,0 +1,239 @@
1
+ import { AgentLoopPolicy } from './AgentLoopService';
2
+
3
+ function trimText(text: string, maxLen: number): string {
4
+ if (text.length <= maxLen) return text;
5
+ return text.slice(0, maxLen) + '\n...[truncated]';
6
+ }
7
+
8
+ /**
9
+ * Estimate token count from text (rough: ~4 chars per token).
10
+ */
11
+ function estimateTokens(text: string): number {
12
+ return Math.ceil(text.length / 4);
13
+ }
14
+
15
+ /**
16
+ * ContextAggregator — builds structured step context for sub-agent system prompts.
17
+ *
18
+ * Responsibilities:
19
+ * 1. Query completed steps of a run
20
+ * 2. Format them as structured XML context
21
+ * 3. Apply truncation/summarization based on policy
22
+ * 4. Inject into sub-agent system prompt
23
+ */
24
+ export class ContextAggregator {
25
+ constructor(private readonly plugin: any) {}
26
+
27
+ get db() {
28
+ return this.plugin.db;
29
+ }
30
+
31
+ get app() {
32
+ return this.plugin.app;
33
+ }
34
+
35
+ /**
36
+ * Build a structured context string from completed steps of a run.
37
+ *
38
+ * @param runId - agentLoopRuns.id
39
+ * @param maxTokens - Maximum tokens for the context output (default 4000)
40
+ * @param options - Strategy options
41
+ */
42
+ async buildStepContext(
43
+ runId: string | number,
44
+ maxTokens?: number,
45
+ options?: {
46
+ strategy?: 'last_n' | 'all';
47
+ includeToolResults?: boolean;
48
+ includeStepOutputs?: boolean;
49
+ },
50
+ ): Promise<string> {
51
+ const effectiveMaxTokens = maxTokens || 4000;
52
+
53
+ let steps: any[];
54
+ try {
55
+ const repo = this.db.getRepository('agentLoopSteps');
56
+ if (!repo) return '';
57
+ steps = await repo.find({
58
+ filter: { runId },
59
+ sort: ['index', 'createdAt'],
60
+ pageSize: 500,
61
+ });
62
+ } catch {
63
+ return '';
64
+ }
65
+
66
+ if (!steps || steps.length === 0) return '';
67
+
68
+ const completedSteps = steps.filter((s: any) => s.status === 'succeeded' || s.status === 'failed');
69
+
70
+ if (completedSteps.length === 0) return '';
71
+
72
+ const config = {
73
+ strategy: options?.strategy || 'all',
74
+ includeToolResults: options?.includeToolResults ?? false,
75
+ includeStepOutputs: options?.includeStepOutputs ?? true,
76
+ };
77
+
78
+ let candidates = completedSteps;
79
+ if (config.strategy === 'last_n') {
80
+ // Take last 10 completed steps
81
+ const n = Math.min(10, completedSteps.length);
82
+ candidates = completedSteps.slice(-n);
83
+ }
84
+
85
+ const parts: string[] = [];
86
+
87
+ for (const step of candidates) {
88
+ const key = step.planKey || `step_${step.index || 0}`;
89
+ const type = step.type || 'unknown';
90
+ const target = step.target || '';
91
+ const title = step.title || key;
92
+ const status = step.status || 'unknown';
93
+
94
+ const lines: string[] = [];
95
+ lines.push(`<step key="${key}" type="${type}" target="${target}" status="${status}">`);
96
+ lines.push(` <title>${this.escapeXml(title)}</title>`);
97
+
98
+ if (step.description) {
99
+ lines.push(` <description>${this.escapeXml(trimText(step.description, 500))}</description>`);
100
+ }
101
+
102
+ if (config.includeStepOutputs && step.output) {
103
+ const outputStr = typeof step.output === 'string' ? step.output : this.safeStringify(step.output);
104
+ lines.push(` <output>${this.escapeXml(trimText(outputStr, 2000))}</output>`);
105
+ }
106
+
107
+ if (config.includeToolResults && step.metadata?.toolResults) {
108
+ const toolStr = this.safeStringify(step.metadata.toolResults);
109
+ lines.push(` <tool_results>${this.escapeXml(trimText(toolStr, 1500))}</tool_results>`);
110
+ }
111
+
112
+ if (step.error) {
113
+ lines.push(` <error>${this.escapeXml(trimText(step.error, 1000))}</error>`);
114
+ }
115
+
116
+ lines.push('</step>');
117
+ parts.push(lines.join('\n'));
118
+ }
119
+
120
+ let context = `<previous_steps>\n${parts.join('\n\n')}\n</previous_steps>`;
121
+
122
+ // Truncate if exceeding token limit
123
+ if (estimateTokens(context) > effectiveMaxTokens) {
124
+ context = this.truncateToTokenLimit(context, effectiveMaxTokens);
125
+ }
126
+
127
+ return context;
128
+ }
129
+
130
+ /**
131
+ * Enrich a base system prompt with step context from the run.
132
+ * Fetches the run from DB to access policy settings (maxContextTokens, etc.).
133
+ *
134
+ * @param basePrompt - The original system prompt to enrich
135
+ * @param runId - agentLoopRuns.id
136
+ * @param _stepId - agentLoopSteps.id (reserved for future per-step context)
137
+ */
138
+ async enrichSystemPrompt(
139
+ basePrompt: string,
140
+ runId: string | number,
141
+ _stepId?: string | number,
142
+ options?: {
143
+ maxContextTokens?: number;
144
+ contextSummaryStrategy?: 'last_n' | 'all';
145
+ includeToolResults?: boolean;
146
+ includeStepOutputs?: boolean;
147
+ },
148
+ ): Promise<string> {
149
+ // Fetch run from DB to get policy settings
150
+ let run: any;
151
+ try {
152
+ const repo = this.db.getRepository('agentLoopRuns');
153
+ if (!repo) return basePrompt;
154
+ run = await repo.findOne({ filter: { id: runId } });
155
+ if (!run) return basePrompt;
156
+ } catch {
157
+ return basePrompt;
158
+ }
159
+
160
+ const policy = (run.policy || {}) as AgentLoopPolicy;
161
+ const maxCtxTokens = options?.maxContextTokens ?? policy.maxContextTokens ?? 4000;
162
+ const strategy = options?.contextSummaryStrategy ?? policy.contextSummaryStrategy ?? 'all';
163
+ const includeToolResults = options?.includeToolResults ?? policy.includeToolResults ?? false;
164
+ const includeStepOutputs = options?.includeStepOutputs ?? policy.includeStepOutputs ?? true;
165
+
166
+ const stepContext = await this.buildStepContext(runId, maxCtxTokens, {
167
+ strategy,
168
+ includeToolResults,
169
+ includeStepOutputs,
170
+ });
171
+
172
+ if (!stepContext) return basePrompt;
173
+
174
+ return `${basePrompt}\n\n<previous_steps_context>\n${stepContext}\n</previous_steps_context>`;
175
+ }
176
+
177
+ private truncateToTokenLimit(text: string, maxTokens: number): string {
178
+ // Simple truncation: remove step details until under limit
179
+ // Strategy: keep first and last steps, remove middle ones
180
+ const outerMatch = text.match(/<previous_steps>\n([\s\S]*)\n<\/previous_steps>/);
181
+ if (!outerMatch) return text;
182
+
183
+ const inner = outerMatch[1];
184
+ const stepBlocks = this.splitStepBlocks(inner);
185
+
186
+ if (stepBlocks.length <= 2) {
187
+ // Just truncate text
188
+ const maxChars = maxTokens * 4;
189
+ if (text.length <= maxChars) return text;
190
+ return text.slice(0, maxChars) + '\n...[truncated]\n</previous_steps>';
191
+ }
192
+
193
+ // Keep first 2 and last 2 steps
194
+ const keepFirst = stepBlocks.slice(0, 2);
195
+ const keepLast = stepBlocks.slice(-2);
196
+ const removed = stepBlocks.length - keepFirst.length - keepLast.length;
197
+
198
+ const rebuilt = [
199
+ '<previous_steps>',
200
+ ...keepFirst,
201
+ ` <!-- ... ${removed} intermediate step(s) omitted due to context limit ... -->`,
202
+ ...keepLast,
203
+ '</previous_steps>',
204
+ ].join('\n');
205
+
206
+ // If still over limit, do a simple text truncation
207
+ const maxChars = maxTokens * 4;
208
+ if (rebuilt.length <= maxChars) return rebuilt;
209
+
210
+ return rebuilt.slice(0, maxChars) + '\n...[truncated]\n</previous_steps>';
211
+ }
212
+
213
+ private splitStepBlocks(text: string): string[] {
214
+ const blocks: string[] = [];
215
+ const regex = /<step[\s\S]*?<\/step>/g;
216
+ let match;
217
+ while ((match = regex.exec(text)) !== null) {
218
+ blocks.push(match[0]);
219
+ }
220
+ return blocks;
221
+ }
222
+
223
+ private escapeXml(value: string): string {
224
+ return value
225
+ .replace(/&/g, '&amp;')
226
+ .replace(/</g, '&lt;')
227
+ .replace(/>/g, '&gt;')
228
+ .replace(/"/g, '&quot;')
229
+ .replace(/'/g, '&apos;');
230
+ }
231
+
232
+ private safeStringify(value: any): string {
233
+ try {
234
+ return JSON.stringify(value, null, 2);
235
+ } catch {
236
+ return String(value);
237
+ }
238
+ }
239
+ }
@@ -1,3 +1,5 @@
1
+ import { toPlain } from '../utils/ctx-utils';
2
+
1
3
  export const ORCHESTRATOR_TRACE_CONTEXT_KEY = '__orchestratorTraceContext';
2
4
 
3
5
  export type OrchestratorTraceContext = {
@@ -33,10 +35,6 @@ type SpanValues = {
33
35
  userId?: string | number;
34
36
  };
35
37
 
36
- function toPlain(record: any) {
37
- return record?.toJSON?.() || record;
38
- }
39
-
40
38
  export class ExecutionSpanService {
41
39
  constructor(private readonly plugin: any) {}
42
40