@build-astron-co/nimbus 0.2.0
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 +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { logger } from '../utils';
|
|
2
|
+
import { LLMRouter } from '../llm/router';
|
|
3
|
+
|
|
4
|
+
// ==========================================
|
|
5
|
+
// Inline Types (from core-engine-service/src/types/agent.ts)
|
|
6
|
+
// ==========================================
|
|
7
|
+
|
|
8
|
+
export interface AgentTask {
|
|
9
|
+
id: string;
|
|
10
|
+
type: 'generate' | 'deploy' | 'verify' | 'rollback' | 'analyze';
|
|
11
|
+
status: 'pending' | 'planning' | 'executing' | 'verifying' | 'completed' | 'failed' | 'cancelled';
|
|
12
|
+
priority: 'low' | 'medium' | 'high' | 'critical';
|
|
13
|
+
user_id: string;
|
|
14
|
+
created_at: Date;
|
|
15
|
+
updated_at: Date;
|
|
16
|
+
completed_at?: Date;
|
|
17
|
+
context: {
|
|
18
|
+
provider: 'aws' | 'gcp' | 'azure';
|
|
19
|
+
environment: string;
|
|
20
|
+
region?: string;
|
|
21
|
+
components: string[];
|
|
22
|
+
requirements?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
execution: {
|
|
25
|
+
plan_id?: string;
|
|
26
|
+
execution_id?: string;
|
|
27
|
+
verification_id?: string;
|
|
28
|
+
};
|
|
29
|
+
result?: {
|
|
30
|
+
success: boolean;
|
|
31
|
+
outputs?: Record<string, unknown>;
|
|
32
|
+
artifacts?: string[];
|
|
33
|
+
errors?: string[];
|
|
34
|
+
};
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentPlan {
|
|
39
|
+
id: string;
|
|
40
|
+
task_id: string;
|
|
41
|
+
status: 'draft' | 'approved' | 'rejected' | 'executing' | 'completed';
|
|
42
|
+
created_at: Date;
|
|
43
|
+
updated_at: Date;
|
|
44
|
+
steps: PlanStep[];
|
|
45
|
+
dependencies: PlanDependency[];
|
|
46
|
+
estimated_duration?: number;
|
|
47
|
+
estimated_cost?: number;
|
|
48
|
+
risks: Risk[];
|
|
49
|
+
risk_level: 'low' | 'medium' | 'high' | 'critical';
|
|
50
|
+
requires_approval: boolean;
|
|
51
|
+
approved_by?: string;
|
|
52
|
+
approved_at?: Date;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PlanStep {
|
|
56
|
+
id: string;
|
|
57
|
+
order: number;
|
|
58
|
+
type: 'generate' | 'validate' | 'deploy' | 'configure' | 'verify';
|
|
59
|
+
description: string;
|
|
60
|
+
component?: string;
|
|
61
|
+
action: string;
|
|
62
|
+
parameters: Record<string, unknown>;
|
|
63
|
+
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
64
|
+
started_at?: Date;
|
|
65
|
+
completed_at?: Date;
|
|
66
|
+
duration?: number;
|
|
67
|
+
depends_on?: string[];
|
|
68
|
+
rollback_action?: string;
|
|
69
|
+
rollback_parameters?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PlanDependency {
|
|
73
|
+
step_id: string;
|
|
74
|
+
depends_on: string[];
|
|
75
|
+
type: 'sequential' | 'parallel';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Risk {
|
|
79
|
+
id: string;
|
|
80
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
81
|
+
category: 'security' | 'cost' | 'availability' | 'performance' | 'compliance';
|
|
82
|
+
description: string;
|
|
83
|
+
mitigation?: string;
|
|
84
|
+
probability: number;
|
|
85
|
+
impact: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ==========================================
|
|
89
|
+
// Constants
|
|
90
|
+
// ==========================================
|
|
91
|
+
|
|
92
|
+
/** System prompt instructing the LLM to generate execution steps as JSON. */
|
|
93
|
+
const PLANNING_PROMPT =
|
|
94
|
+
'You are an infrastructure planning agent. Given the task context, generate an ordered array of execution steps as JSON. ' +
|
|
95
|
+
'Each step has fields: id (string like step_1), name (string), description (string), type (one of: generate, validate, deploy, configure, verify), ' +
|
|
96
|
+
'order (number), estimatedDuration (number in seconds). Return ONLY the JSON array, no markdown.';
|
|
97
|
+
|
|
98
|
+
/** System prompt instructing the LLM to assess risks as JSON. */
|
|
99
|
+
const RISK_ASSESSMENT_PROMPT =
|
|
100
|
+
'You are an infrastructure risk assessor. Given the task and execution steps, identify risks. ' +
|
|
101
|
+
'Return a JSON array of risks with fields: id (string), severity (low|medium|high|critical), ' +
|
|
102
|
+
'category (security|cost|availability|performance|compliance), description (string), mitigation (string), ' +
|
|
103
|
+
'probability (0-1), impact (0-1). Return ONLY the JSON array.';
|
|
104
|
+
|
|
105
|
+
/** Valid step types for plan steps. */
|
|
106
|
+
const VALID_STEP_TYPES = new Set(['generate', 'validate', 'deploy', 'configure', 'verify']);
|
|
107
|
+
|
|
108
|
+
/** Valid severity levels for risks. */
|
|
109
|
+
const VALID_SEVERITIES = new Set(['low', 'medium', 'high', 'critical']);
|
|
110
|
+
|
|
111
|
+
/** Valid risk categories. */
|
|
112
|
+
const VALID_CATEGORIES = new Set(['security', 'cost', 'availability', 'performance', 'compliance']);
|
|
113
|
+
|
|
114
|
+
// ==========================================
|
|
115
|
+
// Planner
|
|
116
|
+
// ==========================================
|
|
117
|
+
|
|
118
|
+
export class Planner {
|
|
119
|
+
private router: LLMRouter;
|
|
120
|
+
|
|
121
|
+
constructor() {
|
|
122
|
+
this.router = new LLMRouter();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate an execution plan for a task
|
|
127
|
+
*/
|
|
128
|
+
async generatePlan(task: AgentTask): Promise<AgentPlan> {
|
|
129
|
+
logger.info(`Generating plan for task: ${task.id}`);
|
|
130
|
+
|
|
131
|
+
const steps = await this.generateSteps(task);
|
|
132
|
+
const dependencies = this.analyzeDependencies(steps);
|
|
133
|
+
const risks = await this.assessRisks(task, steps);
|
|
134
|
+
const riskLevel = this.calculateOverallRiskLevel(risks);
|
|
135
|
+
|
|
136
|
+
const plan: AgentPlan = {
|
|
137
|
+
id: this.generatePlanId(),
|
|
138
|
+
task_id: task.id,
|
|
139
|
+
status: 'draft',
|
|
140
|
+
created_at: new Date(),
|
|
141
|
+
updated_at: new Date(),
|
|
142
|
+
steps,
|
|
143
|
+
dependencies,
|
|
144
|
+
risks,
|
|
145
|
+
risk_level: riskLevel,
|
|
146
|
+
requires_approval: riskLevel === 'high' || riskLevel === 'critical',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Estimate duration and cost
|
|
150
|
+
plan.estimated_duration = this.estimateDuration(steps);
|
|
151
|
+
plan.estimated_cost = await this.estimateCost(task, steps);
|
|
152
|
+
|
|
153
|
+
logger.info(`Generated plan ${plan.id} with ${steps.length} steps, risk level: ${riskLevel}`);
|
|
154
|
+
|
|
155
|
+
return plan;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate execution steps for a task.
|
|
160
|
+
* Attempts LLM-based generation first, falls back to heuristic logic.
|
|
161
|
+
*/
|
|
162
|
+
private async generateSteps(task: AgentTask): Promise<PlanStep[]> {
|
|
163
|
+
try {
|
|
164
|
+
const llmSteps = await this.generateStepsWithLLM(task);
|
|
165
|
+
if (llmSteps.length > 0) {
|
|
166
|
+
logger.info(`Using LLM-generated steps (${llmSteps.length} steps)`);
|
|
167
|
+
return llmSteps;
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.debug(
|
|
171
|
+
`LLM step generation failed, falling back to heuristics: ${(error as Error).message}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return this.generateStepsHeuristic(task);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate steps using the embedded LLM router.
|
|
180
|
+
*/
|
|
181
|
+
private async generateStepsWithLLM(task: AgentTask): Promise<PlanStep[]> {
|
|
182
|
+
const response = await this.router.route({
|
|
183
|
+
messages: [
|
|
184
|
+
{ role: 'system', content: PLANNING_PROMPT },
|
|
185
|
+
{ role: 'user', content: JSON.stringify(task.context) },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const content = response?.content;
|
|
190
|
+
if (!content) {
|
|
191
|
+
throw new Error('LLM response missing content');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const parsed: unknown = JSON.parse(content);
|
|
195
|
+
|
|
196
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
197
|
+
throw new Error('LLM response is not a non-empty array');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const steps: PlanStep[] = [];
|
|
201
|
+
for (const item of parsed) {
|
|
202
|
+
if (
|
|
203
|
+
typeof item !== 'object' ||
|
|
204
|
+
item === null ||
|
|
205
|
+
typeof item.id !== 'string' ||
|
|
206
|
+
typeof item.description !== 'string' ||
|
|
207
|
+
!VALID_STEP_TYPES.has(item.type)
|
|
208
|
+
) {
|
|
209
|
+
throw new Error('LLM response contains invalid step structure');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
steps.push({
|
|
213
|
+
id: item.id,
|
|
214
|
+
order: typeof item.order === 'number' ? item.order : steps.length + 1,
|
|
215
|
+
type: item.type as PlanStep['type'],
|
|
216
|
+
description: item.description,
|
|
217
|
+
action: item.type as string,
|
|
218
|
+
parameters: {},
|
|
219
|
+
status: 'pending',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return steps;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate execution steps using heuristic logic (fallback).
|
|
228
|
+
*/
|
|
229
|
+
private generateStepsHeuristic(task: AgentTask): PlanStep[] {
|
|
230
|
+
const steps: PlanStep[] = [];
|
|
231
|
+
let order = 1;
|
|
232
|
+
|
|
233
|
+
// Step 1: Validate requirements
|
|
234
|
+
steps.push({
|
|
235
|
+
id: `step_${order++}`,
|
|
236
|
+
order: steps.length + 1,
|
|
237
|
+
type: 'validate',
|
|
238
|
+
description: 'Validate infrastructure requirements and constraints',
|
|
239
|
+
action: 'validate_requirements',
|
|
240
|
+
parameters: {
|
|
241
|
+
provider: task.context.provider,
|
|
242
|
+
components: task.context.components,
|
|
243
|
+
requirements: task.context.requirements,
|
|
244
|
+
},
|
|
245
|
+
status: 'pending',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step 2-N: Generate infrastructure components
|
|
249
|
+
for (const component of task.context.components) {
|
|
250
|
+
steps.push({
|
|
251
|
+
id: `step_${order++}`,
|
|
252
|
+
order: steps.length + 1,
|
|
253
|
+
type: 'generate',
|
|
254
|
+
description: `Generate ${component.toUpperCase()} configuration`,
|
|
255
|
+
component,
|
|
256
|
+
action: 'generate_component',
|
|
257
|
+
parameters: {
|
|
258
|
+
component,
|
|
259
|
+
provider: task.context.provider,
|
|
260
|
+
environment: task.context.environment,
|
|
261
|
+
requirements: task.context.requirements,
|
|
262
|
+
},
|
|
263
|
+
status: 'pending',
|
|
264
|
+
depends_on: ['step_1'], // Depends on validation
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Step: Validate generated code
|
|
269
|
+
steps.push({
|
|
270
|
+
id: `step_${order++}`,
|
|
271
|
+
order: steps.length + 1,
|
|
272
|
+
type: 'validate',
|
|
273
|
+
description: 'Validate generated infrastructure code',
|
|
274
|
+
action: 'validate_generated_code',
|
|
275
|
+
parameters: {
|
|
276
|
+
components: task.context.components,
|
|
277
|
+
},
|
|
278
|
+
status: 'pending',
|
|
279
|
+
depends_on: steps.slice(1, -1).map(s => s.id), // Depends on all generation steps
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Step: Apply best practices
|
|
283
|
+
steps.push({
|
|
284
|
+
id: `step_${order++}`,
|
|
285
|
+
order: steps.length + 1,
|
|
286
|
+
type: 'validate',
|
|
287
|
+
description: 'Apply security and best practices',
|
|
288
|
+
action: 'apply_best_practices',
|
|
289
|
+
parameters: {
|
|
290
|
+
components: task.context.components,
|
|
291
|
+
autofix: true,
|
|
292
|
+
},
|
|
293
|
+
status: 'pending',
|
|
294
|
+
depends_on: [steps[steps.length - 1].id],
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// If deployment is requested
|
|
298
|
+
if (task.type === 'deploy') {
|
|
299
|
+
// Step: Plan deployment
|
|
300
|
+
steps.push({
|
|
301
|
+
id: `step_${order++}`,
|
|
302
|
+
order: steps.length + 1,
|
|
303
|
+
type: 'deploy',
|
|
304
|
+
description: 'Plan infrastructure deployment (terraform plan)',
|
|
305
|
+
action: 'plan_deployment',
|
|
306
|
+
parameters: {
|
|
307
|
+
provider: task.context.provider,
|
|
308
|
+
environment: task.context.environment,
|
|
309
|
+
},
|
|
310
|
+
status: 'pending',
|
|
311
|
+
depends_on: [steps[steps.length - 1].id],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Step: Apply deployment
|
|
315
|
+
steps.push({
|
|
316
|
+
id: `step_${order++}`,
|
|
317
|
+
order: steps.length + 1,
|
|
318
|
+
type: 'deploy',
|
|
319
|
+
description: 'Apply infrastructure deployment (terraform apply)',
|
|
320
|
+
action: 'apply_deployment',
|
|
321
|
+
parameters: {
|
|
322
|
+
provider: task.context.provider,
|
|
323
|
+
environment: task.context.environment,
|
|
324
|
+
auto_approve: false,
|
|
325
|
+
},
|
|
326
|
+
status: 'pending',
|
|
327
|
+
depends_on: [steps[steps.length - 1].id],
|
|
328
|
+
rollback_action: 'destroy_deployment',
|
|
329
|
+
rollback_parameters: {
|
|
330
|
+
provider: task.context.provider,
|
|
331
|
+
environment: task.context.environment,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Step: Verify deployment
|
|
336
|
+
steps.push({
|
|
337
|
+
id: `step_${order++}`,
|
|
338
|
+
order: steps.length + 1,
|
|
339
|
+
type: 'verify',
|
|
340
|
+
description: 'Verify deployed infrastructure',
|
|
341
|
+
action: 'verify_deployment',
|
|
342
|
+
parameters: {
|
|
343
|
+
components: task.context.components,
|
|
344
|
+
environment: task.context.environment,
|
|
345
|
+
},
|
|
346
|
+
status: 'pending',
|
|
347
|
+
depends_on: [steps[steps.length - 1].id],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Final step: Generate documentation
|
|
352
|
+
steps.push({
|
|
353
|
+
id: `step_${order++}`,
|
|
354
|
+
order: steps.length + 1,
|
|
355
|
+
type: 'generate',
|
|
356
|
+
description: 'Generate infrastructure documentation',
|
|
357
|
+
action: 'generate_documentation',
|
|
358
|
+
parameters: {
|
|
359
|
+
components: task.context.components,
|
|
360
|
+
include_diagrams: true,
|
|
361
|
+
},
|
|
362
|
+
status: 'pending',
|
|
363
|
+
depends_on: [steps[steps.length - 1].id],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return steps;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Analyze dependencies between steps
|
|
371
|
+
*/
|
|
372
|
+
private analyzeDependencies(steps: PlanStep[]): PlanDependency[] {
|
|
373
|
+
return steps
|
|
374
|
+
.filter(step => step.depends_on && step.depends_on.length > 0)
|
|
375
|
+
.map(step => ({
|
|
376
|
+
step_id: step.id,
|
|
377
|
+
depends_on: step.depends_on!,
|
|
378
|
+
type: (step.depends_on!.length === 1 ? 'sequential' : 'parallel') as
|
|
379
|
+
| 'sequential'
|
|
380
|
+
| 'parallel',
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Assess risks for the plan.
|
|
386
|
+
* Attempts LLM-based assessment first, falls back to heuristic logic.
|
|
387
|
+
*/
|
|
388
|
+
private async assessRisks(task: AgentTask, steps: PlanStep[]): Promise<Risk[]> {
|
|
389
|
+
try {
|
|
390
|
+
const llmRisks = await this.assessRisksWithLLM(task, steps);
|
|
391
|
+
if (llmRisks.length > 0) {
|
|
392
|
+
logger.info(`Using LLM-assessed risks (${llmRisks.length} risks)`);
|
|
393
|
+
return llmRisks;
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger.debug(
|
|
397
|
+
`LLM risk assessment failed, falling back to heuristics: ${(error as Error).message}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return this.assessRisksHeuristic(task, steps);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Assess risks using the embedded LLM router.
|
|
406
|
+
*/
|
|
407
|
+
private async assessRisksWithLLM(task: AgentTask, steps: PlanStep[]): Promise<Risk[]> {
|
|
408
|
+
const response = await this.router.route({
|
|
409
|
+
messages: [
|
|
410
|
+
{ role: 'system', content: RISK_ASSESSMENT_PROMPT },
|
|
411
|
+
{
|
|
412
|
+
role: 'user',
|
|
413
|
+
content: JSON.stringify({
|
|
414
|
+
task: task.context,
|
|
415
|
+
steps: steps.map(s => ({ id: s.id, type: s.type, description: s.description })),
|
|
416
|
+
}),
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const content = response?.content;
|
|
422
|
+
if (!content) {
|
|
423
|
+
throw new Error('LLM response missing content');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const parsed: unknown = JSON.parse(content);
|
|
427
|
+
|
|
428
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
429
|
+
throw new Error('LLM response is not a non-empty array');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const risks: Risk[] = [];
|
|
433
|
+
for (const item of parsed) {
|
|
434
|
+
if (
|
|
435
|
+
typeof item !== 'object' ||
|
|
436
|
+
item === null ||
|
|
437
|
+
typeof item.id !== 'string' ||
|
|
438
|
+
typeof item.description !== 'string' ||
|
|
439
|
+
!VALID_SEVERITIES.has(item.severity) ||
|
|
440
|
+
!VALID_CATEGORIES.has(item.category) ||
|
|
441
|
+
typeof item.probability !== 'number' ||
|
|
442
|
+
typeof item.impact !== 'number'
|
|
443
|
+
) {
|
|
444
|
+
throw new Error('LLM response contains invalid risk structure');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
risks.push({
|
|
448
|
+
id: item.id,
|
|
449
|
+
severity: item.severity as Risk['severity'],
|
|
450
|
+
category: item.category as Risk['category'],
|
|
451
|
+
description: item.description,
|
|
452
|
+
mitigation: typeof item.mitigation === 'string' ? item.mitigation : undefined,
|
|
453
|
+
probability: item.probability,
|
|
454
|
+
impact: item.impact,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return risks;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Assess risks using heuristic logic (fallback).
|
|
463
|
+
*/
|
|
464
|
+
private assessRisksHeuristic(task: AgentTask, steps: PlanStep[]): Risk[] {
|
|
465
|
+
const risks: Risk[] = [];
|
|
466
|
+
|
|
467
|
+
// Security risks
|
|
468
|
+
if (task.context.environment === 'production') {
|
|
469
|
+
risks.push({
|
|
470
|
+
id: 'risk_prod_deploy',
|
|
471
|
+
severity: 'high',
|
|
472
|
+
category: 'availability',
|
|
473
|
+
description: 'Deploying to production environment',
|
|
474
|
+
mitigation: 'Requires approval, automated testing, and gradual rollout',
|
|
475
|
+
probability: 0.3,
|
|
476
|
+
impact: 0.8,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Cost risks
|
|
481
|
+
const hasExpensiveComponents = task.context.components.some(c => ['eks', 'rds'].includes(c));
|
|
482
|
+
if (hasExpensiveComponents) {
|
|
483
|
+
risks.push({
|
|
484
|
+
id: 'risk_high_cost',
|
|
485
|
+
severity: 'medium',
|
|
486
|
+
category: 'cost',
|
|
487
|
+
description: 'Infrastructure includes high-cost components',
|
|
488
|
+
mitigation: 'Review instance types and enable autoscaling',
|
|
489
|
+
probability: 0.6,
|
|
490
|
+
impact: 0.5,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Compliance risks
|
|
495
|
+
if (task.context.components.includes('s3')) {
|
|
496
|
+
risks.push({
|
|
497
|
+
id: 'risk_data_security',
|
|
498
|
+
severity: 'high',
|
|
499
|
+
category: 'security',
|
|
500
|
+
description: 'Storage component requires encryption and access controls',
|
|
501
|
+
mitigation: 'Enable encryption at rest and in transit, implement least privilege access',
|
|
502
|
+
probability: 0.4,
|
|
503
|
+
impact: 0.9,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Deployment risks
|
|
508
|
+
const hasDeploymentSteps = steps.some(s => s.type === 'deploy');
|
|
509
|
+
if (hasDeploymentSteps && !task.context.requirements?.backup_enabled) {
|
|
510
|
+
risks.push({
|
|
511
|
+
id: 'risk_no_backup',
|
|
512
|
+
severity: 'high',
|
|
513
|
+
category: 'availability',
|
|
514
|
+
description: 'No backup strategy defined',
|
|
515
|
+
mitigation: 'Enable automated backups and test restoration procedures',
|
|
516
|
+
probability: 0.5,
|
|
517
|
+
impact: 0.7,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return risks;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Calculate overall risk level
|
|
526
|
+
*/
|
|
527
|
+
private calculateOverallRiskLevel(risks: Risk[]): 'low' | 'medium' | 'high' | 'critical' {
|
|
528
|
+
if (risks.length === 0) {
|
|
529
|
+
return 'low';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const hasCritical = risks.some(r => r.severity === 'critical');
|
|
533
|
+
if (hasCritical) {
|
|
534
|
+
return 'critical';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const highRisks = risks.filter(r => r.severity === 'high');
|
|
538
|
+
if (highRisks.length >= 2) {
|
|
539
|
+
return 'high';
|
|
540
|
+
}
|
|
541
|
+
if (highRisks.length === 1) {
|
|
542
|
+
return 'high';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const mediumRisks = risks.filter(r => r.severity === 'medium');
|
|
546
|
+
if (mediumRisks.length >= 3) {
|
|
547
|
+
return 'high';
|
|
548
|
+
}
|
|
549
|
+
if (mediumRisks.length >= 1) {
|
|
550
|
+
return 'medium';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return 'low';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Estimate duration in seconds
|
|
558
|
+
*/
|
|
559
|
+
private estimateDuration(steps: PlanStep[]): number {
|
|
560
|
+
const durations: Record<string, number> = {
|
|
561
|
+
validate: 30,
|
|
562
|
+
generate: 60,
|
|
563
|
+
deploy: 600, // 10 minutes for deployment
|
|
564
|
+
verify: 120,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
return steps.reduce((total, step) => {
|
|
568
|
+
return total + (durations[step.type] || 60);
|
|
569
|
+
}, 0);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Estimate cost in USD
|
|
574
|
+
*/
|
|
575
|
+
private async estimateCost(task: AgentTask, _steps: PlanStep[]): Promise<number> {
|
|
576
|
+
let monthlyCost = 0;
|
|
577
|
+
|
|
578
|
+
const componentCosts: Record<string, number> = {
|
|
579
|
+
vpc: 0, // VPC itself is free, NAT gateway costs ~$32/month
|
|
580
|
+
eks: 73, // $0.10/hour * 730 hours
|
|
581
|
+
rds: 50, // t3.micro ~$15/month + storage
|
|
582
|
+
s3: 5, // Minimal storage estimate
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (const component of task.context.components) {
|
|
586
|
+
monthlyCost += componentCosts[component] || 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Add NAT gateway cost if VPC is included
|
|
590
|
+
if (task.context.components.includes('vpc')) {
|
|
591
|
+
monthlyCost += 32;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return Math.round(monthlyCost);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Optimize plan for parallel execution
|
|
599
|
+
*/
|
|
600
|
+
optimizePlan(plan: AgentPlan): AgentPlan {
|
|
601
|
+
// Identify steps that can run in parallel
|
|
602
|
+
const optimized = { ...plan };
|
|
603
|
+
|
|
604
|
+
// Group independent generation steps
|
|
605
|
+
const generationSteps = plan.steps.filter(s => s.type === 'generate' && s.component);
|
|
606
|
+
|
|
607
|
+
// Mark independent steps as parallelizable
|
|
608
|
+
for (let i = 0; i < generationSteps.length; i++) {
|
|
609
|
+
const step = generationSteps[i];
|
|
610
|
+
// If steps don't have interdependencies, they can run in parallel
|
|
611
|
+
if (!this.hasInterdependency(step, generationSteps, i)) {
|
|
612
|
+
step.parameters.parallel_group = 'generation';
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return optimized;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Check if step has interdependency with others
|
|
621
|
+
*/
|
|
622
|
+
private hasInterdependency(step: PlanStep, steps: PlanStep[], _index: number): boolean {
|
|
623
|
+
// Check if this step's output is needed by another step in the group
|
|
624
|
+
// Simplified: assume VPC must be created before EKS/RDS
|
|
625
|
+
if (step.component === 'vpc') {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (step.component === 'eks' || step.component === 'rds') {
|
|
629
|
+
return steps.some(s => s.component === 'vpc');
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Validate plan is executable
|
|
636
|
+
*/
|
|
637
|
+
validatePlan(plan: AgentPlan): { valid: boolean; errors: string[] } {
|
|
638
|
+
const errors: string[] = [];
|
|
639
|
+
|
|
640
|
+
// Check for circular dependencies
|
|
641
|
+
if (this.hasCircularDependencies(plan)) {
|
|
642
|
+
errors.push('Plan contains circular dependencies');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Check all dependencies exist
|
|
646
|
+
const stepIds = new Set(plan.steps.map(s => s.id));
|
|
647
|
+
for (const step of plan.steps) {
|
|
648
|
+
if (step.depends_on) {
|
|
649
|
+
for (const depId of step.depends_on) {
|
|
650
|
+
if (!stepIds.has(depId)) {
|
|
651
|
+
errors.push(`Step ${step.id} depends on non-existent step ${depId}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Check step order matches dependencies
|
|
658
|
+
for (const step of plan.steps) {
|
|
659
|
+
if (step.depends_on) {
|
|
660
|
+
for (const depId of step.depends_on) {
|
|
661
|
+
const depStep = plan.steps.find(s => s.id === depId);
|
|
662
|
+
if (depStep && depStep.order >= step.order) {
|
|
663
|
+
errors.push(`Step ${step.id} has invalid order relative to dependency ${depId}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
valid: errors.length === 0,
|
|
671
|
+
errors,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Check for circular dependencies
|
|
677
|
+
*/
|
|
678
|
+
private hasCircularDependencies(plan: AgentPlan): boolean {
|
|
679
|
+
const visited = new Set<string>();
|
|
680
|
+
const recursionStack = new Set<string>();
|
|
681
|
+
|
|
682
|
+
const hasCycle = (stepId: string): boolean => {
|
|
683
|
+
visited.add(stepId);
|
|
684
|
+
recursionStack.add(stepId);
|
|
685
|
+
|
|
686
|
+
const step = plan.steps.find(s => s.id === stepId);
|
|
687
|
+
if (step?.depends_on) {
|
|
688
|
+
for (const depId of step.depends_on) {
|
|
689
|
+
if (!visited.has(depId)) {
|
|
690
|
+
if (hasCycle(depId)) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
} else if (recursionStack.has(depId)) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
recursionStack.delete(stepId);
|
|
700
|
+
return false;
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
for (const step of plan.steps) {
|
|
704
|
+
if (!visited.has(step.id)) {
|
|
705
|
+
if (hasCycle(step.id)) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Generate plan ID
|
|
716
|
+
*/
|
|
717
|
+
private generatePlanId(): string {
|
|
718
|
+
return `plan_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
719
|
+
}
|
|
720
|
+
}
|