plugin-agent-orchestrator 1.0.17 → 1.0.19
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/AIEmployeeSelect.d.ts +11 -0
- package/dist/client/AIEmployeesContext.d.ts +30 -0
- package/dist/client/AgentRunsTab.d.ts +2 -0
- package/dist/client/HarnessProfilesTab.d.ts +2 -0
- package/dist/client/OrchestratorSettings.d.ts +3 -0
- package/dist/client/RulesTab.d.ts +2 -0
- package/dist/client/TracingTab.d.ts +2 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client/plugin.d.ts +6 -0
- package/dist/client/skill-hub/components/ExecutionHistory.d.ts +2 -0
- package/dist/client/skill-hub/components/ExecutionProgress.d.ts +20 -0
- package/dist/client/skill-hub/components/GitSkillImport.d.ts +7 -0
- package/dist/client/skill-hub/components/LoopSettings.d.ts +2 -0
- package/dist/client/skill-hub/components/SkillEditor.d.ts +7 -0
- package/dist/client/skill-hub/components/SkillManager.d.ts +2 -0
- package/dist/client/skill-hub/components/SkillMetrics.d.ts +2 -0
- package/dist/client/skill-hub/components/SkillTestPanel.d.ts +7 -0
- package/dist/client/skill-hub/index.d.ts +11 -0
- package/dist/client/skill-hub/locale.d.ts +3 -0
- package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +6 -0
- package/dist/client/skill-hub/tools/SkillHubCard.d.ts +3 -0
- package/dist/client/skill-hub/tools/loopTemplates.d.ts +22 -0
- package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +1 -0
- package/dist/client/skill-hub/utils/jsonFields.d.ts +3 -0
- package/dist/client/tools/PlanApprovalCard.d.ts +3 -0
- package/dist/client/tools/registerOrchestratorCards.d.ts +1 -0
- package/dist/externalVersion.js +6 -6
- package/dist/index.d.ts +2 -0
- package/dist/server/collections/agent-execution-spans.d.ts +9 -0
- package/dist/server/collections/agent-harness-profiles.d.ts +2 -0
- package/dist/server/collections/agent-harness-profiles.js +89 -0
- package/dist/server/collections/agent-loop-events.d.ts +2 -0
- package/dist/server/collections/agent-loop-events.js +101 -0
- package/dist/server/collections/agent-loop-runs.d.ts +2 -0
- package/dist/server/collections/agent-loop-runs.js +188 -0
- package/dist/server/collections/agent-loop-steps.d.ts +2 -0
- package/dist/server/collections/agent-loop-steps.js +174 -0
- package/dist/server/collections/orchestrator-config.d.ts +2 -0
- package/dist/server/collections/orchestrator-config.js +7 -0
- package/dist/server/collections/orchestrator-logs.d.ts +8 -0
- package/dist/server/collections/skill-definitions.d.ts +3 -0
- package/dist/server/collections/skill-executions.d.ts +3 -0
- package/dist/server/collections/skill-executions.js +12 -0
- package/dist/server/collections/skill-loop-configs.d.ts +3 -0
- package/dist/server/collections/skill-loop-configs.js +94 -0
- package/dist/server/collections/skill-worker-configs.d.ts +3 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +4 -0
- package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +4 -0
- package/dist/server/migrations/20260427000000-add-tracing-detail-fields.d.ts +7 -0
- package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +4 -0
- package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +4 -0
- package/dist/server/migrations/20260429000000-add-llm-fields.d.ts +7 -0
- package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +16 -0
- package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +7 -0
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +7 -0
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.js +55 -0
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +12 -0
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +162 -0
- package/dist/server/plugin.d.ts +16 -0
- package/dist/server/plugin.js +13 -0
- package/dist/server/resources/agent-loop.d.ts +3 -0
- package/dist/server/resources/agent-loop.js +205 -0
- package/dist/server/resources/tracing.d.ts +7 -0
- package/dist/server/services/AgentHarness.d.ts +42 -0
- package/dist/server/services/AgentHarness.js +565 -0
- package/dist/server/services/AgentLoopController.d.ts +205 -0
- package/dist/server/services/AgentLoopController.js +940 -0
- package/dist/server/services/AgentLoopRepository.d.ts +20 -0
- package/dist/server/services/AgentLoopRepository.js +210 -0
- package/dist/server/services/AgentLoopService.d.ts +149 -0
- package/dist/server/services/AgentLoopService.js +133 -0
- package/dist/server/services/AgentPlanValidator.d.ts +4 -0
- package/dist/server/services/AgentPlanValidator.js +99 -0
- package/dist/server/services/AgentPlannerService.d.ts +8 -0
- package/dist/server/services/AgentPlannerService.js +119 -0
- package/dist/server/services/AgentRegistryService.d.ts +13 -0
- package/dist/server/services/AgentRegistryService.js +178 -0
- package/dist/server/services/CodeValidator.d.ts +32 -0
- package/dist/server/services/ExecutionSpanService.d.ts +46 -0
- package/dist/server/services/FileManager.d.ts +28 -0
- package/dist/server/services/SandboxRunner.d.ts +41 -0
- package/dist/server/services/SkillManager.d.ts +6 -0
- package/dist/server/services/SkillRepositoryService.d.ts +22 -0
- package/dist/server/services/WorkerEnvManager.d.ts +26 -0
- package/dist/server/skill-hub/actions/git-import.d.ts +21 -0
- package/dist/server/skill-hub/mcp/McpController.d.ts +15 -0
- package/dist/server/skill-hub/plugin.d.ts +61 -0
- package/dist/server/skill-hub/plugin.js +152 -54
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +16 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +15 -0
- package/dist/server/skill-hub/utils/json-fields.d.ts +7 -0
- package/dist/server/tools/agent-loop.d.ts +235 -0
- package/dist/server/tools/agent-loop.js +406 -0
- package/dist/server/tools/delegate-task.d.ts +19 -0
- package/dist/server/tools/delegate-task.js +19 -368
- package/dist/server/tools/external-rag-search.d.ts +42 -0
- package/dist/server/tools/orchestrator-plan.d.ts +205 -0
- package/dist/server/tools/orchestrator-plan.js +291 -0
- package/dist/server/tools/skill-execute.d.ts +36 -0
- package/dist/server/tools/skill-execute.js +2 -0
- package/package.json +1 -1
- package/src/client/AgentRunsTab.tsx +764 -0
- package/src/client/HarnessProfilesTab.tsx +247 -0
- package/src/client/OrchestratorSettings.tsx +40 -2
- package/src/client/RulesTab.tsx +103 -6
- package/src/client/plugin.tsx +27 -54
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -0
- package/src/client/skill-hub/index.tsx +51 -75
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +56 -16
- package/src/client/skill-hub/tools/SkillHubCard.tsx +35 -4
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -0
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -0
- package/src/client/tools/PlanApprovalCard.tsx +175 -0
- package/src/client/tools/registerOrchestratorCards.ts +7 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -0
- package/src/server/collections/agent-loop-events.ts +71 -0
- package/src/server/collections/agent-loop-runs.ts +158 -0
- package/src/server/collections/agent-loop-steps.ts +144 -0
- package/src/server/collections/orchestrator-config.ts +7 -0
- package/src/server/collections/skill-executions.ts +63 -51
- package/src/server/collections/skill-loop-configs.ts +65 -0
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -0
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -0
- package/src/server/plugin.ts +15 -0
- package/src/server/resources/agent-loop.ts +183 -0
- package/src/server/services/AgentHarness.ts +663 -0
- package/src/server/services/AgentLoopController.ts +1128 -0
- package/src/server/services/AgentLoopRepository.ts +194 -0
- package/src/server/services/AgentLoopService.ts +161 -0
- package/src/server/services/AgentPlanValidator.ts +73 -0
- package/src/server/services/AgentPlannerService.ts +93 -0
- package/src/server/services/AgentRegistryService.ts +169 -0
- package/src/server/services/ExecutionSpanService.ts +2 -0
- package/src/server/skill-hub/plugin.ts +897 -771
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +15 -0
- package/src/server/tools/agent-loop.ts +399 -0
- package/src/server/tools/delegate-task.ts +23 -485
- package/src/server/tools/orchestrator-plan.ts +279 -0
- package/src/server/tools/skill-execute.ts +68 -64
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import { AgentRegistryService } from './AgentRegistryService';
|
|
2
|
+
import { AgentPlannerService } from './AgentPlannerService';
|
|
3
|
+
import { AgentPlanValidator } from './AgentPlanValidator';
|
|
4
|
+
import { AgentLoopRepository } from './AgentLoopRepository';
|
|
5
|
+
import { AgentHarness } from './AgentHarness';
|
|
6
|
+
import { AgentLoopPolicy, AgentLoopPlanStepInput, AgentLoopRunStatus, AgentLoopStepStatus } from './AgentLoopService';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_POLICY: AgentLoopPolicy = {
|
|
10
|
+
maxIterations: 20,
|
|
11
|
+
maxStepAttempts: 2,
|
|
12
|
+
allowReplan: true,
|
|
13
|
+
requireVerification: true,
|
|
14
|
+
stopOnApprovalRequired: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TERMINAL_RUN_STATUSES = new Set<AgentLoopRunStatus>(['succeeded', 'failed', 'rejected', 'canceled']);
|
|
18
|
+
const TERMINAL_STEP_STATUSES = new Set<AgentLoopStepStatus>(['succeeded', 'skipped']);
|
|
19
|
+
const ORCHESTRATOR_CONTROLLER_MAX_STEPS = 100;
|
|
20
|
+
|
|
21
|
+
function now() {
|
|
22
|
+
return new Date();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createRootRunId(seed = '') {
|
|
26
|
+
const hash = createHash('sha1').update(`${Date.now()}::${Math.random()}::${seed}`).digest('hex').slice(0, 10);
|
|
27
|
+
return `loop_${Date.now()}_${hash}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePolicy(policy?: Partial<AgentLoopPolicy>): AgentLoopPolicy {
|
|
31
|
+
const next = { ...DEFAULT_POLICY, ...(policy || {}) };
|
|
32
|
+
next.maxIterations = Math.max(1, Number(next.maxIterations || DEFAULT_POLICY.maxIterations));
|
|
33
|
+
next.maxStepAttempts = Math.max(1, Number(next.maxStepAttempts || DEFAULT_POLICY.maxStepAttempts));
|
|
34
|
+
next.allowReplan = next.allowReplan !== false;
|
|
35
|
+
next.requireVerification = next.requireVerification !== false;
|
|
36
|
+
next.stopOnApprovalRequired = next.stopOnApprovalRequired !== false;
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
|
|
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
|
+
export class AgentLoopController {
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly registryService: AgentRegistryService,
|
|
82
|
+
private readonly plannerService: AgentPlannerService,
|
|
83
|
+
private readonly validator: AgentPlanValidator,
|
|
84
|
+
private readonly repository: AgentLoopRepository,
|
|
85
|
+
private readonly harness: AgentHarness
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async createRun(options: {
|
|
89
|
+
goal: string;
|
|
90
|
+
leaderUsername?: string;
|
|
91
|
+
sessionId?: string;
|
|
92
|
+
messageId?: string;
|
|
93
|
+
userId?: string | number;
|
|
94
|
+
policy?: Partial<AgentLoopPolicy>;
|
|
95
|
+
metadata?: any;
|
|
96
|
+
plan?: AgentLoopPlanStepInput[];
|
|
97
|
+
}) {
|
|
98
|
+
const goal = String(options.goal || '').trim();
|
|
99
|
+
if (!goal) {
|
|
100
|
+
throw new Error('Agent loop goal is required.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const policy = normalizePolicy(options.policy);
|
|
104
|
+
const rootRunId = createRootRunId(options.leaderUsername || goal);
|
|
105
|
+
const run = await this.repository.createRun({
|
|
106
|
+
rootRunId,
|
|
107
|
+
sessionId: options.sessionId,
|
|
108
|
+
messageId: options.messageId,
|
|
109
|
+
leaderUsername: options.leaderUsername,
|
|
110
|
+
goal,
|
|
111
|
+
status: 'planning',
|
|
112
|
+
policy,
|
|
113
|
+
iterationCount: 0,
|
|
114
|
+
metadata: options.metadata || {},
|
|
115
|
+
userId: options.userId,
|
|
116
|
+
startedAt: now(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await this.repository.createEvent({
|
|
120
|
+
runId: run.id,
|
|
121
|
+
type: 'created',
|
|
122
|
+
title: 'Agent loop created',
|
|
123
|
+
content: goal,
|
|
124
|
+
status: 'planning',
|
|
125
|
+
userId: options.userId,
|
|
126
|
+
payload: { rootRunId },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(options.plan) && options.plan.length > 0) {
|
|
130
|
+
this.validator.validate(options.plan);
|
|
131
|
+
await this.replacePlan(run.id, options.plan, {
|
|
132
|
+
userId: options.userId,
|
|
133
|
+
mode: 'append',
|
|
134
|
+
markRunning: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.getRunSnapshot(run.id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async planGoal(options: {
|
|
142
|
+
goal: string;
|
|
143
|
+
leaderUsername?: string;
|
|
144
|
+
sessionId?: string;
|
|
145
|
+
messageId?: string;
|
|
146
|
+
userId?: string | number;
|
|
147
|
+
policy?: Partial<AgentLoopPolicy>;
|
|
148
|
+
metadata?: any;
|
|
149
|
+
plan?: AgentLoopPlanStepInput[];
|
|
150
|
+
planSource?: string;
|
|
151
|
+
plannerModel?: string;
|
|
152
|
+
harnessTag?: string;
|
|
153
|
+
targetAgent?: string;
|
|
154
|
+
runId?: string | number;
|
|
155
|
+
}) {
|
|
156
|
+
const plan = this.plannerService.buildPlan(options.goal, options.plan, options);
|
|
157
|
+
this.validator.validate(plan);
|
|
158
|
+
|
|
159
|
+
const harnessTag = String(options.harnessTag || options.metadata?.harnessTag || 'default').trim() || 'default';
|
|
160
|
+
const harnessProfile = await this.registryService.getHarnessProfile(harnessTag);
|
|
161
|
+
const harnessSettings = asObject(harnessProfile?.settings);
|
|
162
|
+
|
|
163
|
+
if (options.runId) {
|
|
164
|
+
return this.revisePlanGoal(options.runId, plan, {
|
|
165
|
+
goal: options.goal,
|
|
166
|
+
userId: options.userId,
|
|
167
|
+
metadata: options.metadata,
|
|
168
|
+
planSource: options.planSource || (Array.isArray(options.plan) && options.plan.length ? 'provided' : 'template'),
|
|
169
|
+
plannerModel: options.plannerModel,
|
|
170
|
+
harnessTag,
|
|
171
|
+
harnessProfileId: harnessProfile?.id,
|
|
172
|
+
harnessSettings,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const snapshot = await this.createRun({
|
|
177
|
+
goal: options.goal,
|
|
178
|
+
leaderUsername: options.leaderUsername,
|
|
179
|
+
sessionId: options.sessionId,
|
|
180
|
+
messageId: options.messageId,
|
|
181
|
+
userId: options.userId,
|
|
182
|
+
policy: options.policy,
|
|
183
|
+
metadata: {
|
|
184
|
+
...asObject(options.metadata),
|
|
185
|
+
harnessTag,
|
|
186
|
+
harnessProfileId: harnessProfile?.id,
|
|
187
|
+
harnessSettings,
|
|
188
|
+
approvalMode: 'plan_first',
|
|
189
|
+
},
|
|
190
|
+
plan: [],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const runId = snapshot.run.id;
|
|
194
|
+
await this.replacePlan(runId, plan, {
|
|
195
|
+
userId: options.userId,
|
|
196
|
+
mode: 'append',
|
|
197
|
+
markRunning: false,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await this.repository.updateRun(runId, {
|
|
201
|
+
status: 'waiting_plan_approval',
|
|
202
|
+
approvalStatus: 'pending',
|
|
203
|
+
planVersion: 1,
|
|
204
|
+
planSource: options.planSource || (Array.isArray(options.plan) && options.plan.length ? 'provided' : 'template'),
|
|
205
|
+
plannerModel: options.plannerModel || '',
|
|
206
|
+
updatedAt: now(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await this.repository.createEvent({
|
|
210
|
+
runId,
|
|
211
|
+
type: 'plan_approval_requested',
|
|
212
|
+
title: 'Plan approval requested',
|
|
213
|
+
content: `Waiting for user approval before executing ${plan.length} step(s).`,
|
|
214
|
+
status: 'waiting_plan_approval',
|
|
215
|
+
userId: options.userId,
|
|
216
|
+
payload: {
|
|
217
|
+
planVersion: 1,
|
|
218
|
+
harnessTag,
|
|
219
|
+
steps: plan.map((step, index) => ({
|
|
220
|
+
planKey: normalizePlanKey(step, index),
|
|
221
|
+
title: step.title || `Step ${index + 1}`,
|
|
222
|
+
type: normalizeStepType(step.type),
|
|
223
|
+
target: step.target || '',
|
|
224
|
+
dependsOn: asArray(step.dependsOn).map(String),
|
|
225
|
+
})),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return this.getRunDetail(runId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async revisePlanGoal(
|
|
233
|
+
runId: string | number,
|
|
234
|
+
plan: AgentLoopPlanStepInput[],
|
|
235
|
+
options: {
|
|
236
|
+
goal?: string;
|
|
237
|
+
userId?: string | number;
|
|
238
|
+
metadata?: any;
|
|
239
|
+
planSource?: string;
|
|
240
|
+
plannerModel?: string;
|
|
241
|
+
harnessTag?: string;
|
|
242
|
+
harnessProfileId?: string | number;
|
|
243
|
+
harnessSettings?: any;
|
|
244
|
+
} = {}
|
|
245
|
+
) {
|
|
246
|
+
this.validator.validate(plan);
|
|
247
|
+
const run = await this.repository.requireRun(runId);
|
|
248
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
249
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
250
|
+
}
|
|
251
|
+
if (!['waiting_plan_approval', 'needs_replan'].includes(run.status)) {
|
|
252
|
+
throw new Error(`Run ${run.id} is not waiting for plan revision.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await this.replacePlan(run.id, plan, {
|
|
256
|
+
userId: options.userId,
|
|
257
|
+
mode: 'replace_pending',
|
|
258
|
+
reason: 'Plan revised',
|
|
259
|
+
markRunning: false,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const nextPlanVersion = Number(run.planVersion || 1) + 1;
|
|
263
|
+
await this.repository.updateRun(run.id, {
|
|
264
|
+
goal: options.goal || run.goal,
|
|
265
|
+
status: 'waiting_plan_approval',
|
|
266
|
+
approvalStatus: 'pending',
|
|
267
|
+
planVersion: nextPlanVersion,
|
|
268
|
+
planSource: options.planSource || run.planSource || 'provided',
|
|
269
|
+
plannerModel: options.plannerModel || run.plannerModel || '',
|
|
270
|
+
rejectionReason: '',
|
|
271
|
+
changeRequest: '',
|
|
272
|
+
metadata: {
|
|
273
|
+
...asObject(run.metadata),
|
|
274
|
+
...asObject(options.metadata),
|
|
275
|
+
harnessTag: options.harnessTag || asObject(run.metadata).harnessTag || 'default',
|
|
276
|
+
harnessProfileId: options.harnessProfileId || asObject(run.metadata).harnessProfileId,
|
|
277
|
+
harnessSettings: asObject(options.harnessSettings || asObject(run.metadata).harnessSettings),
|
|
278
|
+
approvalMode: 'plan_first',
|
|
279
|
+
},
|
|
280
|
+
updatedAt: now(),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await this.repository.createEvent({
|
|
284
|
+
runId: run.id,
|
|
285
|
+
type: 'plan_revision_requested',
|
|
286
|
+
title: 'Plan revised for approval',
|
|
287
|
+
content: `Waiting for approval of plan version ${nextPlanVersion}.`,
|
|
288
|
+
status: 'waiting_plan_approval',
|
|
289
|
+
userId: options.userId,
|
|
290
|
+
payload: {
|
|
291
|
+
planVersion: nextPlanVersion,
|
|
292
|
+
steps: plan.map((step, index) => normalizePlanKey(step, index)),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return this.getRunDetail(run.id);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async approvePlanAndExecute(
|
|
300
|
+
runId: string | number,
|
|
301
|
+
options: { userId?: string | number; ctx?: any; reason?: string } = {}
|
|
302
|
+
) {
|
|
303
|
+
const run = await this.repository.requireRun(runId);
|
|
304
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
305
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
306
|
+
}
|
|
307
|
+
if (!['waiting_plan_approval', 'approved'].includes(run.status)) {
|
|
308
|
+
throw new Error(`Run ${run.id} is not waiting for plan approval.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await this.repository.updateRun(run.id, {
|
|
312
|
+
status: 'approved',
|
|
313
|
+
approvalStatus: 'approved',
|
|
314
|
+
approvedById: options.userId,
|
|
315
|
+
approvedAt: now(),
|
|
316
|
+
rejectionReason: '',
|
|
317
|
+
changeRequest: '',
|
|
318
|
+
updatedAt: now(),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await this.repository.createEvent({
|
|
322
|
+
runId: run.id,
|
|
323
|
+
type: 'plan_approved',
|
|
324
|
+
title: 'Plan approved',
|
|
325
|
+
content: options.reason || '',
|
|
326
|
+
status: 'approved',
|
|
327
|
+
userId: options.userId,
|
|
328
|
+
payload: { planVersion: run.planVersion || 1 },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return this.executeApprovedPlan(run.id, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async rejectPlan(runId: string | number, options: { userId?: string | number; reason?: string } = {}) {
|
|
335
|
+
const run = await this.repository.requireRun(runId);
|
|
336
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
337
|
+
return this.getRunSnapshot(run.id);
|
|
338
|
+
}
|
|
339
|
+
if (!['waiting_plan_approval', 'approved', 'needs_replan'].includes(run.status)) {
|
|
340
|
+
throw new Error(`Run ${run.id} is not waiting for plan approval.`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const steps = await this.repository.getSteps(run.id);
|
|
344
|
+
for (const step of steps) {
|
|
345
|
+
if (!TERMINAL_STEP_STATUSES.has(step.status) && step.status !== 'failed') {
|
|
346
|
+
await this.repository.updateStep(step.id, {
|
|
347
|
+
status: 'skipped',
|
|
348
|
+
error: options.reason || 'Plan rejected by user.',
|
|
349
|
+
endedAt: now(),
|
|
350
|
+
updatedAt: now(),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await this.repository.updateRun(run.id, {
|
|
356
|
+
status: 'rejected',
|
|
357
|
+
approvalStatus: 'rejected',
|
|
358
|
+
rejectionReason: options.reason || '',
|
|
359
|
+
currentStepId: null,
|
|
360
|
+
endedAt: now(),
|
|
361
|
+
updatedAt: now(),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await this.repository.createEvent({
|
|
365
|
+
runId: run.id,
|
|
366
|
+
type: 'plan_rejected',
|
|
367
|
+
title: 'Plan rejected',
|
|
368
|
+
content: options.reason || '',
|
|
369
|
+
status: 'rejected',
|
|
370
|
+
userId: options.userId,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return this.getRunSnapshot(run.id);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async requestPlanChanges(runId: string | number, options: { userId?: string | number; feedback?: string } = {}) {
|
|
377
|
+
const run = await this.repository.requireRun(runId);
|
|
378
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
379
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
380
|
+
}
|
|
381
|
+
if (!['waiting_plan_approval', 'needs_replan'].includes(run.status)) {
|
|
382
|
+
throw new Error(`Run ${run.id} is not waiting for plan changes.`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await this.repository.updateRun(run.id, {
|
|
386
|
+
status: 'needs_replan',
|
|
387
|
+
approvalStatus: 'changes_requested',
|
|
388
|
+
changeRequest: options.feedback || '',
|
|
389
|
+
updatedAt: now(),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await this.repository.createEvent({
|
|
393
|
+
runId: run.id,
|
|
394
|
+
type: 'plan_changes_requested',
|
|
395
|
+
title: 'Plan changes requested',
|
|
396
|
+
content: options.feedback || '',
|
|
397
|
+
status: 'needs_replan',
|
|
398
|
+
userId: options.userId,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return this.getRunDetail(run.id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async replacePlan(
|
|
405
|
+
runId: string | number,
|
|
406
|
+
plan: AgentLoopPlanStepInput[],
|
|
407
|
+
options: {
|
|
408
|
+
userId?: string | number;
|
|
409
|
+
mode?: 'append' | 'replace_pending';
|
|
410
|
+
reason?: string;
|
|
411
|
+
markRunning?: boolean;
|
|
412
|
+
} = {}
|
|
413
|
+
) {
|
|
414
|
+
if (!Array.isArray(plan) || plan.length === 0) {
|
|
415
|
+
throw new Error('Plan must include at least one step.');
|
|
416
|
+
}
|
|
417
|
+
this.validator.validate(plan);
|
|
418
|
+
|
|
419
|
+
const run = await this.repository.requireRun(runId);
|
|
420
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
421
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const policy = normalizePolicy(run.policy);
|
|
425
|
+
|
|
426
|
+
if (options.mode === 'replace_pending') {
|
|
427
|
+
const existing = await this.repository.getSteps(run.id);
|
|
428
|
+
for (const step of existing) {
|
|
429
|
+
if (!TERMINAL_STEP_STATUSES.has(step.status) && step.status !== 'running') {
|
|
430
|
+
await this.repository.updateStep(step.id, {
|
|
431
|
+
status: 'skipped',
|
|
432
|
+
error: options.reason || 'Replanned',
|
|
433
|
+
endedAt: now(),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const existingSteps = await this.repository.getSteps(run.id);
|
|
440
|
+
const indexStart = existingSteps.reduce((max, step) => Math.max(max, Number(step.index || 0)), -1) + 1;
|
|
441
|
+
const createdSteps = [];
|
|
442
|
+
|
|
443
|
+
for (let i = 0; i < plan.length; i++) {
|
|
444
|
+
const step = plan[i] || {};
|
|
445
|
+
const created = await this.repository.createStep({
|
|
446
|
+
runId: run.id,
|
|
447
|
+
parentStepId: step.parentStepId,
|
|
448
|
+
planKey: normalizePlanKey(step, i),
|
|
449
|
+
index: indexStart + i,
|
|
450
|
+
title: step.title || `Step ${indexStart + i + 1}`,
|
|
451
|
+
description: step.description || '',
|
|
452
|
+
type: normalizeStepType(step.type),
|
|
453
|
+
target: step.target || '',
|
|
454
|
+
input: step.input || {},
|
|
455
|
+
output: {},
|
|
456
|
+
status: 'pending',
|
|
457
|
+
attempt: 0,
|
|
458
|
+
maxAttempts: step.maxAttempts || policy.maxStepAttempts,
|
|
459
|
+
dependsOn: asArray(step.dependsOn).map(String),
|
|
460
|
+
dependencyPolicy: step.dependencyPolicy || step.metadata?.dependencyPolicy || 'require_success',
|
|
461
|
+
metadata: asObject(step.metadata),
|
|
462
|
+
});
|
|
463
|
+
createdSteps.push(created);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const nextStatus = options.markRunning === false ? run.status : 'running';
|
|
467
|
+
await this.repository.updateRun(run.id, {
|
|
468
|
+
status: nextStatus,
|
|
469
|
+
updatedAt: now(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await this.repository.createEvent({
|
|
473
|
+
runId: run.id,
|
|
474
|
+
type: options.reason ? 'replanned' : 'planned',
|
|
475
|
+
title: options.reason ? 'Plan updated' : 'Plan created',
|
|
476
|
+
content: options.reason || `Created ${createdSteps.length} step(s).`,
|
|
477
|
+
status: nextStatus,
|
|
478
|
+
userId: options.userId,
|
|
479
|
+
payload: { mode: options.mode || 'append', steps: createdSteps.map((step) => step.planKey) },
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return createdSteps;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async replan(
|
|
486
|
+
runId: string | number,
|
|
487
|
+
plan: AgentLoopPlanStepInput[],
|
|
488
|
+
options: { reason?: string; mode?: 'append' | 'replace_pending'; userId?: string | number } = {}
|
|
489
|
+
) {
|
|
490
|
+
if (!Array.isArray(plan) || plan.length === 0) {
|
|
491
|
+
throw new Error('Plan must include at least one step.');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const run = await this.repository.requireRun(runId);
|
|
495
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
496
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const policy = normalizePolicy(run.policy);
|
|
500
|
+
if (!policy.allowReplan) {
|
|
501
|
+
throw new Error('Replanning is disabled for this run.');
|
|
502
|
+
}
|
|
503
|
+
if (Number(run.iterationCount || 0) >= policy.maxIterations) {
|
|
504
|
+
throw new Error(`Agent loop reached maxIterations=${policy.maxIterations}.`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await this.repository.updateRun(run.id, {
|
|
508
|
+
iterationCount: Number(run.iterationCount || 0) + 1,
|
|
509
|
+
status: 'running',
|
|
510
|
+
updatedAt: now(),
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return this.replacePlan(run.id, plan, {
|
|
514
|
+
mode: options.mode || 'replace_pending',
|
|
515
|
+
reason: options.reason || 'Replan requested',
|
|
516
|
+
userId: options.userId,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async startStep(
|
|
521
|
+
stepId: string | number,
|
|
522
|
+
options: { userId?: string | number; agentExecutionSpanId?: string | number } = {}
|
|
523
|
+
) {
|
|
524
|
+
const step = await this.repository.requireStep(stepId);
|
|
525
|
+
const run = await this.repository.requireRun(step.runId);
|
|
526
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
527
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (step.status !== 'pending' && step.status !== 'failed') {
|
|
531
|
+
throw new Error(`Step ${step.id} cannot start from status "${step.status}".`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const policy = normalizePolicy(run.policy);
|
|
535
|
+
if (Number(step.attempt || 0) >= Number(step.maxAttempts || policy.maxStepAttempts)) {
|
|
536
|
+
throw new Error(`Step ${step.id} reached maxAttempts=${step.maxAttempts}.`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const nextAttempt = Number(step.attempt || 0) + 1;
|
|
540
|
+
await this.repository.updateStep(step.id, {
|
|
541
|
+
status: 'running',
|
|
542
|
+
attempt: nextAttempt,
|
|
543
|
+
error: '',
|
|
544
|
+
agentExecutionSpanId: options.agentExecutionSpanId || step.agentExecutionSpanId,
|
|
545
|
+
startedAt: now(),
|
|
546
|
+
updatedAt: now(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await this.repository.updateRun(run.id, {
|
|
550
|
+
status: 'running',
|
|
551
|
+
currentStepId: step.id,
|
|
552
|
+
updatedAt: now(),
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await this.repository.createEvent({
|
|
556
|
+
runId: run.id,
|
|
557
|
+
stepId: step.id,
|
|
558
|
+
type: 'step_started',
|
|
559
|
+
title: `Started: ${step.title || step.planKey}`,
|
|
560
|
+
status: 'running',
|
|
561
|
+
userId: options.userId,
|
|
562
|
+
payload: { attempt: nextAttempt },
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (['skill', 'tool', 'sub_agent'].includes(step.type)) {
|
|
566
|
+
await this.repository.createEvent({
|
|
567
|
+
runId: run.id,
|
|
568
|
+
stepId: step.id,
|
|
569
|
+
type: 'tool_called',
|
|
570
|
+
title: `Calling ${step.type}: ${step.target || step.title || step.planKey}`,
|
|
571
|
+
status: 'running',
|
|
572
|
+
userId: options.userId,
|
|
573
|
+
payload: {
|
|
574
|
+
type: step.type,
|
|
575
|
+
target: step.target,
|
|
576
|
+
attempt: nextAttempt,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return this.getRunSnapshot(run.id);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async completeStep(
|
|
585
|
+
stepId: string | number,
|
|
586
|
+
output: any,
|
|
587
|
+
options: {
|
|
588
|
+
userId?: string | number;
|
|
589
|
+
skillExecutionId?: string | number;
|
|
590
|
+
agentExecutionSpanId?: string | number;
|
|
591
|
+
metadata?: any;
|
|
592
|
+
} = {}
|
|
593
|
+
) {
|
|
594
|
+
const step = await this.repository.requireStep(stepId);
|
|
595
|
+
const run = await this.repository.requireRun(step.runId);
|
|
596
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
597
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (step.status !== 'running') {
|
|
601
|
+
throw new Error(`Step ${step.id} cannot complete from status "${step.status}".`);
|
|
602
|
+
}
|
|
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
|
+
|
|
607
|
+
await this.repository.updateStep(step.id, {
|
|
608
|
+
status: 'succeeded',
|
|
609
|
+
output: output === undefined ? {} : output,
|
|
610
|
+
error: '',
|
|
611
|
+
skillExecutionId: options.skillExecutionId || step.skillExecutionId,
|
|
612
|
+
agentExecutionSpanId: options.agentExecutionSpanId || step.agentExecutionSpanId,
|
|
613
|
+
metadata: { ...asObject(step.metadata), ...asObject(options.metadata) },
|
|
614
|
+
endedAt: now(),
|
|
615
|
+
updatedAt: now(),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await this.repository.updateRun(run.id, {
|
|
619
|
+
status: 'running',
|
|
620
|
+
currentStepId: null,
|
|
621
|
+
updatedAt: now(),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
await this.repository.createEvent({
|
|
625
|
+
runId: run.id,
|
|
626
|
+
stepId: step.id,
|
|
627
|
+
type: 'step_succeeded',
|
|
628
|
+
title: `Completed: ${step.title || step.planKey}`,
|
|
629
|
+
content: trimText(output, 2000),
|
|
630
|
+
status: 'succeeded',
|
|
631
|
+
userId: options.userId,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return this.getRunSnapshot(run.id);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async failStep(stepId: string | number, error: string, options: { userId?: string | number; metadata?: any } = {}) {
|
|
638
|
+
const step = await this.repository.requireStep(stepId);
|
|
639
|
+
const run = await this.repository.requireRun(step.runId);
|
|
640
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
641
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (step.status !== 'running') {
|
|
645
|
+
throw new Error(`Step ${step.id} cannot fail from status "${step.status}".`);
|
|
646
|
+
}
|
|
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
|
+
|
|
651
|
+
const policy = normalizePolicy(run.policy);
|
|
652
|
+
|
|
653
|
+
await this.repository.updateStep(step.id, {
|
|
654
|
+
status: 'failed',
|
|
655
|
+
error: trimText(error, 10000),
|
|
656
|
+
metadata: { ...asObject(step.metadata), ...asObject(options.metadata) },
|
|
657
|
+
endedAt: now(),
|
|
658
|
+
updatedAt: now(),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await this.repository.updateRun(run.id, {
|
|
662
|
+
status: 'running',
|
|
663
|
+
currentStepId: null,
|
|
664
|
+
updatedAt: now(),
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
await this.repository.createEvent({
|
|
668
|
+
runId: run.id,
|
|
669
|
+
stepId: step.id,
|
|
670
|
+
type: 'step_failed',
|
|
671
|
+
title: `Failed: ${step.title || step.planKey}`,
|
|
672
|
+
content: error,
|
|
673
|
+
status: 'failed',
|
|
674
|
+
userId: options.userId,
|
|
675
|
+
payload: {
|
|
676
|
+
retryable: Number(step.attempt || 0) < Number(step.maxAttempts || policy.maxStepAttempts),
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
return this.getRunSnapshot(run.id);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async skipStep(stepId: string | number, reason = 'Skipped', options: { userId?: string | number} = {}) {
|
|
684
|
+
const step = await this.repository.requireStep(stepId);
|
|
685
|
+
const run = await this.repository.requireRun(step.runId);
|
|
686
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
687
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!['pending', 'running', 'failed'].includes(step.status)) {
|
|
691
|
+
throw new Error(`Step ${step.id} cannot skip from status "${step.status}".`);
|
|
692
|
+
}
|
|
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
|
+
|
|
699
|
+
await this.repository.updateStep(step.id, {
|
|
700
|
+
status: 'skipped',
|
|
701
|
+
error: reason,
|
|
702
|
+
endedAt: now(),
|
|
703
|
+
updatedAt: now(),
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await this.repository.createEvent({
|
|
707
|
+
runId: run.id,
|
|
708
|
+
stepId: step.id,
|
|
709
|
+
type: 'step_skipped',
|
|
710
|
+
title: `Skipped: ${step.title || step.planKey}`,
|
|
711
|
+
content: reason,
|
|
712
|
+
status: 'skipped',
|
|
713
|
+
userId: options.userId,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return this.getRunSnapshot(run.id);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async requestApproval(
|
|
720
|
+
stepId: string | number,
|
|
721
|
+
approval: any,
|
|
722
|
+
options: { userId?: string | number; reason?: string } = {}
|
|
723
|
+
) {
|
|
724
|
+
const step = await this.repository.requireStep(stepId);
|
|
725
|
+
const run = await this.repository.requireRun(step.runId);
|
|
726
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
727
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!['pending', 'running'].includes(step.status)) {
|
|
731
|
+
throw new Error(`Step ${step.id} cannot request approval for status "${step.status}".`);
|
|
732
|
+
}
|
|
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
|
+
|
|
739
|
+
await this.repository.updateStep(step.id, {
|
|
740
|
+
status: 'waiting_user',
|
|
741
|
+
approval: approval || {},
|
|
742
|
+
updatedAt: now(),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
await this.repository.updateRun(run.id, {
|
|
746
|
+
status: 'waiting_user',
|
|
747
|
+
currentStepId: step.id,
|
|
748
|
+
updatedAt: now(),
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
await this.repository.createEvent({
|
|
752
|
+
runId: run.id,
|
|
753
|
+
stepId: step.id,
|
|
754
|
+
type: 'approval_requested',
|
|
755
|
+
title: `Approval requested: ${step.title || step.planKey}`,
|
|
756
|
+
content: options.reason || approval?.prompt || '',
|
|
757
|
+
status: 'waiting_user',
|
|
758
|
+
userId: options.userId,
|
|
759
|
+
payload: approval || {},
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
return this.getRunSnapshot(run.id);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async resumeRun(
|
|
766
|
+
runId: string | number,
|
|
767
|
+
options: {
|
|
768
|
+
stepId?: string | number;
|
|
769
|
+
approved: boolean;
|
|
770
|
+
editedInput?: any;
|
|
771
|
+
userId?: string | number;
|
|
772
|
+
ctx?: any;
|
|
773
|
+
}
|
|
774
|
+
) {
|
|
775
|
+
const run = await this.repository.requireRun(runId);
|
|
776
|
+
const stepId = options.stepId || run.currentStepId;
|
|
777
|
+
if (!stepId) {
|
|
778
|
+
throw new Error('No waiting step found for this run.');
|
|
779
|
+
}
|
|
780
|
+
const step = await this.repository.requireStep(stepId);
|
|
781
|
+
if (String(step.runId) !== String(run.id)) {
|
|
782
|
+
throw new Error('Step does not belong to the run.');
|
|
783
|
+
}
|
|
784
|
+
if (run.status !== 'waiting_user' || step.status !== 'waiting_user') {
|
|
785
|
+
throw new Error('Run is not waiting for user approval.');
|
|
786
|
+
}
|
|
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
|
+
|
|
791
|
+
if (options.approved) {
|
|
792
|
+
const nextValues: any = {
|
|
793
|
+
status: 'pending',
|
|
794
|
+
approval: { ...(step.approval || {}), approved: true, resolvedAt: now().toISOString() },
|
|
795
|
+
updatedAt: now(),
|
|
796
|
+
};
|
|
797
|
+
if (options.editedInput !== undefined) {
|
|
798
|
+
nextValues.input = options.editedInput;
|
|
799
|
+
}
|
|
800
|
+
await this.repository.updateStep(step.id, nextValues);
|
|
801
|
+
await this.repository.updateRun(run.id, {
|
|
802
|
+
status: 'running',
|
|
803
|
+
currentStepId: null,
|
|
804
|
+
updatedAt: now(),
|
|
805
|
+
});
|
|
806
|
+
} else {
|
|
807
|
+
await this.repository.updateStep(step.id, {
|
|
808
|
+
status: 'failed',
|
|
809
|
+
approval: { ...(step.approval || {}), approved: false, resolvedAt: now().toISOString() },
|
|
810
|
+
error: 'User rejected this step.',
|
|
811
|
+
endedAt: now(),
|
|
812
|
+
updatedAt: now(),
|
|
813
|
+
});
|
|
814
|
+
await this.repository.updateRun(run.id, {
|
|
815
|
+
status: 'running',
|
|
816
|
+
currentStepId: null,
|
|
817
|
+
updatedAt: now(),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
await this.repository.createEvent({
|
|
822
|
+
runId: run.id,
|
|
823
|
+
stepId: step.id,
|
|
824
|
+
type: 'approval_resolved',
|
|
825
|
+
title: options.approved ? 'Approval granted' : 'Approval rejected',
|
|
826
|
+
status: options.approved ? 'running' : 'failed',
|
|
827
|
+
userId: options.userId,
|
|
828
|
+
payload: {
|
|
829
|
+
approved: options.approved,
|
|
830
|
+
editedInput: options.editedInput,
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (options.approved) {
|
|
835
|
+
return this.executeApprovedPlan(run.id, { userId: options.userId, ctx: options.ctx });
|
|
836
|
+
}
|
|
837
|
+
return this.getRunSnapshot(run.id);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async retryStep(stepId: string | number, options: { userId?: string | number } = {}) {
|
|
841
|
+
const step = await this.repository.requireStep(stepId);
|
|
842
|
+
const run = await this.repository.requireRun(step.runId);
|
|
843
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
844
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (step.status !== 'failed') {
|
|
848
|
+
throw new Error('Only failed steps can be retried.');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const policy = normalizePolicy(run.policy);
|
|
852
|
+
if (Number(step.attempt || 0) >= Number(step.maxAttempts || policy.maxStepAttempts)) {
|
|
853
|
+
throw new Error(`Step ${step.id} reached maxAttempts=${step.maxAttempts}.`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
await this.repository.updateStep(step.id, {
|
|
857
|
+
status: 'pending',
|
|
858
|
+
error: '',
|
|
859
|
+
endedAt: null,
|
|
860
|
+
updatedAt: now(),
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
await this.repository.updateRun(run.id, {
|
|
864
|
+
status: 'running',
|
|
865
|
+
updatedAt: now(),
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
await this.repository.createEvent({
|
|
869
|
+
runId: run.id,
|
|
870
|
+
stepId: step.id,
|
|
871
|
+
type: 'step_retry',
|
|
872
|
+
title: `Retry queued: ${step.title || step.planKey}`,
|
|
873
|
+
status: 'pending',
|
|
874
|
+
userId: options.userId,
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
return this.getRunSnapshot(run.id);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async finishRun(
|
|
881
|
+
runId: string | number,
|
|
882
|
+
finalAnswer: string,
|
|
883
|
+
options: {
|
|
884
|
+
status?: Extract<AgentLoopRunStatus, 'succeeded' | 'failed'>;
|
|
885
|
+
summary?: string;
|
|
886
|
+
evidence?: any;
|
|
887
|
+
userId?: string | number;
|
|
888
|
+
} = {}
|
|
889
|
+
) {
|
|
890
|
+
const run = await this.repository.requireRun(runId);
|
|
891
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
892
|
+
throw new Error(`Agent loop run ${run.id} is already ${run.status}.`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const status = options.status || 'succeeded';
|
|
896
|
+
if (status === 'succeeded') {
|
|
897
|
+
const steps = await this.repository.getSteps(run.id);
|
|
898
|
+
const unfinished = steps.filter((step) => !TERMINAL_STEP_STATUSES.has(step.status));
|
|
899
|
+
if (unfinished.length) {
|
|
900
|
+
throw new Error(`Cannot finish run ${run.id}: ${unfinished.length} step(s) are not complete.`);
|
|
901
|
+
}
|
|
902
|
+
const policy = normalizePolicy(run.policy);
|
|
903
|
+
const verificationPassed = steps.some((step) => step.type === 'verification' && step.status === 'succeeded');
|
|
904
|
+
if (policy.requireVerification && !verificationPassed) {
|
|
905
|
+
throw new Error('Cannot finish run: policy requires a succeeded verification step.');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
await this.repository.updateRun(run.id, {
|
|
910
|
+
status,
|
|
911
|
+
finalAnswer: finalAnswer || '',
|
|
912
|
+
summary: options.summary || run.summary,
|
|
913
|
+
currentStepId: null,
|
|
914
|
+
metadata: { ...asObject(run.metadata), evidence: options.evidence },
|
|
915
|
+
endedAt: now(),
|
|
916
|
+
updatedAt: now(),
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
await this.repository.createEvent({
|
|
920
|
+
runId: run.id,
|
|
921
|
+
type: 'finished',
|
|
922
|
+
title: status === 'succeeded' ? 'Agent loop finished' : 'Agent loop failed',
|
|
923
|
+
content: finalAnswer,
|
|
924
|
+
status,
|
|
925
|
+
userId: options.userId,
|
|
926
|
+
payload: { evidence: options.evidence },
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
return this.getRunSnapshot(run.id);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async cancelRun(runId: string | number, options: { userId?: string | number; reason?: string } = {}) {
|
|
933
|
+
const run = await this.repository.requireRun(runId);
|
|
934
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
935
|
+
return this.getRunSnapshot(run.id);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const steps = await this.repository.getSteps(run.id);
|
|
939
|
+
for (const step of steps) {
|
|
940
|
+
if (!TERMINAL_STEP_STATUSES.has(step.status) && step.status !== 'failed') {
|
|
941
|
+
await this.repository.updateStep(step.id, {
|
|
942
|
+
status: 'skipped',
|
|
943
|
+
error: options.reason || 'Run canceled.',
|
|
944
|
+
endedAt: now(),
|
|
945
|
+
updatedAt: now(),
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
await this.repository.updateRun(run.id, {
|
|
951
|
+
status: 'canceled',
|
|
952
|
+
currentStepId: null,
|
|
953
|
+
endedAt: now(),
|
|
954
|
+
updatedAt: now(),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
await this.repository.createEvent({
|
|
958
|
+
runId: run.id,
|
|
959
|
+
type: 'canceled',
|
|
960
|
+
title: 'Agent loop canceled',
|
|
961
|
+
content: options.reason || '',
|
|
962
|
+
status: 'canceled',
|
|
963
|
+
userId: options.userId,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
return this.getRunSnapshot(run.id);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async executeApprovedPlan(runId: string | number, options: { userId?: string | number; ctx?: any } = {}) {
|
|
970
|
+
// Concurrency check using a unique process-specific database lock token
|
|
971
|
+
const lockToken = `exec-${runId}-${Math.random().toString(36).slice(2, 10)}`;
|
|
972
|
+
const acquired = await this.repository.lockRun(runId, lockToken, 300000); // 5 mins lock
|
|
973
|
+
if (!acquired) {
|
|
974
|
+
throw new Error(`Run ${runId} is currently locked and executing in another process.`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
try {
|
|
978
|
+
let iterations = 0;
|
|
979
|
+
let snapshot = await this.getRunSnapshot(runId);
|
|
980
|
+
const harnessSettings = asObject(snapshot.run?.metadata?.harnessSettings);
|
|
981
|
+
const maxControllerSteps = Math.max(
|
|
982
|
+
1,
|
|
983
|
+
Math.min(
|
|
984
|
+
ORCHESTRATOR_CONTROLLER_MAX_STEPS,
|
|
985
|
+
Number(harnessSettings.maxControllerSteps || ORCHESTRATOR_CONTROLLER_MAX_STEPS)
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
const policy = normalizePolicy(snapshot.run.policy);
|
|
990
|
+
|
|
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;
|
|
996
|
+
|
|
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
|
+
}, {
|
|
1009
|
+
userId: options.userId,
|
|
1010
|
+
reason: 'Dynamic tool approval required by policy.',
|
|
1011
|
+
});
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
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
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
snapshot = await this.getRunSnapshot(runId);
|
|
1027
|
+
if (iterations >= maxControllerSteps && snapshot.nextStep) {
|
|
1028
|
+
return this.finishRun(runId, `Agent loop stopped after ${maxControllerSteps} controller steps.`, {
|
|
1029
|
+
status: 'failed',
|
|
1030
|
+
summary: 'Controller iteration limit reached.',
|
|
1031
|
+
userId: options.userId,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// If waiting for approval or user, exit loop but don't finish
|
|
1036
|
+
if (snapshot.run.status === 'waiting_user') {
|
|
1037
|
+
return snapshot;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const steps = snapshot.steps || [];
|
|
1041
|
+
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');
|
|
1043
|
+
if (failed.length || unfinished.length) {
|
|
1044
|
+
return this.finishRun(
|
|
1045
|
+
runId,
|
|
1046
|
+
failed.length
|
|
1047
|
+
? `Agent loop failed at ${failed.length} step(s).`
|
|
1048
|
+
: `Agent loop stopped with ${unfinished.length} unfinished step(s).`,
|
|
1049
|
+
{
|
|
1050
|
+
status: 'failed',
|
|
1051
|
+
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) },
|
|
1053
|
+
userId: options.userId,
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const verification = steps.find((step: any) => step.type === 'verification' && step.status === 'succeeded');
|
|
1059
|
+
return this.finishRun(runId, 'Agent loop completed after plan execution.', {
|
|
1060
|
+
status: 'succeeded',
|
|
1061
|
+
summary: verification?.output?.summary || 'All approved plan steps completed.',
|
|
1062
|
+
evidence: { stepCount: steps.length, controllerIterations: iterations },
|
|
1063
|
+
userId: options.userId,
|
|
1064
|
+
});
|
|
1065
|
+
} finally {
|
|
1066
|
+
// Unlock only if we held this specific lockToken
|
|
1067
|
+
const current = await this.repository.getRun(runId);
|
|
1068
|
+
if (current && current.lockedBy === lockToken) {
|
|
1069
|
+
await this.repository.unlockRun(runId);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async getRunSnapshot(runId: string | number) {
|
|
1075
|
+
const run = await this.repository.requireRun(runId);
|
|
1076
|
+
const steps = await this.repository.getSteps(run.id);
|
|
1077
|
+
const nextStep = this.pickNextStep(steps, run.policy);
|
|
1078
|
+
return {
|
|
1079
|
+
run,
|
|
1080
|
+
steps,
|
|
1081
|
+
nextStep,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async getRunDetail(runId: string | number) {
|
|
1086
|
+
const snapshot = await this.getRunSnapshot(runId);
|
|
1087
|
+
const events = await this.repository.getEvents(snapshot.run.id);
|
|
1088
|
+
const spans = await this.repository.getLinkedSpans(snapshot.run.id, snapshot.run.rootRunId);
|
|
1089
|
+
const skillExecutions = await this.repository.getLinkedSkillExecutions(snapshot.run.id, snapshot.steps);
|
|
1090
|
+
return {
|
|
1091
|
+
...snapshot,
|
|
1092
|
+
events,
|
|
1093
|
+
spans,
|
|
1094
|
+
skillExecutions,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
pickNextStep(steps: any[], runPolicy?: any) {
|
|
1099
|
+
const byPlanKey = new Map(steps.map((step) => [String(step.planKey), step]));
|
|
1100
|
+
const policy = normalizePolicy(runPolicy);
|
|
1101
|
+
|
|
1102
|
+
const candidates = steps
|
|
1103
|
+
.filter(
|
|
1104
|
+
(step) =>
|
|
1105
|
+
step.status === 'pending' ||
|
|
1106
|
+
(step.status === 'failed' &&
|
|
1107
|
+
Number(step.attempt || 0) < Number(step.maxAttempts || policy.maxStepAttempts))
|
|
1108
|
+
)
|
|
1109
|
+
.sort((a, b) => Number(a.index || 0) - Number(b.index || 0));
|
|
1110
|
+
|
|
1111
|
+
for (const step of candidates) {
|
|
1112
|
+
const dependencies = asArray(step.dependsOn).map(String);
|
|
1113
|
+
const allowSkipped =
|
|
1114
|
+
step.dependencyPolicy === 'allow_skipped' || step.metadata?.dependencyPolicy === 'allow_skipped';
|
|
1115
|
+
const ready = dependencies.every((key) => {
|
|
1116
|
+
const dependency = byPlanKey.get(key);
|
|
1117
|
+
return dependency?.status === 'succeeded' || (allowSkipped && dependency?.status === 'skipped');
|
|
1118
|
+
});
|
|
1119
|
+
if (ready) {
|
|
1120
|
+
return {
|
|
1121
|
+
...step,
|
|
1122
|
+
retryable: step.status === 'failed',
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
}
|