@defai.digital/agent-domain 13.0.3
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/LICENSE +214 -0
- package/dist/enhanced-executor.d.ts +170 -0
- package/dist/enhanced-executor.d.ts.map +1 -0
- package/dist/enhanced-executor.js +1072 -0
- package/dist/enhanced-executor.js.map +1 -0
- package/dist/executor.d.ts +120 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +929 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +50 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +160 -0
- package/dist/loader.js.map +1 -0
- package/dist/persistent-registry.d.ts +105 -0
- package/dist/persistent-registry.d.ts.map +1 -0
- package/dist/persistent-registry.js +183 -0
- package/dist/persistent-registry.js.map +1 -0
- package/dist/production-factories.d.ts +70 -0
- package/dist/production-factories.d.ts.map +1 -0
- package/dist/production-factories.js +434 -0
- package/dist/production-factories.js.map +1 -0
- package/dist/prompt-executor.d.ts +119 -0
- package/dist/prompt-executor.d.ts.map +1 -0
- package/dist/prompt-executor.js +211 -0
- package/dist/prompt-executor.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +123 -0
- package/dist/registry.js.map +1 -0
- package/dist/selection-service.d.ts +74 -0
- package/dist/selection-service.d.ts.map +1 -0
- package/dist/selection-service.js +322 -0
- package/dist/selection-service.js.map +1 -0
- package/dist/selector.d.ts +51 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/selector.js +249 -0
- package/dist/selector.js.map +1 -0
- package/dist/stub-checkpoint.d.ts +23 -0
- package/dist/stub-checkpoint.d.ts.map +1 -0
- package/dist/stub-checkpoint.js +137 -0
- package/dist/stub-checkpoint.js.map +1 -0
- package/dist/stub-delegation-tracker.d.ts +25 -0
- package/dist/stub-delegation-tracker.d.ts.map +1 -0
- package/dist/stub-delegation-tracker.js +118 -0
- package/dist/stub-delegation-tracker.js.map +1 -0
- package/dist/stub-parallel-executor.d.ts +19 -0
- package/dist/stub-parallel-executor.d.ts.map +1 -0
- package/dist/stub-parallel-executor.js +176 -0
- package/dist/stub-parallel-executor.js.map +1 -0
- package/dist/types.d.ts +614 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-templates.d.ts +117 -0
- package/dist/workflow-templates.d.ts.map +1 -0
- package/dist/workflow-templates.js +342 -0
- package/dist/workflow-templates.js.map +1 -0
- package/package.json +51 -0
- package/src/enhanced-executor.ts +1395 -0
- package/src/executor.ts +1153 -0
- package/src/index.ts +172 -0
- package/src/loader.ts +191 -0
- package/src/persistent-registry.ts +235 -0
- package/src/production-factories.ts +613 -0
- package/src/prompt-executor.ts +310 -0
- package/src/registry.ts +167 -0
- package/src/selection-service.ts +411 -0
- package/src/selector.ts +299 -0
- package/src/stub-checkpoint.ts +187 -0
- package/src/stub-delegation-tracker.ts +161 -0
- package/src/stub-parallel-executor.ts +224 -0
- package/src/types.ts +784 -0
- package/src/workflow-templates.ts +393 -0
package/src/executor.ts
ADDED
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Executor Implementation
|
|
3
|
+
*
|
|
4
|
+
* Executes agent workflows with step-by-step processing.
|
|
5
|
+
* Integrates with DelegationTracker for INV-DT-001 (depth limits) and INV-DT-002 (no circular).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type AgentResult,
|
|
10
|
+
type AgentRunOptions,
|
|
11
|
+
type AgentError,
|
|
12
|
+
type AgentWorkflowStep,
|
|
13
|
+
AgentErrorCode,
|
|
14
|
+
AgentRunOptionsSchema,
|
|
15
|
+
LIMIT_ABILITY_TOKENS_AGENT,
|
|
16
|
+
} from '@defai.digital/contracts';
|
|
17
|
+
import type {
|
|
18
|
+
AgentExecutor,
|
|
19
|
+
AgentRegistry,
|
|
20
|
+
ExecutionStatus,
|
|
21
|
+
StepExecutionContext,
|
|
22
|
+
StepExecutionResult,
|
|
23
|
+
StepExecutorFn,
|
|
24
|
+
AgentDomainConfig,
|
|
25
|
+
PromptExecutor,
|
|
26
|
+
PromptStepConfig,
|
|
27
|
+
AbilityManagerLike,
|
|
28
|
+
ToolExecutor,
|
|
29
|
+
DelegationTrackerFactory,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
import { createStubPromptExecutor } from './prompt-executor.js';
|
|
32
|
+
import { stubDelegationTrackerFactory } from './stub-delegation-tracker.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default agent executor implementation
|
|
36
|
+
*
|
|
37
|
+
* Supports optional ability injection via AbilityManager.
|
|
38
|
+
* INV-AGT-ABL-001: When abilityManager is provided, abilities are injected before prompt execution
|
|
39
|
+
* INV-AGT-ABL-002: Core abilities from agent profile are always included
|
|
40
|
+
* INV-AGT-ABL-003: Token limits are respected during injection
|
|
41
|
+
*/
|
|
42
|
+
export class DefaultAgentExecutor implements AgentExecutor {
|
|
43
|
+
private readonly executions = new Map<string, ExecutionState>();
|
|
44
|
+
private readonly stepExecutors = new Map<string, StepExecutorFn>();
|
|
45
|
+
private readonly promptExecutor: PromptExecutor;
|
|
46
|
+
private readonly toolExecutor: ToolExecutor | undefined;
|
|
47
|
+
private readonly abilityManager: AbilityManagerLike | undefined;
|
|
48
|
+
private readonly enableAbilityInjection: boolean;
|
|
49
|
+
private readonly maxAbilityTokens: number;
|
|
50
|
+
private readonly delegationTrackerFactory: DelegationTrackerFactory;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly registry: AgentRegistry,
|
|
54
|
+
private readonly config: AgentDomainConfig
|
|
55
|
+
) {
|
|
56
|
+
// Use provided prompt executor or create a stub for testing
|
|
57
|
+
this.promptExecutor = config.promptExecutor ?? createStubPromptExecutor(
|
|
58
|
+
config.defaultProvider ?? 'claude'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Tool executor for executing MCP tools (optional)
|
|
62
|
+
this.toolExecutor = config.toolExecutor;
|
|
63
|
+
|
|
64
|
+
// Ability injection configuration
|
|
65
|
+
this.abilityManager = config.abilityManager;
|
|
66
|
+
this.enableAbilityInjection = config.enableAbilityInjection ?? (config.abilityManager !== undefined);
|
|
67
|
+
this.maxAbilityTokens = config.maxAbilityTokens ?? LIMIT_ABILITY_TOKENS_AGENT;
|
|
68
|
+
|
|
69
|
+
// Delegation tracker factory (use stub if not provided)
|
|
70
|
+
this.delegationTrackerFactory = config.delegationTrackerFactory ?? stubDelegationTrackerFactory;
|
|
71
|
+
|
|
72
|
+
// Register default step executors
|
|
73
|
+
this.registerDefaultExecutors();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute an agent with the given input
|
|
78
|
+
* Validates AgentRunOptions at entry point per contract
|
|
79
|
+
*/
|
|
80
|
+
async execute(
|
|
81
|
+
agentId: string,
|
|
82
|
+
input: unknown,
|
|
83
|
+
options?: AgentRunOptions
|
|
84
|
+
): Promise<AgentResult> {
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
const executionId = crypto.randomUUID();
|
|
87
|
+
|
|
88
|
+
// Validate options at entry point
|
|
89
|
+
let validatedOptions: AgentRunOptions | undefined;
|
|
90
|
+
if (options !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
validatedOptions = AgentRunOptionsSchema.parse(options);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return this.createErrorResult(
|
|
95
|
+
agentId,
|
|
96
|
+
startTime,
|
|
97
|
+
AgentErrorCode.AGENT_VALIDATION_ERROR,
|
|
98
|
+
`Invalid run options: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get agent profile
|
|
104
|
+
const agent = await this.registry.get(agentId);
|
|
105
|
+
if (agent === undefined) {
|
|
106
|
+
return this.createErrorResult(
|
|
107
|
+
agentId,
|
|
108
|
+
startTime,
|
|
109
|
+
AgentErrorCode.AGENT_NOT_FOUND,
|
|
110
|
+
`Agent "${agentId}" not found`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if agent is enabled
|
|
115
|
+
if (!agent.enabled) {
|
|
116
|
+
return this.createErrorResult(
|
|
117
|
+
agentId,
|
|
118
|
+
startTime,
|
|
119
|
+
AgentErrorCode.AGENT_PERMISSION_DENIED,
|
|
120
|
+
`Agent "${agentId}" is disabled`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Initialize execution state
|
|
125
|
+
const state: ExecutionState = {
|
|
126
|
+
executionId,
|
|
127
|
+
agentId,
|
|
128
|
+
status: 'running',
|
|
129
|
+
startedAt: new Date().toISOString(),
|
|
130
|
+
stepResults: [],
|
|
131
|
+
previousOutputs: {},
|
|
132
|
+
};
|
|
133
|
+
this.executions.set(executionId, state);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Execute workflow steps
|
|
137
|
+
const workflow = agent.workflow ?? [];
|
|
138
|
+
const stepResults: StepResult[] = [];
|
|
139
|
+
|
|
140
|
+
// Handle agents with no workflow defined
|
|
141
|
+
if (workflow.length === 0) {
|
|
142
|
+
state.status = 'completed';
|
|
143
|
+
state.completedAt = new Date().toISOString();
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
agentId,
|
|
148
|
+
sessionId: validatedOptions?.sessionId,
|
|
149
|
+
output: {
|
|
150
|
+
message: `Agent "${agentId}" has no workflow steps defined. Register the agent with a workflow to enable execution.`,
|
|
151
|
+
agent: {
|
|
152
|
+
agentId: agent.agentId,
|
|
153
|
+
description: agent.description,
|
|
154
|
+
capabilities: agent.capabilities,
|
|
155
|
+
systemPrompt: agent.systemPrompt ? '(defined)' : undefined,
|
|
156
|
+
},
|
|
157
|
+
input,
|
|
158
|
+
},
|
|
159
|
+
stepResults: [],
|
|
160
|
+
totalDurationMs: Date.now() - startTime,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const step of workflow) {
|
|
165
|
+
// Check for cancellation
|
|
166
|
+
if (state.status === 'cancelled') {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
state.currentStep = step.stepId;
|
|
171
|
+
|
|
172
|
+
// Check dependencies
|
|
173
|
+
const dependenciesMet = this.checkDependencies(
|
|
174
|
+
step,
|
|
175
|
+
stepResults
|
|
176
|
+
);
|
|
177
|
+
if (!dependenciesMet) {
|
|
178
|
+
stepResults.push({
|
|
179
|
+
stepId: step.stepId,
|
|
180
|
+
success: false,
|
|
181
|
+
durationMs: 0,
|
|
182
|
+
retryCount: 0,
|
|
183
|
+
skipped: true,
|
|
184
|
+
error: {
|
|
185
|
+
code: AgentErrorCode.AGENT_DEPENDENCY_FAILED,
|
|
186
|
+
message: 'Dependencies not met',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check condition
|
|
193
|
+
if (step.condition !== undefined) {
|
|
194
|
+
const conditionMet = this.evaluateCondition(
|
|
195
|
+
step.condition,
|
|
196
|
+
state.previousOutputs
|
|
197
|
+
);
|
|
198
|
+
if (!conditionMet) {
|
|
199
|
+
stepResults.push({
|
|
200
|
+
stepId: step.stepId,
|
|
201
|
+
success: true,
|
|
202
|
+
durationMs: 0,
|
|
203
|
+
retryCount: 0,
|
|
204
|
+
skipped: true,
|
|
205
|
+
error: undefined,
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Execute step with retries
|
|
212
|
+
const result = await this.executeStep(
|
|
213
|
+
step,
|
|
214
|
+
{
|
|
215
|
+
agentId,
|
|
216
|
+
executionId,
|
|
217
|
+
sessionId: validatedOptions?.sessionId,
|
|
218
|
+
input,
|
|
219
|
+
previousOutputs: state.previousOutputs,
|
|
220
|
+
memory: undefined,
|
|
221
|
+
provider: validatedOptions?.provider,
|
|
222
|
+
model: validatedOptions?.model,
|
|
223
|
+
delegationContext: validatedOptions?.delegationContext,
|
|
224
|
+
},
|
|
225
|
+
step.retryPolicy?.maxAttempts ?? 1
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
stepResults.push({
|
|
229
|
+
stepId: step.stepId,
|
|
230
|
+
success: result.success,
|
|
231
|
+
output: result.output,
|
|
232
|
+
durationMs: result.durationMs,
|
|
233
|
+
retryCount: result.retryCount,
|
|
234
|
+
skipped: false,
|
|
235
|
+
error: result.error,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Store output for dependencies
|
|
239
|
+
if (result.success && result.output !== undefined) {
|
|
240
|
+
state.previousOutputs[step.stepId] = result.output;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Stop on failure (unless parallel execution)
|
|
244
|
+
if (!result.success && !step.parallel) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Determine final status
|
|
250
|
+
const allSuccess = stepResults.every((r) => r.success || r.skipped);
|
|
251
|
+
const finalOutput = this.collectOutputs(stepResults, state.previousOutputs);
|
|
252
|
+
|
|
253
|
+
state.status = allSuccess ? 'completed' : 'failed';
|
|
254
|
+
state.completedAt = new Date().toISOString();
|
|
255
|
+
state.stepResults = stepResults;
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
success: allSuccess,
|
|
259
|
+
agentId,
|
|
260
|
+
sessionId: validatedOptions?.sessionId,
|
|
261
|
+
output: finalOutput,
|
|
262
|
+
stepResults,
|
|
263
|
+
totalDurationMs: Date.now() - startTime,
|
|
264
|
+
error: allSuccess
|
|
265
|
+
? undefined
|
|
266
|
+
: stepResults.find((r) => r.error !== undefined)?.error,
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
state.status = 'failed';
|
|
270
|
+
state.completedAt = new Date().toISOString();
|
|
271
|
+
|
|
272
|
+
return this.createErrorResult(
|
|
273
|
+
agentId,
|
|
274
|
+
startTime,
|
|
275
|
+
AgentErrorCode.AGENT_STAGE_FAILED,
|
|
276
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Cancel a running agent execution
|
|
283
|
+
*/
|
|
284
|
+
async cancel(executionId: string): Promise<void> {
|
|
285
|
+
const state = this.executions.get(executionId);
|
|
286
|
+
if (state?.status === 'running') {
|
|
287
|
+
state.status = 'cancelled';
|
|
288
|
+
state.completedAt = new Date().toISOString();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get execution status
|
|
294
|
+
*/
|
|
295
|
+
async getStatus(executionId: string): Promise<ExecutionStatus | undefined> {
|
|
296
|
+
const state = this.executions.get(executionId);
|
|
297
|
+
if (state === undefined) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const completedSteps = state.stepResults.filter(
|
|
302
|
+
(r) => r.success || r.skipped
|
|
303
|
+
).length;
|
|
304
|
+
const totalSteps = state.stepResults.length;
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
executionId: state.executionId,
|
|
308
|
+
agentId: state.agentId,
|
|
309
|
+
status: state.status,
|
|
310
|
+
currentStep: state.currentStep,
|
|
311
|
+
startedAt: state.startedAt,
|
|
312
|
+
completedAt: state.completedAt,
|
|
313
|
+
progress: {
|
|
314
|
+
totalSteps,
|
|
315
|
+
completedSteps,
|
|
316
|
+
currentStepName: state.currentStep,
|
|
317
|
+
percentComplete:
|
|
318
|
+
totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Register a step executor for a specific step type
|
|
325
|
+
*/
|
|
326
|
+
registerStepExecutor(type: string, executor: StepExecutorFn): void {
|
|
327
|
+
this.stepExecutors.set(type, executor);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Execute a single step with retry logic
|
|
332
|
+
*/
|
|
333
|
+
private async executeStep(
|
|
334
|
+
step: AgentWorkflowStep,
|
|
335
|
+
context: StepExecutionContext,
|
|
336
|
+
maxAttempts: number
|
|
337
|
+
): Promise<StepExecutionResult> {
|
|
338
|
+
const executor = this.stepExecutors.get(step.type);
|
|
339
|
+
if (executor === undefined) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
error: {
|
|
343
|
+
code: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
344
|
+
message: `No executor for step type: ${step.type}`,
|
|
345
|
+
stepId: step.stepId,
|
|
346
|
+
},
|
|
347
|
+
durationMs: 0,
|
|
348
|
+
retryCount: 0,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let lastError: AgentError | undefined;
|
|
353
|
+
let retryCount = 0;
|
|
354
|
+
|
|
355
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
356
|
+
// Apply timeout with proper cleanup
|
|
357
|
+
const timeout = step.timeoutMs ?? this.config.defaultTimeout;
|
|
358
|
+
const { promise: timeoutPromise, cancel: cancelTimeout } = this.createTimeout(timeout, step.stepId);
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const result = await Promise.race([
|
|
362
|
+
executor(step, context),
|
|
363
|
+
timeoutPromise,
|
|
364
|
+
]);
|
|
365
|
+
cancelTimeout(); // Clean up the timer
|
|
366
|
+
|
|
367
|
+
if (result.success) {
|
|
368
|
+
return {
|
|
369
|
+
...result,
|
|
370
|
+
retryCount,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
lastError = result.error;
|
|
375
|
+
retryCount++;
|
|
376
|
+
|
|
377
|
+
// Apply backoff
|
|
378
|
+
if (attempt < maxAttempts - 1 && step.retryPolicy !== undefined) {
|
|
379
|
+
const backoff =
|
|
380
|
+
step.retryPolicy.backoffMs *
|
|
381
|
+
Math.pow(step.retryPolicy.backoffMultiplier, attempt);
|
|
382
|
+
await this.sleep(backoff);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
cancelTimeout(); // Clean up the timer on error too
|
|
386
|
+
lastError = {
|
|
387
|
+
code: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
388
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
389
|
+
stepId: step.stepId,
|
|
390
|
+
};
|
|
391
|
+
retryCount++;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: lastError,
|
|
398
|
+
durationMs: 0,
|
|
399
|
+
retryCount,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if step dependencies are met
|
|
405
|
+
*/
|
|
406
|
+
private checkDependencies(
|
|
407
|
+
step: AgentWorkflowStep,
|
|
408
|
+
results: StepResult[]
|
|
409
|
+
): boolean {
|
|
410
|
+
if (step.dependencies === undefined || step.dependencies.length === 0) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return step.dependencies.every((depId) => {
|
|
415
|
+
const depResult = results.find((r) => r.stepId === depId);
|
|
416
|
+
return depResult !== undefined && depResult.success;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Evaluate a condition expression safely without using eval/Function
|
|
422
|
+
*
|
|
423
|
+
* Supports patterns:
|
|
424
|
+
* - ${variable} === value
|
|
425
|
+
* - ${variable} !== value
|
|
426
|
+
* - ${variable} > value
|
|
427
|
+
* - ${variable} < value
|
|
428
|
+
* - ${variable} >= value
|
|
429
|
+
* - ${variable} <= value
|
|
430
|
+
* - ${variable} (truthy check)
|
|
431
|
+
* - !${variable} (falsy check)
|
|
432
|
+
* - condition && condition
|
|
433
|
+
* - condition || condition
|
|
434
|
+
*/
|
|
435
|
+
private evaluateCondition(
|
|
436
|
+
condition: string,
|
|
437
|
+
outputs: Record<string, unknown>
|
|
438
|
+
): boolean {
|
|
439
|
+
try {
|
|
440
|
+
// Handle logical operators by recursively evaluating sub-conditions
|
|
441
|
+
// Check for || first (lower precedence)
|
|
442
|
+
const orParts = this.splitByOperator(condition, '||');
|
|
443
|
+
if (orParts.length > 1) {
|
|
444
|
+
return orParts.some((part) => this.evaluateCondition(part.trim(), outputs));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check for && (higher precedence than ||)
|
|
448
|
+
const andParts = this.splitByOperator(condition, '&&');
|
|
449
|
+
if (andParts.length > 1) {
|
|
450
|
+
return andParts.every((part) => this.evaluateCondition(part.trim(), outputs));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Handle negation
|
|
454
|
+
const trimmed = condition.trim();
|
|
455
|
+
if (trimmed.startsWith('!')) {
|
|
456
|
+
return !this.evaluateCondition(trimmed.slice(1).trim(), outputs);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle parentheses
|
|
460
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
461
|
+
return this.evaluateCondition(trimmed.slice(1, -1), outputs);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Parse comparison expression: ${variable} op value
|
|
465
|
+
const comparisonMatch = /^\$\{(\w+(?:\.\w+)*)\}\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/.exec(trimmed);
|
|
466
|
+
if (comparisonMatch) {
|
|
467
|
+
const [, keyPath, op, rawValue] = comparisonMatch;
|
|
468
|
+
const actualValue = this.getConditionValue(outputs, keyPath!);
|
|
469
|
+
const expectedValue = this.parseValue(rawValue!.trim());
|
|
470
|
+
return this.compareValues(actualValue, expectedValue, op!);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Handle simple variable reference (truthy check): ${variable}
|
|
474
|
+
const varMatch = /^\$\{(\w+(?:\.\w+)*)\}$/.exec(trimmed);
|
|
475
|
+
if (varMatch) {
|
|
476
|
+
const value = this.getConditionValue(outputs, varMatch[1]!);
|
|
477
|
+
return Boolean(value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Handle literal boolean
|
|
481
|
+
if (trimmed === 'true') return true;
|
|
482
|
+
if (trimmed === 'false') return false;
|
|
483
|
+
|
|
484
|
+
// Unknown pattern - fail safely
|
|
485
|
+
console.warn(`[evaluateCondition] Unknown condition pattern: ${condition}`);
|
|
486
|
+
return false;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.warn(`[evaluateCondition] Error evaluating condition: ${condition}`, error);
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Split condition by operator, respecting parentheses
|
|
495
|
+
*/
|
|
496
|
+
private splitByOperator(condition: string, operator: string): string[] {
|
|
497
|
+
const parts: string[] = [];
|
|
498
|
+
let current = '';
|
|
499
|
+
let depth = 0;
|
|
500
|
+
let i = 0;
|
|
501
|
+
|
|
502
|
+
while (i < condition.length) {
|
|
503
|
+
const char = condition[i];
|
|
504
|
+
|
|
505
|
+
if (char === '(') {
|
|
506
|
+
depth++;
|
|
507
|
+
current += char;
|
|
508
|
+
} else if (char === ')') {
|
|
509
|
+
depth--;
|
|
510
|
+
current += char;
|
|
511
|
+
} else if (depth === 0 && condition.slice(i, i + operator.length) === operator) {
|
|
512
|
+
parts.push(current);
|
|
513
|
+
current = '';
|
|
514
|
+
i += operator.length;
|
|
515
|
+
continue;
|
|
516
|
+
} else {
|
|
517
|
+
current += char;
|
|
518
|
+
}
|
|
519
|
+
i++;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (current) {
|
|
523
|
+
parts.push(current);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return parts;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Get nested value from object using dot notation (for condition evaluation)
|
|
531
|
+
*/
|
|
532
|
+
private getConditionValue(obj: Record<string, unknown>, path: string): unknown {
|
|
533
|
+
const parts = path.split('.');
|
|
534
|
+
let current: unknown = obj;
|
|
535
|
+
|
|
536
|
+
for (const part of parts) {
|
|
537
|
+
if (current === null || current === undefined) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
current = (current as Record<string, unknown>)[part];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return current;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Parse a value string into its actual type
|
|
548
|
+
*/
|
|
549
|
+
private parseValue(value: string): unknown {
|
|
550
|
+
// String literal
|
|
551
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
552
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
553
|
+
return value.slice(1, -1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Null/undefined
|
|
557
|
+
if (value === 'null') return null;
|
|
558
|
+
if (value === 'undefined') return undefined;
|
|
559
|
+
|
|
560
|
+
// Boolean
|
|
561
|
+
if (value === 'true') return true;
|
|
562
|
+
if (value === 'false') return false;
|
|
563
|
+
|
|
564
|
+
// Number
|
|
565
|
+
const num = Number(value);
|
|
566
|
+
if (!isNaN(num)) return num;
|
|
567
|
+
|
|
568
|
+
// Default to string
|
|
569
|
+
return value;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Compare two values using the specified operator
|
|
574
|
+
*/
|
|
575
|
+
private compareValues(actual: unknown, expected: unknown, op: string): boolean {
|
|
576
|
+
switch (op) {
|
|
577
|
+
case '===':
|
|
578
|
+
return actual === expected;
|
|
579
|
+
case '!==':
|
|
580
|
+
return actual !== expected;
|
|
581
|
+
case '==':
|
|
582
|
+
return actual == expected;
|
|
583
|
+
case '!=':
|
|
584
|
+
return actual != expected;
|
|
585
|
+
case '>':
|
|
586
|
+
return (actual as number) > (expected as number);
|
|
587
|
+
case '<':
|
|
588
|
+
return (actual as number) < (expected as number);
|
|
589
|
+
case '>=':
|
|
590
|
+
return (actual as number) >= (expected as number);
|
|
591
|
+
case '<=':
|
|
592
|
+
return (actual as number) <= (expected as number);
|
|
593
|
+
default:
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Collect final outputs from step results
|
|
600
|
+
*/
|
|
601
|
+
private collectOutputs(
|
|
602
|
+
results: StepResult[],
|
|
603
|
+
_previousOutputs: Record<string, unknown>
|
|
604
|
+
): Record<string, unknown> {
|
|
605
|
+
const output: Record<string, unknown> = {};
|
|
606
|
+
|
|
607
|
+
for (const result of results) {
|
|
608
|
+
if (result.success && result.output !== undefined) {
|
|
609
|
+
output[result.stepId] = result.output;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return output;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Create error result
|
|
618
|
+
*/
|
|
619
|
+
private createErrorResult(
|
|
620
|
+
agentId: string,
|
|
621
|
+
startTime: number,
|
|
622
|
+
code: string,
|
|
623
|
+
message: string
|
|
624
|
+
): AgentResult {
|
|
625
|
+
return {
|
|
626
|
+
success: false,
|
|
627
|
+
agentId,
|
|
628
|
+
error: { code, message },
|
|
629
|
+
totalDurationMs: Date.now() - startTime,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Create timeout promise with cancellation support
|
|
635
|
+
*/
|
|
636
|
+
private createTimeout(ms: number, stepId: string): {
|
|
637
|
+
promise: Promise<StepExecutionResult>;
|
|
638
|
+
cancel: () => void;
|
|
639
|
+
} {
|
|
640
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
641
|
+
|
|
642
|
+
const promise = new Promise<StepExecutionResult>((_, reject) => {
|
|
643
|
+
timeoutId = setTimeout(
|
|
644
|
+
() =>
|
|
645
|
+
{ reject({
|
|
646
|
+
success: false,
|
|
647
|
+
error: {
|
|
648
|
+
code: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
649
|
+
message: `Step "${stepId}" timed out after ${ms}ms`,
|
|
650
|
+
stepId,
|
|
651
|
+
retryable: true,
|
|
652
|
+
},
|
|
653
|
+
durationMs: ms,
|
|
654
|
+
retryCount: 0,
|
|
655
|
+
}); },
|
|
656
|
+
ms
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const cancel = (): void => {
|
|
661
|
+
if (timeoutId !== undefined) {
|
|
662
|
+
clearTimeout(timeoutId);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
return { promise, cancel };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Sleep for specified duration
|
|
671
|
+
*/
|
|
672
|
+
private sleep(ms: number): Promise<void> {
|
|
673
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Substitute template variables in a string
|
|
678
|
+
* Supports: ${input}, ${input.field}, ${previousOutputs.stepId}, ${agent.field}
|
|
679
|
+
*/
|
|
680
|
+
private substituteVariables(
|
|
681
|
+
template: string,
|
|
682
|
+
context: StepExecutionContext,
|
|
683
|
+
agentProfile: { description?: string; systemPrompt?: string; agentId: string }
|
|
684
|
+
): string {
|
|
685
|
+
return template.replace(/\$\{([^}]+)\}/g, (match, path) => {
|
|
686
|
+
const parts = path.split('.');
|
|
687
|
+
let value: unknown;
|
|
688
|
+
|
|
689
|
+
switch (parts[0]) {
|
|
690
|
+
case 'input':
|
|
691
|
+
if (parts.length === 1) {
|
|
692
|
+
value = typeof context.input === 'string'
|
|
693
|
+
? context.input
|
|
694
|
+
: JSON.stringify(context.input, null, 2);
|
|
695
|
+
} else {
|
|
696
|
+
value = this.getNestedValue(context.input, parts.slice(1));
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
case 'previousOutputs':
|
|
700
|
+
if (parts.length > 1) {
|
|
701
|
+
value = this.getNestedValue(context.previousOutputs, parts.slice(1));
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
case 'agent':
|
|
705
|
+
if (parts.length > 1) {
|
|
706
|
+
value = this.getNestedValue(agentProfile, parts.slice(1));
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
default:
|
|
710
|
+
// Try to get from input directly
|
|
711
|
+
value = this.getNestedValue(context.input, parts);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (value === undefined) {
|
|
715
|
+
return match; // Keep original if not found
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Get nested value from object using path array
|
|
724
|
+
*/
|
|
725
|
+
private getNestedValue(obj: unknown, path: string[]): unknown {
|
|
726
|
+
let current: unknown = obj;
|
|
727
|
+
for (const key of path) {
|
|
728
|
+
if (current === null || current === undefined) {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
if (typeof current !== 'object') {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
current = (current as Record<string, unknown>)[key];
|
|
735
|
+
}
|
|
736
|
+
return current;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Register default step executors
|
|
741
|
+
*/
|
|
742
|
+
private registerDefaultExecutors(): void {
|
|
743
|
+
// Prompt step executor - executes real LLM calls via promptExecutor
|
|
744
|
+
// INV-AGT-ABL-001: Abilities injected before prompt execution
|
|
745
|
+
this.stepExecutors.set('prompt', async (step, context) => {
|
|
746
|
+
const startTime = Date.now();
|
|
747
|
+
const config = (step.config ?? {}) as PromptStepConfig;
|
|
748
|
+
|
|
749
|
+
// Get agent profile for system prompt and variable substitution
|
|
750
|
+
const agent = await this.registry.get(context.agentId);
|
|
751
|
+
const agentProfile: { description?: string; systemPrompt?: string; agentId: string } = {
|
|
752
|
+
agentId: context.agentId,
|
|
753
|
+
};
|
|
754
|
+
if (agent?.description !== undefined) {
|
|
755
|
+
agentProfile.description = agent.description;
|
|
756
|
+
}
|
|
757
|
+
if (agent?.systemPrompt !== undefined) {
|
|
758
|
+
agentProfile.systemPrompt = agent.systemPrompt;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// INV-AGT-ABL-001: Inject abilities if ability manager is available
|
|
762
|
+
// INV-AGT-ABL-002: Core abilities from agent profile are always included
|
|
763
|
+
// INV-AGT-ABL-003: Token limits are respected
|
|
764
|
+
let abilityContent = '';
|
|
765
|
+
if (this.enableAbilityInjection && this.abilityManager !== undefined) {
|
|
766
|
+
try {
|
|
767
|
+
// Get core abilities from agent profile
|
|
768
|
+
const coreAbilities = agent?.abilities?.core ?? [];
|
|
769
|
+
|
|
770
|
+
// Determine task from input for relevance matching
|
|
771
|
+
const task = typeof context.input === 'string'
|
|
772
|
+
? context.input
|
|
773
|
+
: typeof context.input === 'object' && context.input !== null && 'task' in context.input
|
|
774
|
+
? String((context.input as { task: unknown }).task)
|
|
775
|
+
: typeof context.input === 'object' && context.input !== null && 'prompt' in context.input
|
|
776
|
+
? String((context.input as { prompt: unknown }).prompt)
|
|
777
|
+
: agent?.description ?? '';
|
|
778
|
+
|
|
779
|
+
// Inject abilities
|
|
780
|
+
const injectionResult = await this.abilityManager.injectAbilities(
|
|
781
|
+
context.agentId,
|
|
782
|
+
task,
|
|
783
|
+
coreAbilities,
|
|
784
|
+
{
|
|
785
|
+
maxTokens: this.maxAbilityTokens,
|
|
786
|
+
includeMetadata: true,
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (injectionResult.combinedContent.length > 0) {
|
|
791
|
+
abilityContent = `\n\n## Relevant Knowledge & Abilities\n\n${injectionResult.combinedContent}\n\n---\n\n`;
|
|
792
|
+
}
|
|
793
|
+
} catch {
|
|
794
|
+
// Ability injection failure should not block execution
|
|
795
|
+
// Just log and continue without abilities
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Build the prompt from config or input
|
|
800
|
+
let promptText: string;
|
|
801
|
+
if (config.prompt) {
|
|
802
|
+
// Use template from config with variable substitution
|
|
803
|
+
promptText = this.substituteVariables(config.prompt, context, agentProfile);
|
|
804
|
+
} else if (typeof context.input === 'string') {
|
|
805
|
+
// Use input directly as prompt
|
|
806
|
+
promptText = context.input;
|
|
807
|
+
} else if (
|
|
808
|
+
context.input !== null &&
|
|
809
|
+
typeof context.input === 'object' &&
|
|
810
|
+
'prompt' in context.input &&
|
|
811
|
+
typeof (context.input as { prompt: unknown }).prompt === 'string'
|
|
812
|
+
) {
|
|
813
|
+
// Use input.prompt
|
|
814
|
+
promptText = (context.input as { prompt: string }).prompt;
|
|
815
|
+
} else {
|
|
816
|
+
// Default: stringify input as the prompt
|
|
817
|
+
promptText = JSON.stringify(context.input, null, 2);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Determine system prompt with injected abilities
|
|
821
|
+
const baseSystemPrompt = config.systemPrompt ?? agent?.systemPrompt ?? '';
|
|
822
|
+
const systemPrompt = abilityContent
|
|
823
|
+
? `${baseSystemPrompt}${abilityContent}`
|
|
824
|
+
: baseSystemPrompt || undefined;
|
|
825
|
+
|
|
826
|
+
// Execute the prompt
|
|
827
|
+
const response = await this.promptExecutor.execute({
|
|
828
|
+
prompt: promptText,
|
|
829
|
+
systemPrompt: systemPrompt || undefined,
|
|
830
|
+
provider: config.provider ?? context.provider,
|
|
831
|
+
model: config.model ?? context.model,
|
|
832
|
+
maxTokens: config.maxTokens,
|
|
833
|
+
temperature: config.temperature,
|
|
834
|
+
timeout: config.timeout ?? step.timeoutMs,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
if (response.success) {
|
|
838
|
+
return {
|
|
839
|
+
success: true,
|
|
840
|
+
output: {
|
|
841
|
+
content: response.content,
|
|
842
|
+
provider: response.provider,
|
|
843
|
+
model: response.model,
|
|
844
|
+
usage: response.usage,
|
|
845
|
+
},
|
|
846
|
+
error: undefined,
|
|
847
|
+
durationMs: Date.now() - startTime,
|
|
848
|
+
retryCount: 0,
|
|
849
|
+
};
|
|
850
|
+
} else {
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error: {
|
|
854
|
+
code: response.errorCode ?? AgentErrorCode.AGENT_STAGE_FAILED,
|
|
855
|
+
message: response.error ?? 'Prompt execution failed',
|
|
856
|
+
stepId: step.stepId,
|
|
857
|
+
retryable: true,
|
|
858
|
+
},
|
|
859
|
+
durationMs: Date.now() - startTime,
|
|
860
|
+
retryCount: 0,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Tool step executor (INV-TOOL-001, INV-TOOL-002, INV-TOOL-003)
|
|
866
|
+
this.stepExecutors.set('tool', async (step, context) => {
|
|
867
|
+
const startTime = Date.now();
|
|
868
|
+
const config = step.config as {
|
|
869
|
+
toolName?: string;
|
|
870
|
+
toolInput?: Record<string, unknown>;
|
|
871
|
+
} | undefined;
|
|
872
|
+
|
|
873
|
+
const toolName = config?.toolName;
|
|
874
|
+
const toolInput = (config?.toolInput ?? context.input ?? {}) as Record<string, unknown>;
|
|
875
|
+
|
|
876
|
+
// If no tool executor is configured, return placeholder result
|
|
877
|
+
if (this.toolExecutor === undefined) {
|
|
878
|
+
return {
|
|
879
|
+
success: true,
|
|
880
|
+
output: {
|
|
881
|
+
step: step.stepId,
|
|
882
|
+
type: 'tool',
|
|
883
|
+
toolName: toolName ?? 'unknown',
|
|
884
|
+
status: 'no_executor',
|
|
885
|
+
message: 'Tool execution requires a ToolExecutor. Configure AgentDomainConfig.toolExecutor.',
|
|
886
|
+
},
|
|
887
|
+
error: undefined,
|
|
888
|
+
durationMs: Date.now() - startTime,
|
|
889
|
+
retryCount: 0,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Check if tool name is provided
|
|
894
|
+
if (toolName === undefined || toolName.trim() === '') {
|
|
895
|
+
return {
|
|
896
|
+
success: false,
|
|
897
|
+
output: undefined,
|
|
898
|
+
error: {
|
|
899
|
+
code: 'TOOL_CONFIG_ERROR',
|
|
900
|
+
message: `Tool step "${step.stepId}" requires toolName in config`,
|
|
901
|
+
stepId: step.stepId,
|
|
902
|
+
},
|
|
903
|
+
durationMs: Date.now() - startTime,
|
|
904
|
+
retryCount: 0,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// INV-TOOL-001: Check if tool is available
|
|
909
|
+
if (!this.toolExecutor.isToolAvailable(toolName)) {
|
|
910
|
+
return {
|
|
911
|
+
success: false,
|
|
912
|
+
output: undefined,
|
|
913
|
+
error: {
|
|
914
|
+
code: 'TOOL_NOT_FOUND',
|
|
915
|
+
message: `Tool "${toolName}" is not available`,
|
|
916
|
+
stepId: step.stepId,
|
|
917
|
+
},
|
|
918
|
+
durationMs: Date.now() - startTime,
|
|
919
|
+
retryCount: 0,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Execute the tool
|
|
924
|
+
try {
|
|
925
|
+
const result = await this.toolExecutor.execute(toolName, toolInput);
|
|
926
|
+
return {
|
|
927
|
+
success: result.success,
|
|
928
|
+
output: {
|
|
929
|
+
step: step.stepId,
|
|
930
|
+
type: 'tool',
|
|
931
|
+
toolName,
|
|
932
|
+
toolOutput: result.output,
|
|
933
|
+
},
|
|
934
|
+
error: result.success ? undefined : {
|
|
935
|
+
code: result.errorCode ?? 'TOOL_EXECUTION_ERROR',
|
|
936
|
+
message: result.error ?? 'Tool execution failed',
|
|
937
|
+
stepId: step.stepId,
|
|
938
|
+
retryable: result.retryable ?? false,
|
|
939
|
+
},
|
|
940
|
+
durationMs: Date.now() - startTime,
|
|
941
|
+
retryCount: 0,
|
|
942
|
+
};
|
|
943
|
+
} catch (error) {
|
|
944
|
+
return {
|
|
945
|
+
success: false,
|
|
946
|
+
output: undefined,
|
|
947
|
+
error: {
|
|
948
|
+
code: 'TOOL_EXECUTION_ERROR',
|
|
949
|
+
message: error instanceof Error ? error.message : 'Unknown tool execution error',
|
|
950
|
+
stepId: step.stepId,
|
|
951
|
+
retryable: true,
|
|
952
|
+
},
|
|
953
|
+
durationMs: Date.now() - startTime,
|
|
954
|
+
retryCount: 0,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Conditional step executor
|
|
960
|
+
this.stepExecutors.set('conditional', async (step, _context) => {
|
|
961
|
+
const startTime = Date.now();
|
|
962
|
+
return {
|
|
963
|
+
success: true,
|
|
964
|
+
output: { step: step.stepId, type: 'conditional' },
|
|
965
|
+
error: undefined,
|
|
966
|
+
durationMs: Date.now() - startTime,
|
|
967
|
+
retryCount: 0,
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Loop step executor
|
|
972
|
+
this.stepExecutors.set('loop', async (step, _context) => {
|
|
973
|
+
const startTime = Date.now();
|
|
974
|
+
return {
|
|
975
|
+
success: true,
|
|
976
|
+
output: { step: step.stepId, type: 'loop' },
|
|
977
|
+
error: undefined,
|
|
978
|
+
durationMs: Date.now() - startTime,
|
|
979
|
+
retryCount: 0,
|
|
980
|
+
};
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Parallel step executor
|
|
984
|
+
this.stepExecutors.set('parallel', async (step, _context) => {
|
|
985
|
+
const startTime = Date.now();
|
|
986
|
+
return {
|
|
987
|
+
success: true,
|
|
988
|
+
output: { step: step.stepId, type: 'parallel' },
|
|
989
|
+
error: undefined,
|
|
990
|
+
durationMs: Date.now() - startTime,
|
|
991
|
+
retryCount: 0,
|
|
992
|
+
};
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Delegate step executor - integrates with DelegationTracker
|
|
996
|
+
// INV-DT-001: Depth never exceeds maxDepth
|
|
997
|
+
// INV-DT-002: No circular delegations
|
|
998
|
+
this.stepExecutors.set('delegate', async (step, context) => {
|
|
999
|
+
const startTime = Date.now();
|
|
1000
|
+
const config = step.config as {
|
|
1001
|
+
targetAgentId?: string;
|
|
1002
|
+
task?: string;
|
|
1003
|
+
input?: unknown;
|
|
1004
|
+
} | undefined;
|
|
1005
|
+
|
|
1006
|
+
if (!config?.targetAgentId) {
|
|
1007
|
+
return {
|
|
1008
|
+
success: false,
|
|
1009
|
+
error: {
|
|
1010
|
+
code: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
1011
|
+
message: 'Delegate step requires targetAgentId in config',
|
|
1012
|
+
stepId: step.stepId,
|
|
1013
|
+
},
|
|
1014
|
+
durationMs: Date.now() - startTime,
|
|
1015
|
+
retryCount: 0,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Create delegation tracker using injected factory
|
|
1020
|
+
const delegationTracker = this.delegationTrackerFactory(
|
|
1021
|
+
context.agentId,
|
|
1022
|
+
context.delegationContext,
|
|
1023
|
+
this.config.maxDelegationDepth
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// Check if delegation is allowed (INV-DT-001, INV-DT-002)
|
|
1027
|
+
const checkResult = delegationTracker.canDelegate(config.targetAgentId);
|
|
1028
|
+
if (!checkResult.allowed) {
|
|
1029
|
+
return {
|
|
1030
|
+
success: false,
|
|
1031
|
+
error: {
|
|
1032
|
+
code: checkResult.reason === 'MAX_DEPTH_EXCEEDED'
|
|
1033
|
+
? AgentErrorCode.AGENT_DELEGATION_DEPTH_EXCEEDED
|
|
1034
|
+
: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
1035
|
+
message: checkResult.message ?? `Cannot delegate to ${config.targetAgentId}`,
|
|
1036
|
+
stepId: step.stepId,
|
|
1037
|
+
retryable: false,
|
|
1038
|
+
},
|
|
1039
|
+
durationMs: Date.now() - startTime,
|
|
1040
|
+
retryCount: 0,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Get target agent profile
|
|
1045
|
+
const targetAgent = await this.registry.get(config.targetAgentId);
|
|
1046
|
+
if (targetAgent === undefined) {
|
|
1047
|
+
return {
|
|
1048
|
+
success: false,
|
|
1049
|
+
error: {
|
|
1050
|
+
code: AgentErrorCode.AGENT_NOT_FOUND,
|
|
1051
|
+
message: `Delegation target "${config.targetAgentId}" not found`,
|
|
1052
|
+
stepId: step.stepId,
|
|
1053
|
+
},
|
|
1054
|
+
durationMs: Date.now() - startTime,
|
|
1055
|
+
retryCount: 0,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Create child context for the delegated agent
|
|
1060
|
+
const childContext = delegationTracker.createChildContext(config.targetAgentId);
|
|
1061
|
+
|
|
1062
|
+
// Execute the delegated agent recursively
|
|
1063
|
+
try {
|
|
1064
|
+
const delegateResult = await this.execute(
|
|
1065
|
+
config.targetAgentId,
|
|
1066
|
+
config.input ?? context.input,
|
|
1067
|
+
{
|
|
1068
|
+
sessionId: context.sessionId,
|
|
1069
|
+
provider: context.provider,
|
|
1070
|
+
model: context.model,
|
|
1071
|
+
delegationContext: childContext,
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
// Record the delegation result
|
|
1076
|
+
delegationTracker.recordResult({
|
|
1077
|
+
success: delegateResult.success,
|
|
1078
|
+
handledBy: config.targetAgentId,
|
|
1079
|
+
result: delegateResult.output,
|
|
1080
|
+
durationMs: delegateResult.totalDurationMs,
|
|
1081
|
+
finalDepth: childContext.currentDepth,
|
|
1082
|
+
error: delegateResult.error ? {
|
|
1083
|
+
code: delegateResult.error.code,
|
|
1084
|
+
message: delegateResult.error.message,
|
|
1085
|
+
retryable: false,
|
|
1086
|
+
} : undefined,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
success: delegateResult.success,
|
|
1091
|
+
output: {
|
|
1092
|
+
step: step.stepId,
|
|
1093
|
+
type: 'delegate',
|
|
1094
|
+
delegatedTo: config.targetAgentId,
|
|
1095
|
+
result: delegateResult.output,
|
|
1096
|
+
delegationDepth: childContext.currentDepth,
|
|
1097
|
+
},
|
|
1098
|
+
error: delegateResult.error,
|
|
1099
|
+
durationMs: Date.now() - startTime,
|
|
1100
|
+
retryCount: 0,
|
|
1101
|
+
};
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
return {
|
|
1104
|
+
success: false,
|
|
1105
|
+
error: {
|
|
1106
|
+
code: AgentErrorCode.AGENT_STAGE_FAILED,
|
|
1107
|
+
message: error instanceof Error ? error.message : 'Delegation failed',
|
|
1108
|
+
stepId: step.stepId,
|
|
1109
|
+
},
|
|
1110
|
+
durationMs: Date.now() - startTime,
|
|
1111
|
+
retryCount: 0,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Execution state (internal)
|
|
1120
|
+
*/
|
|
1121
|
+
interface ExecutionState {
|
|
1122
|
+
executionId: string;
|
|
1123
|
+
agentId: string;
|
|
1124
|
+
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
|
1125
|
+
startedAt: string;
|
|
1126
|
+
completedAt?: string;
|
|
1127
|
+
currentStep?: string;
|
|
1128
|
+
stepResults: StepResult[];
|
|
1129
|
+
previousOutputs: Record<string, unknown>;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Step result
|
|
1134
|
+
*/
|
|
1135
|
+
interface StepResult {
|
|
1136
|
+
stepId: string;
|
|
1137
|
+
success: boolean;
|
|
1138
|
+
output?: unknown;
|
|
1139
|
+
durationMs: number;
|
|
1140
|
+
retryCount: number;
|
|
1141
|
+
skipped: boolean;
|
|
1142
|
+
error: AgentError | undefined;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Creates a new agent executor
|
|
1147
|
+
*/
|
|
1148
|
+
export function createAgentExecutor(
|
|
1149
|
+
registry: AgentRegistry,
|
|
1150
|
+
config: AgentDomainConfig
|
|
1151
|
+
): AgentExecutor {
|
|
1152
|
+
return new DefaultAgentExecutor(registry, config);
|
|
1153
|
+
}
|